2718 lines
108 KiB
JavaScript
2718 lines
108 KiB
JavaScript
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
const os = require('node:os');
|
|
const fs = require('fs-extra');
|
|
const yaml = require('yaml');
|
|
const csv = require('csv-parse/sync');
|
|
const { getSourcePath } = require('../../../lib/project-root');
|
|
const { validateHelpSidecarContractFile, HELP_SIDECAR_ERROR_CODES } = require('./sidecar-contract-validator');
|
|
const { validateHelpAuthoritySplitAndPrecedence, HELP_FRONTMATTER_MISMATCH_ERROR_CODES } = require('./help-authority-validator');
|
|
const { ManifestGenerator } = require('./manifest-generator');
|
|
const { buildSidecarAwareExemplarHelpRow } = require('./help-catalog-generator');
|
|
const { CodexSetup } = require('../ide/codex');
|
|
|
|
const HELP_VALIDATION_ERROR_CODES = Object.freeze({
|
|
REQUIRED_ARTIFACT_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ARTIFACT_MISSING',
|
|
CSV_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_CSV_SCHEMA_MISMATCH',
|
|
REQUIRED_ROW_IDENTITY_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ROW_IDENTITY_MISSING',
|
|
REQUIRED_EVIDENCE_LINK_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_EVIDENCE_LINK_MISSING',
|
|
EVIDENCE_LINK_REFERENCE_INVALID: 'ERR_HELP_VALIDATION_EVIDENCE_LINK_REFERENCE_INVALID',
|
|
BINDING_EVIDENCE_INVALID: 'ERR_HELP_VALIDATION_BINDING_EVIDENCE_INVALID',
|
|
ISSUER_PREREQUISITE_MISSING: 'ERR_HELP_VALIDATION_ISSUER_PREREQUISITE_MISSING',
|
|
SELF_ATTESTED_ISSUER_CLAIM: 'ERR_HELP_VALIDATION_SELF_ATTESTED_ISSUER_CLAIM',
|
|
YAML_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_YAML_SCHEMA_MISMATCH',
|
|
DECISION_RECORD_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_DECISION_RECORD_SCHEMA_MISMATCH',
|
|
DECISION_RECORD_PARSE_FAILED: 'ERR_HELP_VALIDATION_DECISION_RECORD_PARSE_FAILED',
|
|
});
|
|
|
|
const SIDEcar_AUTHORITY_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml';
|
|
const SOURCE_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md';
|
|
const EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/help-validation-harness.js';
|
|
|
|
const FRONTMATTER_MISMATCH_DETAILS = Object.freeze({
|
|
[HELP_FRONTMATTER_MISMATCH_ERROR_CODES.CANONICAL_ID_MISMATCH]: 'frontmatter canonicalId must match sidecar canonicalId',
|
|
[HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DISPLAY_NAME_MISMATCH]: 'frontmatter name must match sidecar displayName',
|
|
[HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DESCRIPTION_MISMATCH]: 'frontmatter description must match sidecar description',
|
|
[HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DEPENDENCIES_REQUIRES_MISMATCH]:
|
|
'frontmatter dependencies.requires must match sidecar dependencies.requires',
|
|
});
|
|
|
|
const HELP_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
|
|
Object.freeze({
|
|
artifactId: 1,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-sidecar-snapshot.yaml'),
|
|
type: 'yaml',
|
|
requiredTopLevelKeys: ['schemaVersion', 'canonicalId', 'artifactType', 'module', 'sourcePath', 'displayName', 'description', 'status'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 2,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-runtime-comparison.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'surface',
|
|
'runtimePath',
|
|
'sourcePath',
|
|
'canonicalId',
|
|
'normalizedCapabilityKey',
|
|
'visibleName',
|
|
'inclusionClassification',
|
|
'contentAuthoritySourceType',
|
|
'contentAuthoritySourcePath',
|
|
'metadataAuthoritySourceType',
|
|
'metadataAuthoritySourcePath',
|
|
'status',
|
|
],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 3,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-issued-artifact-provenance.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'rowIdentity',
|
|
'artifactPath',
|
|
'canonicalId',
|
|
'issuerOwnerClass',
|
|
'evidenceIssuerComponent',
|
|
'evidenceMethod',
|
|
'issuingComponent',
|
|
'issuingComponentBindingBasis',
|
|
'issuingComponentBindingEvidence',
|
|
'claimScope',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['rowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 4,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-manifest-comparison.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'surface',
|
|
'sourcePath',
|
|
'legacyName',
|
|
'canonicalId',
|
|
'displayName',
|
|
'normalizedCapabilityKey',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'issuerOwnerClass',
|
|
'issuingComponent',
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'issuingComponentBindingEvidence',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['issuedArtifactEvidenceRowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 5,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-alias-table.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'rowIdentity',
|
|
'canonicalId',
|
|
'alias',
|
|
'aliasType',
|
|
'normalizedAliasValue',
|
|
'rawIdentityHasLeadingSlash',
|
|
'resolutionEligibility',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['rowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 6,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-description-provenance.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'surface',
|
|
'sourcePath',
|
|
'canonicalId',
|
|
'descriptionValue',
|
|
'expectedDescriptionValue',
|
|
'descriptionAuthoritySourceType',
|
|
'descriptionAuthoritySourcePath',
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['issuedArtifactEvidenceRowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 7,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-export-comparison.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'exportPath',
|
|
'sourcePath',
|
|
'canonicalId',
|
|
'visibleId',
|
|
'visibleSurfaceClass',
|
|
'normalizedVisibleKey',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'exportIdDerivationSourceType',
|
|
'exportIdDerivationSourcePath',
|
|
'issuerOwnerClass',
|
|
'issuingComponent',
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'issuingComponentBindingEvidence',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['issuedArtifactEvidenceRowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 8,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-command-label-report.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'surface',
|
|
'sourcePath',
|
|
'canonicalId',
|
|
'rawCommandValue',
|
|
'displayedCommandLabel',
|
|
'normalizedDisplayedLabel',
|
|
'rowCountForCanonicalId',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['issuedArtifactEvidenceRowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 9,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-catalog-pipeline.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'stage',
|
|
'artifactPath',
|
|
'rowIdentity',
|
|
'canonicalId',
|
|
'sourcePath',
|
|
'rowCountForStageCanonicalId',
|
|
'commandValue',
|
|
'expectedCommandValue',
|
|
'descriptionValue',
|
|
'expectedDescriptionValue',
|
|
'descriptionAuthoritySourceType',
|
|
'descriptionAuthoritySourcePath',
|
|
'commandAuthoritySourceType',
|
|
'commandAuthoritySourcePath',
|
|
'issuerOwnerClass',
|
|
'issuingComponent',
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'issuingComponentBindingEvidence',
|
|
'stageStatus',
|
|
'status',
|
|
],
|
|
requiredRowIdentityFields: ['rowIdentity', 'issuedArtifactEvidenceRowIdentity'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 10,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-duplicate-report.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'surface',
|
|
'ownerClass',
|
|
'sourcePath',
|
|
'canonicalId',
|
|
'normalizedCapabilityKey',
|
|
'visibleName',
|
|
'visibleId',
|
|
'visibleSurfaceClass',
|
|
'normalizedVisibleKey',
|
|
'authorityRole',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'authoritativePresenceKey',
|
|
'groupedAuthoritativePresenceCount',
|
|
'groupedAuthoritativeSourceRecordCount',
|
|
'groupedAuthoritativeSourcePathSet',
|
|
'rawIdentityHasLeadingSlash',
|
|
'preAliasNormalizedValue',
|
|
'postAliasCanonicalId',
|
|
'aliasRowLocator',
|
|
'aliasResolutionEvidence',
|
|
'aliasResolutionSourcePath',
|
|
'conflictingProjectedRecordCount',
|
|
'wrapperAuthoritativeRecordCount',
|
|
'status',
|
|
],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 11,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-dependency-report.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'declaredIn',
|
|
'sourcePath',
|
|
'targetType',
|
|
'targetId',
|
|
'normalizedTargetId',
|
|
'expectedOwnerClass',
|
|
'resolutionCandidateCount',
|
|
'resolvedOwnerClass',
|
|
'resolvedSurface',
|
|
'resolvedPath',
|
|
'authoritySourceType',
|
|
'authoritySourcePath',
|
|
'failureReason',
|
|
'status',
|
|
],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 12,
|
|
relativePath: path.join('decision-records', 'help-native-skills-exit.md'),
|
|
type: 'markdown',
|
|
requiredFrontmatterKeys: ['capability', 'goNoGo', 'status'],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 13,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-sidecar-negative-validation.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'scenario',
|
|
'fixturePath',
|
|
'observedSchemaVersion',
|
|
'observedSourcePathValue',
|
|
'observedSidecarBasename',
|
|
'expectedFailureCode',
|
|
'observedFailureCode',
|
|
'expectedFailureDetail',
|
|
'observedFailureDetail',
|
|
'status',
|
|
],
|
|
}),
|
|
Object.freeze({
|
|
artifactId: 14,
|
|
relativePath: path.join('validation', 'help', 'bmad-help-frontmatter-mismatch-validation.csv'),
|
|
type: 'csv',
|
|
columns: [
|
|
'scenario',
|
|
'fixturePath',
|
|
'frontmatterSurfacePath',
|
|
'observedFrontmatterKeyPath',
|
|
'mismatchedField',
|
|
'observedFrontmatterValue',
|
|
'expectedSidecarValue',
|
|
'expectedAuthoritativeSourceType',
|
|
'expectedAuthoritativeSourcePath',
|
|
'expectedFailureCode',
|
|
'observedFailureCode',
|
|
'expectedFailureDetail',
|
|
'observedFailureDetail',
|
|
'observedAuthoritativeSourceType',
|
|
'observedAuthoritativeSourcePath',
|
|
'status',
|
|
],
|
|
}),
|
|
]);
|
|
|
|
class HelpValidationHarnessError extends Error {
|
|
constructor({ code, detail, artifactId, fieldPath, sourcePath, observedValue, expectedValue }) {
|
|
const message = `${code}: ${detail} (artifact=${artifactId}, fieldPath=${fieldPath}, sourcePath=${sourcePath})`;
|
|
super(message);
|
|
this.name = 'HelpValidationHarnessError';
|
|
this.code = code;
|
|
this.detail = detail;
|
|
this.artifactId = artifactId;
|
|
this.fieldPath = fieldPath;
|
|
this.sourcePath = sourcePath;
|
|
this.observedValue = observedValue;
|
|
this.expectedValue = expectedValue;
|
|
}
|
|
}
|
|
|
|
function normalizePath(value) {
|
|
return String(value || '').replaceAll('\\', '/');
|
|
}
|
|
|
|
function normalizeValue(value) {
|
|
return String(value ?? '').trim();
|
|
}
|
|
|
|
function normalizeDependencyTargets(value) {
|
|
const normalized = Array.isArray(value)
|
|
? value
|
|
.map((target) => normalizeValue(String(target || '').toLowerCase()))
|
|
.filter((target) => target.length > 0)
|
|
.sort()
|
|
: [];
|
|
return JSON.stringify(normalized);
|
|
}
|
|
|
|
function computeSha256(value) {
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(String(value || ''), 'utf8')
|
|
.digest('hex');
|
|
}
|
|
|
|
function sortObjectKeysDeep(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => sortObjectKeysDeep(item));
|
|
}
|
|
if (!value || typeof value !== 'object') {
|
|
return value;
|
|
}
|
|
const sorted = {};
|
|
for (const key of Object.keys(value).sort()) {
|
|
sorted[key] = sortObjectKeysDeep(value[key]);
|
|
}
|
|
return sorted;
|
|
}
|
|
|
|
function canonicalJsonStringify(value) {
|
|
return JSON.stringify(sortObjectKeysDeep(value));
|
|
}
|
|
|
|
function buildIssuedArtifactRowIdentity(artifactPath) {
|
|
return `issued-artifact:${String(artifactPath || '').replaceAll('/', '-')}`;
|
|
}
|
|
|
|
function buildAliasResolutionEvidence(preAliasNormalizedValue, rawIdentityHasLeadingSlash, aliasRowLocator) {
|
|
const canonicalId = 'bmad-help';
|
|
return `applied:${preAliasNormalizedValue}|leadingSlash:${rawIdentityHasLeadingSlash}->${canonicalId}|rows:${aliasRowLocator}`;
|
|
}
|
|
|
|
function parseCsvRows(csvContent) {
|
|
return csv.parse(String(csvContent || ''), {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
trim: true,
|
|
});
|
|
}
|
|
|
|
function parseCsvHeader(csvContent) {
|
|
const parsed = csv.parse(String(csvContent || ''), {
|
|
to_line: 1,
|
|
skip_empty_lines: true,
|
|
trim: true,
|
|
});
|
|
return Array.isArray(parsed) && parsed.length > 0 ? parsed[0].map(String) : [];
|
|
}
|
|
|
|
function escapeCsv(value) {
|
|
return `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
}
|
|
|
|
function sortRowsDeterministically(rows, columns) {
|
|
return [...rows].sort((left, right) => {
|
|
const leftKey = columns.map((column) => normalizeValue(left[column])).join('|');
|
|
const rightKey = columns.map((column) => normalizeValue(right[column])).join('|');
|
|
return leftKey.localeCompare(rightKey);
|
|
});
|
|
}
|
|
|
|
function parseFrontmatter(markdownContent) {
|
|
const frontmatterMatch = String(markdownContent || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
if (!frontmatterMatch) return {};
|
|
const parsed = yaml.parse(frontmatterMatch[1]);
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
return {};
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function serializeCsv(columns, rows) {
|
|
const lines = [columns.join(',')];
|
|
for (const row of rows) {
|
|
const serialized = columns.map((column) => escapeCsv(Object.prototype.hasOwnProperty.call(row, column) ? row[column] : ''));
|
|
lines.push(serialized.join(','));
|
|
}
|
|
return `${lines.join('\n')}\n`;
|
|
}
|
|
|
|
const MODULE_HELP_COMPAT_COLUMNS = Object.freeze([
|
|
'module',
|
|
'phase',
|
|
'name',
|
|
'code',
|
|
'sequence',
|
|
'workflow-file',
|
|
'command',
|
|
'required',
|
|
'agent',
|
|
'options',
|
|
'description',
|
|
'output-location',
|
|
'outputs',
|
|
]);
|
|
|
|
const HELP_CATALOG_COLUMNS = Object.freeze([
|
|
'module',
|
|
'phase',
|
|
'name',
|
|
'code',
|
|
'sequence',
|
|
'workflow-file',
|
|
'command',
|
|
'required',
|
|
'agent-name',
|
|
'agent-command',
|
|
'agent-display-name',
|
|
'agent-title',
|
|
'options',
|
|
'description',
|
|
'output-location',
|
|
'outputs',
|
|
]);
|
|
|
|
function countExemplarSkillProjectionRows(markdownContent) {
|
|
const frontmatter = parseFrontmatter(markdownContent);
|
|
return normalizeValue(frontmatter.name) === 'bmad-help' ? 1 : 0;
|
|
}
|
|
|
|
function countManifestClaimRows(csvContent, runtimeFolder) {
|
|
const expectedTaskPath = normalizePath(`${runtimeFolder}/core/tasks/help.md`).toLowerCase();
|
|
return parseCsvRows(csvContent).filter((row) => {
|
|
const canonicalId = normalizeValue(row.canonicalId).toLowerCase();
|
|
const moduleName = normalizeValue(row.module).toLowerCase();
|
|
const name = normalizeValue(row.name).toLowerCase();
|
|
const taskPath = normalizePath(normalizeValue(row.path)).toLowerCase();
|
|
return canonicalId === 'bmad-help' && moduleName === 'core' && name === 'help' && taskPath === expectedTaskPath;
|
|
}).length;
|
|
}
|
|
|
|
function countHelpCatalogClaimRows(csvContent) {
|
|
return parseCsvRows(csvContent).filter((row) => {
|
|
const command = normalizeValue(row.command).toLowerCase().replace(/^\/+/, '');
|
|
const workflowFile = normalizePath(normalizeValue(row['workflow-file'])).toLowerCase();
|
|
return command === 'bmad-help' && workflowFile.endsWith('/core/tasks/help.md');
|
|
}).length;
|
|
}
|
|
|
|
function buildReplaySidecarFixture({ canonicalId = 'bmad-help', description = 'Help command' } = {}) {
|
|
return {
|
|
schemaVersion: 1,
|
|
canonicalId,
|
|
artifactType: 'task',
|
|
module: 'core',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
displayName: 'help',
|
|
description,
|
|
dependencies: {
|
|
requires: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function replayFailurePayload(error) {
|
|
return canonicalJsonStringify({
|
|
replayFailureCode: normalizeValue(error?.code || 'ERR_HELP_VALIDATION_REPLAY_COMPONENT_FAILED'),
|
|
replayFailureDetail: normalizeValue(error?.detail || error?.message || 'component replay failed'),
|
|
});
|
|
}
|
|
|
|
function isSha256(value) {
|
|
return /^[a-f0-9]{64}$/.test(String(value || ''));
|
|
}
|
|
|
|
class HelpValidationHarness {
|
|
constructor() {
|
|
this.registry = HELP_VALIDATION_ARTIFACT_REGISTRY;
|
|
}
|
|
|
|
getArtifactRegistry() {
|
|
return this.registry;
|
|
}
|
|
|
|
resolveOutputPaths(options = {}) {
|
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
const planningArtifactsRoot = path.join(projectDir, '_bmad-output', 'planning-artifacts');
|
|
const validationRoot = path.join(planningArtifactsRoot, 'validation', 'help');
|
|
const decisionRecordsRoot = path.join(planningArtifactsRoot, 'decision-records');
|
|
return {
|
|
projectDir,
|
|
planningArtifactsRoot,
|
|
validationRoot,
|
|
decisionRecordsRoot,
|
|
};
|
|
}
|
|
|
|
resolveSourceArtifactPaths(options = {}) {
|
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
|
|
const sidecarCandidates = [
|
|
options.sidecarPath,
|
|
path.join(projectDir, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'),
|
|
path.join(projectDir, 'src', 'core', 'tasks', 'help.artifact.yaml'),
|
|
getSourcePath('core', 'tasks', 'help.artifact.yaml'),
|
|
].filter(Boolean);
|
|
|
|
const sourceMarkdownCandidates = [
|
|
options.sourceMarkdownPath,
|
|
path.join(projectDir, 'bmad-fork', 'src', 'core', 'tasks', 'help.md'),
|
|
path.join(projectDir, 'src', 'core', 'tasks', 'help.md'),
|
|
getSourcePath('core', 'tasks', 'help.md'),
|
|
].filter(Boolean);
|
|
|
|
const resolveExistingPath = async (candidates) => {
|
|
for (const candidate of candidates) {
|
|
if (await fs.pathExists(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return candidates[0];
|
|
};
|
|
|
|
return Promise.all([resolveExistingPath(sidecarCandidates), resolveExistingPath(sourceMarkdownCandidates)]).then(
|
|
([sidecarPath, sourceMarkdownPath]) => ({
|
|
sidecarPath,
|
|
sourceMarkdownPath,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async readSidecarMetadata(sidecarPath) {
|
|
const parsed = yaml.parse(await fs.readFile(sidecarPath, 'utf8'));
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
return {
|
|
schemaVersion: 1,
|
|
canonicalId: 'bmad-help',
|
|
artifactType: 'task',
|
|
module: 'core',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
displayName: 'help',
|
|
description: 'Help command',
|
|
dependencies: { requires: [] },
|
|
};
|
|
}
|
|
return {
|
|
schemaVersion: parsed.schemaVersion ?? 1,
|
|
canonicalId: normalizeValue(parsed.canonicalId || 'bmad-help'),
|
|
artifactType: normalizeValue(parsed.artifactType || 'task'),
|
|
module: normalizeValue(parsed.module || 'core'),
|
|
sourcePath: normalizeValue(parsed.sourcePath || SOURCE_MARKDOWN_SOURCE_PATH),
|
|
displayName: normalizeValue(parsed.displayName || 'help'),
|
|
description: normalizeValue(parsed.description || 'Help command'),
|
|
dependencies: parsed.dependencies && typeof parsed.dependencies === 'object' ? parsed.dependencies : { requires: [] },
|
|
};
|
|
}
|
|
|
|
async readCsvSurface(csvPath) {
|
|
if (!(await fs.pathExists(csvPath))) {
|
|
return [];
|
|
}
|
|
const content = await fs.readFile(csvPath, 'utf8');
|
|
return parseCsvRows(content);
|
|
}
|
|
|
|
async assertRequiredInputSurfaceExists({ artifactId, absolutePath, sourcePath, description }) {
|
|
if (await fs.pathExists(absolutePath)) {
|
|
return;
|
|
}
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
|
|
detail: `Required input surface is missing (${description})`,
|
|
artifactId,
|
|
fieldPath: '<file>',
|
|
sourcePath: normalizePath(sourcePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: normalizePath(sourcePath),
|
|
});
|
|
}
|
|
|
|
requireRow({ rows, predicate, artifactId, fieldPath, sourcePath, detail }) {
|
|
const match = (rows || []).find(predicate);
|
|
if (match) {
|
|
return match;
|
|
}
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail,
|
|
artifactId,
|
|
fieldPath,
|
|
sourcePath: normalizePath(sourcePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: 'required row',
|
|
});
|
|
}
|
|
|
|
async writeCsvArtifact(filePath, columns, rows) {
|
|
const sortedRows = sortRowsDeterministically(rows, columns);
|
|
await fs.writeFile(filePath, serializeCsv(columns, sortedRows), 'utf8');
|
|
}
|
|
|
|
async ensureValidationFixtures(outputPaths, sidecarMetadata) {
|
|
const sidecarNegativeRoot = path.join(outputPaths.validationRoot, 'fixtures', 'sidecar-negative');
|
|
const frontmatterMismatchRoot = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch');
|
|
await fs.ensureDir(sidecarNegativeRoot);
|
|
await fs.ensureDir(frontmatterMismatchRoot);
|
|
|
|
const unknownMajorFixturePath = path.join(sidecarNegativeRoot, 'unknown-major-version', 'help.artifact.yaml');
|
|
const basenameMismatchFixturePath = path.join(sidecarNegativeRoot, 'basename-path-mismatch', 'help.artifact.yaml');
|
|
await fs.ensureDir(path.dirname(unknownMajorFixturePath));
|
|
await fs.ensureDir(path.dirname(basenameMismatchFixturePath));
|
|
|
|
const unknownMajorFixture = {
|
|
...sidecarMetadata,
|
|
schemaVersion: 2,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
};
|
|
const basenameMismatchFixture = {
|
|
...sidecarMetadata,
|
|
schemaVersion: 1,
|
|
sourcePath: 'bmad-fork/src/core/tasks/not-help.md',
|
|
};
|
|
|
|
await fs.writeFile(unknownMajorFixturePath, yaml.stringify(unknownMajorFixture), 'utf8');
|
|
await fs.writeFile(basenameMismatchFixturePath, yaml.stringify(basenameMismatchFixture), 'utf8');
|
|
|
|
const sourceMismatchRoot = path.join(frontmatterMismatchRoot, 'source');
|
|
const runtimeMismatchRoot = path.join(frontmatterMismatchRoot, 'runtime');
|
|
await fs.ensureDir(sourceMismatchRoot);
|
|
await fs.ensureDir(runtimeMismatchRoot);
|
|
|
|
const baseFrontmatter = {
|
|
name: sidecarMetadata.displayName,
|
|
description: sidecarMetadata.description,
|
|
canonicalId: sidecarMetadata.canonicalId,
|
|
dependencies: {
|
|
requires: Array.isArray(sidecarMetadata.dependencies.requires) ? sidecarMetadata.dependencies.requires : [],
|
|
},
|
|
};
|
|
|
|
const buildMarkdown = (frontmatter) => `---\n${yaml.stringify(frontmatter).trimEnd()}\n---\n\n# Fixture\n`;
|
|
|
|
const scenarios = [
|
|
{
|
|
id: 'canonical-id-mismatch',
|
|
keyPath: 'canonicalId',
|
|
mismatchField: 'canonicalId',
|
|
makeFrontmatter: () => ({ ...baseFrontmatter, canonicalId: 'legacy-help' }),
|
|
},
|
|
{
|
|
id: 'display-name-mismatch',
|
|
keyPath: 'name',
|
|
mismatchField: 'displayName',
|
|
makeFrontmatter: () => ({ ...baseFrontmatter, name: 'BMAD Help' }),
|
|
},
|
|
{
|
|
id: 'description-mismatch',
|
|
keyPath: 'description',
|
|
mismatchField: 'description',
|
|
makeFrontmatter: () => ({ ...baseFrontmatter, description: 'Runtime override' }),
|
|
},
|
|
{
|
|
id: 'dependencies-mismatch',
|
|
keyPath: 'dependencies.requires',
|
|
mismatchField: 'dependencies.requires',
|
|
makeFrontmatter: () => ({ ...baseFrontmatter, dependencies: { requires: ['skill:demo'] } }),
|
|
},
|
|
];
|
|
|
|
for (const scenario of scenarios) {
|
|
const sourcePath = path.join(sourceMismatchRoot, `${scenario.id}.md`);
|
|
const runtimePath = path.join(runtimeMismatchRoot, `${scenario.id}.md`);
|
|
await fs.writeFile(sourcePath, buildMarkdown(scenario.makeFrontmatter()), 'utf8');
|
|
await fs.writeFile(runtimePath, buildMarkdown(scenario.makeFrontmatter()), 'utf8');
|
|
}
|
|
|
|
return {
|
|
unknownMajorFixturePath,
|
|
basenameMismatchFixturePath,
|
|
sourceMismatchRoot,
|
|
runtimeMismatchRoot,
|
|
};
|
|
}
|
|
|
|
buildArtifactPathsMap(outputPaths) {
|
|
const artifactPaths = new Map();
|
|
for (const artifact of this.registry) {
|
|
artifactPaths.set(artifact.artifactId, path.join(outputPaths.planningArtifactsRoot, artifact.relativePath));
|
|
}
|
|
return artifactPaths;
|
|
}
|
|
|
|
resolveReplayContract({ artifactPath, componentPath, rowIdentity, runtimeFolder }) {
|
|
const claimedRowIdentity = normalizeValue(rowIdentity);
|
|
if (!claimedRowIdentity) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Claimed replay rowIdentity is required',
|
|
artifactId: 3,
|
|
fieldPath: 'rowIdentity',
|
|
sourcePath: artifactPath,
|
|
observedValue: claimedRowIdentity,
|
|
expectedValue: 'non-empty value',
|
|
});
|
|
}
|
|
|
|
const expectedRowIdentity = buildIssuedArtifactRowIdentity(artifactPath);
|
|
if (claimedRowIdentity !== expectedRowIdentity) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Claimed replay rowIdentity does not match artifact claim rowIdentity contract',
|
|
artifactId: 3,
|
|
fieldPath: 'rowIdentity',
|
|
sourcePath: artifactPath,
|
|
observedValue: claimedRowIdentity,
|
|
expectedValue: expectedRowIdentity,
|
|
});
|
|
}
|
|
|
|
const contractsByClaimRowIdentity = new Map([
|
|
[
|
|
buildIssuedArtifactRowIdentity(`${runtimeFolder}/_config/task-manifest.csv`),
|
|
{
|
|
artifactPath: `${runtimeFolder}/_config/task-manifest.csv`,
|
|
componentPathIncludes: 'manifest-generator.js',
|
|
mutationKind: 'component-input-perturbation:manifest-generator/tasks',
|
|
run: ({ workspaceRoot, perturbed }) => this.runManifestGeneratorReplay({ workspaceRoot, runtimeFolder, perturbed }),
|
|
},
|
|
],
|
|
[
|
|
buildIssuedArtifactRowIdentity(`${runtimeFolder}/core/module-help.csv`),
|
|
{
|
|
artifactPath: `${runtimeFolder}/core/module-help.csv`,
|
|
componentPathIncludes: 'help-catalog-generator.js',
|
|
mutationKind: 'component-input-perturbation:help-catalog-generator/sidecar-canonical-id',
|
|
run: ({ workspaceRoot, perturbed }) =>
|
|
this.runHelpCatalogGeneratorReplay({
|
|
workspaceRoot,
|
|
runtimeFolder,
|
|
perturbed,
|
|
}),
|
|
},
|
|
],
|
|
[
|
|
buildIssuedArtifactRowIdentity(`${runtimeFolder}/_config/bmad-help.csv`),
|
|
{
|
|
artifactPath: `${runtimeFolder}/_config/bmad-help.csv`,
|
|
componentPathIncludes: 'installer.js::mergemodulehelpcatalogs',
|
|
mutationKind: 'component-input-perturbation:installer/help-authority-records',
|
|
run: ({ workspaceRoot, perturbed }) =>
|
|
this.runInstallerMergeReplay({
|
|
workspaceRoot,
|
|
runtimeFolder,
|
|
perturbed,
|
|
}),
|
|
},
|
|
],
|
|
[
|
|
buildIssuedArtifactRowIdentity('.agents/skills/bmad-help/SKILL.md'),
|
|
{
|
|
artifactPath: '.agents/skills/bmad-help/SKILL.md',
|
|
componentPathIncludes: 'ide/codex.js',
|
|
mutationKind: 'component-input-perturbation:codex/sidecar-canonical-id',
|
|
run: ({ workspaceRoot, perturbed }) => this.runCodexExportReplay({ workspaceRoot, perturbed }),
|
|
},
|
|
],
|
|
]);
|
|
|
|
const contract = contractsByClaimRowIdentity.get(claimedRowIdentity);
|
|
if (!contract) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Claimed rowIdentity is not mapped to a replay contract',
|
|
artifactId: 3,
|
|
fieldPath: 'rowIdentity',
|
|
sourcePath: artifactPath,
|
|
observedValue: claimedRowIdentity,
|
|
expectedValue: 'known issued-artifact claim rowIdentity',
|
|
});
|
|
}
|
|
|
|
const normalizedComponentPath = normalizeValue(componentPath).toLowerCase();
|
|
if (
|
|
normalizeValue(artifactPath) !== normalizeValue(contract.artifactPath) ||
|
|
!normalizedComponentPath.includes(String(contract.componentPathIncludes || '').toLowerCase())
|
|
) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Claimed replay rowIdentity/component pair does not match replay contract mapping',
|
|
artifactId: 3,
|
|
fieldPath: 'issuingComponent',
|
|
sourcePath: artifactPath,
|
|
observedValue: canonicalJsonStringify({
|
|
artifactPath,
|
|
componentPath,
|
|
rowIdentity: claimedRowIdentity,
|
|
}),
|
|
expectedValue: canonicalJsonStringify({
|
|
artifactPath: contract.artifactPath,
|
|
componentPathIncludes: contract.componentPathIncludes,
|
|
rowIdentity: claimedRowIdentity,
|
|
}),
|
|
});
|
|
}
|
|
|
|
return contract;
|
|
}
|
|
|
|
async runManifestGeneratorReplay({ workspaceRoot, runtimeFolder, perturbed }) {
|
|
const bmadDir = path.join(workspaceRoot, runtimeFolder);
|
|
const cfgDir = path.join(bmadDir, '_config');
|
|
await fs.ensureDir(cfgDir);
|
|
|
|
const generator = new ManifestGenerator();
|
|
generator.bmadFolderName = runtimeFolder;
|
|
generator.taskAuthorityRecords = [
|
|
{
|
|
recordType: 'metadata-authority',
|
|
canonicalId: 'bmad-help',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
},
|
|
];
|
|
generator.helpAuthorityRecords = [...generator.taskAuthorityRecords];
|
|
generator.tasks = perturbed
|
|
? []
|
|
: [
|
|
{
|
|
name: 'help',
|
|
displayName: 'help',
|
|
description: 'Help command',
|
|
module: 'core',
|
|
path: `${runtimeFolder}/core/tasks/help.md`,
|
|
standalone: 'true',
|
|
},
|
|
];
|
|
|
|
await generator.writeTaskManifest(cfgDir);
|
|
const outputPath = path.join(cfgDir, 'task-manifest.csv');
|
|
const content = await fs.readFile(outputPath, 'utf8');
|
|
return {
|
|
content,
|
|
targetRowCount: countManifestClaimRows(content, runtimeFolder),
|
|
};
|
|
}
|
|
|
|
async runHelpCatalogGeneratorReplay({ workspaceRoot, runtimeFolder, perturbed }) {
|
|
const sidecarPath = path.join(workspaceRoot, 'src', 'core', 'tasks', 'help.artifact.yaml');
|
|
await fs.ensureDir(path.dirname(sidecarPath));
|
|
await fs.writeFile(
|
|
sidecarPath,
|
|
yaml.stringify(
|
|
buildReplaySidecarFixture({
|
|
canonicalId: perturbed ? 'bmad-help-replay-perturbed' : 'bmad-help',
|
|
}),
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
const generated = await buildSidecarAwareExemplarHelpRow({
|
|
sidecarPath,
|
|
bmadFolderName: runtimeFolder,
|
|
});
|
|
const content = serializeCsv(HELP_CATALOG_COLUMNS, [generated.row]);
|
|
return {
|
|
content,
|
|
targetRowCount: countHelpCatalogClaimRows(content),
|
|
};
|
|
}
|
|
|
|
async runInstallerMergeReplay({ workspaceRoot, runtimeFolder, perturbed }) {
|
|
const { Installer } = require('./installer');
|
|
const bmadDir = path.join(workspaceRoot, runtimeFolder);
|
|
const coreDir = path.join(bmadDir, 'core');
|
|
const cfgDir = path.join(bmadDir, '_config');
|
|
await fs.ensureDir(coreDir);
|
|
await fs.ensureDir(cfgDir);
|
|
|
|
const moduleHelpFixtureRows = [
|
|
{
|
|
module: 'core',
|
|
phase: 'anytime',
|
|
name: 'bmad-help',
|
|
code: 'BH',
|
|
sequence: '',
|
|
'workflow-file': `${runtimeFolder}/core/tasks/help.md`,
|
|
command: 'bmad-help',
|
|
required: 'false',
|
|
agent: '',
|
|
options: '',
|
|
description: 'Help command',
|
|
'output-location': '',
|
|
outputs: '',
|
|
},
|
|
{
|
|
module: 'core',
|
|
phase: 'anytime',
|
|
name: 'Shard Document',
|
|
code: 'SD',
|
|
sequence: '',
|
|
'workflow-file': `${runtimeFolder}/core/tasks/shard-doc.xml`,
|
|
command: 'bmad-shard-doc',
|
|
required: 'false',
|
|
agent: '',
|
|
options: '',
|
|
description: 'Split large markdown documents into smaller files by section with an index.',
|
|
'output-location': '',
|
|
outputs: '',
|
|
},
|
|
{
|
|
module: 'core',
|
|
phase: 'anytime',
|
|
name: 'Index Docs',
|
|
code: 'ID',
|
|
sequence: '',
|
|
'workflow-file': `${runtimeFolder}/core/tasks/index-docs.xml`,
|
|
command: 'bmad-index-docs',
|
|
required: 'false',
|
|
agent: '',
|
|
options: '',
|
|
description:
|
|
'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.',
|
|
'output-location': '',
|
|
outputs: '',
|
|
},
|
|
];
|
|
await fs.writeFile(path.join(coreDir, 'module-help.csv'), serializeCsv(MODULE_HELP_COMPAT_COLUMNS, moduleHelpFixtureRows), 'utf8');
|
|
await fs.writeFile(
|
|
path.join(cfgDir, 'agent-manifest.csv'),
|
|
'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n',
|
|
'utf8',
|
|
);
|
|
|
|
const installer = new Installer();
|
|
installer.bmadFolderName = runtimeFolder;
|
|
installer.installedFiles = new Set();
|
|
installer.helpAuthorityRecords = perturbed
|
|
? [
|
|
{
|
|
canonicalId: 'bmad-help-replay-perturbed',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
},
|
|
]
|
|
: [];
|
|
|
|
await installer.mergeModuleHelpCatalogs(bmadDir);
|
|
const outputPath = path.join(cfgDir, 'bmad-help.csv');
|
|
const content = await fs.readFile(outputPath, 'utf8');
|
|
return {
|
|
content,
|
|
targetRowCount: countHelpCatalogClaimRows(content),
|
|
};
|
|
}
|
|
|
|
async runCodexExportReplay({ workspaceRoot, perturbed }) {
|
|
const projectDir = workspaceRoot;
|
|
const sourceDir = path.join(projectDir, 'src', 'core', 'tasks');
|
|
await fs.ensureDir(sourceDir);
|
|
await fs.writeFile(
|
|
path.join(sourceDir, 'help.artifact.yaml'),
|
|
yaml.stringify(
|
|
buildReplaySidecarFixture({
|
|
canonicalId: perturbed ? 'bmad-help-replay-perturbed' : 'bmad-help',
|
|
}),
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
const codex = new CodexSetup();
|
|
codex.exportDerivationRecords = [];
|
|
const artifact = {
|
|
type: 'task',
|
|
name: 'help',
|
|
displayName: 'help',
|
|
module: 'core',
|
|
sourcePath: path.join(sourceDir, 'help.md'),
|
|
relativePath: path.join('core', 'tasks', 'help.md'),
|
|
content: '---\nname: help\ndescription: Help command\n---\n\n# Help\n',
|
|
};
|
|
|
|
const destDir = path.join(projectDir, '.agents', 'skills');
|
|
await fs.ensureDir(destDir);
|
|
await codex.writeSkillArtifacts(destDir, [artifact], 'task', { projectDir });
|
|
|
|
const outputPath = path.join(destDir, 'bmad-help', 'SKILL.md');
|
|
const content = await fs.readFile(outputPath, 'utf8');
|
|
return {
|
|
content,
|
|
targetRowCount: countExemplarSkillProjectionRows(content),
|
|
};
|
|
}
|
|
|
|
async executeIsolatedReplay({ artifactPath, componentPath, rowIdentity, runtimeFolder }) {
|
|
const contract = this.resolveReplayContract({
|
|
artifactPath,
|
|
componentPath,
|
|
rowIdentity,
|
|
runtimeFolder,
|
|
});
|
|
const baselineWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'help-replay-baseline-'));
|
|
const perturbedWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'help-replay-perturbed-'));
|
|
|
|
try {
|
|
const baseline = await contract.run({ workspaceRoot: baselineWorkspaceRoot, perturbed: false });
|
|
if (Number(baseline.targetRowCount) <= 0) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Claimed rowIdentity target is absent in baseline component replay output',
|
|
artifactId: 3,
|
|
fieldPath: 'rowIdentity',
|
|
sourcePath: artifactPath,
|
|
observedValue: Number(baseline.targetRowCount),
|
|
expectedValue: `at least one row bound to ${normalizeValue(rowIdentity)}`,
|
|
});
|
|
}
|
|
|
|
let mutated;
|
|
try {
|
|
mutated = await contract.run({ workspaceRoot: perturbedWorkspaceRoot, perturbed: true });
|
|
} catch (error) {
|
|
mutated = {
|
|
content: replayFailurePayload(error),
|
|
targetRowCount: 0,
|
|
};
|
|
}
|
|
|
|
return {
|
|
baselineContent: baseline.content,
|
|
mutatedContent: mutated.content,
|
|
baselineTargetRowCount: Number(baseline.targetRowCount),
|
|
mutatedTargetRowCount: Number(mutated.targetRowCount),
|
|
perturbationApplied: true,
|
|
mutationKind: contract.mutationKind,
|
|
targetedRowLocator: normalizeValue(rowIdentity),
|
|
};
|
|
} finally {
|
|
await fs.remove(baselineWorkspaceRoot);
|
|
await fs.remove(perturbedWorkspaceRoot);
|
|
}
|
|
}
|
|
|
|
async buildObservedBindingEvidence({ artifactPath, absolutePath, componentPath, rowIdentity, optionalSurface = false, runtimeFolder }) {
|
|
const exists = await fs.pathExists(absolutePath);
|
|
if (!exists && optionalSurface) {
|
|
const sentinelHash = computeSha256('surface-not-required');
|
|
const payload = {
|
|
evidenceVersion: 1,
|
|
observationMethod: 'validator-observed-optional-surface-omitted',
|
|
observationOutcome: 'surface-not-required',
|
|
artifactPath,
|
|
componentPath,
|
|
baselineArtifactSha256: sentinelHash,
|
|
mutatedArtifactSha256: sentinelHash,
|
|
baselineRowIdentity: rowIdentity,
|
|
mutatedRowIdentity: rowIdentity,
|
|
targetedRowLocator: normalizeValue(rowIdentity),
|
|
rowLevelDiffSha256: computeSha256(`${artifactPath}|${componentPath}|surface-not-required`),
|
|
perturbationApplied: false,
|
|
baselineTargetRowCount: 0,
|
|
mutatedTargetRowCount: 0,
|
|
mutationKind: 'not-applicable',
|
|
serializationFormat: 'json-canonical-v1',
|
|
encoding: 'utf-8',
|
|
lineEndings: 'lf',
|
|
worktreePath: 'in-memory-isolated-replay',
|
|
commitSha: 'not-applicable',
|
|
timestampUtc: '1970-01-01T00:00:00Z',
|
|
};
|
|
return {
|
|
evidenceMethod: 'validator-observed-optional-surface-omitted',
|
|
issuingComponentBindingBasis: 'validator-observed-optional-surface-omitted',
|
|
issuingComponentBindingEvidence: canonicalJsonStringify(payload),
|
|
status: 'SKIP',
|
|
};
|
|
}
|
|
|
|
const mutationResult = await this.executeIsolatedReplay({
|
|
artifactPath,
|
|
componentPath,
|
|
rowIdentity,
|
|
runtimeFolder: normalizeValue(runtimeFolder || '_bmad'),
|
|
});
|
|
|
|
const baselineArtifactSha256 = computeSha256(mutationResult.baselineContent);
|
|
const mutatedArtifactSha256 = computeSha256(mutationResult.mutatedContent);
|
|
const diffPayload = {
|
|
artifactPath,
|
|
componentPath,
|
|
rowIdentity,
|
|
mutationKind: mutationResult.mutationKind,
|
|
targetedRowLocator: mutationResult.targetedRowLocator,
|
|
baselineTargetRowCount: mutationResult.baselineTargetRowCount,
|
|
mutatedTargetRowCount: mutationResult.mutatedTargetRowCount,
|
|
baselineArtifactSha256,
|
|
mutatedArtifactSha256,
|
|
};
|
|
const payload = {
|
|
evidenceVersion: 1,
|
|
observationMethod: 'validator-observed-baseline-plus-isolated-single-component-perturbation',
|
|
observationOutcome: mutationResult.perturbationApplied ? 'observed-impact' : 'no-impact-observed',
|
|
artifactPath,
|
|
componentPath,
|
|
baselineArtifactSha256,
|
|
mutatedArtifactSha256,
|
|
baselineRowIdentity: rowIdentity,
|
|
mutatedRowIdentity: rowIdentity,
|
|
rowLevelDiffSha256: computeSha256(canonicalJsonStringify(diffPayload)),
|
|
perturbationApplied: Boolean(mutationResult.perturbationApplied),
|
|
baselineTargetRowCount: mutationResult.baselineTargetRowCount,
|
|
mutatedTargetRowCount: mutationResult.mutatedTargetRowCount,
|
|
mutationKind: mutationResult.mutationKind,
|
|
targetedRowLocator: mutationResult.targetedRowLocator,
|
|
serializationFormat: 'json-canonical-v1',
|
|
encoding: 'utf-8',
|
|
lineEndings: 'lf',
|
|
worktreePath: 'in-memory-isolated-replay',
|
|
commitSha: 'not-applicable',
|
|
timestampUtc: '1970-01-01T00:00:00Z',
|
|
};
|
|
|
|
return {
|
|
evidenceMethod: 'validator-observed-baseline-plus-isolated-single-component-perturbation',
|
|
issuingComponentBindingBasis: 'validator-observed-baseline-plus-isolated-single-component-perturbation',
|
|
issuingComponentBindingEvidence: canonicalJsonStringify(payload),
|
|
status: 'PASS',
|
|
};
|
|
}
|
|
|
|
async createIssuedArtifactProvenanceRows({ runtimeFolder, bmadDir, projectDir, requireExportSkillProjection }) {
|
|
const artifactBindings = [
|
|
{
|
|
artifactPath: `${runtimeFolder}/_config/task-manifest.csv`,
|
|
absolutePath: path.join(bmadDir, '_config', 'task-manifest.csv'),
|
|
issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/manifest-generator.js',
|
|
},
|
|
{
|
|
artifactPath: `${runtimeFolder}/core/module-help.csv`,
|
|
absolutePath: path.join(bmadDir, 'core', 'module-help.csv'),
|
|
issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/help-catalog-generator.js::buildSidecarAwareExemplarHelpRow()',
|
|
},
|
|
{
|
|
artifactPath: `${runtimeFolder}/_config/bmad-help.csv`,
|
|
absolutePath: path.join(bmadDir, '_config', 'bmad-help.csv'),
|
|
issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/installer.js::mergeModuleHelpCatalogs()',
|
|
},
|
|
{
|
|
artifactPath: '.agents/skills/bmad-help/SKILL.md',
|
|
absolutePath: path.join(projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md'),
|
|
issuingComponent: 'bmad-fork/tools/cli/installers/lib/ide/codex.js',
|
|
optionalSurface: !requireExportSkillProjection,
|
|
},
|
|
];
|
|
|
|
const provenanceRows = [];
|
|
for (const binding of artifactBindings) {
|
|
const rowIdentity = buildIssuedArtifactRowIdentity(binding.artifactPath);
|
|
const evidence = await this.buildObservedBindingEvidence({
|
|
artifactPath: binding.artifactPath,
|
|
absolutePath: binding.absolutePath,
|
|
componentPath: binding.issuingComponent,
|
|
rowIdentity,
|
|
optionalSurface: Boolean(binding.optionalSurface),
|
|
runtimeFolder,
|
|
});
|
|
provenanceRows.push({
|
|
rowIdentity,
|
|
artifactPath: binding.artifactPath,
|
|
canonicalId: 'bmad-help',
|
|
issuerOwnerClass: 'independent-validator',
|
|
evidenceIssuerComponent: EVIDENCE_ISSUER_COMPONENT,
|
|
evidenceMethod: evidence.evidenceMethod,
|
|
issuingComponent: binding.issuingComponent,
|
|
issuingComponentBindingBasis: evidence.issuingComponentBindingBasis,
|
|
issuingComponentBindingEvidence: evidence.issuingComponentBindingEvidence,
|
|
claimScope: binding.artifactPath,
|
|
status: evidence.status,
|
|
});
|
|
}
|
|
|
|
return provenanceRows;
|
|
}
|
|
|
|
makeEvidenceLookup(provenanceRows) {
|
|
const byArtifactPath = new Map();
|
|
for (const row of provenanceRows) {
|
|
byArtifactPath.set(row.artifactPath, row);
|
|
}
|
|
return byArtifactPath;
|
|
}
|
|
|
|
async generateValidationArtifacts(options = {}) {
|
|
const outputPaths = this.resolveOutputPaths(options);
|
|
const runtimeFolder = normalizeValue(options.bmadFolderName || '_bmad');
|
|
const bmadDir = path.resolve(options.bmadDir || path.join(outputPaths.projectDir, runtimeFolder));
|
|
const artifactPaths = this.buildArtifactPathsMap(outputPaths);
|
|
const sourcePaths = await this.resolveSourceArtifactPaths({
|
|
...options,
|
|
projectDir: outputPaths.projectDir,
|
|
});
|
|
const sidecarMetadata = await this.readSidecarMetadata(sourcePaths.sidecarPath);
|
|
|
|
await fs.ensureDir(outputPaths.validationRoot);
|
|
await fs.ensureDir(outputPaths.decisionRecordsRoot);
|
|
|
|
const runtimeTaskPath = `${runtimeFolder}/core/tasks/help.md`;
|
|
const runtimeModuleHelpPath = `${runtimeFolder}/core/module-help.csv`;
|
|
const runtimeTaskManifestPath = `${runtimeFolder}/_config/task-manifest.csv`;
|
|
const runtimeAliasPath = `${runtimeFolder}/_config/canonical-aliases.csv`;
|
|
const runtimeHelpCatalogPath = `${runtimeFolder}/_config/bmad-help.csv`;
|
|
const runtimePipelinePath = `${runtimeFolder}/_config/bmad-help-catalog-pipeline.csv`;
|
|
const runtimeCommandLabelPath = `${runtimeFolder}/_config/bmad-help-command-label-report.csv`;
|
|
const evidenceArtifactPath = '_bmad-output/planning-artifacts/validation/help/bmad-help-issued-artifact-provenance.csv';
|
|
const exportSkillPath = '.agents/skills/bmad-help/SKILL.md';
|
|
const exportSkillAbsolutePath = path.join(outputPaths.projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md');
|
|
const codexExportRows =
|
|
Array.isArray(options.codexExportDerivationRecords) && options.codexExportDerivationRecords.length > 0
|
|
? [...options.codexExportDerivationRecords]
|
|
: [];
|
|
const requireExportSkillProjection = options.requireExportSkillProjection !== false || codexExportRows.length > 0;
|
|
const exportSkillProjectionExists = await fs.pathExists(exportSkillAbsolutePath);
|
|
|
|
const requiredInputSurfaces = [
|
|
{
|
|
artifactId: 1,
|
|
absolutePath: sourcePaths.sidecarPath,
|
|
sourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
description: 'sidecar metadata authority',
|
|
},
|
|
{
|
|
artifactId: 2,
|
|
absolutePath: sourcePaths.sourceMarkdownPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
description: 'source markdown authority',
|
|
},
|
|
{
|
|
artifactId: 2,
|
|
absolutePath: path.join(bmadDir, 'core', 'tasks', 'help.md'),
|
|
sourcePath: runtimeTaskPath,
|
|
description: 'runtime help markdown projection',
|
|
},
|
|
{
|
|
artifactId: 4,
|
|
absolutePath: path.join(bmadDir, '_config', 'task-manifest.csv'),
|
|
sourcePath: runtimeTaskManifestPath,
|
|
description: 'task-manifest projection',
|
|
},
|
|
{
|
|
artifactId: 5,
|
|
absolutePath: path.join(bmadDir, '_config', 'canonical-aliases.csv'),
|
|
sourcePath: runtimeAliasPath,
|
|
description: 'canonical-aliases projection',
|
|
},
|
|
{
|
|
artifactId: 6,
|
|
absolutePath: path.join(bmadDir, 'core', 'module-help.csv'),
|
|
sourcePath: runtimeModuleHelpPath,
|
|
description: 'module-help projection',
|
|
},
|
|
{
|
|
artifactId: 8,
|
|
absolutePath: path.join(bmadDir, '_config', 'bmad-help.csv'),
|
|
sourcePath: runtimeHelpCatalogPath,
|
|
description: 'merged help-catalog projection',
|
|
},
|
|
{
|
|
artifactId: 8,
|
|
absolutePath: path.join(bmadDir, '_config', 'bmad-help-command-label-report.csv'),
|
|
sourcePath: runtimeCommandLabelPath,
|
|
description: 'command-label report projection',
|
|
},
|
|
{
|
|
artifactId: 9,
|
|
absolutePath: path.join(bmadDir, '_config', 'bmad-help-catalog-pipeline.csv'),
|
|
sourcePath: runtimePipelinePath,
|
|
description: 'help-catalog pipeline projection',
|
|
},
|
|
];
|
|
if (requireExportSkillProjection) {
|
|
requiredInputSurfaces.push({
|
|
artifactId: 7,
|
|
absolutePath: exportSkillAbsolutePath,
|
|
sourcePath: exportSkillPath,
|
|
description: 'export skill projection',
|
|
});
|
|
}
|
|
for (const requiredSurface of requiredInputSurfaces) {
|
|
// Story 3.1 is fail-fast: required projection inputs must exist before generating validator outputs.
|
|
await this.assertRequiredInputSurfaceExists(requiredSurface);
|
|
}
|
|
|
|
const taskManifestRows = await this.readCsvSurface(path.join(bmadDir, '_config', 'task-manifest.csv'));
|
|
const aliasRows = await this.readCsvSurface(path.join(bmadDir, '_config', 'canonical-aliases.csv'));
|
|
const moduleHelpRows = await this.readCsvSurface(path.join(bmadDir, 'core', 'module-help.csv'));
|
|
const helpCatalogRows = await this.readCsvSurface(path.join(bmadDir, '_config', 'bmad-help.csv'));
|
|
|
|
const pipelineRowsInput = Array.isArray(options.helpCatalogPipelineRows) && options.helpCatalogPipelineRows.length > 0;
|
|
const commandLabelRowsInput =
|
|
Array.isArray(options.helpCatalogCommandLabelReportRows) && options.helpCatalogCommandLabelReportRows.length > 0;
|
|
|
|
const pipelineRows = pipelineRowsInput
|
|
? [...options.helpCatalogPipelineRows]
|
|
: await this.readCsvSurface(path.join(bmadDir, '_config', 'bmad-help-catalog-pipeline.csv'));
|
|
const commandLabelRows = commandLabelRowsInput
|
|
? [...options.helpCatalogCommandLabelReportRows]
|
|
: await this.readCsvSurface(path.join(bmadDir, '_config', 'bmad-help-command-label-report.csv'));
|
|
|
|
const provenanceRows = await this.createIssuedArtifactProvenanceRows({
|
|
runtimeFolder,
|
|
bmadDir,
|
|
projectDir: outputPaths.projectDir,
|
|
requireExportSkillProjection,
|
|
});
|
|
const evidenceLookup = this.makeEvidenceLookup(provenanceRows);
|
|
|
|
// Artifact 1: sidecar snapshot
|
|
const sidecarSnapshot = {
|
|
schemaVersion: sidecarMetadata.schemaVersion,
|
|
canonicalId: sidecarMetadata.canonicalId || 'bmad-help',
|
|
artifactType: sidecarMetadata.artifactType || 'task',
|
|
module: sidecarMetadata.module || 'core',
|
|
sourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
displayName: sidecarMetadata.displayName || 'help',
|
|
description: sidecarMetadata.description || 'Help command',
|
|
dependencies: {
|
|
requires: Array.isArray(sidecarMetadata.dependencies.requires) ? sidecarMetadata.dependencies.requires : [],
|
|
},
|
|
status: 'PASS',
|
|
};
|
|
await fs.writeFile(artifactPaths.get(1), yaml.stringify(sidecarSnapshot), 'utf8');
|
|
|
|
// Artifact 2: runtime comparison
|
|
const runtimeComparisonRows = [
|
|
{
|
|
surface: runtimeTaskPath,
|
|
runtimePath: runtimeTaskPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
inclusionClassification: 'included-runtime-content',
|
|
contentAuthoritySourceType: 'source-markdown',
|
|
contentAuthoritySourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
metadataAuthoritySourceType: 'sidecar',
|
|
metadataAuthoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeModuleHelpPath,
|
|
runtimePath: runtimeModuleHelpPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
inclusionClassification: 'excluded-non-content-projection',
|
|
contentAuthoritySourceType: 'n/a',
|
|
contentAuthoritySourcePath: 'n/a',
|
|
metadataAuthoritySourceType: 'sidecar',
|
|
metadataAuthoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(2), this.registry[1].columns, runtimeComparisonRows);
|
|
|
|
// Artifact 3: issued artifact provenance
|
|
await this.writeCsvArtifact(artifactPaths.get(3), this.registry[2].columns, provenanceRows);
|
|
|
|
const manifestHelpRow = this.requireRow({
|
|
rows: taskManifestRows,
|
|
predicate: (row) => normalizeValue(row.canonicalId) === 'bmad-help',
|
|
artifactId: 4,
|
|
fieldPath: 'rows[canonicalId=bmad-help]',
|
|
sourcePath: runtimeTaskManifestPath,
|
|
detail: 'Required task-manifest exemplar row is missing',
|
|
});
|
|
const manifestEvidence = this.requireRow({
|
|
rows: provenanceRows,
|
|
predicate: (row) => normalizeValue(row.artifactPath) === runtimeTaskManifestPath && normalizeValue(row.status) === 'PASS',
|
|
artifactId: 4,
|
|
fieldPath: 'rows[artifactPath=_bmad/_config/task-manifest.csv]',
|
|
sourcePath: evidenceArtifactPath,
|
|
detail: 'Required manifest issuing-component binding evidence row is missing',
|
|
});
|
|
|
|
// Artifact 4: manifest comparison
|
|
const manifestComparisonRows = [
|
|
{
|
|
surface: runtimeTaskManifestPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
legacyName: normalizeValue(manifestHelpRow.legacyName || manifestHelpRow.name || 'help'),
|
|
canonicalId: normalizeValue(manifestHelpRow.canonicalId || 'bmad-help'),
|
|
displayName: normalizeValue(manifestHelpRow.displayName || 'help'),
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
authoritySourceType: normalizeValue(manifestHelpRow.authoritySourceType || 'sidecar'),
|
|
authoritySourcePath: normalizeValue(manifestHelpRow.authoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
issuerOwnerClass: 'independent-validator',
|
|
issuingComponent: manifestEvidence.issuingComponent,
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: manifestEvidence.rowIdentity,
|
|
issuingComponentBindingEvidence: manifestEvidence.issuingComponentBindingEvidence,
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(4), this.registry[3].columns, manifestComparisonRows);
|
|
|
|
// Artifact 5: alias table
|
|
const aliasRowsForExemplar = aliasRows
|
|
.filter((row) => normalizeValue(row.canonicalId) === 'bmad-help')
|
|
.map((row) => ({
|
|
rowIdentity: normalizeValue(row.rowIdentity),
|
|
canonicalId: normalizeValue(row.canonicalId),
|
|
alias: normalizeValue(row.alias),
|
|
aliasType: normalizeValue(row.aliasType),
|
|
normalizedAliasValue: normalizeValue(row.normalizedAliasValue),
|
|
rawIdentityHasLeadingSlash: normalizeValue(row.rawIdentityHasLeadingSlash),
|
|
resolutionEligibility: normalizeValue(row.resolutionEligibility),
|
|
authoritySourceType: normalizeValue(row.authoritySourceType || 'sidecar'),
|
|
authoritySourcePath: normalizeValue(row.authoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
status: 'PASS',
|
|
}));
|
|
if (aliasRowsForExemplar.length === 0) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Required canonical alias rows for exemplar are missing',
|
|
artifactId: 5,
|
|
fieldPath: 'rows[canonicalId=bmad-help]',
|
|
sourcePath: runtimeAliasPath,
|
|
observedValue: '<missing>',
|
|
expectedValue: 'required row',
|
|
});
|
|
}
|
|
await this.writeCsvArtifact(artifactPaths.get(5), this.registry[4].columns, aliasRowsForExemplar);
|
|
|
|
// Artifact 6: description provenance
|
|
const moduleHelpRow = this.requireRow({
|
|
rows: moduleHelpRows,
|
|
predicate: (row) => normalizeValue(row.command).replace(/^\/+/, '') === 'bmad-help',
|
|
artifactId: 6,
|
|
fieldPath: 'rows[command=bmad-help]',
|
|
sourcePath: runtimeModuleHelpPath,
|
|
detail: 'Required module-help exemplar command row is missing',
|
|
});
|
|
const helpCatalogRow = this.requireRow({
|
|
rows: helpCatalogRows,
|
|
predicate: (row) => normalizeValue(row.command).replace(/^\/+/, '') === 'bmad-help',
|
|
artifactId: 6,
|
|
fieldPath: 'rows[command=bmad-help]',
|
|
sourcePath: runtimeHelpCatalogPath,
|
|
detail: 'Required merged help-catalog exemplar command row is missing',
|
|
});
|
|
|
|
const descriptionProvenanceRows = [
|
|
{
|
|
surface: runtimeTaskManifestPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
descriptionValue: normalizeValue(manifestHelpRow.description || sidecarMetadata.description),
|
|
expectedDescriptionValue: sidecarMetadata.description,
|
|
descriptionAuthoritySourceType: 'sidecar',
|
|
descriptionAuthoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: manifestEvidence.rowIdentity,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeModuleHelpPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
descriptionValue: normalizeValue(moduleHelpRow.description || sidecarMetadata.description),
|
|
expectedDescriptionValue: sidecarMetadata.description,
|
|
descriptionAuthoritySourceType: 'sidecar',
|
|
descriptionAuthoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: this.requireRow({
|
|
rows: provenanceRows,
|
|
predicate: (row) => normalizeValue(row.artifactPath) === runtimeModuleHelpPath && normalizeValue(row.status) === 'PASS',
|
|
artifactId: 6,
|
|
fieldPath: 'rows[artifactPath=_bmad/core/module-help.csv]',
|
|
sourcePath: evidenceArtifactPath,
|
|
detail: 'Required module-help issuing-component binding evidence row is missing',
|
|
}).rowIdentity,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeHelpCatalogPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
descriptionValue: normalizeValue(helpCatalogRow.description || sidecarMetadata.description),
|
|
expectedDescriptionValue: sidecarMetadata.description,
|
|
descriptionAuthoritySourceType: 'sidecar',
|
|
descriptionAuthoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: this.requireRow({
|
|
rows: provenanceRows,
|
|
predicate: (row) => normalizeValue(row.artifactPath) === runtimeHelpCatalogPath && normalizeValue(row.status) === 'PASS',
|
|
artifactId: 6,
|
|
fieldPath: 'rows[artifactPath=_bmad/_config/bmad-help.csv]',
|
|
sourcePath: evidenceArtifactPath,
|
|
detail: 'Required merged help-catalog issuing-component binding evidence row is missing',
|
|
}).rowIdentity,
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(6), this.registry[5].columns, descriptionProvenanceRows);
|
|
|
|
// Artifact 7: export comparison
|
|
const exportEvidence = evidenceLookup.get(exportSkillPath);
|
|
const exportRowIdentity = normalizeValue(exportEvidence?.rowIdentity || buildIssuedArtifactRowIdentity(exportSkillPath));
|
|
const exportIssuingComponent = normalizeValue(exportEvidence?.issuingComponent || 'not-applicable');
|
|
const exportBindingEvidence = normalizeValue(exportEvidence?.issuingComponentBindingEvidence || '');
|
|
const exportStatus = requireExportSkillProjection || exportSkillProjectionExists ? 'PASS' : 'SKIP';
|
|
const exportSkillFrontmatter = exportSkillProjectionExists ? parseFrontmatter(await fs.readFile(exportSkillAbsolutePath, 'utf8')) : {};
|
|
const codexRecord = codexExportRows.find((row) => normalizeValue(row.canonicalId) === 'bmad-help');
|
|
const exportPath = normalizeValue(codexRecord?.exportPath || exportSkillPath);
|
|
const exportComparisonRows = [
|
|
{
|
|
exportPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
visibleId: normalizeValue(codexRecord?.visibleId || exportSkillFrontmatter.name || sidecarMetadata.canonicalId || 'bmad-help'),
|
|
visibleSurfaceClass: normalizeValue(codexRecord?.visibleSurfaceClass || 'export-id'),
|
|
normalizedVisibleKey: 'export-id:bmad-help',
|
|
authoritySourceType: normalizeValue(codexRecord?.authoritySourceType || 'sidecar'),
|
|
authoritySourcePath: normalizeValue(codexRecord?.authoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
exportIdDerivationSourceType: normalizeValue(codexRecord?.exportIdDerivationSourceType || 'sidecar-canonical-id'),
|
|
exportIdDerivationSourcePath: normalizeValue(codexRecord?.exportIdDerivationSourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
issuerOwnerClass: exportStatus === 'PASS' ? 'independent-validator' : 'not-applicable',
|
|
issuingComponent: exportIssuingComponent,
|
|
issuedArtifactEvidencePath: exportStatus === 'PASS' ? evidenceArtifactPath : 'not-applicable',
|
|
issuedArtifactEvidenceRowIdentity: exportRowIdentity,
|
|
issuingComponentBindingEvidence: exportBindingEvidence,
|
|
status: exportStatus,
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(7), this.registry[6].columns, exportComparisonRows);
|
|
|
|
// Artifact 8: command label report
|
|
const commandLabelRow = this.requireRow({
|
|
rows: commandLabelRows,
|
|
predicate: (row) => normalizeValue(row.canonicalId) === 'bmad-help',
|
|
artifactId: 8,
|
|
fieldPath: 'rows[canonicalId=bmad-help]',
|
|
sourcePath: runtimeCommandLabelPath,
|
|
detail: 'Required command-label report exemplar row is missing',
|
|
});
|
|
const commandLabelEvidence = this.requireRow({
|
|
rows: provenanceRows,
|
|
predicate: (row) => normalizeValue(row.artifactPath) === runtimeHelpCatalogPath && normalizeValue(row.status) === 'PASS',
|
|
artifactId: 8,
|
|
fieldPath: 'rows[artifactPath=_bmad/_config/bmad-help.csv]',
|
|
sourcePath: evidenceArtifactPath,
|
|
detail: 'Required command-label issuing-component binding evidence row is missing',
|
|
});
|
|
const validationCommandLabelRows = [
|
|
{
|
|
surface: runtimeHelpCatalogPath,
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
rawCommandValue: normalizeValue(commandLabelRow.rawCommandValue || 'bmad-help').replace(/^\/+/, ''),
|
|
displayedCommandLabel: normalizeValue(commandLabelRow.displayedCommandLabel || '/bmad-help'),
|
|
normalizedDisplayedLabel: normalizeValue(commandLabelRow.normalizedDisplayedLabel || '/bmad-help'),
|
|
rowCountForCanonicalId: normalizeValue(commandLabelRow.rowCountForCanonicalId || 1),
|
|
authoritySourceType: normalizeValue(commandLabelRow.authoritySourceType || 'sidecar'),
|
|
authoritySourcePath: normalizeValue(commandLabelRow.authoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: commandLabelEvidence.rowIdentity,
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(8), this.registry[7].columns, validationCommandLabelRows);
|
|
|
|
// Artifact 9: catalog pipeline
|
|
const pipelineWithEvidence = pipelineRows
|
|
.filter((row) => normalizeValue(row.canonicalId) === 'bmad-help')
|
|
.map((row) => {
|
|
const artifactPath = normalizeValue(row.artifactPath);
|
|
const evidenceRow = evidenceLookup.get(artifactPath) || null;
|
|
return {
|
|
stage: normalizeValue(row.stage),
|
|
artifactPath,
|
|
rowIdentity: normalizeValue(row.rowIdentity),
|
|
canonicalId: 'bmad-help',
|
|
sourcePath: normalizeValue(row.sourcePath || SOURCE_MARKDOWN_SOURCE_PATH),
|
|
rowCountForStageCanonicalId: normalizeValue(row.rowCountForStageCanonicalId || 1),
|
|
commandValue: normalizeValue(row.commandValue || 'bmad-help'),
|
|
expectedCommandValue: normalizeValue(row.expectedCommandValue || 'bmad-help'),
|
|
descriptionValue: normalizeValue(row.descriptionValue || sidecarMetadata.description),
|
|
expectedDescriptionValue: normalizeValue(row.expectedDescriptionValue || sidecarMetadata.description),
|
|
descriptionAuthoritySourceType: normalizeValue(row.descriptionAuthoritySourceType || 'sidecar'),
|
|
descriptionAuthoritySourcePath: normalizeValue(row.descriptionAuthoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
commandAuthoritySourceType: normalizeValue(row.commandAuthoritySourceType || 'sidecar'),
|
|
commandAuthoritySourcePath: normalizeValue(row.commandAuthoritySourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
|
|
issuerOwnerClass: 'independent-validator',
|
|
issuingComponent: normalizeValue(evidenceRow?.issuingComponent || row.issuingComponent),
|
|
issuedArtifactEvidencePath: evidenceArtifactPath,
|
|
issuedArtifactEvidenceRowIdentity: normalizeValue(evidenceRow?.rowIdentity || ''),
|
|
issuingComponentBindingEvidence: normalizeValue(evidenceRow?.issuingComponentBindingEvidence || ''),
|
|
stageStatus: normalizeValue(row.stageStatus || row.status || 'PASS'),
|
|
status: normalizeValue(row.status || 'PASS'),
|
|
};
|
|
});
|
|
if (pipelineWithEvidence.length === 0) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Required help-catalog pipeline exemplar rows are missing',
|
|
artifactId: 9,
|
|
fieldPath: 'rows[canonicalId=bmad-help]',
|
|
sourcePath: runtimePipelinePath,
|
|
observedValue: '<missing>',
|
|
expectedValue: 'required row',
|
|
});
|
|
}
|
|
await this.writeCsvArtifact(artifactPaths.get(9), this.registry[8].columns, pipelineWithEvidence);
|
|
|
|
// Artifact 10: duplicate report
|
|
const groupedSourcePathSet = `${SIDEcar_AUTHORITY_SOURCE_PATH}|${SOURCE_MARKDOWN_SOURCE_PATH}`;
|
|
const duplicateRows = [
|
|
{
|
|
surface: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
ownerClass: 'bmad-source',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'source-markdown',
|
|
normalizedVisibleKey: 'source-markdown:help',
|
|
authorityRole: 'authoritative',
|
|
authoritySourceType: 'source-markdown',
|
|
authoritySourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:legacy-name',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('help', false, 'alias-row:bmad-help:legacy-name'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
ownerClass: 'bmad-source',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'sidecar',
|
|
normalizedVisibleKey: 'sidecar:bmad-help',
|
|
authorityRole: 'authoritative',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'bmad-help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:canonical-id',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('bmad-help', false, 'alias-row:bmad-help:canonical-id'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeTaskPath,
|
|
ownerClass: 'bmad-generated-runtime',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'runtime-markdown',
|
|
normalizedVisibleKey: 'runtime-markdown:help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'source-markdown',
|
|
authoritySourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:legacy-name',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('help', false, 'alias-row:bmad-help:legacy-name'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeModuleHelpPath,
|
|
ownerClass: 'bmad-generated-runtime',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'bmad-help',
|
|
visibleId: '/bmad-help',
|
|
visibleSurfaceClass: 'module-help-command',
|
|
normalizedVisibleKey: 'module-help-command:/bmad-help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'true',
|
|
preAliasNormalizedValue: 'bmad-help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:slash-command',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('bmad-help', true, 'alias-row:bmad-help:slash-command'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeTaskManifestPath,
|
|
ownerClass: 'bmad-generated-config',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'task-manifest',
|
|
normalizedVisibleKey: 'task-manifest:help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:legacy-name',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('help', false, 'alias-row:bmad-help:legacy-name'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeAliasPath,
|
|
ownerClass: 'bmad-generated-config',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'bmad-help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'canonical-alias-table',
|
|
normalizedVisibleKey: 'canonical-alias-table:bmad-help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'bmad-help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:canonical-id',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('bmad-help', false, 'alias-row:bmad-help:canonical-id'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: runtimeHelpCatalogPath,
|
|
ownerClass: 'bmad-generated-config',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'bmad-help',
|
|
visibleId: '/bmad-help',
|
|
visibleSurfaceClass: 'help-catalog-command',
|
|
normalizedVisibleKey: 'help-catalog-command:/bmad-help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'true',
|
|
preAliasNormalizedValue: 'bmad-help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:slash-command',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('bmad-help', true, 'alias-row:bmad-help:slash-command'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
{
|
|
surface: '.agents/skills/bmad-help/SKILL.md',
|
|
ownerClass: 'bmad-generated-export',
|
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
canonicalId: 'bmad-help',
|
|
normalizedCapabilityKey: 'capability:bmad-help',
|
|
visibleName: 'bmad-help',
|
|
visibleId: 'bmad-help',
|
|
visibleSurfaceClass: 'export-id',
|
|
normalizedVisibleKey: 'export-id:bmad-help',
|
|
authorityRole: 'projected',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
authoritativePresenceKey: 'capability:bmad-help',
|
|
groupedAuthoritativePresenceCount: 1,
|
|
groupedAuthoritativeSourceRecordCount: 2,
|
|
groupedAuthoritativeSourcePathSet: groupedSourcePathSet,
|
|
rawIdentityHasLeadingSlash: 'false',
|
|
preAliasNormalizedValue: 'bmad-help',
|
|
postAliasCanonicalId: 'bmad-help',
|
|
aliasRowLocator: 'alias-row:bmad-help:canonical-id',
|
|
aliasResolutionEvidence: buildAliasResolutionEvidence('bmad-help', false, 'alias-row:bmad-help:canonical-id'),
|
|
aliasResolutionSourcePath: `${runtimeFolder}/_config/canonical-aliases.csv`,
|
|
conflictingProjectedRecordCount: 0,
|
|
wrapperAuthoritativeRecordCount: 0,
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(10), this.registry[9].columns, duplicateRows);
|
|
|
|
// Artifact 11: dependency report
|
|
const dependencyRows = [
|
|
{
|
|
declaredIn: 'sidecar',
|
|
sourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
targetType: 'declaration',
|
|
targetId: '[]',
|
|
normalizedTargetId: '[]',
|
|
expectedOwnerClass: 'none',
|
|
resolutionCandidateCount: 0,
|
|
resolvedOwnerClass: 'none',
|
|
resolvedSurface: 'none',
|
|
resolvedPath: 'none',
|
|
authoritySourceType: 'sidecar',
|
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
failureReason: 'none',
|
|
status: 'PASS',
|
|
},
|
|
];
|
|
await this.writeCsvArtifact(artifactPaths.get(11), this.registry[10].columns, dependencyRows);
|
|
|
|
// Artifact 12: decision record
|
|
const decisionRecord = {
|
|
capability: 'bmad-help',
|
|
goNoGo: 'GO',
|
|
status: 'PASS',
|
|
};
|
|
const decisionRecordContent = `---\n${yaml.stringify(decisionRecord).trimEnd()}\n---\n\n# Help Native Skills Exit\n\nStatus: PASS\n`;
|
|
await fs.writeFile(artifactPaths.get(12), decisionRecordContent, 'utf8');
|
|
|
|
// Fixtures for artifacts 13 and 14
|
|
const fixtures = await this.ensureValidationFixtures(outputPaths, sidecarMetadata);
|
|
|
|
// Artifact 13: sidecar negative validation
|
|
const sidecarNegativeRows = [];
|
|
const sidecarNegativeScenarios = [
|
|
{
|
|
scenario: 'unknown-major-version',
|
|
fixturePath: '_bmad-output/planning-artifacts/validation/help/fixtures/sidecar-negative/unknown-major-version/help.artifact.yaml',
|
|
absolutePath: fixtures.unknownMajorFixturePath,
|
|
expectedFailureCode: HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
|
|
expectedFailureDetail: 'sidecar schema major version is unsupported',
|
|
},
|
|
{
|
|
scenario: 'basename-path-mismatch',
|
|
fixturePath: '_bmad-output/planning-artifacts/validation/help/fixtures/sidecar-negative/basename-path-mismatch/help.artifact.yaml',
|
|
absolutePath: fixtures.basenameMismatchFixturePath,
|
|
expectedFailureCode: HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH,
|
|
expectedFailureDetail: 'sidecar basename does not match sourcePath basename',
|
|
},
|
|
];
|
|
for (const scenario of sidecarNegativeScenarios) {
|
|
const fixtureData = yaml.parse(await fs.readFile(scenario.absolutePath, 'utf8'));
|
|
let observedFailureCode = '';
|
|
let observedFailureDetail = '';
|
|
try {
|
|
await validateHelpSidecarContractFile(scenario.absolutePath, {
|
|
errorSourcePath: scenario.fixturePath,
|
|
});
|
|
} catch (error) {
|
|
observedFailureCode = normalizeValue(error.code);
|
|
observedFailureDetail = normalizeValue(error.detail);
|
|
}
|
|
sidecarNegativeRows.push({
|
|
scenario: scenario.scenario,
|
|
fixturePath: scenario.fixturePath,
|
|
observedSchemaVersion: normalizeValue(fixtureData.schemaVersion),
|
|
observedSourcePathValue: normalizeValue(fixtureData.sourcePath),
|
|
observedSidecarBasename: normalizeValue(path.basename(scenario.absolutePath)),
|
|
expectedFailureCode: scenario.expectedFailureCode,
|
|
observedFailureCode,
|
|
expectedFailureDetail: scenario.expectedFailureDetail,
|
|
observedFailureDetail,
|
|
status:
|
|
observedFailureCode === scenario.expectedFailureCode && observedFailureDetail === scenario.expectedFailureDetail
|
|
? 'PASS'
|
|
: 'FAIL',
|
|
});
|
|
}
|
|
await this.writeCsvArtifact(artifactPaths.get(13), this.registry[12].columns, sidecarNegativeRows);
|
|
|
|
// Artifact 14: frontmatter mismatch validation
|
|
const mismatchRows = [];
|
|
const mismatchScenarios = [
|
|
{
|
|
scenario: 'canonical-id-mismatch',
|
|
fieldPath: 'canonicalId',
|
|
mismatchField: 'canonicalId',
|
|
expectedFailureCode: HELP_FRONTMATTER_MISMATCH_ERROR_CODES.CANONICAL_ID_MISMATCH,
|
|
},
|
|
{
|
|
scenario: 'display-name-mismatch',
|
|
fieldPath: 'name',
|
|
mismatchField: 'displayName',
|
|
expectedFailureCode: HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DISPLAY_NAME_MISMATCH,
|
|
},
|
|
{
|
|
scenario: 'description-mismatch',
|
|
fieldPath: 'description',
|
|
mismatchField: 'description',
|
|
expectedFailureCode: HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DESCRIPTION_MISMATCH,
|
|
},
|
|
{
|
|
scenario: 'dependencies-mismatch',
|
|
fieldPath: 'dependencies.requires',
|
|
mismatchField: 'dependencies.requires',
|
|
expectedFailureCode: HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DEPENDENCIES_REQUIRES_MISMATCH,
|
|
},
|
|
];
|
|
|
|
const makeValidFrontmatterMarkdown = () =>
|
|
`---\n${yaml
|
|
.stringify({
|
|
name: sidecarMetadata.displayName,
|
|
description: sidecarMetadata.description,
|
|
canonicalId: sidecarMetadata.canonicalId,
|
|
dependencies: {
|
|
requires: Array.isArray(sidecarMetadata.dependencies.requires) ? sidecarMetadata.dependencies.requires : [],
|
|
},
|
|
})
|
|
.trimEnd()}\n---\n\n# Valid\n`;
|
|
|
|
const tempValidRuntimePath = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch', 'runtime-valid.md');
|
|
const tempValidSourcePath = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch', 'source-valid.md');
|
|
await fs.writeFile(tempValidRuntimePath, makeValidFrontmatterMarkdown(), 'utf8');
|
|
await fs.writeFile(tempValidSourcePath, makeValidFrontmatterMarkdown(), 'utf8');
|
|
|
|
for (const scope of ['source', 'runtime']) {
|
|
for (const scenario of mismatchScenarios) {
|
|
const fixturePath = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch', scope, `${scenario.scenario}.md`);
|
|
const fixtureRelativePath = `_bmad-output/planning-artifacts/validation/help/fixtures/frontmatter-mismatch/${scope}/${scenario.scenario}.md`;
|
|
let observedFailureCode = '';
|
|
let observedFailureDetail = '';
|
|
let observedFrontmatterValue = '';
|
|
let expectedSidecarValue = '';
|
|
let observedAuthoritativeSourceType = '';
|
|
let observedAuthoritativeSourcePath = '';
|
|
|
|
const parsedFixture = parseFrontmatter(await fs.readFile(fixturePath, 'utf8'));
|
|
if (scenario.fieldPath === 'dependencies.requires') {
|
|
observedFrontmatterValue = normalizeDependencyTargets(parsedFixture.dependencies?.requires);
|
|
expectedSidecarValue = normalizeDependencyTargets(sidecarMetadata.dependencies.requires);
|
|
} else {
|
|
observedFrontmatterValue = normalizeValue(parsedFixture[scenario.fieldPath]);
|
|
if (scenario.fieldPath === 'canonicalId') {
|
|
expectedSidecarValue = sidecarMetadata.canonicalId;
|
|
} else if (scenario.fieldPath === 'name') {
|
|
expectedSidecarValue = sidecarMetadata.displayName;
|
|
} else {
|
|
expectedSidecarValue = sidecarMetadata.description;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await validateHelpAuthoritySplitAndPrecedence({
|
|
sidecarPath: sourcePaths.sidecarPath,
|
|
sourceMarkdownPath: scope === 'source' ? fixturePath : tempValidSourcePath,
|
|
runtimeMarkdownPath: scope === 'runtime' ? fixturePath : tempValidRuntimePath,
|
|
sidecarSourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
sourceMarkdownSourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
|
runtimeMarkdownSourcePath: `${runtimeFolder}/core/tasks/help.md`,
|
|
});
|
|
} catch (error) {
|
|
observedFailureCode = normalizeValue(error.code);
|
|
observedFailureDetail = normalizeValue(error.detail);
|
|
observedAuthoritativeSourceType = 'sidecar';
|
|
observedAuthoritativeSourcePath = SIDEcar_AUTHORITY_SOURCE_PATH;
|
|
}
|
|
|
|
mismatchRows.push({
|
|
scenario: scenario.scenario,
|
|
fixturePath: fixtureRelativePath,
|
|
frontmatterSurfacePath: scope === 'source' ? SOURCE_MARKDOWN_SOURCE_PATH : `${runtimeFolder}/core/tasks/help.md`,
|
|
observedFrontmatterKeyPath: scenario.fieldPath,
|
|
mismatchedField: scenario.mismatchField,
|
|
observedFrontmatterValue,
|
|
expectedSidecarValue,
|
|
expectedAuthoritativeSourceType: 'sidecar',
|
|
expectedAuthoritativeSourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
|
expectedFailureCode: scenario.expectedFailureCode,
|
|
observedFailureCode,
|
|
expectedFailureDetail: FRONTMATTER_MISMATCH_DETAILS[scenario.expectedFailureCode],
|
|
observedFailureDetail,
|
|
observedAuthoritativeSourceType,
|
|
observedAuthoritativeSourcePath,
|
|
status:
|
|
observedFailureCode === scenario.expectedFailureCode &&
|
|
observedFailureDetail === FRONTMATTER_MISMATCH_DETAILS[scenario.expectedFailureCode]
|
|
? 'PASS'
|
|
: 'FAIL',
|
|
});
|
|
}
|
|
}
|
|
await this.writeCsvArtifact(artifactPaths.get(14), this.registry[13].columns, mismatchRows);
|
|
|
|
return {
|
|
projectDir: outputPaths.projectDir,
|
|
planningArtifactsRoot: outputPaths.planningArtifactsRoot,
|
|
validationRoot: outputPaths.validationRoot,
|
|
decisionRecordsRoot: outputPaths.decisionRecordsRoot,
|
|
generatedArtifactCount: this.registry.length,
|
|
artifactPaths: Object.fromEntries([...artifactPaths.entries()].map(([artifactId, artifactPath]) => [artifactId, artifactPath])),
|
|
};
|
|
}
|
|
|
|
parseBindingEvidencePayload({ payloadRaw, artifactId, fieldPath, sourcePath }) {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(String(payloadRaw || ''));
|
|
} catch (error) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: `Binding evidence payload is not valid JSON (${error.message})`,
|
|
artifactId,
|
|
fieldPath,
|
|
sourcePath,
|
|
observedValue: String(payloadRaw || ''),
|
|
expectedValue: 'valid JSON payload',
|
|
});
|
|
}
|
|
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Binding evidence payload must be a JSON object',
|
|
artifactId,
|
|
fieldPath,
|
|
sourcePath,
|
|
observedValue: typeof parsed,
|
|
expectedValue: 'object',
|
|
});
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
validateProvenanceReplayEvidenceRow(row, sourcePath) {
|
|
const artifactId = 3;
|
|
const rowStatus = normalizeValue(row.status || 'PASS');
|
|
const payload = this.parseBindingEvidencePayload({
|
|
payloadRaw: row.issuingComponentBindingEvidence,
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence',
|
|
sourcePath,
|
|
});
|
|
|
|
if (normalizeValue(payload.evidenceVersion) !== '1') {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Binding evidence payload must use evidenceVersion=1',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence.evidenceVersion',
|
|
sourcePath,
|
|
observedValue: normalizeValue(payload.evidenceVersion),
|
|
expectedValue: '1',
|
|
});
|
|
}
|
|
|
|
if (rowStatus === 'SKIP') {
|
|
if (normalizeValue(payload.observationMethod) !== 'validator-observed-optional-surface-omitted') {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Optional-surface provenance rows must use optional-surface evidence method',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence.observationMethod',
|
|
sourcePath,
|
|
observedValue: normalizeValue(payload.observationMethod),
|
|
expectedValue: 'validator-observed-optional-surface-omitted',
|
|
});
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
const requiredPayloadFields = [
|
|
'observationMethod',
|
|
'artifactPath',
|
|
'componentPath',
|
|
'baselineArtifactSha256',
|
|
'mutatedArtifactSha256',
|
|
'baselineRowIdentity',
|
|
'mutatedRowIdentity',
|
|
'targetedRowLocator',
|
|
'rowLevelDiffSha256',
|
|
'perturbationApplied',
|
|
'baselineTargetRowCount',
|
|
'mutatedTargetRowCount',
|
|
];
|
|
for (const key of requiredPayloadFields) {
|
|
if (normalizeValue(payload[key]).length === 0 && payload[key] !== false) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Required binding evidence field is missing',
|
|
artifactId,
|
|
fieldPath: `issuingComponentBindingEvidence.${key}`,
|
|
sourcePath,
|
|
observedValue: '<missing>',
|
|
expectedValue: key,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (
|
|
normalizeValue(payload.observationMethod) !== 'validator-observed-baseline-plus-isolated-single-component-perturbation' ||
|
|
normalizeValue(row.evidenceMethod) !== 'validator-observed-baseline-plus-isolated-single-component-perturbation' ||
|
|
normalizeValue(row.issuingComponentBindingBasis) !== 'validator-observed-baseline-plus-isolated-single-component-perturbation'
|
|
) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Replay evidence must use the baseline-plus-isolated-perturbation method',
|
|
artifactId,
|
|
fieldPath: 'evidenceMethod',
|
|
sourcePath,
|
|
observedValue: normalizeValue(row.evidenceMethod),
|
|
expectedValue: 'validator-observed-baseline-plus-isolated-single-component-perturbation',
|
|
});
|
|
}
|
|
|
|
if (
|
|
normalizeValue(payload.artifactPath) !== normalizeValue(row.artifactPath) ||
|
|
normalizeValue(payload.componentPath) !== normalizeValue(row.issuingComponent) ||
|
|
normalizeValue(payload.baselineRowIdentity) !== normalizeValue(row.rowIdentity) ||
|
|
normalizeValue(payload.mutatedRowIdentity) !== normalizeValue(row.rowIdentity) ||
|
|
normalizeValue(payload.targetedRowLocator) !== normalizeValue(row.rowIdentity)
|
|
) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Binding evidence payload does not match provenance row contract fields',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence',
|
|
sourcePath,
|
|
observedValue: canonicalJsonStringify(payload),
|
|
expectedValue: 'payload fields aligned with provenance row fields',
|
|
});
|
|
}
|
|
|
|
if (!isSha256(payload.baselineArtifactSha256) || !isSha256(payload.mutatedArtifactSha256) || !isSha256(payload.rowLevelDiffSha256)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Replay evidence hashes must be sha256 hex values',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence.*Sha256',
|
|
sourcePath,
|
|
observedValue: canonicalJsonStringify({
|
|
baselineArtifactSha256: payload.baselineArtifactSha256,
|
|
mutatedArtifactSha256: payload.mutatedArtifactSha256,
|
|
rowLevelDiffSha256: payload.rowLevelDiffSha256,
|
|
}),
|
|
expectedValue: '64-char lowercase hex values',
|
|
});
|
|
}
|
|
|
|
if (payload.baselineArtifactSha256 === payload.mutatedArtifactSha256 || payload.perturbationApplied !== true) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Replay evidence must show isolated perturbation impact',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence.perturbationApplied',
|
|
sourcePath,
|
|
observedValue: canonicalJsonStringify({
|
|
perturbationApplied: payload.perturbationApplied,
|
|
baselineArtifactSha256: payload.baselineArtifactSha256,
|
|
mutatedArtifactSha256: payload.mutatedArtifactSha256,
|
|
}),
|
|
expectedValue: 'perturbationApplied=true and differing baseline/mutated hashes',
|
|
});
|
|
}
|
|
|
|
if (Number(payload.baselineTargetRowCount) <= Number(payload.mutatedTargetRowCount)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
|
|
detail: 'Replay evidence must show reduced target-row impact after perturbation',
|
|
artifactId,
|
|
fieldPath: 'issuingComponentBindingEvidence.baselineTargetRowCount',
|
|
sourcePath,
|
|
observedValue: canonicalJsonStringify({
|
|
baselineTargetRowCount: payload.baselineTargetRowCount,
|
|
mutatedTargetRowCount: payload.mutatedTargetRowCount,
|
|
}),
|
|
expectedValue: 'baselineTargetRowCount > mutatedTargetRowCount',
|
|
});
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
assertRequiredEvidenceField({ value, artifactId, fieldPath, sourcePath }) {
|
|
if (normalizeValue(value).length > 0) {
|
|
return;
|
|
}
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING,
|
|
detail: 'Required evidence-link field is missing or empty',
|
|
artifactId,
|
|
fieldPath,
|
|
sourcePath,
|
|
observedValue: normalizeValue(value),
|
|
expectedValue: 'non-empty value',
|
|
});
|
|
}
|
|
|
|
validateEvidenceLinkedRows({ rows, artifactId, sourcePath, evidencePath, provenanceByIdentity, requiredFields, rowArtifactPathField }) {
|
|
for (const [index, row] of rows.entries()) {
|
|
const status = normalizeValue(row.status || row.stageStatus || 'PASS');
|
|
if (status !== 'PASS') continue;
|
|
|
|
for (const field of requiredFields) {
|
|
this.assertRequiredEvidenceField({
|
|
value: row[field],
|
|
artifactId,
|
|
fieldPath: `rows[${index}].${field}`,
|
|
sourcePath,
|
|
});
|
|
}
|
|
|
|
if (normalizeValue(row.issuedArtifactEvidencePath) !== evidencePath) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
|
|
detail: 'Evidence-link path does not point to required provenance artifact',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].issuedArtifactEvidencePath`,
|
|
sourcePath,
|
|
observedValue: normalizeValue(row.issuedArtifactEvidencePath),
|
|
expectedValue: evidencePath,
|
|
});
|
|
}
|
|
|
|
const linkedEvidenceRowIdentity = normalizeValue(row.issuedArtifactEvidenceRowIdentity);
|
|
const provenanceRow = provenanceByIdentity.get(linkedEvidenceRowIdentity);
|
|
if (!provenanceRow) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
|
|
detail: 'Evidence-link row identity does not resolve to provenance artifact row',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`,
|
|
sourcePath,
|
|
observedValue: linkedEvidenceRowIdentity,
|
|
expectedValue: 'existing artifact-3 rowIdentity',
|
|
});
|
|
}
|
|
|
|
if (normalizeValue(provenanceRow.status) !== 'PASS') {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING,
|
|
detail: 'Terminal PASS requires linked provenance rows to be PASS',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`,
|
|
sourcePath,
|
|
observedValue: normalizeValue(provenanceRow.status),
|
|
expectedValue: 'PASS',
|
|
});
|
|
}
|
|
|
|
if (rowArtifactPathField && normalizeValue(row[rowArtifactPathField]) !== normalizeValue(provenanceRow.artifactPath)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
|
|
detail: 'Evidence-linked provenance row does not match claimed artifact path',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].${rowArtifactPathField}`,
|
|
sourcePath,
|
|
observedValue: normalizeValue(row[rowArtifactPathField]),
|
|
expectedValue: normalizeValue(provenanceRow.artifactPath),
|
|
});
|
|
}
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(row, 'issuingComponent') &&
|
|
normalizeValue(row.issuingComponent).length > 0 &&
|
|
normalizeValue(row.issuingComponent) !== normalizeValue(provenanceRow.issuingComponent)
|
|
) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
|
|
detail: 'Issuer component claim diverges from validator-linked provenance evidence',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].issuingComponent`,
|
|
sourcePath,
|
|
observedValue: normalizeValue(row.issuingComponent),
|
|
expectedValue: normalizeValue(provenanceRow.issuingComponent),
|
|
});
|
|
}
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(row, 'issuingComponentBindingEvidence') &&
|
|
normalizeValue(row.issuingComponentBindingEvidence).length > 0 &&
|
|
normalizeValue(row.issuingComponentBindingEvidence) !== normalizeValue(provenanceRow.issuingComponentBindingEvidence)
|
|
) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
|
|
detail: 'Issuer binding evidence claim diverges from validator-linked provenance evidence',
|
|
artifactId,
|
|
fieldPath: `rows[${index}].issuingComponentBindingEvidence`,
|
|
sourcePath,
|
|
observedValue: normalizeValue(row.issuingComponentBindingEvidence),
|
|
expectedValue: normalizeValue(provenanceRow.issuingComponentBindingEvidence),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
validateIssuerPrerequisites({ artifactDataById, runtimeFolder, requireExportSkillProjection }) {
|
|
const evidencePath = '_bmad-output/planning-artifacts/validation/help/bmad-help-issued-artifact-provenance.csv';
|
|
const provenanceArtifact = artifactDataById.get(3) || { rows: [] };
|
|
const provenanceRows = Array.isArray(provenanceArtifact.rows) ? provenanceArtifact.rows : [];
|
|
const provenanceByIdentity = new Map();
|
|
const provenanceByArtifactPath = new Map();
|
|
|
|
for (const [index, row] of provenanceRows.entries()) {
|
|
const sourcePath = normalizePath((provenanceArtifact.relativePath || '').replaceAll('\\', '/'));
|
|
const rowIdentity = normalizeValue(row.rowIdentity);
|
|
this.assertRequiredEvidenceField({
|
|
value: rowIdentity,
|
|
artifactId: 3,
|
|
fieldPath: `rows[${index}].rowIdentity`,
|
|
sourcePath,
|
|
});
|
|
this.validateProvenanceReplayEvidenceRow(row, sourcePath);
|
|
provenanceByIdentity.set(rowIdentity, row);
|
|
provenanceByArtifactPath.set(normalizeValue(row.artifactPath), row);
|
|
}
|
|
|
|
const requiredProvenanceArtifactPaths = [
|
|
`${runtimeFolder}/_config/task-manifest.csv`,
|
|
`${runtimeFolder}/core/module-help.csv`,
|
|
`${runtimeFolder}/_config/bmad-help.csv`,
|
|
];
|
|
if (requireExportSkillProjection) {
|
|
requiredProvenanceArtifactPaths.push('.agents/skills/bmad-help/SKILL.md');
|
|
}
|
|
|
|
for (const artifactPath of requiredProvenanceArtifactPaths) {
|
|
const row = provenanceByArtifactPath.get(artifactPath);
|
|
if (!row || normalizeValue(row.status) !== 'PASS') {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING,
|
|
detail: 'Terminal PASS requires provenance prerequisite rows for all required issuing-component claims',
|
|
artifactId: 3,
|
|
fieldPath: `rows[artifactPath=${artifactPath}]`,
|
|
sourcePath: normalizePath(provenanceArtifact.relativePath),
|
|
observedValue: row ? normalizeValue(row.status) : '<missing>',
|
|
expectedValue: 'PASS',
|
|
});
|
|
}
|
|
}
|
|
|
|
const artifact4 = artifactDataById.get(4) || { rows: [], relativePath: '' };
|
|
this.validateEvidenceLinkedRows({
|
|
rows: artifact4.rows || [],
|
|
artifactId: 4,
|
|
sourcePath: normalizePath(artifact4.relativePath),
|
|
evidencePath,
|
|
provenanceByIdentity,
|
|
requiredFields: ['issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity', 'issuingComponentBindingEvidence'],
|
|
});
|
|
|
|
const artifact6 = artifactDataById.get(6) || { rows: [], relativePath: '' };
|
|
this.validateEvidenceLinkedRows({
|
|
rows: artifact6.rows || [],
|
|
artifactId: 6,
|
|
sourcePath: normalizePath(artifact6.relativePath),
|
|
evidencePath,
|
|
provenanceByIdentity,
|
|
requiredFields: ['issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity'],
|
|
});
|
|
|
|
const artifact7 = artifactDataById.get(7) || { rows: [], relativePath: '' };
|
|
this.validateEvidenceLinkedRows({
|
|
rows: artifact7.rows || [],
|
|
artifactId: 7,
|
|
sourcePath: normalizePath(artifact7.relativePath),
|
|
evidencePath,
|
|
provenanceByIdentity,
|
|
requiredFields: ['issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity', 'issuingComponentBindingEvidence'],
|
|
});
|
|
|
|
const artifact8 = artifactDataById.get(8) || { rows: [], relativePath: '' };
|
|
this.validateEvidenceLinkedRows({
|
|
rows: artifact8.rows || [],
|
|
artifactId: 8,
|
|
sourcePath: normalizePath(artifact8.relativePath),
|
|
evidencePath,
|
|
provenanceByIdentity,
|
|
requiredFields: ['issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity'],
|
|
});
|
|
|
|
const artifact9 = artifactDataById.get(9) || { rows: [], relativePath: '' };
|
|
this.validateEvidenceLinkedRows({
|
|
rows: artifact9.rows || [],
|
|
artifactId: 9,
|
|
sourcePath: normalizePath(artifact9.relativePath),
|
|
evidencePath,
|
|
provenanceByIdentity,
|
|
requiredFields: [
|
|
'issuedArtifactEvidencePath',
|
|
'issuedArtifactEvidenceRowIdentity',
|
|
'issuingComponentBindingEvidence',
|
|
'issuingComponent',
|
|
],
|
|
rowArtifactPathField: 'artifactPath',
|
|
});
|
|
}
|
|
|
|
inferRequireExportSkillProjection({ artifactDataById, optionsRequireExportSkillProjection }) {
|
|
if (typeof optionsRequireExportSkillProjection === 'boolean') {
|
|
return optionsRequireExportSkillProjection;
|
|
}
|
|
|
|
const exportSurfacePath = '.agents/skills/bmad-help/SKILL.md';
|
|
const provenanceArtifact = artifactDataById.get(3) || { rows: [] };
|
|
const provenanceRows = Array.isArray(provenanceArtifact.rows) ? provenanceArtifact.rows : [];
|
|
const exportProvenanceRow = provenanceRows.find((row) => normalizeValue(row.artifactPath) === exportSurfacePath);
|
|
if (exportProvenanceRow) {
|
|
return normalizeValue(exportProvenanceRow.status) === 'PASS';
|
|
}
|
|
|
|
const exportArtifact = artifactDataById.get(7) || { rows: [] };
|
|
const exportRows = Array.isArray(exportArtifact.rows) ? exportArtifact.rows : [];
|
|
if (exportRows.length > 0) {
|
|
return exportRows.some((row) => {
|
|
const status = normalizeValue(row.status || row.stageStatus || '');
|
|
return status === 'PASS';
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async validateGeneratedArtifacts(options = {}) {
|
|
const outputPaths = this.resolveOutputPaths(options);
|
|
const planningArtifactsRoot = outputPaths.planningArtifactsRoot;
|
|
const artifactDataById = new Map();
|
|
|
|
for (const artifact of this.registry) {
|
|
const artifactPath = path.join(planningArtifactsRoot, artifact.relativePath);
|
|
if (!(await fs.pathExists(artifactPath))) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
|
|
detail: 'Required help validation artifact is missing',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: '<file>',
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: normalizePath(artifact.relativePath),
|
|
});
|
|
}
|
|
|
|
switch (artifact.type) {
|
|
case 'csv': {
|
|
const content = await fs.readFile(artifactPath, 'utf8');
|
|
const observedHeader = parseCsvHeader(content);
|
|
const expectedHeader = artifact.columns || [];
|
|
const rows = parseCsvRows(content);
|
|
artifactDataById.set(artifact.artifactId, {
|
|
type: 'csv',
|
|
relativePath: artifact.relativePath,
|
|
header: observedHeader,
|
|
rows,
|
|
});
|
|
|
|
if (observedHeader.length !== expectedHeader.length) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
|
|
detail: 'CSV header length does not match required schema',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: '<header>',
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: observedHeader.join(','),
|
|
expectedValue: expectedHeader.join(','),
|
|
});
|
|
}
|
|
|
|
for (const [index, expectedValue] of expectedHeader.entries()) {
|
|
const observed = normalizeValue(observedHeader[index]);
|
|
const expected = normalizeValue(expectedValue);
|
|
if (observed !== expected) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
|
|
detail: 'CSV header ordering does not match required schema',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: `header[${index}]`,
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: observed,
|
|
expectedValue: expected,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(artifact.requiredRowIdentityFields) && artifact.requiredRowIdentityFields.length > 0) {
|
|
if (rows.length === 0) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: 'Required row identity rows are missing',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: 'rows',
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: '<empty>',
|
|
expectedValue: 'at least one row',
|
|
});
|
|
}
|
|
for (const field of artifact.requiredRowIdentityFields) {
|
|
if (!expectedHeader.includes(field)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
|
|
detail: 'Required row identity field is missing from artifact schema',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: `header.${field}`,
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: field,
|
|
});
|
|
}
|
|
|
|
for (const [rowIndex, row] of rows.entries()) {
|
|
if (normalizeValue(row[field]).length === 0) {
|
|
const isEvidenceLinkField = field === 'issuedArtifactEvidenceRowIdentity';
|
|
throw new HelpValidationHarnessError({
|
|
code: isEvidenceLinkField
|
|
? HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING
|
|
: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
|
|
detail: isEvidenceLinkField
|
|
? 'Required evidence-link row identity is missing or empty'
|
|
: 'Required row identity value is missing or empty',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: `rows[${rowIndex}].${field}`,
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: normalizeValue(row[field]),
|
|
expectedValue: 'non-empty value',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'yaml': {
|
|
const parsed = yaml.parse(await fs.readFile(artifactPath, 'utf8'));
|
|
artifactDataById.set(artifact.artifactId, {
|
|
type: 'yaml',
|
|
relativePath: artifact.relativePath,
|
|
parsed,
|
|
});
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
|
|
detail: 'YAML artifact root must be a mapping object',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: '<document>',
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: typeof parsed,
|
|
expectedValue: 'object',
|
|
});
|
|
}
|
|
for (const requiredKey of artifact.requiredTopLevelKeys || []) {
|
|
if (!Object.prototype.hasOwnProperty.call(parsed, requiredKey)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
|
|
detail: 'Required YAML key is missing',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: requiredKey,
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: requiredKey,
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'markdown': {
|
|
const content = await fs.readFile(artifactPath, 'utf8');
|
|
artifactDataById.set(artifact.artifactId, {
|
|
type: 'markdown',
|
|
relativePath: artifact.relativePath,
|
|
content,
|
|
});
|
|
let frontmatter;
|
|
try {
|
|
frontmatter = parseFrontmatter(content);
|
|
} catch (error) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.DECISION_RECORD_PARSE_FAILED,
|
|
detail: `Unable to parse decision record frontmatter (${error.message})`,
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: '<frontmatter>',
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
});
|
|
}
|
|
for (const requiredKey of artifact.requiredFrontmatterKeys || []) {
|
|
if (!Object.prototype.hasOwnProperty.call(frontmatter, requiredKey)) {
|
|
throw new HelpValidationHarnessError({
|
|
code: HELP_VALIDATION_ERROR_CODES.DECISION_RECORD_SCHEMA_MISMATCH,
|
|
detail: 'Required decision-record key is missing',
|
|
artifactId: artifact.artifactId,
|
|
fieldPath: requiredKey,
|
|
sourcePath: normalizePath(artifact.relativePath),
|
|
observedValue: '<missing>',
|
|
expectedValue: requiredKey,
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const inferredRequireExportSkillProjection = this.inferRequireExportSkillProjection({
|
|
artifactDataById,
|
|
optionsRequireExportSkillProjection: options.requireExportSkillProjection,
|
|
});
|
|
|
|
this.validateIssuerPrerequisites({
|
|
artifactDataById,
|
|
runtimeFolder: normalizeValue(options.bmadFolderName || '_bmad'),
|
|
requireExportSkillProjection: inferredRequireExportSkillProjection,
|
|
});
|
|
|
|
return {
|
|
status: 'PASS',
|
|
validatedArtifactCount: this.registry.length,
|
|
};
|
|
}
|
|
|
|
async generateAndValidate(options = {}) {
|
|
const generated = await this.generateValidationArtifacts(options);
|
|
const validation = await this.validateGeneratedArtifacts(options);
|
|
return {
|
|
...generated,
|
|
terminalStatus: validation.status,
|
|
validatedArtifactCount: validation.validatedArtifactCount,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
HELP_VALIDATION_ERROR_CODES,
|
|
HELP_VALIDATION_ARTIFACT_REGISTRY,
|
|
HelpValidationHarnessError,
|
|
HelpValidationHarness,
|
|
};
|