refactor(installer): replace stage labels with capability-based shard-doc/help naming

This commit is contained in:
Dicky Moore 2026-03-04 16:53:06 +00:00
parent 96528d9bd3
commit a770fa5808
9 changed files with 314 additions and 315 deletions

View File

@ -61,9 +61,9 @@ const {
const { const {
PROJECTION_COMPATIBILITY_ERROR_CODES, PROJECTION_COMPATIBILITY_ERROR_CODES,
TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS,
TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS, TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS,
HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS,
HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS, HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS,
validateTaskManifestCompatibilitySurface, validateTaskManifestCompatibilitySurface,
validateTaskManifestLoaderEntries, validateTaskManifestLoaderEntries,
validateHelpCatalogCompatibilitySurface, validateHelpCatalogCompatibilitySurface,
@ -72,15 +72,15 @@ const {
validateCommandDocSurfaceConsistency, validateCommandDocSurfaceConsistency,
} = require('../tools/cli/installers/lib/core/projection-compatibility-validator'); } = require('../tools/cli/installers/lib/core/projection-compatibility-validator');
const { const {
WAVE1_VALIDATION_ERROR_CODES, HELP_VALIDATION_ERROR_CODES,
WAVE1_VALIDATION_ARTIFACT_REGISTRY, HELP_VALIDATION_ARTIFACT_REGISTRY,
Wave1ValidationHarness, HelpValidationHarness,
} = require('../tools/cli/installers/lib/core/wave-1-validation-harness'); } = require('../tools/cli/installers/lib/core/help-validation-harness');
const { const {
WAVE2_VALIDATION_ERROR_CODES, SHARD_DOC_VALIDATION_ERROR_CODES,
WAVE2_VALIDATION_ARTIFACT_REGISTRY, SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY,
Wave2ValidationHarness, ShardDocValidationHarness,
} = require('../tools/cli/installers/lib/core/wave-2-validation-harness'); } = require('../tools/cli/installers/lib/core/shard-doc-validation-harness');
// ANSI colors // ANSI colors
const colors = { const colors = {
@ -399,9 +399,9 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test 4b: Wave-2 shard-doc Sidecar Contract Validation // Test 4b: Shard-doc Sidecar Contract Validation
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 4b: Wave-2 shard-doc Sidecar Contract Validation${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 4b: Shard-doc Sidecar Contract Validation${colors.reset}\n`);
const validShardDocSidecar = { const validShardDocSidecar = {
schemaVersion: 1, schemaVersion: 1,
@ -416,7 +416,7 @@ async function runTests() {
}, },
}; };
const shardDocFixtureRoot = path.join(projectRoot, 'test', 'fixtures', 'wave-2', 'sidecar-negative'); const shardDocFixtureRoot = path.join(projectRoot, 'test', 'fixtures', 'shard-doc', 'sidecar-negative');
const unknownMajorFixturePath = path.join(shardDocFixtureRoot, 'unknown-major-version', 'shard-doc.artifact.yaml'); const unknownMajorFixturePath = path.join(shardDocFixtureRoot, 'unknown-major-version', 'shard-doc.artifact.yaml');
const basenameMismatchFixturePath = path.join(shardDocFixtureRoot, 'basename-path-mismatch', 'shard-doc.artifact.yaml'); const basenameMismatchFixturePath = path.join(shardDocFixtureRoot, 'basename-path-mismatch', 'shard-doc.artifact.yaml');
@ -587,7 +587,7 @@ async function runTests() {
'Shard-doc non-empty dependencies.requires', 'Shard-doc non-empty dependencies.requires',
); );
} catch (error) { } catch (error) {
assert(false, 'Wave-2 shard-doc sidecar validation suite setup', error.message); assert(false, 'Shard-doc sidecar validation suite setup', error.message);
} finally { } finally {
await fs.remove(tempShardDocRoot); await fs.remove(tempShardDocRoot);
} }
@ -1109,7 +1109,7 @@ async function runTests() {
} }
} }
// 6b: Shard-doc fail-fast covers Wave-2 negative matrix classes. // 6b: Shard-doc fail-fast covers Shard-doc negative matrix classes.
{ {
const deterministicShardDocFailFastSourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; const deterministicShardDocFailFastSourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml';
const shardDocFailureScenarios = [ const shardDocFailureScenarios = [
@ -1967,7 +1967,7 @@ async function runTests() {
assert( assert(
writtenTaskManifestLines[0] === expectedHeader, writtenTaskManifestLines[0] === expectedHeader,
'Task manifest writes compatibility-prefix columns with locked wave-1 appended column order', 'Task manifest writes compatibility-prefix columns with locked canonical appended column order',
); );
const writtenTaskManifestRecords = csv.parse(writtenTaskManifestRaw, { const writtenTaskManifestRecords = csv.parse(writtenTaskManifestRaw, {
@ -3147,7 +3147,7 @@ async function runTests() {
const taskManifestColumns = [ const taskManifestColumns = [
...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS,
...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS,
'futureAdditiveField', 'futureAdditiveField',
]; ];
const validTaskRows = [ const validTaskRows = [
@ -3162,7 +3162,7 @@ async function runTests() {
canonicalId: 'bmad-help', canonicalId: 'bmad-help',
authoritySourceType: 'sidecar', authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
futureAdditiveField: 'wave-1', futureAdditiveField: 'canonical-additive',
}, },
{ {
name: 'create-story', name: 'create-story',
@ -3175,7 +3175,7 @@ async function runTests() {
canonicalId: '', canonicalId: '',
authoritySourceType: '', authoritySourceType: '',
authoritySourcePath: '', authoritySourcePath: '',
futureAdditiveField: 'wave-1', futureAdditiveField: 'canonical-additive',
}, },
]; ];
const validTaskManifestCsv = const validTaskManifestCsv =
@ -3188,11 +3188,11 @@ async function runTests() {
assert( assert(
validatedTaskSurface.headerColumns[0] === 'name' && validatedTaskSurface.headerColumns[0] === 'name' &&
validatedTaskSurface.headerColumns[TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length] === 'legacyName', validatedTaskSurface.headerColumns[TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length] === 'legacyName',
'Task-manifest compatibility validator enforces locked prefix plus additive wave-1 ordering', 'Task-manifest compatibility validator enforces locked prefix plus additive canonical ordering',
); );
assert( assert(
validatedTaskSurface.headerColumns.at(-1) === 'futureAdditiveField', validatedTaskSurface.headerColumns.at(-1) === 'futureAdditiveField',
'Task-manifest compatibility validator allows additive columns appended after locked wave-1 columns', 'Task-manifest compatibility validator allows additive columns appended after locked canonical columns',
); );
validateTaskManifestLoaderEntries(validatedTaskSurface.rows, { validateTaskManifestLoaderEntries(validatedTaskSurface.rows, {
@ -3229,8 +3229,8 @@ async function runTests() {
assert(false, 'Task-manifest strict validator rejects legacy prefix-only header without migration mode'); assert(false, 'Task-manifest strict validator rejects legacy prefix-only header without migration mode');
} catch (error) { } catch (error) {
assert( assert(
error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_WAVE1_MISMATCH, error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_CANONICAL_MISMATCH,
'Task-manifest strict validator emits deterministic wave-1 mismatch error for legacy prefix-only headers', 'Task-manifest strict validator emits deterministic canonical mismatch error for legacy prefix-only headers',
); );
} }
@ -3252,7 +3252,7 @@ async function runTests() {
const helpCatalogColumns = [ const helpCatalogColumns = [
...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS,
...HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS,
'futureAdditiveField', 'futureAdditiveField',
]; ];
const validHelpRows = [ const validHelpRows = [
@ -3273,7 +3273,7 @@ async function runTests() {
description: 'Help command', description: 'Help command',
'output-location': '', 'output-location': '',
outputs: '', outputs: '',
futureAdditiveField: 'wave-1', futureAdditiveField: 'canonical-additive',
}, },
{ {
module: 'core', module: 'core',
@ -3292,7 +3292,7 @@ async function runTests() {
description: 'Shard document command', description: 'Shard document command',
'output-location': '', 'output-location': '',
outputs: '', outputs: '',
futureAdditiveField: 'wave-1', futureAdditiveField: 'canonical-additive',
}, },
{ {
module: 'bmm', module: 'bmm',
@ -3311,7 +3311,7 @@ async function runTests() {
description: 'Create next story', description: 'Create next story',
'output-location': '', 'output-location': '',
outputs: '', outputs: '',
futureAdditiveField: 'wave-1', futureAdditiveField: 'canonical-additive',
}, },
]; ];
const validHelpCatalogCsv = const validHelpCatalogCsv =
@ -3327,7 +3327,7 @@ async function runTests() {
); );
assert( assert(
validatedHelpSurface.headerColumns.at(-1) === 'futureAdditiveField', validatedHelpSurface.headerColumns.at(-1) === 'futureAdditiveField',
'Help-catalog compatibility validator allows additive columns appended after locked wave-1 columns', 'Help-catalog compatibility validator allows additive columns appended after locked canonical columns',
); );
validateHelpCatalogLoaderEntries(validatedHelpSurface.rows, { validateHelpCatalogLoaderEntries(validatedHelpSurface.rows, {
@ -3438,7 +3438,7 @@ async function runTests() {
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 14: Deterministic Validation Artifact Suite${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 14: Deterministic Validation Artifact Suite${colors.reset}\n`);
const tempValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-wave1-validation-suite-')); const tempValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-validation-suite-'));
try { try {
const tempProjectRoot = tempValidationHarnessRoot; const tempProjectRoot = tempValidationHarnessRoot;
const tempBmadDir = path.join(tempProjectRoot, '_bmad'); const tempBmadDir = path.join(tempProjectRoot, '_bmad');
@ -3509,7 +3509,7 @@ async function runTests() {
await writeCsv( await writeCsv(
path.join(tempConfigDir, 'task-manifest.csv'), path.join(tempConfigDir, 'task-manifest.csv'),
[...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS], [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS],
[ [
{ {
name: 'help', name: 'help',
@ -3576,7 +3576,7 @@ async function runTests() {
); );
await writeCsv( await writeCsv(
path.join(tempConfigDir, 'bmad-help.csv'), path.join(tempConfigDir, 'bmad-help.csv'),
[...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS], [...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS],
[ [
{ {
module: 'core', module: 'core',
@ -3764,7 +3764,7 @@ async function runTests() {
], ],
); );
const harness = new Wave1ValidationHarness(); const harness = new HelpValidationHarness();
const firstRun = await harness.generateAndValidate({ const firstRun = await harness.generateAndValidate({
projectDir: tempProjectRoot, projectDir: tempProjectRoot,
bmadDir: tempBmadDir, bmadDir: tempBmadDir,
@ -3773,18 +3773,18 @@ async function runTests() {
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
}); });
assert( assert(
firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === WAVE1_VALIDATION_ARTIFACT_REGISTRY.length, firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === HELP_VALIDATION_ARTIFACT_REGISTRY.length,
'Wave-1 validation harness generates and validates all required artifacts', 'Help validation harness generates and validates all required artifacts',
); );
const artifactPathsById = new Map( const artifactPathsById = new Map(
WAVE1_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [ HELP_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [
artifact.artifactId, artifact.artifactId,
path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath), path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath),
]), ]),
); );
for (const [artifactId, artifactPath] of artifactPathsById.entries()) { for (const [artifactId, artifactPath] of artifactPathsById.entries()) {
assert(await fs.pathExists(artifactPath), `Wave-1 validation harness outputs artifact ${artifactId}`); assert(await fs.pathExists(artifactPath), `Help validation harness outputs artifact ${artifactId}`);
} }
const artifactThreeBaselineRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), { const artifactThreeBaselineRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), {
@ -3810,7 +3810,7 @@ async function runTests() {
manifestReplayEvidence.perturbationApplied === true && manifestReplayEvidence.perturbationApplied === true &&
Number(manifestReplayEvidence.baselineTargetRowCount) > Number(manifestReplayEvidence.mutatedTargetRowCount) && Number(manifestReplayEvidence.baselineTargetRowCount) > Number(manifestReplayEvidence.mutatedTargetRowCount) &&
manifestReplayEvidence.targetedRowLocator === manifestProvenanceRow.rowIdentity, manifestReplayEvidence.targetedRowLocator === manifestProvenanceRow.rowIdentity,
'Wave-1 validation harness emits validator-observed replay evidence with baseline/perturbation impact', 'Help validation harness emits validator-observed replay evidence with baseline/perturbation impact',
); );
const firstArtifactContents = new Map(); const firstArtifactContents = new Map();
@ -3834,18 +3834,18 @@ async function runTests() {
break; break;
} }
} }
assert(deterministicOutputs, 'Wave-1 validation harness outputs are byte-stable across unchanged repeated runs'); assert(deterministicOutputs, 'Help validation harness outputs are byte-stable across unchanged repeated runs');
await fs.remove(path.join(tempSkillDir, 'SKILL.md')); await fs.remove(path.join(tempSkillDir, 'SKILL.md'));
const noIdeInstaller = new Installer(); const noIdeInstaller = new Installer();
noIdeInstaller.codexExportDerivationRecords = []; noIdeInstaller.codexExportDerivationRecords = [];
const noIdeValidationOptions = await noIdeInstaller.buildWave1ValidationOptions({ const noIdeValidationOptions = await noIdeInstaller.buildHelpValidationOptions({
projectDir: tempProjectRoot, projectDir: tempProjectRoot,
bmadDir: tempBmadDir, bmadDir: tempBmadDir,
}); });
assert( assert(
noIdeValidationOptions.requireExportSkillProjection === false, noIdeValidationOptions.requireExportSkillProjection === false,
'Installer wave-1 validation options disable export-surface requirement for no-IDE/non-Codex flow', 'Installer help validation options disable export-surface requirement for no-IDE/non-Codex flow',
); );
const noIdeRun = await harness.generateAndValidate({ const noIdeRun = await harness.generateAndValidate({
...noIdeValidationOptions, ...noIdeValidationOptions,
@ -3854,7 +3854,7 @@ async function runTests() {
}); });
assert( assert(
noIdeRun.terminalStatus === 'PASS', noIdeRun.terminalStatus === 'PASS',
'Wave-1 validation harness remains terminal-PASS for no-IDE/non-Codex flow when core projection surfaces are present', 'Help validation harness remains terminal-PASS for no-IDE/non-Codex flow when core projection surfaces are present',
); );
const noIdeStandaloneValidation = await harness.validateGeneratedArtifacts({ const noIdeStandaloneValidation = await harness.validateGeneratedArtifacts({
projectDir: tempProjectRoot, projectDir: tempProjectRoot,
@ -3862,7 +3862,7 @@ async function runTests() {
}); });
assert( assert(
noIdeStandaloneValidation.status === 'PASS', noIdeStandaloneValidation.status === 'PASS',
'Wave-1 validation harness infers no-IDE export prerequisite context during standalone validation when options are omitted', 'Help validation harness infers no-IDE export prerequisite context during standalone validation when options are omitted',
); );
try { try {
await harness.buildObservedBindingEvidence({ await harness.buildObservedBindingEvidence({
@ -3873,11 +3873,11 @@ async function runTests() {
optionalSurface: false, optionalSurface: false,
runtimeFolder: '_bmad', runtimeFolder: '_bmad',
}); });
assert(false, 'Wave-1 replay evidence generation rejects unmapped claimed rowIdentity'); assert(false, 'Help replay evidence generation rejects unmapped claimed rowIdentity');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 replay evidence generation emits deterministic missing-claimed-rowIdentity error code', 'Help replay evidence generation emits deterministic missing-claimed-rowIdentity error code',
); );
} }
await fs.writeFile( await fs.writeFile(
@ -3895,16 +3895,16 @@ async function runTests() {
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
}); });
assert(false, 'Wave-1 validation harness fails when required projection input surfaces are missing'); assert(false, 'Help validation harness fails when required projection input surfaces are missing');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-1 validation harness emits deterministic missing-input-surface error code', 'Help validation harness emits deterministic missing-input-surface error code',
); );
} }
await writeCsv( await writeCsv(
path.join(tempConfigDir, 'task-manifest.csv'), path.join(tempConfigDir, 'task-manifest.csv'),
[...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS], [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS],
[ [
{ {
name: 'help', name: 'help',
@ -3931,11 +3931,11 @@ async function runTests() {
await fs.remove(artifactPathsById.get(14)); await fs.remove(artifactPathsById.get(14));
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness fails when a required artifact is missing'); assert(false, 'Help validation harness fails when a required artifact is missing');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-1 validation harness emits deterministic missing-artifact error code', 'Help validation harness emits deterministic missing-artifact error code',
); );
} }
@ -3954,11 +3954,11 @@ async function runTests() {
await fs.writeFile(artifactTwoPath, artifactTwoLines.join('\n'), 'utf8'); await fs.writeFile(artifactTwoPath, artifactTwoLines.join('\n'), 'utf8');
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects schema/header drift'); assert(false, 'Help validation harness rejects schema/header drift');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, error.code === HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
'Wave-1 validation harness emits deterministic schema-mismatch error code', 'Help validation harness emits deterministic schema-mismatch error code',
); );
} }
@ -3975,11 +3975,11 @@ async function runTests() {
await fs.writeFile(artifactNinePath, `${artifactNineHeader}\n`, 'utf8'); await fs.writeFile(artifactNinePath, `${artifactNineHeader}\n`, 'utf8');
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects header-only required-identity artifacts'); assert(false, 'Help validation harness rejects header-only required-identity artifacts');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 validation harness emits deterministic missing-row error code for header-only artifacts', 'Help validation harness emits deterministic missing-row error code for header-only artifacts',
); );
} }
@ -4017,11 +4017,11 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects missing required row identity values'); assert(false, 'Help validation harness rejects missing required row identity values');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 validation harness emits deterministic row-identity error code', 'Help validation harness emits deterministic row-identity error code',
); );
} }
@ -4061,11 +4061,11 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects PASS rows missing required evidence-link fields'); assert(false, 'Help validation harness rejects PASS rows missing required evidence-link fields');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING, error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING,
'Wave-1 validation harness emits deterministic evidence-link error code for missing row identity link', 'Help validation harness emits deterministic evidence-link error code for missing row identity link',
); );
} }
@ -4111,11 +4111,11 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects self-attested issuer claims that diverge from validator evidence'); assert(false, 'Help validation harness rejects self-attested issuer claims that diverge from validator evidence');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM, error.code === HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
'Wave-1 validation harness emits deterministic self-attested issuer-claim rejection code', 'Help validation harness emits deterministic self-attested issuer-claim rejection code',
); );
} }
@ -4151,11 +4151,11 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects malformed replay-evidence payloads'); assert(false, 'Help validation harness rejects malformed replay-evidence payloads');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, error.code === HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
'Wave-1 validation harness emits deterministic replay-evidence validation error code', 'Help validation harness emits deterministic replay-evidence validation error code',
); );
} }
} catch (error) { } catch (error) {
@ -4167,13 +4167,13 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test 15: Wave-2 shard-doc Validation Artifact Suite // Test 15: Shard-doc Validation Artifact Suite
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 15: Wave-2 shard-doc Validation Artifact Suite${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 15: Shard-doc Validation Artifact Suite${colors.reset}\n`);
const tempWave2ValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-wave2-validation-suite-')); const tempShardDocValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-shard-doc-validation-suite-'));
try { try {
const tempProjectRoot = tempWave2ValidationHarnessRoot; const tempProjectRoot = tempShardDocValidationHarnessRoot;
const tempBmadDir = path.join(tempProjectRoot, '_bmad'); const tempBmadDir = path.join(tempProjectRoot, '_bmad');
const tempConfigDir = path.join(tempBmadDir, '_config'); const tempConfigDir = path.join(tempBmadDir, '_config');
const tempSourceTasksDir = path.join(tempProjectRoot, 'bmad-fork', 'src', 'core', 'tasks'); const tempSourceTasksDir = path.join(tempProjectRoot, 'bmad-fork', 'src', 'core', 'tasks');
@ -4245,7 +4245,7 @@ async function runTests() {
await writeCsv( await writeCsv(
path.join(tempConfigDir, 'task-manifest.csv'), path.join(tempConfigDir, 'task-manifest.csv'),
[...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS], [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS],
[ [
{ {
name: 'shard-doc', name: 'shard-doc',
@ -4263,7 +4263,7 @@ async function runTests() {
); );
await writeCsv( await writeCsv(
path.join(tempConfigDir, 'bmad-help.csv'), path.join(tempConfigDir, 'bmad-help.csv'),
[...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS], [...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS],
[ [
{ {
module: 'core', module: 'core',
@ -4353,7 +4353,7 @@ async function runTests() {
}, },
]; ];
const harness = new Wave2ValidationHarness(); const harness = new ShardDocValidationHarness();
const firstRun = await harness.generateAndValidate({ const firstRun = await harness.generateAndValidate({
projectDir: tempProjectRoot, projectDir: tempProjectRoot,
bmadDir: tempBmadDir, bmadDir: tempBmadDir,
@ -4361,18 +4361,18 @@ async function runTests() {
shardDocAuthorityRecords: authorityRecords, shardDocAuthorityRecords: authorityRecords,
}); });
assert( assert(
firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === WAVE2_VALIDATION_ARTIFACT_REGISTRY.length, firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY.length,
'Wave-2 validation harness generates and validates all required artifacts', 'Shard-doc validation harness generates and validates all required artifacts',
); );
const artifactPathsById = new Map( const artifactPathsById = new Map(
WAVE2_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [ SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [
artifact.artifactId, artifact.artifactId,
path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath), path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath),
]), ]),
); );
for (const [artifactId, artifactPath] of artifactPathsById.entries()) { for (const [artifactId, artifactPath] of artifactPathsById.entries()) {
assert(await fs.pathExists(artifactPath), `Wave-2 validation harness outputs artifact ${artifactId}`); assert(await fs.pathExists(artifactPath), `Shard-doc validation harness outputs artifact ${artifactId}`);
} }
const firstArtifactContents = new Map(); const firstArtifactContents = new Map();
@ -4394,16 +4394,16 @@ async function runTests() {
break; break;
} }
} }
assert(deterministicOutputs, 'Wave-2 validation harness outputs are byte-stable across unchanged repeated runs'); assert(deterministicOutputs, 'Shard-doc validation harness outputs are byte-stable across unchanged repeated runs');
await fs.remove(artifactPathsById.get(8)); await fs.remove(artifactPathsById.get(8));
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-2 validation harness fails when a required artifact is missing'); assert(false, 'Shard-doc validation harness fails when a required artifact is missing');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-2 validation harness emits deterministic missing-artifact error code', 'Shard-doc validation harness emits deterministic missing-artifact error code',
); );
} }
@ -4422,11 +4422,11 @@ async function runTests() {
bmadFolderName: '_bmad', bmadFolderName: '_bmad',
shardDocAuthorityRecords: authorityRecords, shardDocAuthorityRecords: authorityRecords,
}); });
assert(false, 'Wave-2 validation harness rejects missing command-label report input surface'); assert(false, 'Shard-doc validation harness rejects missing command-label report input surface');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-2 validation harness emits deterministic missing-input-surface error code', 'Shard-doc validation harness emits deterministic missing-input-surface error code',
); );
} }
await writeCsv(commandLabelReportPath, commandLabelReportColumns, commandLabelReportRows); await writeCsv(commandLabelReportPath, commandLabelReportColumns, commandLabelReportRows);
@ -4437,11 +4437,11 @@ async function runTests() {
await fs.writeFile(artifactSixPath, artifactSixLines.join('\n'), 'utf8'); await fs.writeFile(artifactSixPath, artifactSixLines.join('\n'), 'utf8');
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-2 validation harness rejects schema/header drift'); assert(false, 'Shard-doc validation harness rejects schema/header drift');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE2_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, error.code === SHARD_DOC_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
'Wave-2 validation harness emits deterministic schema-mismatch error code', 'Shard-doc validation harness emits deterministic schema-mismatch error code',
); );
} }
@ -4459,7 +4459,7 @@ async function runTests() {
}); });
const artifactSixInventoryRow = artifactEightRows.find((row) => row.artifactId === '6'); const artifactSixInventoryRow = artifactEightRows.find((row) => row.artifactId === '6');
if (artifactSixInventoryRow) { if (artifactSixInventoryRow) {
artifactSixInventoryRow.artifactPath = 'validation/wave-2/drifted-command-label-report.csv'; artifactSixInventoryRow.artifactPath = 'validation/shard-doc/drifted-command-label-report.csv';
} }
await writeCsv( await writeCsv(
artifactEightPath, artifactEightPath,
@ -4468,11 +4468,11 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-2 validation harness rejects inventory deterministic-identifier drift'); assert(false, 'Shard-doc validation harness rejects inventory deterministic-identifier drift');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
'Wave-2 validation harness emits deterministic inventory-row validation error code', 'Shard-doc validation harness emits deterministic inventory-row validation error code',
); );
} }
@ -4496,17 +4496,17 @@ async function runTests() {
); );
try { try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-2 validation harness rejects missing source-body authority records'); assert(false, 'Shard-doc validation harness rejects missing source-body authority records');
} catch (error) { } catch (error) {
assert( assert(
error.code === WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
'Wave-2 validation harness emits deterministic missing-row error code', 'Shard-doc validation harness emits deterministic missing-row error code',
); );
} }
} catch (error) { } catch (error) {
assert(false, 'Wave-2 shard-doc validation artifact suite setup', error.message); assert(false, 'Shard-doc validation artifact suite setup', error.message);
} finally { } finally {
await fs.remove(tempWave2ValidationHarnessRoot); await fs.remove(tempShardDocValidationHarnessRoot);
} }
console.log(''); console.log('');

View File

@ -11,23 +11,23 @@ const { ManifestGenerator } = require('./manifest-generator');
const { buildSidecarAwareExemplarHelpRow } = require('./help-catalog-generator'); const { buildSidecarAwareExemplarHelpRow } = require('./help-catalog-generator');
const { CodexSetup } = require('../ide/codex'); const { CodexSetup } = require('../ide/codex');
const WAVE1_VALIDATION_ERROR_CODES = Object.freeze({ const HELP_VALIDATION_ERROR_CODES = Object.freeze({
REQUIRED_ARTIFACT_MISSING: 'ERR_WAVE1_VALIDATION_REQUIRED_ARTIFACT_MISSING', REQUIRED_ARTIFACT_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ARTIFACT_MISSING',
CSV_SCHEMA_MISMATCH: 'ERR_WAVE1_VALIDATION_CSV_SCHEMA_MISMATCH', CSV_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_CSV_SCHEMA_MISMATCH',
REQUIRED_ROW_IDENTITY_MISSING: 'ERR_WAVE1_VALIDATION_REQUIRED_ROW_IDENTITY_MISSING', REQUIRED_ROW_IDENTITY_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ROW_IDENTITY_MISSING',
REQUIRED_EVIDENCE_LINK_MISSING: 'ERR_WAVE1_VALIDATION_REQUIRED_EVIDENCE_LINK_MISSING', REQUIRED_EVIDENCE_LINK_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_EVIDENCE_LINK_MISSING',
EVIDENCE_LINK_REFERENCE_INVALID: 'ERR_WAVE1_VALIDATION_EVIDENCE_LINK_REFERENCE_INVALID', EVIDENCE_LINK_REFERENCE_INVALID: 'ERR_HELP_VALIDATION_EVIDENCE_LINK_REFERENCE_INVALID',
BINDING_EVIDENCE_INVALID: 'ERR_WAVE1_VALIDATION_BINDING_EVIDENCE_INVALID', BINDING_EVIDENCE_INVALID: 'ERR_HELP_VALIDATION_BINDING_EVIDENCE_INVALID',
ISSUER_PREREQUISITE_MISSING: 'ERR_WAVE1_VALIDATION_ISSUER_PREREQUISITE_MISSING', ISSUER_PREREQUISITE_MISSING: 'ERR_HELP_VALIDATION_ISSUER_PREREQUISITE_MISSING',
SELF_ATTESTED_ISSUER_CLAIM: 'ERR_WAVE1_VALIDATION_SELF_ATTESTED_ISSUER_CLAIM', SELF_ATTESTED_ISSUER_CLAIM: 'ERR_HELP_VALIDATION_SELF_ATTESTED_ISSUER_CLAIM',
YAML_SCHEMA_MISMATCH: 'ERR_WAVE1_VALIDATION_YAML_SCHEMA_MISMATCH', YAML_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_YAML_SCHEMA_MISMATCH',
DECISION_RECORD_SCHEMA_MISMATCH: 'ERR_WAVE1_VALIDATION_DECISION_RECORD_SCHEMA_MISMATCH', DECISION_RECORD_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_DECISION_RECORD_SCHEMA_MISMATCH',
DECISION_RECORD_PARSE_FAILED: 'ERR_WAVE1_VALIDATION_DECISION_RECORD_PARSE_FAILED', 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 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 SOURCE_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md';
const EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/wave-1-validation-harness.js'; const EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/help-validation-harness.js';
const FRONTMATTER_MISMATCH_DETAILS = Object.freeze({ 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.CANONICAL_ID_MISMATCH]: 'frontmatter canonicalId must match sidecar canonicalId',
@ -37,16 +37,16 @@ const FRONTMATTER_MISMATCH_DETAILS = Object.freeze({
'frontmatter dependencies.requires must match sidecar dependencies.requires', 'frontmatter dependencies.requires must match sidecar dependencies.requires',
}); });
const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([ const HELP_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
Object.freeze({ Object.freeze({
artifactId: 1, artifactId: 1,
relativePath: path.join('validation', 'wave-1', 'bmad-help-sidecar-snapshot.yaml'), relativePath: path.join('validation', 'help', 'bmad-help-sidecar-snapshot.yaml'),
type: 'yaml', type: 'yaml',
requiredTopLevelKeys: ['schemaVersion', 'canonicalId', 'artifactType', 'module', 'sourcePath', 'displayName', 'description', 'status'], requiredTopLevelKeys: ['schemaVersion', 'canonicalId', 'artifactType', 'module', 'sourcePath', 'displayName', 'description', 'status'],
}), }),
Object.freeze({ Object.freeze({
artifactId: 2, artifactId: 2,
relativePath: path.join('validation', 'wave-1', 'bmad-help-runtime-comparison.csv'), relativePath: path.join('validation', 'help', 'bmad-help-runtime-comparison.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -65,7 +65,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 3, artifactId: 3,
relativePath: path.join('validation', 'wave-1', 'bmad-help-issued-artifact-provenance.csv'), relativePath: path.join('validation', 'help', 'bmad-help-issued-artifact-provenance.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'rowIdentity', 'rowIdentity',
@ -84,7 +84,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 4, artifactId: 4,
relativePath: path.join('validation', 'wave-1', 'bmad-help-manifest-comparison.csv'), relativePath: path.join('validation', 'help', 'bmad-help-manifest-comparison.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -106,7 +106,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 5, artifactId: 5,
relativePath: path.join('validation', 'wave-1', 'bmad-help-alias-table.csv'), relativePath: path.join('validation', 'help', 'bmad-help-alias-table.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'rowIdentity', 'rowIdentity',
@ -124,7 +124,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 6, artifactId: 6,
relativePath: path.join('validation', 'wave-1', 'bmad-help-description-provenance.csv'), relativePath: path.join('validation', 'help', 'bmad-help-description-provenance.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -142,7 +142,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 7, artifactId: 7,
relativePath: path.join('validation', 'wave-1', 'bmad-help-export-comparison.csv'), relativePath: path.join('validation', 'help', 'bmad-help-export-comparison.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'exportPath', 'exportPath',
@ -166,7 +166,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 8, artifactId: 8,
relativePath: path.join('validation', 'wave-1', 'bmad-help-command-label-report.csv'), relativePath: path.join('validation', 'help', 'bmad-help-command-label-report.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -186,7 +186,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 9, artifactId: 9,
relativePath: path.join('validation', 'wave-1', 'bmad-help-catalog-pipeline.csv'), relativePath: path.join('validation', 'help', 'bmad-help-catalog-pipeline.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'stage', 'stage',
@ -215,7 +215,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 10, artifactId: 10,
relativePath: path.join('validation', 'wave-1', 'bmad-help-duplicate-report.csv'), relativePath: path.join('validation', 'help', 'bmad-help-duplicate-report.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -247,7 +247,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 11, artifactId: 11,
relativePath: path.join('validation', 'wave-1', 'bmad-help-dependency-report.csv'), relativePath: path.join('validation', 'help', 'bmad-help-dependency-report.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'declaredIn', 'declaredIn',
@ -268,13 +268,13 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 12, artifactId: 12,
relativePath: path.join('decision-records', 'wave-1-native-skills-exit.md'), relativePath: path.join('decision-records', 'help-native-skills-exit.md'),
type: 'markdown', type: 'markdown',
requiredFrontmatterKeys: ['wave', 'goNoGo', 'status'], requiredFrontmatterKeys: ['capability', 'goNoGo', 'status'],
}), }),
Object.freeze({ Object.freeze({
artifactId: 13, artifactId: 13,
relativePath: path.join('validation', 'wave-1', 'bmad-help-sidecar-negative-validation.csv'), relativePath: path.join('validation', 'help', 'bmad-help-sidecar-negative-validation.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'scenario', 'scenario',
@ -291,7 +291,7 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 14, artifactId: 14,
relativePath: path.join('validation', 'wave-1', 'bmad-help-frontmatter-mismatch-validation.csv'), relativePath: path.join('validation', 'help', 'bmad-help-frontmatter-mismatch-validation.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'scenario', 'scenario',
@ -314,11 +314,11 @@ const WAVE1_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
]); ]);
class Wave1ValidationHarnessError extends Error { class HelpValidationHarnessError extends Error {
constructor({ code, detail, artifactId, fieldPath, sourcePath, observedValue, expectedValue }) { constructor({ code, detail, artifactId, fieldPath, sourcePath, observedValue, expectedValue }) {
const message = `${code}: ${detail} (artifact=${artifactId}, fieldPath=${fieldPath}, sourcePath=${sourcePath})`; const message = `${code}: ${detail} (artifact=${artifactId}, fieldPath=${fieldPath}, sourcePath=${sourcePath})`;
super(message); super(message);
this.name = 'Wave1ValidationHarnessError'; this.name = 'HelpValidationHarnessError';
this.code = code; this.code = code;
this.detail = detail; this.detail = detail;
this.artifactId = artifactId; this.artifactId = artifactId;
@ -505,7 +505,7 @@ function buildReplaySidecarFixture({ canonicalId = 'bmad-help', description = 'H
function replayFailurePayload(error) { function replayFailurePayload(error) {
return canonicalJsonStringify({ return canonicalJsonStringify({
replayFailureCode: normalizeValue(error?.code || 'ERR_WAVE1_REPLAY_COMPONENT_FAILED'), replayFailureCode: normalizeValue(error?.code || 'ERR_HELP_VALIDATION_REPLAY_COMPONENT_FAILED'),
replayFailureDetail: normalizeValue(error?.detail || error?.message || 'component replay failed'), replayFailureDetail: normalizeValue(error?.detail || error?.message || 'component replay failed'),
}); });
} }
@ -514,9 +514,9 @@ function isSha256(value) {
return /^[a-f0-9]{64}$/.test(String(value || '')); return /^[a-f0-9]{64}$/.test(String(value || ''));
} }
class Wave1ValidationHarness { class HelpValidationHarness {
constructor() { constructor() {
this.registry = WAVE1_VALIDATION_ARTIFACT_REGISTRY; this.registry = HELP_VALIDATION_ARTIFACT_REGISTRY;
} }
getArtifactRegistry() { getArtifactRegistry() {
@ -526,7 +526,7 @@ class Wave1ValidationHarness {
resolveOutputPaths(options = {}) { resolveOutputPaths(options = {}) {
const projectDir = path.resolve(options.projectDir || process.cwd()); const projectDir = path.resolve(options.projectDir || process.cwd());
const planningArtifactsRoot = path.join(projectDir, '_bmad-output', 'planning-artifacts'); const planningArtifactsRoot = path.join(projectDir, '_bmad-output', 'planning-artifacts');
const validationRoot = path.join(planningArtifactsRoot, 'validation', 'wave-1'); const validationRoot = path.join(planningArtifactsRoot, 'validation', 'help');
const decisionRecordsRoot = path.join(planningArtifactsRoot, 'decision-records'); const decisionRecordsRoot = path.join(planningArtifactsRoot, 'decision-records');
return { return {
projectDir, projectDir,
@ -608,8 +608,8 @@ class Wave1ValidationHarness {
if (await fs.pathExists(absolutePath)) { if (await fs.pathExists(absolutePath)) {
return; return;
} }
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
detail: `Required input surface is missing (${description})`, detail: `Required input surface is missing (${description})`,
artifactId, artifactId,
fieldPath: '<file>', fieldPath: '<file>',
@ -624,8 +624,8 @@ class Wave1ValidationHarness {
if (match) { if (match) {
return match; return match;
} }
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail, detail,
artifactId, artifactId,
fieldPath, fieldPath,
@ -734,8 +734,8 @@ class Wave1ValidationHarness {
resolveReplayContract({ artifactPath, componentPath, rowIdentity, runtimeFolder }) { resolveReplayContract({ artifactPath, componentPath, rowIdentity, runtimeFolder }) {
const claimedRowIdentity = normalizeValue(rowIdentity); const claimedRowIdentity = normalizeValue(rowIdentity);
if (!claimedRowIdentity) { if (!claimedRowIdentity) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Claimed replay rowIdentity is required', detail: 'Claimed replay rowIdentity is required',
artifactId: 3, artifactId: 3,
fieldPath: 'rowIdentity', fieldPath: 'rowIdentity',
@ -747,8 +747,8 @@ class Wave1ValidationHarness {
const expectedRowIdentity = buildIssuedArtifactRowIdentity(artifactPath); const expectedRowIdentity = buildIssuedArtifactRowIdentity(artifactPath);
if (claimedRowIdentity !== expectedRowIdentity) { if (claimedRowIdentity !== expectedRowIdentity) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Claimed replay rowIdentity does not match artifact claim rowIdentity contract', detail: 'Claimed replay rowIdentity does not match artifact claim rowIdentity contract',
artifactId: 3, artifactId: 3,
fieldPath: 'rowIdentity', fieldPath: 'rowIdentity',
@ -809,8 +809,8 @@ class Wave1ValidationHarness {
const contract = contractsByClaimRowIdentity.get(claimedRowIdentity); const contract = contractsByClaimRowIdentity.get(claimedRowIdentity);
if (!contract) { if (!contract) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Claimed rowIdentity is not mapped to a replay contract', detail: 'Claimed rowIdentity is not mapped to a replay contract',
artifactId: 3, artifactId: 3,
fieldPath: 'rowIdentity', fieldPath: 'rowIdentity',
@ -825,8 +825,8 @@ class Wave1ValidationHarness {
normalizeValue(artifactPath) !== normalizeValue(contract.artifactPath) || normalizeValue(artifactPath) !== normalizeValue(contract.artifactPath) ||
!normalizedComponentPath.includes(String(contract.componentPathIncludes || '').toLowerCase()) !normalizedComponentPath.includes(String(contract.componentPathIncludes || '').toLowerCase())
) { ) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Claimed replay rowIdentity/component pair does not match replay contract mapping', detail: 'Claimed replay rowIdentity/component pair does not match replay contract mapping',
artifactId: 3, artifactId: 3,
fieldPath: 'issuingComponent', fieldPath: 'issuingComponent',
@ -1024,14 +1024,14 @@ class Wave1ValidationHarness {
rowIdentity, rowIdentity,
runtimeFolder, runtimeFolder,
}); });
const baselineWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'wave1-replay-baseline-')); const baselineWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'help-replay-baseline-'));
const perturbedWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'wave1-replay-perturbed-')); const perturbedWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'help-replay-perturbed-'));
try { try {
const baseline = await contract.run({ workspaceRoot: baselineWorkspaceRoot, perturbed: false }); const baseline = await contract.run({ workspaceRoot: baselineWorkspaceRoot, perturbed: false });
if (Number(baseline.targetRowCount) <= 0) { if (Number(baseline.targetRowCount) <= 0) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Claimed rowIdentity target is absent in baseline component replay output', detail: 'Claimed rowIdentity target is absent in baseline component replay output',
artifactId: 3, artifactId: 3,
fieldPath: 'rowIdentity', fieldPath: 'rowIdentity',
@ -1236,7 +1236,7 @@ class Wave1ValidationHarness {
const runtimeHelpCatalogPath = `${runtimeFolder}/_config/bmad-help.csv`; const runtimeHelpCatalogPath = `${runtimeFolder}/_config/bmad-help.csv`;
const runtimePipelinePath = `${runtimeFolder}/_config/bmad-help-catalog-pipeline.csv`; const runtimePipelinePath = `${runtimeFolder}/_config/bmad-help-catalog-pipeline.csv`;
const runtimeCommandLabelPath = `${runtimeFolder}/_config/bmad-help-command-label-report.csv`; const runtimeCommandLabelPath = `${runtimeFolder}/_config/bmad-help-command-label-report.csv`;
const evidenceArtifactPath = '_bmad-output/planning-artifacts/validation/wave-1/bmad-help-issued-artifact-provenance.csv'; const evidenceArtifactPath = '_bmad-output/planning-artifacts/validation/help/bmad-help-issued-artifact-provenance.csv';
const exportSkillPath = '.agents/skills/bmad-help/SKILL.md'; const exportSkillPath = '.agents/skills/bmad-help/SKILL.md';
const exportSkillAbsolutePath = path.join(outputPaths.projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md'); const exportSkillAbsolutePath = path.join(outputPaths.projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md');
const codexExportRows = const codexExportRows =
@ -1445,8 +1445,8 @@ class Wave1ValidationHarness {
status: 'PASS', status: 'PASS',
})); }));
if (aliasRowsForExemplar.length === 0) { if (aliasRowsForExemplar.length === 0) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Required canonical alias rows for exemplar are missing', detail: 'Required canonical alias rows for exemplar are missing',
artifactId: 5, artifactId: 5,
fieldPath: 'rows[canonicalId=bmad-help]', fieldPath: 'rows[canonicalId=bmad-help]',
@ -1626,8 +1626,8 @@ class Wave1ValidationHarness {
}; };
}); });
if (pipelineWithEvidence.length === 0) { if (pipelineWithEvidence.length === 0) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Required help-catalog pipeline exemplar rows are missing', detail: 'Required help-catalog pipeline exemplar rows are missing',
artifactId: 9, artifactId: 9,
fieldPath: 'rows[canonicalId=bmad-help]', fieldPath: 'rows[canonicalId=bmad-help]',
@ -1883,11 +1883,11 @@ class Wave1ValidationHarness {
// Artifact 12: decision record // Artifact 12: decision record
const decisionRecord = { const decisionRecord = {
wave: 1, capability: 'bmad-help',
goNoGo: 'GO', goNoGo: 'GO',
status: 'PASS', status: 'PASS',
}; };
const decisionRecordContent = `---\n${yaml.stringify(decisionRecord).trimEnd()}\n---\n\n# Wave 1 Native Skills Exit\n\nStatus: PASS\n`; 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'); await fs.writeFile(artifactPaths.get(12), decisionRecordContent, 'utf8');
// Fixtures for artifacts 13 and 14 // Fixtures for artifacts 13 and 14
@ -1898,15 +1898,14 @@ class Wave1ValidationHarness {
const sidecarNegativeScenarios = [ const sidecarNegativeScenarios = [
{ {
scenario: 'unknown-major-version', scenario: 'unknown-major-version',
fixturePath: '_bmad-output/planning-artifacts/validation/wave-1/fixtures/sidecar-negative/unknown-major-version/help.artifact.yaml', fixturePath: '_bmad-output/planning-artifacts/validation/help/fixtures/sidecar-negative/unknown-major-version/help.artifact.yaml',
absolutePath: fixtures.unknownMajorFixturePath, absolutePath: fixtures.unknownMajorFixturePath,
expectedFailureCode: HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, expectedFailureCode: HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
expectedFailureDetail: 'sidecar schema major version is unsupported', expectedFailureDetail: 'sidecar schema major version is unsupported',
}, },
{ {
scenario: 'basename-path-mismatch', scenario: 'basename-path-mismatch',
fixturePath: fixturePath: '_bmad-output/planning-artifacts/validation/help/fixtures/sidecar-negative/basename-path-mismatch/help.artifact.yaml',
'_bmad-output/planning-artifacts/validation/wave-1/fixtures/sidecar-negative/basename-path-mismatch/help.artifact.yaml',
absolutePath: fixtures.basenameMismatchFixturePath, absolutePath: fixtures.basenameMismatchFixturePath,
expectedFailureCode: HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, expectedFailureCode: HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH,
expectedFailureDetail: 'sidecar basename does not match sourcePath basename', expectedFailureDetail: 'sidecar basename does not match sourcePath basename',
@ -1991,7 +1990,7 @@ class Wave1ValidationHarness {
for (const scope of ['source', 'runtime']) { for (const scope of ['source', 'runtime']) {
for (const scenario of mismatchScenarios) { for (const scenario of mismatchScenarios) {
const fixturePath = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch', scope, `${scenario.scenario}.md`); const fixturePath = path.join(outputPaths.validationRoot, 'fixtures', 'frontmatter-mismatch', scope, `${scenario.scenario}.md`);
const fixtureRelativePath = `_bmad-output/planning-artifacts/validation/wave-1/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 observedFailureCode = '';
let observedFailureDetail = ''; let observedFailureDetail = '';
let observedFrontmatterValue = ''; let observedFrontmatterValue = '';
@ -2071,8 +2070,8 @@ class Wave1ValidationHarness {
try { try {
parsed = JSON.parse(String(payloadRaw || '')); parsed = JSON.parse(String(payloadRaw || ''));
} catch (error) { } catch (error) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: `Binding evidence payload is not valid JSON (${error.message})`, detail: `Binding evidence payload is not valid JSON (${error.message})`,
artifactId, artifactId,
fieldPath, fieldPath,
@ -2083,8 +2082,8 @@ class Wave1ValidationHarness {
} }
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Binding evidence payload must be a JSON object', detail: 'Binding evidence payload must be a JSON object',
artifactId, artifactId,
fieldPath, fieldPath,
@ -2108,8 +2107,8 @@ class Wave1ValidationHarness {
}); });
if (normalizeValue(payload.evidenceVersion) !== '1') { if (normalizeValue(payload.evidenceVersion) !== '1') {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Binding evidence payload must use evidenceVersion=1', detail: 'Binding evidence payload must use evidenceVersion=1',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence.evidenceVersion', fieldPath: 'issuingComponentBindingEvidence.evidenceVersion',
@ -2121,8 +2120,8 @@ class Wave1ValidationHarness {
if (rowStatus === 'SKIP') { if (rowStatus === 'SKIP') {
if (normalizeValue(payload.observationMethod) !== 'validator-observed-optional-surface-omitted') { if (normalizeValue(payload.observationMethod) !== 'validator-observed-optional-surface-omitted') {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Optional-surface provenance rows must use optional-surface evidence method', detail: 'Optional-surface provenance rows must use optional-surface evidence method',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence.observationMethod', fieldPath: 'issuingComponentBindingEvidence.observationMethod',
@ -2150,8 +2149,8 @@ class Wave1ValidationHarness {
]; ];
for (const key of requiredPayloadFields) { for (const key of requiredPayloadFields) {
if (normalizeValue(payload[key]).length === 0 && payload[key] !== false) { if (normalizeValue(payload[key]).length === 0 && payload[key] !== false) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Required binding evidence field is missing', detail: 'Required binding evidence field is missing',
artifactId, artifactId,
fieldPath: `issuingComponentBindingEvidence.${key}`, fieldPath: `issuingComponentBindingEvidence.${key}`,
@ -2167,8 +2166,8 @@ class Wave1ValidationHarness {
normalizeValue(row.evidenceMethod) !== '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' normalizeValue(row.issuingComponentBindingBasis) !== 'validator-observed-baseline-plus-isolated-single-component-perturbation'
) { ) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Replay evidence must use the baseline-plus-isolated-perturbation method', detail: 'Replay evidence must use the baseline-plus-isolated-perturbation method',
artifactId, artifactId,
fieldPath: 'evidenceMethod', fieldPath: 'evidenceMethod',
@ -2185,8 +2184,8 @@ class Wave1ValidationHarness {
normalizeValue(payload.mutatedRowIdentity) !== normalizeValue(row.rowIdentity) || normalizeValue(payload.mutatedRowIdentity) !== normalizeValue(row.rowIdentity) ||
normalizeValue(payload.targetedRowLocator) !== normalizeValue(row.rowIdentity) normalizeValue(payload.targetedRowLocator) !== normalizeValue(row.rowIdentity)
) { ) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Binding evidence payload does not match provenance row contract fields', detail: 'Binding evidence payload does not match provenance row contract fields',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence', fieldPath: 'issuingComponentBindingEvidence',
@ -2197,8 +2196,8 @@ class Wave1ValidationHarness {
} }
if (!isSha256(payload.baselineArtifactSha256) || !isSha256(payload.mutatedArtifactSha256) || !isSha256(payload.rowLevelDiffSha256)) { if (!isSha256(payload.baselineArtifactSha256) || !isSha256(payload.mutatedArtifactSha256) || !isSha256(payload.rowLevelDiffSha256)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Replay evidence hashes must be sha256 hex values', detail: 'Replay evidence hashes must be sha256 hex values',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence.*Sha256', fieldPath: 'issuingComponentBindingEvidence.*Sha256',
@ -2213,8 +2212,8 @@ class Wave1ValidationHarness {
} }
if (payload.baselineArtifactSha256 === payload.mutatedArtifactSha256 || payload.perturbationApplied !== true) { if (payload.baselineArtifactSha256 === payload.mutatedArtifactSha256 || payload.perturbationApplied !== true) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Replay evidence must show isolated perturbation impact', detail: 'Replay evidence must show isolated perturbation impact',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence.perturbationApplied', fieldPath: 'issuingComponentBindingEvidence.perturbationApplied',
@ -2229,8 +2228,8 @@ class Wave1ValidationHarness {
} }
if (Number(payload.baselineTargetRowCount) <= Number(payload.mutatedTargetRowCount)) { if (Number(payload.baselineTargetRowCount) <= Number(payload.mutatedTargetRowCount)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
detail: 'Replay evidence must show reduced target-row impact after perturbation', detail: 'Replay evidence must show reduced target-row impact after perturbation',
artifactId, artifactId,
fieldPath: 'issuingComponentBindingEvidence.baselineTargetRowCount', fieldPath: 'issuingComponentBindingEvidence.baselineTargetRowCount',
@ -2250,8 +2249,8 @@ class Wave1ValidationHarness {
if (normalizeValue(value).length > 0) { if (normalizeValue(value).length > 0) {
return; return;
} }
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING,
detail: 'Required evidence-link field is missing or empty', detail: 'Required evidence-link field is missing or empty',
artifactId, artifactId,
fieldPath, fieldPath,
@ -2276,8 +2275,8 @@ class Wave1ValidationHarness {
} }
if (normalizeValue(row.issuedArtifactEvidencePath) !== evidencePath) { if (normalizeValue(row.issuedArtifactEvidencePath) !== evidencePath) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
detail: 'Evidence-link path does not point to required provenance artifact', detail: 'Evidence-link path does not point to required provenance artifact',
artifactId, artifactId,
fieldPath: `rows[${index}].issuedArtifactEvidencePath`, fieldPath: `rows[${index}].issuedArtifactEvidencePath`,
@ -2290,8 +2289,8 @@ class Wave1ValidationHarness {
const linkedEvidenceRowIdentity = normalizeValue(row.issuedArtifactEvidenceRowIdentity); const linkedEvidenceRowIdentity = normalizeValue(row.issuedArtifactEvidenceRowIdentity);
const provenanceRow = provenanceByIdentity.get(linkedEvidenceRowIdentity); const provenanceRow = provenanceByIdentity.get(linkedEvidenceRowIdentity);
if (!provenanceRow) { if (!provenanceRow) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
detail: 'Evidence-link row identity does not resolve to provenance artifact row', detail: 'Evidence-link row identity does not resolve to provenance artifact row',
artifactId, artifactId,
fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`, fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`,
@ -2302,8 +2301,8 @@ class Wave1ValidationHarness {
} }
if (normalizeValue(provenanceRow.status) !== 'PASS') { if (normalizeValue(provenanceRow.status) !== 'PASS') {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING, code: HELP_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING,
detail: 'Terminal PASS requires linked provenance rows to be PASS', detail: 'Terminal PASS requires linked provenance rows to be PASS',
artifactId, artifactId,
fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`, fieldPath: `rows[${index}].issuedArtifactEvidenceRowIdentity`,
@ -2314,8 +2313,8 @@ class Wave1ValidationHarness {
} }
if (rowArtifactPathField && normalizeValue(row[rowArtifactPathField]) !== normalizeValue(provenanceRow.artifactPath)) { if (rowArtifactPathField && normalizeValue(row[rowArtifactPathField]) !== normalizeValue(provenanceRow.artifactPath)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID, code: HELP_VALIDATION_ERROR_CODES.EVIDENCE_LINK_REFERENCE_INVALID,
detail: 'Evidence-linked provenance row does not match claimed artifact path', detail: 'Evidence-linked provenance row does not match claimed artifact path',
artifactId, artifactId,
fieldPath: `rows[${index}].${rowArtifactPathField}`, fieldPath: `rows[${index}].${rowArtifactPathField}`,
@ -2330,8 +2329,8 @@ class Wave1ValidationHarness {
normalizeValue(row.issuingComponent).length > 0 && normalizeValue(row.issuingComponent).length > 0 &&
normalizeValue(row.issuingComponent) !== normalizeValue(provenanceRow.issuingComponent) normalizeValue(row.issuingComponent) !== normalizeValue(provenanceRow.issuingComponent)
) { ) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM, code: HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
detail: 'Issuer component claim diverges from validator-linked provenance evidence', detail: 'Issuer component claim diverges from validator-linked provenance evidence',
artifactId, artifactId,
fieldPath: `rows[${index}].issuingComponent`, fieldPath: `rows[${index}].issuingComponent`,
@ -2346,8 +2345,8 @@ class Wave1ValidationHarness {
normalizeValue(row.issuingComponentBindingEvidence).length > 0 && normalizeValue(row.issuingComponentBindingEvidence).length > 0 &&
normalizeValue(row.issuingComponentBindingEvidence) !== normalizeValue(provenanceRow.issuingComponentBindingEvidence) normalizeValue(row.issuingComponentBindingEvidence) !== normalizeValue(provenanceRow.issuingComponentBindingEvidence)
) { ) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM, code: HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
detail: 'Issuer binding evidence claim diverges from validator-linked provenance evidence', detail: 'Issuer binding evidence claim diverges from validator-linked provenance evidence',
artifactId, artifactId,
fieldPath: `rows[${index}].issuingComponentBindingEvidence`, fieldPath: `rows[${index}].issuingComponentBindingEvidence`,
@ -2360,7 +2359,7 @@ class Wave1ValidationHarness {
} }
validateIssuerPrerequisites({ artifactDataById, runtimeFolder, requireExportSkillProjection }) { validateIssuerPrerequisites({ artifactDataById, runtimeFolder, requireExportSkillProjection }) {
const evidencePath = '_bmad-output/planning-artifacts/validation/wave-1/bmad-help-issued-artifact-provenance.csv'; const evidencePath = '_bmad-output/planning-artifacts/validation/help/bmad-help-issued-artifact-provenance.csv';
const provenanceArtifact = artifactDataById.get(3) || { rows: [] }; const provenanceArtifact = artifactDataById.get(3) || { rows: [] };
const provenanceRows = Array.isArray(provenanceArtifact.rows) ? provenanceArtifact.rows : []; const provenanceRows = Array.isArray(provenanceArtifact.rows) ? provenanceArtifact.rows : [];
const provenanceByIdentity = new Map(); const provenanceByIdentity = new Map();
@ -2392,8 +2391,8 @@ class Wave1ValidationHarness {
for (const artifactPath of requiredProvenanceArtifactPaths) { for (const artifactPath of requiredProvenanceArtifactPaths) {
const row = provenanceByArtifactPath.get(artifactPath); const row = provenanceByArtifactPath.get(artifactPath);
if (!row || normalizeValue(row.status) !== 'PASS') { if (!row || normalizeValue(row.status) !== 'PASS') {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING, code: HELP_VALIDATION_ERROR_CODES.ISSUER_PREREQUISITE_MISSING,
detail: 'Terminal PASS requires provenance prerequisite rows for all required issuing-component claims', detail: 'Terminal PASS requires provenance prerequisite rows for all required issuing-component claims',
artifactId: 3, artifactId: 3,
fieldPath: `rows[artifactPath=${artifactPath}]`, fieldPath: `rows[artifactPath=${artifactPath}]`,
@ -2494,9 +2493,9 @@ class Wave1ValidationHarness {
for (const artifact of this.registry) { for (const artifact of this.registry) {
const artifactPath = path.join(planningArtifactsRoot, artifact.relativePath); const artifactPath = path.join(planningArtifactsRoot, artifact.relativePath);
if (!(await fs.pathExists(artifactPath))) { if (!(await fs.pathExists(artifactPath))) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
detail: 'Required wave-1 validation artifact is missing', detail: 'Required help validation artifact is missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<file>', fieldPath: '<file>',
sourcePath: normalizePath(artifact.relativePath), sourcePath: normalizePath(artifact.relativePath),
@ -2519,8 +2518,8 @@ class Wave1ValidationHarness {
}); });
if (observedHeader.length !== expectedHeader.length) { if (observedHeader.length !== expectedHeader.length) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
detail: 'CSV header length does not match required schema', detail: 'CSV header length does not match required schema',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<header>', fieldPath: '<header>',
@ -2534,8 +2533,8 @@ class Wave1ValidationHarness {
const observed = normalizeValue(observedHeader[index]); const observed = normalizeValue(observedHeader[index]);
const expected = normalizeValue(expectedValue); const expected = normalizeValue(expectedValue);
if (observed !== expected) { if (observed !== expected) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
detail: 'CSV header ordering does not match required schema', detail: 'CSV header ordering does not match required schema',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: `header[${index}]`, fieldPath: `header[${index}]`,
@ -2548,8 +2547,8 @@ class Wave1ValidationHarness {
if (Array.isArray(artifact.requiredRowIdentityFields) && artifact.requiredRowIdentityFields.length > 0) { if (Array.isArray(artifact.requiredRowIdentityFields) && artifact.requiredRowIdentityFields.length > 0) {
if (rows.length === 0) { if (rows.length === 0) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, code: HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: 'Required row identity rows are missing', detail: 'Required row identity rows are missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: 'rows', fieldPath: 'rows',
@ -2560,8 +2559,8 @@ class Wave1ValidationHarness {
} }
for (const field of artifact.requiredRowIdentityFields) { for (const field of artifact.requiredRowIdentityFields) {
if (!expectedHeader.includes(field)) { if (!expectedHeader.includes(field)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
detail: 'Required row identity field is missing from artifact schema', detail: 'Required row identity field is missing from artifact schema',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: `header.${field}`, fieldPath: `header.${field}`,
@ -2574,10 +2573,10 @@ class Wave1ValidationHarness {
for (const [rowIndex, row] of rows.entries()) { for (const [rowIndex, row] of rows.entries()) {
if (normalizeValue(row[field]).length === 0) { if (normalizeValue(row[field]).length === 0) {
const isEvidenceLinkField = field === 'issuedArtifactEvidenceRowIdentity'; const isEvidenceLinkField = field === 'issuedArtifactEvidenceRowIdentity';
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: isEvidenceLinkField code: isEvidenceLinkField
? WAVE1_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING ? HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING
: WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, : HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
detail: isEvidenceLinkField detail: isEvidenceLinkField
? 'Required evidence-link row identity is missing or empty' ? 'Required evidence-link row identity is missing or empty'
: 'Required row identity value is missing or empty', : 'Required row identity value is missing or empty',
@ -2601,8 +2600,8 @@ class Wave1ValidationHarness {
parsed, parsed,
}); });
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
detail: 'YAML artifact root must be a mapping object', detail: 'YAML artifact root must be a mapping object',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<document>', fieldPath: '<document>',
@ -2613,8 +2612,8 @@ class Wave1ValidationHarness {
} }
for (const requiredKey of artifact.requiredTopLevelKeys || []) { for (const requiredKey of artifact.requiredTopLevelKeys || []) {
if (!Object.prototype.hasOwnProperty.call(parsed, requiredKey)) { if (!Object.prototype.hasOwnProperty.call(parsed, requiredKey)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
detail: 'Required YAML key is missing', detail: 'Required YAML key is missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: requiredKey, fieldPath: requiredKey,
@ -2637,8 +2636,8 @@ class Wave1ValidationHarness {
try { try {
frontmatter = parseFrontmatter(content); frontmatter = parseFrontmatter(content);
} catch (error) { } catch (error) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.DECISION_RECORD_PARSE_FAILED, code: HELP_VALIDATION_ERROR_CODES.DECISION_RECORD_PARSE_FAILED,
detail: `Unable to parse decision record frontmatter (${error.message})`, detail: `Unable to parse decision record frontmatter (${error.message})`,
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<frontmatter>', fieldPath: '<frontmatter>',
@ -2647,8 +2646,8 @@ class Wave1ValidationHarness {
} }
for (const requiredKey of artifact.requiredFrontmatterKeys || []) { for (const requiredKey of artifact.requiredFrontmatterKeys || []) {
if (!Object.prototype.hasOwnProperty.call(frontmatter, requiredKey)) { if (!Object.prototype.hasOwnProperty.call(frontmatter, requiredKey)) {
throw new Wave1ValidationHarnessError({ throw new HelpValidationHarnessError({
code: WAVE1_VALIDATION_ERROR_CODES.DECISION_RECORD_SCHEMA_MISMATCH, code: HELP_VALIDATION_ERROR_CODES.DECISION_RECORD_SCHEMA_MISMATCH,
detail: 'Required decision-record key is missing', detail: 'Required decision-record key is missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: requiredKey, fieldPath: requiredKey,
@ -2695,8 +2694,8 @@ class Wave1ValidationHarness {
} }
module.exports = { module.exports = {
WAVE1_VALIDATION_ERROR_CODES, HELP_VALIDATION_ERROR_CODES,
WAVE1_VALIDATION_ARTIFACT_REGISTRY, HELP_VALIDATION_ARTIFACT_REGISTRY,
Wave1ValidationHarnessError, HelpValidationHarnessError,
Wave1ValidationHarness, HelpValidationHarness,
}; };

View File

@ -20,8 +20,8 @@ const {
renderDisplayedCommandLabel, renderDisplayedCommandLabel,
} = require('./help-catalog-generator'); } = require('./help-catalog-generator');
const { validateHelpCatalogCompatibilitySurface } = require('./projection-compatibility-validator'); const { validateHelpCatalogCompatibilitySurface } = require('./projection-compatibility-validator');
const { Wave1ValidationHarness } = require('./wave-1-validation-harness'); const { HelpValidationHarness } = require('./help-validation-harness');
const { Wave2ValidationHarness } = require('./wave-2-validation-harness'); const { ShardDocValidationHarness } = require('./shard-doc-validation-harness');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator'); const { ManifestGenerator } = require('./manifest-generator');
@ -59,10 +59,10 @@ class Installer {
this.helpCatalogPipelineRows = []; this.helpCatalogPipelineRows = [];
this.helpCatalogCommandLabelReportRows = []; this.helpCatalogCommandLabelReportRows = [];
this.codexExportDerivationRecords = []; this.codexExportDerivationRecords = [];
this.latestWave1ValidationRun = null; this.latestHelpValidationRun = null;
this.latestWave2ValidationRun = null; this.latestShardDocValidationRun = null;
this.wave1ValidationHarness = new Wave1ValidationHarness(); this.helpValidationHarness = new HelpValidationHarness();
this.wave2ValidationHarness = new Wave2ValidationHarness(); this.shardDocValidationHarness = new ShardDocValidationHarness();
} }
async runConfigurationGenerationTask({ message, bmadDir, moduleConfigs, config, allModules, addResult }) { async runConfigurationGenerationTask({ message, bmadDir, moduleConfigs, config, allModules, addResult }) {
@ -151,7 +151,7 @@ class Installer {
return 'Configurations generated'; return 'Configurations generated';
} }
async buildWave1ValidationOptions({ projectDir, bmadDir }) { async buildHelpValidationOptions({ projectDir, bmadDir }) {
const exportSkillProjectionPath = path.join(projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md'); const exportSkillProjectionPath = path.join(projectDir, '.agents', 'skills', 'bmad-help', 'SKILL.md');
const hasCodexExportDerivationRecords = const hasCodexExportDerivationRecords =
Array.isArray(this.codexExportDerivationRecords) && this.codexExportDerivationRecords.length > 0; Array.isArray(this.codexExportDerivationRecords) && this.codexExportDerivationRecords.length > 0;
@ -169,7 +169,7 @@ class Installer {
}; };
} }
async buildWave2ValidationOptions({ projectDir, bmadDir }) { async buildShardDocValidationOptions({ projectDir, bmadDir }) {
return { return {
projectDir, projectDir,
bmadDir, bmadDir,
@ -1356,25 +1356,25 @@ class Installer {
postIdeTasks.push({ postIdeTasks.push({
title: 'Generating validation artifacts', title: 'Generating validation artifacts',
task: async (message) => { task: async (message) => {
message('Generating deterministic wave-1 validation artifact suite...'); message('Generating deterministic help validation artifact suite...');
const validationOptions = await this.buildWave1ValidationOptions({ const validationOptions = await this.buildHelpValidationOptions({
projectDir, projectDir,
bmadDir, bmadDir,
}); });
const validationRun = await this.wave1ValidationHarness.generateAndValidate(validationOptions); const validationRun = await this.helpValidationHarness.generateAndValidate(validationOptions);
this.latestWave1ValidationRun = validationRun; this.latestHelpValidationRun = validationRun;
addResult('Wave-1 validation artifacts', 'ok', `${validationRun.generatedArtifactCount} artifacts`); addResult('Help validation artifacts', 'ok', `${validationRun.generatedArtifactCount} artifacts`);
message('Generating deterministic wave-2 shard-doc validation artifact suite...'); message('Generating deterministic shard-doc validation artifact suite...');
const wave2ValidationOptions = await this.buildWave2ValidationOptions({ const shardDocValidationOptions = await this.buildShardDocValidationOptions({
projectDir, projectDir,
bmadDir, bmadDir,
}); });
const wave2ValidationRun = await this.wave2ValidationHarness.generateAndValidate(wave2ValidationOptions); const shardDocValidationRun = await this.shardDocValidationHarness.generateAndValidate(shardDocValidationOptions);
this.latestWave2ValidationRun = wave2ValidationRun; this.latestShardDocValidationRun = shardDocValidationRun;
addResult('Wave-2 validation artifacts', 'ok', `${wave2ValidationRun.generatedArtifactCount} artifacts`); addResult('Shard-doc validation artifacts', 'ok', `${shardDocValidationRun.generatedArtifactCount} artifacts`);
return `${validationRun.generatedArtifactCount + wave2ValidationRun.generatedArtifactCount} validation artifacts generated`; return `${validationRun.generatedArtifactCount + shardDocValidationRun.generatedArtifactCount} validation artifacts generated`;
}, },
}); });

View File

@ -1099,7 +1099,7 @@ class ManifestGenerator {
} }
} }
// Create CSV header with compatibility-prefix columns followed by additive wave-1 columns. // Create CSV header with compatibility-prefix columns followed by additive canonical-identity columns.
let csvContent = 'name,displayName,description,module,path,standalone,legacyName,canonicalId,authoritySourceType,authoritySourcePath\n'; let csvContent = 'name,displayName,description,module,path,standalone,legacyName,canonicalId,authoritySourceType,authoritySourcePath\n';
// Combine existing and new tasks // Combine existing and new tasks

View File

@ -2,7 +2,7 @@ const csv = require('csv-parse/sync');
const TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS = Object.freeze(['name', 'displayName', 'description', 'module', 'path', 'standalone']); const TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS = Object.freeze(['name', 'displayName', 'description', 'module', 'path', 'standalone']);
const TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS = Object.freeze(['legacyName', 'canonicalId', 'authoritySourceType', 'authoritySourcePath']); const TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS = Object.freeze(['legacyName', 'canonicalId', 'authoritySourceType', 'authoritySourcePath']);
const HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS = Object.freeze([ const HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS = Object.freeze([
'module', 'module',
@ -15,7 +15,7 @@ const HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS = Object.freeze([
'required', 'required',
]); ]);
const HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS = Object.freeze([ const HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS = Object.freeze([
'agent-name', 'agent-name',
'agent-command', 'agent-command',
'agent-display-name', 'agent-display-name',
@ -29,12 +29,12 @@ const HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS = Object.freeze([
const PROJECTION_COMPATIBILITY_ERROR_CODES = Object.freeze({ const PROJECTION_COMPATIBILITY_ERROR_CODES = Object.freeze({
TASK_MANIFEST_CSV_PARSE_FAILED: 'ERR_TASK_MANIFEST_COMPAT_PARSE_FAILED', TASK_MANIFEST_CSV_PARSE_FAILED: 'ERR_TASK_MANIFEST_COMPAT_PARSE_FAILED',
TASK_MANIFEST_HEADER_PREFIX_MISMATCH: 'ERR_TASK_MANIFEST_COMPAT_HEADER_PREFIX_MISMATCH', TASK_MANIFEST_HEADER_PREFIX_MISMATCH: 'ERR_TASK_MANIFEST_COMPAT_HEADER_PREFIX_MISMATCH',
TASK_MANIFEST_HEADER_WAVE1_MISMATCH: 'ERR_TASK_MANIFEST_COMPAT_HEADER_WAVE1_MISMATCH', TASK_MANIFEST_HEADER_CANONICAL_MISMATCH: 'ERR_TASK_MANIFEST_COMPAT_HEADER_CANONICAL_MISMATCH',
TASK_MANIFEST_REQUIRED_COLUMN_MISSING: 'ERR_TASK_MANIFEST_COMPAT_REQUIRED_COLUMN_MISSING', TASK_MANIFEST_REQUIRED_COLUMN_MISSING: 'ERR_TASK_MANIFEST_COMPAT_REQUIRED_COLUMN_MISSING',
TASK_MANIFEST_ROW_FIELD_EMPTY: 'ERR_TASK_MANIFEST_COMPAT_ROW_FIELD_EMPTY', TASK_MANIFEST_ROW_FIELD_EMPTY: 'ERR_TASK_MANIFEST_COMPAT_ROW_FIELD_EMPTY',
HELP_CATALOG_CSV_PARSE_FAILED: 'ERR_HELP_CATALOG_COMPAT_PARSE_FAILED', HELP_CATALOG_CSV_PARSE_FAILED: 'ERR_HELP_CATALOG_COMPAT_PARSE_FAILED',
HELP_CATALOG_HEADER_PREFIX_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_PREFIX_MISMATCH', HELP_CATALOG_HEADER_PREFIX_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_PREFIX_MISMATCH',
HELP_CATALOG_HEADER_WAVE1_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_WAVE1_MISMATCH', HELP_CATALOG_HEADER_CANONICAL_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_CANONICAL_MISMATCH',
HELP_CATALOG_REQUIRED_COLUMN_MISSING: 'ERR_HELP_CATALOG_COMPAT_REQUIRED_COLUMN_MISSING', HELP_CATALOG_REQUIRED_COLUMN_MISSING: 'ERR_HELP_CATALOG_COMPAT_REQUIRED_COLUMN_MISSING',
HELP_CATALOG_EXEMPLAR_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_EXEMPLAR_ROW_CONTRACT_FAILED', HELP_CATALOG_EXEMPLAR_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_EXEMPLAR_ROW_CONTRACT_FAILED',
HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_SHARD_DOC_ROW_CONTRACT_FAILED', HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_SHARD_DOC_ROW_CONTRACT_FAILED',
@ -375,10 +375,10 @@ function validateTaskManifestCompatibilitySurface(csvContent, options = {}) {
}); });
assertLockedColumns({ assertLockedColumns({
headerColumns, headerColumns,
expectedColumns: TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS, expectedColumns: TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS,
offset: TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length, offset: TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length,
code: PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_WAVE1_MISMATCH, code: PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_CANONICAL_MISMATCH,
detail: 'Task-manifest wave-1 additive columns must remain appended after compatibility-prefix columns', detail: 'Task-manifest canonical additive columns must remain appended after compatibility-prefix columns',
surface, surface,
sourcePath, sourcePath,
}); });
@ -419,10 +419,10 @@ function validateHelpCatalogCompatibilitySurface(csvContent, options = {}) {
}); });
assertLockedColumns({ assertLockedColumns({
headerColumns, headerColumns,
expectedColumns: HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS, expectedColumns: HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS,
offset: HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS.length, offset: HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS.length,
code: PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_HEADER_WAVE1_MISMATCH, code: PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_HEADER_CANONICAL_MISMATCH,
detail: 'Help-catalog wave-1 additive columns must remain appended after compatibility-prefix columns', detail: 'Help-catalog canonical additive columns must remain appended after compatibility-prefix columns',
surface, surface,
sourcePath, sourcePath,
}); });
@ -528,9 +528,9 @@ module.exports = {
PROJECTION_COMPATIBILITY_ERROR_CODES, PROJECTION_COMPATIBILITY_ERROR_CODES,
ProjectionCompatibilityError, ProjectionCompatibilityError,
TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS,
TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS, TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS,
HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS,
HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS, HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS,
validateTaskManifestCompatibilitySurface, validateTaskManifestCompatibilitySurface,
validateTaskManifestLoaderEntries, validateTaskManifestLoaderEntries,
validateHelpCatalogCompatibilitySurface, validateHelpCatalogCompatibilitySurface,

View File

@ -8,23 +8,23 @@ const { normalizeDisplayedCommandLabel } = require('./help-catalog-generator');
const SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; const SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml';
const SHARD_DOC_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml'; const SHARD_DOC_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
const WAVE2_VALIDATION_ERROR_CODES = Object.freeze({ const SHARD_DOC_VALIDATION_ERROR_CODES = Object.freeze({
REQUIRED_ARTIFACT_MISSING: 'ERR_WAVE2_VALIDATION_REQUIRED_ARTIFACT_MISSING', REQUIRED_ARTIFACT_MISSING: 'ERR_SHARD_DOC_VALIDATION_REQUIRED_ARTIFACT_MISSING',
CSV_SCHEMA_MISMATCH: 'ERR_WAVE2_VALIDATION_CSV_SCHEMA_MISMATCH', CSV_SCHEMA_MISMATCH: 'ERR_SHARD_DOC_VALIDATION_CSV_SCHEMA_MISMATCH',
REQUIRED_ROW_MISSING: 'ERR_WAVE2_VALIDATION_REQUIRED_ROW_MISSING', REQUIRED_ROW_MISSING: 'ERR_SHARD_DOC_VALIDATION_REQUIRED_ROW_MISSING',
YAML_SCHEMA_MISMATCH: 'ERR_WAVE2_VALIDATION_YAML_SCHEMA_MISMATCH', YAML_SCHEMA_MISMATCH: 'ERR_SHARD_DOC_VALIDATION_YAML_SCHEMA_MISMATCH',
}); });
const WAVE2_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([ const SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
Object.freeze({ Object.freeze({
artifactId: 1, artifactId: 1,
relativePath: path.join('validation', 'wave-2', 'shard-doc-sidecar-snapshot.yaml'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-sidecar-snapshot.yaml'),
type: 'yaml', type: 'yaml',
requiredTopLevelKeys: ['schemaVersion', 'canonicalId', 'artifactType', 'module', 'sourcePath', 'displayName', 'description', 'status'], requiredTopLevelKeys: ['schemaVersion', 'canonicalId', 'artifactType', 'module', 'sourcePath', 'displayName', 'description', 'status'],
}), }),
Object.freeze({ Object.freeze({
artifactId: 2, artifactId: 2,
relativePath: path.join('validation', 'wave-2', 'shard-doc-authority-records.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-authority-records.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'rowIdentity', 'rowIdentity',
@ -39,7 +39,7 @@ const WAVE2_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 3, artifactId: 3,
relativePath: path.join('validation', 'wave-2', 'shard-doc-task-manifest-comparison.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-task-manifest-comparison.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -56,13 +56,13 @@ const WAVE2_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 4, artifactId: 4,
relativePath: path.join('validation', 'wave-2', 'shard-doc-help-catalog-comparison.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-help-catalog-comparison.csv'),
type: 'csv', type: 'csv',
columns: ['surface', 'sourcePath', 'name', 'workflowFile', 'command', 'rowCountForCanonicalCommand', 'status'], columns: ['surface', 'sourcePath', 'name', 'workflowFile', 'command', 'rowCountForCanonicalCommand', 'status'],
}), }),
Object.freeze({ Object.freeze({
artifactId: 5, artifactId: 5,
relativePath: path.join('validation', 'wave-2', 'shard-doc-alias-table.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-alias-table.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'rowIdentity', 'rowIdentity',
@ -80,7 +80,7 @@ const WAVE2_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 6, artifactId: 6,
relativePath: path.join('validation', 'wave-2', 'shard-doc-command-label-report.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-command-label-report.csv'),
type: 'csv', type: 'csv',
columns: [ columns: [
'surface', 'surface',
@ -96,24 +96,24 @@ const WAVE2_VALIDATION_ARTIFACT_REGISTRY = Object.freeze([
}), }),
Object.freeze({ Object.freeze({
artifactId: 7, artifactId: 7,
relativePath: path.join('validation', 'wave-2', 'shard-doc-duplicate-report.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-duplicate-report.csv'),
type: 'csv', type: 'csv',
columns: ['surface', 'canonicalId', 'normalizedVisibleKey', 'matchingRowCount', 'status'], columns: ['surface', 'canonicalId', 'normalizedVisibleKey', 'matchingRowCount', 'status'],
}), }),
Object.freeze({ Object.freeze({
artifactId: 8, artifactId: 8,
relativePath: path.join('validation', 'wave-2', 'shard-doc-artifact-inventory.csv'), relativePath: path.join('validation', 'shard-doc', 'shard-doc-artifact-inventory.csv'),
type: 'csv', type: 'csv',
columns: ['rowIdentity', 'artifactId', 'artifactPath', 'artifactType', 'required', 'rowCount', 'exists', 'schemaVersion', 'status'], columns: ['rowIdentity', 'artifactId', 'artifactPath', 'artifactType', 'required', 'rowCount', 'exists', 'schemaVersion', 'status'],
requiredRowIdentityFields: ['rowIdentity'], requiredRowIdentityFields: ['rowIdentity'],
}), }),
]); ]);
class Wave2ValidationHarnessError extends Error { class ShardDocValidationHarnessError extends Error {
constructor({ code, detail, artifactId, fieldPath, sourcePath, observedValue, expectedValue }) { constructor({ code, detail, artifactId, fieldPath, sourcePath, observedValue, expectedValue }) {
const message = `${code}: ${detail} (artifact=${artifactId}, fieldPath=${fieldPath}, sourcePath=${sourcePath})`; const message = `${code}: ${detail} (artifact=${artifactId}, fieldPath=${fieldPath}, sourcePath=${sourcePath})`;
super(message); super(message);
this.name = 'Wave2ValidationHarnessError'; this.name = 'ShardDocValidationHarnessError';
this.code = code; this.code = code;
this.detail = detail; this.detail = detail;
this.artifactId = artifactId; this.artifactId = artifactId;
@ -170,9 +170,9 @@ function sortRowsDeterministically(rows, columns) {
}); });
} }
class Wave2ValidationHarness { class ShardDocValidationHarness {
constructor() { constructor() {
this.registry = WAVE2_VALIDATION_ARTIFACT_REGISTRY; this.registry = SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY;
} }
getArtifactRegistry() { getArtifactRegistry() {
@ -182,7 +182,7 @@ class Wave2ValidationHarness {
resolveOutputPaths(options = {}) { resolveOutputPaths(options = {}) {
const projectDir = path.resolve(options.projectDir || process.cwd()); const projectDir = path.resolve(options.projectDir || process.cwd());
const planningArtifactsRoot = path.join(projectDir, '_bmad-output', 'planning-artifacts'); const planningArtifactsRoot = path.join(projectDir, '_bmad-output', 'planning-artifacts');
const validationRoot = path.join(planningArtifactsRoot, 'validation', 'wave-2'); const validationRoot = path.join(planningArtifactsRoot, 'validation', 'shard-doc');
return { return {
projectDir, projectDir,
planningArtifactsRoot, planningArtifactsRoot,
@ -207,8 +207,8 @@ class Wave2ValidationHarness {
if (await fs.pathExists(absolutePath)) { if (await fs.pathExists(absolutePath)) {
return; return;
} }
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
detail: `Required input surface is missing (${description})`, detail: `Required input surface is missing (${description})`,
artifactId, artifactId,
fieldPath: '<file>', fieldPath: '<file>',
@ -223,8 +223,8 @@ class Wave2ValidationHarness {
if (match) { if (match) {
return match; return match;
} }
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail, detail,
artifactId, artifactId,
fieldPath, fieldPath,
@ -318,8 +318,8 @@ class Wave2ValidationHarness {
normalizePath(normalizeValue(row['workflow-file'])).toLowerCase().endsWith('/core/tasks/shard-doc.xml'), normalizePath(normalizeValue(row['workflow-file'])).toLowerCase().endsWith('/core/tasks/shard-doc.xml'),
); );
if (shardDocHelpRows.length !== 1) { if (shardDocHelpRows.length !== 1) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Expected exactly one shard-doc help-catalog command row', detail: 'Expected exactly one shard-doc help-catalog command row',
artifactId: 4, artifactId: 4,
fieldPath: 'rows[*].command', fieldPath: 'rows[*].command',
@ -334,8 +334,8 @@ class Wave2ValidationHarness {
const observedAliasTypes = new Set(shardDocAliasRows.map((row) => normalizeValue(row.aliasType))); const observedAliasTypes = new Set(shardDocAliasRows.map((row) => normalizeValue(row.aliasType)));
for (const aliasType of requiredAliasTypes) { for (const aliasType of requiredAliasTypes) {
if (!observedAliasTypes.has(aliasType)) { if (!observedAliasTypes.has(aliasType)) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Required shard-doc alias type row is missing', detail: 'Required shard-doc alias type row is missing',
artifactId: 5, artifactId: 5,
fieldPath: 'rows[*].aliasType', fieldPath: 'rows[*].aliasType',
@ -348,8 +348,8 @@ class Wave2ValidationHarness {
const shardDocCommandLabelRows = commandLabelRows.filter((row) => normalizeValue(row.canonicalId) === 'bmad-shard-doc'); const shardDocCommandLabelRows = commandLabelRows.filter((row) => normalizeValue(row.canonicalId) === 'bmad-shard-doc');
if (shardDocCommandLabelRows.length !== 1) { if (shardDocCommandLabelRows.length !== 1) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Expected exactly one shard-doc command-label row', detail: 'Expected exactly one shard-doc command-label row',
artifactId: 6, artifactId: 6,
fieldPath: 'rows[*].canonicalId', fieldPath: 'rows[*].canonicalId',
@ -536,9 +536,9 @@ class Wave2ValidationHarness {
for (const artifact of this.registry) { for (const artifact of this.registry) {
const artifactPath = path.join(outputPaths.planningArtifactsRoot, artifact.relativePath); const artifactPath = path.join(outputPaths.planningArtifactsRoot, artifact.relativePath);
if (!(await fs.pathExists(artifactPath))) { if (!(await fs.pathExists(artifactPath))) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
detail: 'Required wave-2 validation artifact is missing', detail: 'Required shard-doc validation artifact is missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<file>', fieldPath: '<file>',
sourcePath: normalizePath(artifact.relativePath), sourcePath: normalizePath(artifact.relativePath),
@ -552,8 +552,8 @@ class Wave2ValidationHarness {
const observedHeader = parseCsvHeader(content); const observedHeader = parseCsvHeader(content);
const expectedHeader = artifact.columns || []; const expectedHeader = artifact.columns || [];
if (observedHeader.length !== expectedHeader.length) { if (observedHeader.length !== expectedHeader.length) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, code: SHARD_DOC_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
detail: 'CSV header length does not match required schema', detail: 'CSV header length does not match required schema',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<header>', fieldPath: '<header>',
@ -567,8 +567,8 @@ class Wave2ValidationHarness {
const observed = normalizeValue(observedHeader[index]); const observed = normalizeValue(observedHeader[index]);
const expected = normalizeValue(expectedValue); const expected = normalizeValue(expectedValue);
if (observed !== expected) { if (observed !== expected) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, code: SHARD_DOC_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
detail: 'CSV header ordering does not match required schema', detail: 'CSV header ordering does not match required schema',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: `header[${index}]`, fieldPath: `header[${index}]`,
@ -581,8 +581,8 @@ class Wave2ValidationHarness {
const rows = parseCsvRows(content); const rows = parseCsvRows(content);
if (rows.length === 0) { if (rows.length === 0) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Required CSV artifact rows are missing', detail: 'Required CSV artifact rows are missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: 'rows', fieldPath: 'rows',
@ -594,8 +594,8 @@ class Wave2ValidationHarness {
for (const requiredField of artifact.requiredRowIdentityFields || []) { for (const requiredField of artifact.requiredRowIdentityFields || []) {
for (const [rowIndex, row] of rows.entries()) { for (const [rowIndex, row] of rows.entries()) {
if (!normalizeValue(row[requiredField])) { if (!normalizeValue(row[requiredField])) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Required row identity field is empty', detail: 'Required row identity field is empty',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: `rows[${rowIndex}].${requiredField}`, fieldPath: `rows[${rowIndex}].${requiredField}`,
@ -611,8 +611,8 @@ class Wave2ValidationHarness {
} else if (artifact.type === 'yaml') { } else if (artifact.type === 'yaml') {
const parsed = yaml.parse(await fs.readFile(artifactPath, 'utf8')); const parsed = yaml.parse(await fs.readFile(artifactPath, 'utf8'));
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH, code: SHARD_DOC_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
detail: 'YAML artifact root must be a mapping object', detail: 'YAML artifact root must be a mapping object',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: '<document>', fieldPath: '<document>',
@ -623,8 +623,8 @@ class Wave2ValidationHarness {
} }
for (const key of artifact.requiredTopLevelKeys || []) { for (const key of artifact.requiredTopLevelKeys || []) {
if (!Object.prototype.hasOwnProperty.call(parsed, key)) { if (!Object.prototype.hasOwnProperty.call(parsed, key)) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH, code: SHARD_DOC_VALIDATION_ERROR_CODES.YAML_SCHEMA_MISMATCH,
detail: 'Required YAML key is missing', detail: 'Required YAML key is missing',
artifactId: artifact.artifactId, artifactId: artifact.artifactId,
fieldPath: key, fieldPath: key,
@ -664,8 +664,8 @@ class Wave2ValidationHarness {
const inventoryRows = artifactDataById.get(8)?.rows || []; const inventoryRows = artifactDataById.get(8)?.rows || [];
if (inventoryRows.length !== this.registry.length) { if (inventoryRows.length !== this.registry.length) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Artifact inventory must include one row per required artifact', detail: 'Artifact inventory must include one row per required artifact',
artifactId: 8, artifactId: 8,
fieldPath: 'rows', fieldPath: 'rows',
@ -699,8 +699,8 @@ class Wave2ValidationHarness {
Number.isFinite(observedRowCount) && Number.isFinite(observedRowCount) &&
(expectedInventoryRowCount === null ? observedRowCount >= 1 : observedRowCount === expectedInventoryRowCount); (expectedInventoryRowCount === null ? observedRowCount >= 1 : observedRowCount === expectedInventoryRowCount);
if (!rowCountIsValid) { if (!rowCountIsValid) {
throw new Wave2ValidationHarnessError({ throw new ShardDocValidationHarnessError({
code: WAVE2_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, code: SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING,
detail: 'Artifact inventory rowCount does not satisfy deterministic contract', detail: 'Artifact inventory rowCount does not satisfy deterministic contract',
artifactId: 8, artifactId: 8,
fieldPath: `rows[artifactId=${artifact.artifactId}].rowCount`, fieldPath: `rows[artifactId=${artifact.artifactId}].rowCount`,
@ -729,8 +729,8 @@ class Wave2ValidationHarness {
} }
module.exports = { module.exports = {
WAVE2_VALIDATION_ERROR_CODES, SHARD_DOC_VALIDATION_ERROR_CODES,
WAVE2_VALIDATION_ARTIFACT_REGISTRY, SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY,
Wave2ValidationHarnessError, ShardDocValidationHarnessError,
Wave2ValidationHarness, ShardDocValidationHarness,
}; };

View File

@ -230,9 +230,9 @@ function validateHelpSidecarContractData(sidecarData, options = {}) {
missingDependenciesDetail: 'Exemplar sidecar requires an explicit dependencies block.', missingDependenciesDetail: 'Exemplar sidecar requires an explicit dependencies block.',
dependenciesObjectDetail: 'Exemplar sidecar requires an explicit dependencies object.', dependenciesObjectDetail: 'Exemplar sidecar requires an explicit dependencies object.',
dependenciesRequiresArrayDetail: 'Exemplar dependencies.requires must be an array.', dependenciesRequiresArrayDetail: 'Exemplar dependencies.requires must be an array.',
dependenciesRequiresNotEmptyDetail: 'Wave-1 exemplar requires explicit zero dependencies: dependencies.requires must be [].', dependenciesRequiresNotEmptyDetail: 'help exemplar requires explicit zero dependencies: dependencies.requires must be [].',
artifactTypeDetail: 'Wave-1 exemplar requires artifactType to equal "task".', artifactTypeDetail: 'help exemplar requires artifactType to equal "task".',
moduleDetail: 'Wave-1 exemplar requires module to equal "core".', moduleDetail: 'help exemplar requires module to equal "core".',
requiresMustBeEmpty: true, requiresMustBeEmpty: true,
}); });
} }
@ -250,9 +250,9 @@ function validateShardDocSidecarContractData(sidecarData, options = {}) {
missingDependenciesDetail: 'Shard-doc sidecar requires an explicit dependencies block.', missingDependenciesDetail: 'Shard-doc sidecar requires an explicit dependencies block.',
dependenciesObjectDetail: 'Shard-doc sidecar requires an explicit dependencies object.', dependenciesObjectDetail: 'Shard-doc sidecar requires an explicit dependencies object.',
dependenciesRequiresArrayDetail: 'Shard-doc dependencies.requires must be an array.', dependenciesRequiresArrayDetail: 'Shard-doc dependencies.requires must be an array.',
dependenciesRequiresNotEmptyDetail: 'Wave-2 shard-doc contract requires explicit zero dependencies: dependencies.requires must be [].', dependenciesRequiresNotEmptyDetail: 'Shard-doc contract requires explicit zero dependencies: dependencies.requires must be [].',
artifactTypeDetail: 'Wave-2 shard-doc contract requires artifactType to equal "task".', artifactTypeDetail: 'Shard-doc contract requires artifactType to equal "task".',
moduleDetail: 'Wave-2 shard-doc contract requires module to equal "core".', moduleDetail: 'Shard-doc contract requires module to equal "core".',
requiresMustBeEmpty: true, requiresMustBeEmpty: true,
}); });
} }