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: '', sourcePath: normalizePath(sourcePath), observedValue: '', 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: '', 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: '', 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: '', 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: '', 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) : '', 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: '', sourcePath: normalizePath(artifact.relativePath), observedValue: '', 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: '
', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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, };