/** * Installation Component Tests * * Tests individual installation components in isolation: * - Agent YAML → XML compilation * - Manifest generation * - Path resolution * - Customization merging * * These are deterministic unit tests that don't require full installation. * Usage: node test/test-installation-components.js */ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { Installer } = require('../tools/cli/installers/lib/core/installer'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { TaskToolCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/task-tool-command-generator'); const { GitHubCopilotSetup } = require('../tools/cli/installers/lib/ide/github-copilot'); const { HELP_ALIAS_NORMALIZATION_ERROR_CODES, LOCKED_EXEMPLAR_ALIAS_ROWS, normalizeRawIdentityToTuple, resolveAliasTupleFromRows, resolveAliasTupleUsingCanonicalAliasCsv, normalizeAndResolveExemplarAlias, } = require('../tools/cli/installers/lib/core/help-alias-normalizer'); const { HELP_SIDECAR_REQUIRED_FIELDS, HELP_SIDECAR_ERROR_CODES, SHARD_DOC_SIDECAR_REQUIRED_FIELDS, SHARD_DOC_SIDECAR_ERROR_CODES, INDEX_DOCS_SIDECAR_REQUIRED_FIELDS, INDEX_DOCS_SIDECAR_ERROR_CODES, validateHelpSidecarContractFile, validateShardDocSidecarContractFile, validateIndexDocsSidecarContractFile, } = require('../tools/cli/installers/lib/core/sidecar-contract-validator'); const { HELP_FRONTMATTER_MISMATCH_ERROR_CODES, validateHelpAuthoritySplitAndPrecedence, } = require('../tools/cli/installers/lib/core/help-authority-validator'); const { SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES, validateShardDocAuthoritySplitAndPrecedence, } = require('../tools/cli/installers/lib/core/shard-doc-authority-validator'); const { INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES, validateIndexDocsAuthoritySplitAndPrecedence, } = require('../tools/cli/installers/lib/core/index-docs-authority-validator'); const { HELP_CATALOG_GENERATION_ERROR_CODES, EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH, EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT, INSTALLER_HELP_CATALOG_MERGE_COMPONENT, buildSidecarAwareExemplarHelpRow, evaluateExemplarCommandLabelReportRows, } = require('../tools/cli/installers/lib/core/help-catalog-generator'); const { CodexSetup, CODEX_EXPORT_DERIVATION_ERROR_CODES, EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE, } = require('../tools/cli/installers/lib/ide/codex'); const { PROJECTION_COMPATIBILITY_ERROR_CODES, TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS, HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS, validateTaskManifestCompatibilitySurface, validateTaskManifestLoaderEntries, validateHelpCatalogCompatibilitySurface, validateHelpCatalogLoaderEntries, validateGithubCopilotHelpLoaderEntries, validateCommandDocSurfaceConsistency, } = require('../tools/cli/installers/lib/core/projection-compatibility-validator'); const { HELP_VALIDATION_ERROR_CODES, HELP_VALIDATION_ARTIFACT_REGISTRY, HelpValidationHarness, } = require('../tools/cli/installers/lib/core/help-validation-harness'); const { SHARD_DOC_VALIDATION_ERROR_CODES, SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY, ShardDocValidationHarness, } = require('../tools/cli/installers/lib/core/shard-doc-validation-harness'); // ANSI colors const colors = { reset: '\u001B[0m', green: '\u001B[32m', red: '\u001B[31m', yellow: '\u001B[33m', cyan: '\u001B[36m', dim: '\u001B[2m', }; let passed = 0; let failed = 0; /** * Test helper: Assert condition */ function assert(condition, testName, errorMessage = '') { if (condition) { console.log(`${colors.green}✓${colors.reset} ${testName}`); passed++; } else { console.log(`${colors.red}✗${colors.reset} ${testName}`); if (errorMessage) { console.log(` ${colors.dim}${errorMessage}${colors.reset}`); } failed++; } } /** * Test Suite */ async function runTests() { console.log(`${colors.cyan}========================================`); console.log('Installation Component Tests'); console.log(`========================================${colors.reset}\n`); const projectRoot = path.join(__dirname, '..'); // ============================================================ // Test 1: YAML → XML Agent Compilation (In-Memory) // ============================================================ console.log(`${colors.yellow}Test Suite 1: Agent Compilation${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); const pmAgentPath = path.join(projectRoot, 'src/bmm/agents/pm.agent.yaml'); // Create temp output path const tempOutput = path.join(__dirname, 'temp-pm-agent.md'); try { const result = await builder.buildAgent(pmAgentPath, null, tempOutput, { includeMetadata: true }); assert(result && result.outputPath === tempOutput, 'Agent compilation returns result object with outputPath'); // Read the output const compiled = await fs.readFile(tempOutput, 'utf8'); assert(compiled.includes(' tag'); assert(compiled.includes(''), 'Compiled agent contains tag'); assert(compiled.includes(''), 'Compiled agent contains tag'); assert(compiled.includes('Product Manager'), 'Compiled agent contains agent title'); // Cleanup await fs.remove(tempOutput); } catch (error) { assert(false, 'Agent compilation succeeds', error.message); } } catch (error) { assert(false, 'YamlXmlBuilder instantiates', error.message); } console.log(''); // ============================================================ // Test 2: Customization Merging // ============================================================ console.log(`${colors.yellow}Test Suite 2: Customization Merging${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); // Test deepMerge function const base = { agent: { metadata: { name: 'John', title: 'PM' }, persona: { role: 'Product Manager', style: 'Analytical' }, }, }; const customize = { agent: { metadata: { name: 'Sarah' }, // Override name only persona: { style: 'Concise' }, // Override style only }, }; const merged = builder.deepMerge(base, customize); assert(merged.agent.metadata.name === 'Sarah', 'Deep merge overrides customized name'); assert(merged.agent.metadata.title === 'PM', 'Deep merge preserves non-overridden title'); assert(merged.agent.persona.role === 'Product Manager', 'Deep merge preserves non-overridden role'); assert(merged.agent.persona.style === 'Concise', 'Deep merge overrides customized style'); } catch (error) { assert(false, 'Customization merging works', error.message); } console.log(''); // ============================================================ // Test 3: Path Resolution // ============================================================ console.log(`${colors.yellow}Test Suite 3: Path Variable Resolution${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); // Test path resolution logic (if exposed) // This would test {project-root}, {installed_path}, {config_source} resolution const testPath = '{project-root}/bmad/bmm/config.yaml'; const expectedPattern = /\/bmad\/bmm\/config\.yaml$/; assert( true, // Placeholder - would test actual resolution 'Path variable resolution pattern matches expected format', 'Note: This test validates path resolution logic exists', ); } catch (error) { assert(false, 'Path resolution works', error.message); } console.log(''); // ============================================================ // Test 4: Exemplar Sidecar Contract Validation // ============================================================ console.log(`${colors.yellow}Test Suite 4: Sidecar Contract Validation${colors.reset}\n`); const validHelpSidecar = { schemaVersion: 1, canonicalId: 'bmad-help', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Help command', dependencies: { requires: [], }, }; const tempSidecarRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-sidecar-')); const tempSidecarPath = path.join(tempSidecarRoot, 'help.artifact.yaml'); const deterministicSourcePath = 'bmad-fork/src/core/tasks/help.artifact.yaml'; const expectedUnsupportedMajorDetail = 'sidecar schema major version is unsupported'; const expectedBasenameMismatchDetail = 'sidecar basename does not match sourcePath basename'; const writeTempSidecar = async (data) => { await fs.writeFile(tempSidecarPath, yaml.stringify(data), 'utf8'); }; const expectValidationError = async (data, expectedCode, expectedFieldPath, testLabel, expectedDetail = null) => { await writeTempSidecar(data); try { await validateHelpSidecarContractFile(tempSidecarPath, { errorSourcePath: deterministicSourcePath }); assert(false, testLabel, 'Expected validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(deterministicSourcePath), `${testLabel} includes deterministic message context`, ); if (expectedDetail !== null) { assert( error.detail === expectedDetail, `${testLabel} returns locked detail string`, `Expected "${expectedDetail}", got "${error.detail}"`, ); } } }; try { await writeTempSidecar(validHelpSidecar); await validateHelpSidecarContractFile(tempSidecarPath, { errorSourcePath: deterministicSourcePath }); assert(true, 'Valid sidecar contract passes'); for (const requiredField of HELP_SIDECAR_REQUIRED_FIELDS.filter((field) => field !== 'dependencies')) { const invalidSidecar = structuredClone(validHelpSidecar); delete invalidSidecar[requiredField]; await expectValidationError( invalidSidecar, HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING, requiredField, `Missing required field "${requiredField}"`, ); } await expectValidationError( { ...validHelpSidecar, artifactType: 'workflow' }, HELP_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID, 'artifactType', 'Invalid artifactType', ); await expectValidationError( { ...validHelpSidecar, module: 'bmm' }, HELP_SIDECAR_ERROR_CODES.MODULE_INVALID, 'module', 'Invalid module', ); await expectValidationError( { ...validHelpSidecar, schemaVersion: 2 }, HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, 'schemaVersion', 'Unsupported sidecar major schema version', expectedUnsupportedMajorDetail, ); await expectValidationError( { ...validHelpSidecar, canonicalId: ' ' }, HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'canonicalId', 'Empty canonicalId', ); await expectValidationError( { ...validHelpSidecar, sourcePath: '' }, HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'sourcePath', 'Empty sourcePath', ); await expectValidationError( { ...validHelpSidecar, sourcePath: 'bmad-fork/src/core/tasks/not-help.md' }, HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'sourcePath', 'Source path mismatch with exemplar contract', expectedBasenameMismatchDetail, ); const mismatchedBasenamePath = path.join(tempSidecarRoot, 'not-help.artifact.yaml'); await fs.writeFile(mismatchedBasenamePath, yaml.stringify(validHelpSidecar), 'utf8'); try { await validateHelpSidecarContractFile(mismatchedBasenamePath, { errorSourcePath: 'bmad-fork/src/core/tasks/not-help.artifact.yaml', }); assert(false, 'Sidecar basename mismatch returns validation error', 'Expected validation error but validation passed'); } catch (error) { assert(error.code === HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'Sidecar basename mismatch returns expected error code'); assert( error.fieldPath === 'sourcePath', 'Sidecar basename mismatch returns expected field path', `Expected sourcePath, got ${error.fieldPath}`, ); assert( typeof error.message === 'string' && error.message.includes(HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH) && error.message.includes('bmad-fork/src/core/tasks/not-help.artifact.yaml'), 'Sidecar basename mismatch includes deterministic message context', ); assert( error.detail === expectedBasenameMismatchDetail, 'Sidecar basename mismatch returns locked detail string', `Expected "${expectedBasenameMismatchDetail}", got "${error.detail}"`, ); } const missingDependencies = structuredClone(validHelpSidecar); delete missingDependencies.dependencies; await expectValidationError( missingDependencies, HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING, 'dependencies', 'Missing dependencies block', ); await expectValidationError( { ...validHelpSidecar, dependencies: { requires: 'skill:bmad-help' } }, HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID, 'dependencies.requires', 'Non-array dependencies.requires', ); await expectValidationError( { ...validHelpSidecar, dependencies: { requires: ['skill:bmad-help'] } }, HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY, 'dependencies.requires', 'Non-empty dependencies.requires', ); } catch (error) { assert(false, 'Sidecar validation suite setup', error.message); } finally { await fs.remove(tempSidecarRoot); } console.log(''); // ============================================================ // Test 4b: Shard-doc Sidecar Contract Validation // ============================================================ console.log(`${colors.yellow}Test Suite 4b: Shard-doc Sidecar Contract Validation${colors.reset}\n`); const validShardDocSidecar = { schemaVersion: 1, canonicalId: 'bmad-shard-doc', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', dependencies: { requires: [], }, }; const shardDocFixtureRoot = path.join(projectRoot, 'test', 'fixtures', 'shard-doc', 'sidecar-negative'); 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 tempShardDocRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-shard-doc-sidecar-')); const tempShardDocSidecarPath = path.join(tempShardDocRoot, 'shard-doc.artifact.yaml'); const deterministicShardDocSourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; const writeTempShardDocSidecar = async (data) => { await fs.writeFile(tempShardDocSidecarPath, yaml.stringify(data), 'utf8'); }; const expectShardDocValidationError = async (data, expectedCode, expectedFieldPath, testLabel, expectedDetail = null) => { await writeTempShardDocSidecar(data); try { await validateShardDocSidecarContractFile(tempShardDocSidecarPath, { errorSourcePath: deterministicShardDocSourcePath }); assert(false, testLabel, 'Expected validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === deterministicShardDocSourcePath, `${testLabel} returns expected source path`, `Expected ${deterministicShardDocSourcePath}, got ${error.sourcePath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(deterministicShardDocSourcePath), `${testLabel} includes deterministic message context`, ); if (expectedDetail !== null) { assert( error.detail === expectedDetail, `${testLabel} returns locked detail string`, `Expected "${expectedDetail}", got "${error.detail}"`, ); } } }; try { await writeTempShardDocSidecar(validShardDocSidecar); await validateShardDocSidecarContractFile(tempShardDocSidecarPath, { errorSourcePath: deterministicShardDocSourcePath }); assert(true, 'Valid shard-doc sidecar contract passes'); for (const requiredField of SHARD_DOC_SIDECAR_REQUIRED_FIELDS.filter((field) => field !== 'dependencies')) { const invalidSidecar = structuredClone(validShardDocSidecar); delete invalidSidecar[requiredField]; await expectShardDocValidationError( invalidSidecar, SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING, requiredField, `Shard-doc missing required field "${requiredField}"`, ); } const unknownMajorFixture = yaml.parse(await fs.readFile(unknownMajorFixturePath, 'utf8')); await expectShardDocValidationError( unknownMajorFixture, SHARD_DOC_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, 'schemaVersion', 'Shard-doc unsupported sidecar major schema version', 'sidecar schema major version is unsupported', ); const basenameMismatchFixture = yaml.parse(await fs.readFile(basenameMismatchFixturePath, 'utf8')); await expectShardDocValidationError( basenameMismatchFixture, SHARD_DOC_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'sourcePath', 'Shard-doc sourcePath mismatch', 'sidecar basename does not match sourcePath basename', ); const mismatchedShardDocBasenamePath = path.join(tempShardDocRoot, 'not-shard-doc.artifact.yaml'); await fs.writeFile(mismatchedShardDocBasenamePath, yaml.stringify(validShardDocSidecar), 'utf8'); try { await validateShardDocSidecarContractFile(mismatchedShardDocBasenamePath, { errorSourcePath: 'bmad-fork/src/core/tasks/not-shard-doc.artifact.yaml', }); assert(false, 'Shard-doc basename mismatch returns validation error', 'Expected validation error but validation passed'); } catch (error) { assert( error.code === SHARD_DOC_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'Shard-doc basename mismatch returns expected error code', ); assert( error.fieldPath === 'sourcePath', 'Shard-doc basename mismatch returns expected field path', `Expected sourcePath, got ${error.fieldPath}`, ); assert( typeof error.message === 'string' && error.message.includes(SHARD_DOC_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH) && error.message.includes('bmad-fork/src/core/tasks/not-shard-doc.artifact.yaml'), 'Shard-doc basename mismatch includes deterministic message context', ); } await expectShardDocValidationError( { ...validShardDocSidecar, artifactType: 'workflow' }, SHARD_DOC_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID, 'artifactType', 'Shard-doc invalid artifactType', ); await expectShardDocValidationError( { ...validShardDocSidecar, module: 'bmm' }, SHARD_DOC_SIDECAR_ERROR_CODES.MODULE_INVALID, 'module', 'Shard-doc invalid module', ); await expectShardDocValidationError( { ...validShardDocSidecar, canonicalId: ' ' }, SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'canonicalId', 'Shard-doc empty canonicalId', ); await expectShardDocValidationError( { ...validShardDocSidecar, sourcePath: '' }, SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'sourcePath', 'Shard-doc empty sourcePath', ); await expectShardDocValidationError( { ...validShardDocSidecar, description: '' }, SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'description', 'Shard-doc empty description', ); await expectShardDocValidationError( { ...validShardDocSidecar, displayName: '' }, SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'displayName', 'Shard-doc empty displayName', ); const missingShardDocDependencies = structuredClone(validShardDocSidecar); delete missingShardDocDependencies.dependencies; await expectShardDocValidationError( missingShardDocDependencies, SHARD_DOC_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING, 'dependencies', 'Shard-doc missing dependencies block', ); await expectShardDocValidationError( { ...validShardDocSidecar, dependencies: { requires: 'skill:bmad-help' } }, SHARD_DOC_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID, 'dependencies.requires', 'Shard-doc non-array dependencies.requires', ); await expectShardDocValidationError( { ...validShardDocSidecar, dependencies: { requires: ['skill:bmad-help'] } }, SHARD_DOC_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY, 'dependencies.requires', 'Shard-doc non-empty dependencies.requires', ); } catch (error) { assert(false, 'Shard-doc sidecar validation suite setup', error.message); } finally { await fs.remove(tempShardDocRoot); } console.log(''); // ============================================================ // Test 4c: Index-docs Sidecar Contract Validation // ============================================================ console.log(`${colors.yellow}Test Suite 4c: Index-docs Sidecar Contract Validation${colors.reset}\n`); const validIndexDocsSidecar = { schemaVersion: 1, canonicalId: 'bmad-index-docs', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', displayName: 'Index Docs', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', dependencies: { requires: [], }, }; const indexDocsFixtureRoot = path.join(projectRoot, 'test', 'fixtures', 'index-docs', 'sidecar-negative'); const indexDocsUnknownMajorFixturePath = path.join(indexDocsFixtureRoot, 'unknown-major-version', 'index-docs.artifact.yaml'); const indexDocsBasenameMismatchFixturePath = path.join(indexDocsFixtureRoot, 'basename-path-mismatch', 'index-docs.artifact.yaml'); const tempIndexDocsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-index-docs-sidecar-')); const tempIndexDocsSidecarPath = path.join(tempIndexDocsRoot, 'index-docs.artifact.yaml'); const deterministicIndexDocsSourcePath = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml'; const writeTempIndexDocsSidecar = async (data) => { await fs.writeFile(tempIndexDocsSidecarPath, yaml.stringify(data), 'utf8'); }; const expectIndexDocsValidationError = async (data, expectedCode, expectedFieldPath, testLabel, expectedDetail = null) => { await writeTempIndexDocsSidecar(data); try { await validateIndexDocsSidecarContractFile(tempIndexDocsSidecarPath, { errorSourcePath: deterministicIndexDocsSourcePath }); assert(false, testLabel, 'Expected validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === deterministicIndexDocsSourcePath, `${testLabel} returns expected source path`, `Expected ${deterministicIndexDocsSourcePath}, got ${error.sourcePath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(deterministicIndexDocsSourcePath), `${testLabel} includes deterministic message context`, ); if (expectedDetail !== null) { assert( error.detail === expectedDetail, `${testLabel} returns locked detail string`, `Expected "${expectedDetail}", got "${error.detail}"`, ); } } }; try { await writeTempIndexDocsSidecar(validIndexDocsSidecar); await validateIndexDocsSidecarContractFile(tempIndexDocsSidecarPath, { errorSourcePath: deterministicIndexDocsSourcePath }); assert(true, 'Valid index-docs sidecar contract passes'); for (const requiredField of INDEX_DOCS_SIDECAR_REQUIRED_FIELDS.filter((field) => field !== 'dependencies')) { const invalidSidecar = structuredClone(validIndexDocsSidecar); delete invalidSidecar[requiredField]; await expectIndexDocsValidationError( invalidSidecar, INDEX_DOCS_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING, requiredField, `Index-docs missing required field "${requiredField}"`, ); } const unknownMajorFixture = yaml.parse(await fs.readFile(indexDocsUnknownMajorFixturePath, 'utf8')); await expectIndexDocsValidationError( unknownMajorFixture, INDEX_DOCS_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, 'schemaVersion', 'Index-docs unsupported sidecar major schema version', 'sidecar schema major version is unsupported', ); const basenameMismatchFixture = yaml.parse(await fs.readFile(indexDocsBasenameMismatchFixturePath, 'utf8')); await expectIndexDocsValidationError( basenameMismatchFixture, INDEX_DOCS_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'sourcePath', 'Index-docs sourcePath mismatch', 'sidecar basename does not match sourcePath basename', ); const mismatchedIndexDocsBasenamePath = path.join(tempIndexDocsRoot, 'not-index-docs.artifact.yaml'); await fs.writeFile(mismatchedIndexDocsBasenamePath, yaml.stringify(validIndexDocsSidecar), 'utf8'); try { await validateIndexDocsSidecarContractFile(mismatchedIndexDocsBasenamePath, { errorSourcePath: 'bmad-fork/src/core/tasks/not-index-docs.artifact.yaml', }); assert(false, 'Index-docs basename mismatch returns validation error', 'Expected validation error but validation passed'); } catch (error) { assert( error.code === INDEX_DOCS_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'Index-docs basename mismatch returns expected error code', ); assert( error.fieldPath === 'sourcePath', 'Index-docs basename mismatch returns expected field path', `Expected sourcePath, got ${error.fieldPath}`, ); assert( typeof error.message === 'string' && error.message.includes(INDEX_DOCS_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH) && error.message.includes('bmad-fork/src/core/tasks/not-index-docs.artifact.yaml'), 'Index-docs basename mismatch includes deterministic message context', ); } await expectIndexDocsValidationError( { ...validIndexDocsSidecar, artifactType: 'workflow' }, INDEX_DOCS_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID, 'artifactType', 'Index-docs invalid artifactType', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, module: 'bmm' }, INDEX_DOCS_SIDECAR_ERROR_CODES.MODULE_INVALID, 'module', 'Index-docs invalid module', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, canonicalId: ' ' }, INDEX_DOCS_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'canonicalId', 'Index-docs empty canonicalId', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, sourcePath: '' }, INDEX_DOCS_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'sourcePath', 'Index-docs empty sourcePath', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, description: '' }, INDEX_DOCS_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'description', 'Index-docs empty description', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, displayName: '' }, INDEX_DOCS_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, 'displayName', 'Index-docs empty displayName', ); const missingIndexDocsDependencies = structuredClone(validIndexDocsSidecar); delete missingIndexDocsDependencies.dependencies; await expectIndexDocsValidationError( missingIndexDocsDependencies, INDEX_DOCS_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING, 'dependencies', 'Index-docs missing dependencies block', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, dependencies: { requires: 'skill:bmad-help' } }, INDEX_DOCS_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID, 'dependencies.requires', 'Index-docs non-array dependencies.requires', ); await expectIndexDocsValidationError( { ...validIndexDocsSidecar, dependencies: { requires: ['skill:bmad-help'] } }, INDEX_DOCS_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY, 'dependencies.requires', 'Index-docs non-empty dependencies.requires', ); } catch (error) { assert(false, 'Index-docs sidecar validation suite setup', error.message); } finally { await fs.remove(tempIndexDocsRoot); } console.log(''); // ============================================================ // Test 5: Authority Split and Frontmatter Precedence // ============================================================ console.log(`${colors.yellow}Test Suite 5: Authority Split and Precedence${colors.reset}\n`); const tempAuthorityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-authority-')); const tempAuthoritySidecarPath = path.join(tempAuthorityRoot, 'help.artifact.yaml'); const tempAuthoritySourcePath = path.join(tempAuthorityRoot, 'help-source.md'); const tempAuthorityRuntimePath = path.join(tempAuthorityRoot, 'help-runtime.md'); const deterministicAuthorityPaths = { sidecar: 'bmad-fork/src/core/tasks/help.artifact.yaml', source: 'bmad-fork/src/core/tasks/help.md', runtime: '_bmad/core/tasks/help.md', }; const writeMarkdownWithFrontmatter = async (filePath, frontmatter) => { const frontmatterBody = yaml.stringify(frontmatter).trimEnd(); await fs.writeFile(filePath, `---\n${frontmatterBody}\n---\n\n# Placeholder\n`, 'utf8'); }; const validAuthoritySidecar = { schemaVersion: 1, canonicalId: 'bmad-help', artifactType: 'task', module: 'core', sourcePath: deterministicAuthorityPaths.source, displayName: 'help', description: 'Help command', dependencies: { requires: [], }, }; const validAuthorityFrontmatter = { name: 'help', description: 'Help command', canonicalId: 'bmad-help', dependencies: { requires: [], }, }; const runAuthorityValidation = async () => validateHelpAuthoritySplitAndPrecedence({ sidecarPath: tempAuthoritySidecarPath, sourceMarkdownPath: tempAuthoritySourcePath, runtimeMarkdownPath: tempAuthorityRuntimePath, sidecarSourcePath: deterministicAuthorityPaths.sidecar, sourceMarkdownSourcePath: deterministicAuthorityPaths.source, runtimeMarkdownSourcePath: deterministicAuthorityPaths.runtime, }); const expectAuthorityValidationError = async ( sourceFrontmatter, runtimeFrontmatter, expectedCode, expectedFieldPath, expectedSourcePath, testLabel, ) => { await writeMarkdownWithFrontmatter(tempAuthoritySourcePath, sourceFrontmatter); await writeMarkdownWithFrontmatter(tempAuthorityRuntimePath, runtimeFrontmatter); try { await runAuthorityValidation(); assert(false, testLabel, 'Expected authority validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === expectedSourcePath, `${testLabel} returns expected source path`, `Expected ${expectedSourcePath}, got ${error.sourcePath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(expectedSourcePath), `${testLabel} includes deterministic message context`, ); } }; try { await fs.writeFile(tempAuthoritySidecarPath, yaml.stringify(validAuthoritySidecar), 'utf8'); await writeMarkdownWithFrontmatter(tempAuthoritySourcePath, validAuthorityFrontmatter); await writeMarkdownWithFrontmatter(tempAuthorityRuntimePath, validAuthorityFrontmatter); const authorityValidation = await runAuthorityValidation(); assert( authorityValidation.authoritativePresenceKey === 'capability:bmad-help', 'Authority validation returns shared authoritative presence key', ); assert( Array.isArray(authorityValidation.authoritativeRecords) && authorityValidation.authoritativeRecords.length === 2, 'Authority validation returns sidecar and source authority records', ); const sidecarRecord = authorityValidation.authoritativeRecords.find((record) => record.authoritySourceType === 'sidecar'); const sourceRecord = authorityValidation.authoritativeRecords.find((record) => record.authoritySourceType === 'source-markdown'); assert( sidecarRecord && sourceRecord && sidecarRecord.authoritativePresenceKey === sourceRecord.authoritativePresenceKey, 'Source markdown and sidecar records share one authoritative presence key', ); assert( sidecarRecord && sidecarRecord.authoritySourcePath === deterministicAuthorityPaths.sidecar, 'Sidecar authority record preserves truthful sidecar source path', ); assert( sourceRecord && sourceRecord.authoritySourcePath === deterministicAuthorityPaths.source, 'Source body authority record preserves truthful source markdown path', ); const manifestGenerator = new ManifestGenerator(); manifestGenerator.modules = ['core']; manifestGenerator.bmadDir = tempAuthorityRoot; manifestGenerator.selectedIdes = []; manifestGenerator.helpAuthorityRecords = authorityValidation.authoritativeRecords; const tempManifestConfigDir = path.join(tempAuthorityRoot, '_config'); await fs.ensureDir(tempManifestConfigDir); await manifestGenerator.writeMainManifest(tempManifestConfigDir); const writtenManifestRaw = await fs.readFile(path.join(tempManifestConfigDir, 'manifest.yaml'), 'utf8'); const writtenManifest = yaml.parse(writtenManifestRaw); assert( writtenManifest.helpAuthority && Array.isArray(writtenManifest.helpAuthority.records), 'Manifest generation persists help authority records', ); assert( writtenManifest.helpAuthority && writtenManifest.helpAuthority.records && writtenManifest.helpAuthority.records.length === 2, 'Manifest generation persists both authority records', ); assert( writtenManifest.helpAuthority && writtenManifest.helpAuthority.records.some( (record) => record.authoritySourceType === 'sidecar' && record.authoritySourcePath === deterministicAuthorityPaths.sidecar, ), 'Manifest generation preserves sidecar authority provenance', ); assert( writtenManifest.helpAuthority && writtenManifest.helpAuthority.records.some( (record) => record.authoritySourceType === 'source-markdown' && record.authoritySourcePath === deterministicAuthorityPaths.source, ), 'Manifest generation preserves source-markdown authority provenance', ); await expectAuthorityValidationError( { ...validAuthorityFrontmatter, canonicalId: 'legacy-help' }, validAuthorityFrontmatter, HELP_FRONTMATTER_MISMATCH_ERROR_CODES.CANONICAL_ID_MISMATCH, 'canonicalId', deterministicAuthorityPaths.source, 'Source canonicalId mismatch', ); await expectAuthorityValidationError( { ...validAuthorityFrontmatter, name: 'BMAD Help' }, validAuthorityFrontmatter, HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DISPLAY_NAME_MISMATCH, 'name', deterministicAuthorityPaths.source, 'Source display-name mismatch', ); await expectAuthorityValidationError( validAuthorityFrontmatter, { ...validAuthorityFrontmatter, description: 'Runtime override' }, HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DESCRIPTION_MISMATCH, 'description', deterministicAuthorityPaths.runtime, 'Runtime description mismatch', ); await expectAuthorityValidationError( { ...validAuthorityFrontmatter, dependencies: { requires: ['skill:other'] } }, validAuthorityFrontmatter, HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DEPENDENCIES_REQUIRES_MISMATCH, 'dependencies.requires', deterministicAuthorityPaths.source, 'Source dependencies.requires mismatch', ); const tempShardDocAuthoritySidecarPath = path.join(tempAuthorityRoot, 'shard-doc.artifact.yaml'); const tempShardDocAuthoritySourcePath = path.join(tempAuthorityRoot, 'shard-doc.xml'); const tempShardDocModuleHelpPath = path.join(tempAuthorityRoot, 'module-help.csv'); const deterministicShardDocAuthorityPaths = { sidecar: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', source: 'bmad-fork/src/core/tasks/shard-doc.xml', compatibility: 'bmad-fork/src/core/module-help.csv', workflowFile: '_bmad/core/tasks/shard-doc.xml', }; const validShardDocAuthoritySidecar = { schemaVersion: 1, canonicalId: 'bmad-shard-doc', artifactType: 'task', module: 'core', sourcePath: deterministicShardDocAuthorityPaths.source, displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', dependencies: { requires: [], }, }; const writeModuleHelpCsv = async (rows) => { const header = 'module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs'; const lines = rows.map((row) => [ row.module ?? 'core', row.phase ?? 'anytime', row.name ?? 'Shard Document', row.code ?? 'SD', row.sequence ?? '', row.workflowFile ?? '', row.command ?? '', row.required ?? 'false', row.agent ?? '', row.options ?? '', row.description ?? 'Compatibility row', row.outputLocation ?? '', row.outputs ?? '', ].join(','), ); await fs.writeFile(tempShardDocModuleHelpPath, [header, ...lines].join('\n'), 'utf8'); }; const runShardDocAuthorityValidation = async () => validateShardDocAuthoritySplitAndPrecedence({ sidecarPath: tempShardDocAuthoritySidecarPath, sourceXmlPath: tempShardDocAuthoritySourcePath, compatibilityCatalogPath: tempShardDocModuleHelpPath, sidecarSourcePath: deterministicShardDocAuthorityPaths.sidecar, sourceXmlSourcePath: deterministicShardDocAuthorityPaths.source, compatibilityCatalogSourcePath: deterministicShardDocAuthorityPaths.compatibility, compatibilityWorkflowFilePath: deterministicShardDocAuthorityPaths.workflowFile, }); const expectShardDocAuthorityValidationError = async ( rows, expectedCode, expectedFieldPath, testLabel, expectedSourcePath = deterministicShardDocAuthorityPaths.compatibility, ) => { await writeModuleHelpCsv(rows); try { await runShardDocAuthorityValidation(); assert(false, testLabel, 'Expected shard-doc authority validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === expectedSourcePath, `${testLabel} returns expected source path`, `Expected ${expectedSourcePath}, got ${error.sourcePath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(expectedSourcePath), `${testLabel} includes deterministic message context`, ); } }; await fs.writeFile(tempShardDocAuthoritySidecarPath, yaml.stringify(validShardDocAuthoritySidecar), 'utf8'); await fs.writeFile(tempShardDocAuthoritySourcePath, '\n', 'utf8'); await writeModuleHelpCsv([ { workflowFile: deterministicShardDocAuthorityPaths.workflowFile, command: 'bmad-shard-doc', name: 'Shard Document', }, ]); const shardDocAuthorityValidation = await runShardDocAuthorityValidation(); assert( shardDocAuthorityValidation.authoritativePresenceKey === 'capability:bmad-shard-doc', 'Shard-doc authority validation returns expected authoritative presence key', ); assert( Array.isArray(shardDocAuthorityValidation.authoritativeRecords) && shardDocAuthorityValidation.authoritativeRecords.length === 2, 'Shard-doc authority validation returns sidecar and source authority records', ); const shardDocSidecarRecord = shardDocAuthorityValidation.authoritativeRecords.find( (record) => record.authoritySourceType === 'sidecar', ); const shardDocSourceRecord = shardDocAuthorityValidation.authoritativeRecords.find( (record) => record.authoritySourceType === 'source-xml', ); assert( shardDocSidecarRecord && shardDocSourceRecord && shardDocSidecarRecord.authoritativePresenceKey === shardDocSourceRecord.authoritativePresenceKey, 'Shard-doc sidecar and source-xml records share one authoritative presence key', ); assert( shardDocSidecarRecord && shardDocSourceRecord && shardDocSidecarRecord.authoritativePresenceKey === 'capability:bmad-shard-doc' && shardDocSourceRecord.authoritativePresenceKey === 'capability:bmad-shard-doc', 'Shard-doc authority records lock authoritative presence key to capability:bmad-shard-doc', ); assert( shardDocSidecarRecord && shardDocSidecarRecord.authoritySourcePath === deterministicShardDocAuthorityPaths.sidecar, 'Shard-doc metadata authority record preserves sidecar source path', ); assert( shardDocSourceRecord && shardDocSourceRecord.authoritySourcePath === deterministicShardDocAuthorityPaths.source, 'Shard-doc source-body authority record preserves source XML path', ); await expectShardDocAuthorityValidationError( [ { workflowFile: deterministicShardDocAuthorityPaths.workflowFile, command: 'legacy-shard-doc', name: 'Shard Document', }, ], SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH, 'command', 'Shard-doc compatibility command mismatch', ); await expectShardDocAuthorityValidationError( [ { workflowFile: '_bmad/core/tasks/help.md', command: 'bmad-shard-doc', name: 'Shard Document', }, ], SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_MISSING, 'workflow-file', 'Shard-doc missing compatibility row', ); await expectShardDocAuthorityValidationError( [ { workflowFile: deterministicShardDocAuthorityPaths.workflowFile, command: 'bmad-shard-doc', name: 'Shard Document', }, { workflowFile: '_bmad/core/tasks/another.xml', command: 'bmad-shard-doc', name: 'Shard Document', }, ], SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.DUPLICATE_CANONICAL_COMMAND, 'command', 'Shard-doc duplicate canonical command rows', ); await fs.writeFile( tempShardDocAuthoritySidecarPath, yaml.stringify({ ...validShardDocAuthoritySidecar, canonicalId: 'bmad-shard-doc-renamed', }), 'utf8', ); await expectShardDocAuthorityValidationError( [ { workflowFile: deterministicShardDocAuthorityPaths.workflowFile, command: 'bmad-shard-doc-renamed', name: 'Shard Document', }, ], SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH, 'canonicalId', 'Shard-doc canonicalId drift fails deterministic authority validation', deterministicShardDocAuthorityPaths.sidecar, ); await fs.writeFile(tempShardDocAuthoritySidecarPath, yaml.stringify(validShardDocAuthoritySidecar), 'utf8'); const tempIndexDocsAuthoritySidecarPath = path.join(tempAuthorityRoot, 'index-docs.artifact.yaml'); const tempIndexDocsAuthoritySourcePath = path.join(tempAuthorityRoot, 'index-docs.xml'); const tempIndexDocsModuleHelpPath = path.join(tempAuthorityRoot, 'index-docs-module-help.csv'); const deterministicIndexDocsAuthorityPaths = { sidecar: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', source: 'bmad-fork/src/core/tasks/index-docs.xml', compatibility: 'bmad-fork/src/core/module-help.csv', workflowFile: '_bmad/core/tasks/index-docs.xml', }; const validIndexDocsAuthoritySidecar = { schemaVersion: 1, canonicalId: 'bmad-index-docs', artifactType: 'task', module: 'core', sourcePath: deterministicIndexDocsAuthorityPaths.source, displayName: 'Index Docs', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', dependencies: { requires: [], }, }; const writeIndexDocsModuleHelpCsv = async (rows) => { const header = 'module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs'; const lines = rows.map((row) => [ row.module ?? 'core', row.phase ?? 'anytime', row.name ?? 'Index Docs', row.code ?? 'ID', row.sequence ?? '', row.workflowFile ?? '', row.command ?? '', row.required ?? 'false', row.agent ?? '', row.options ?? '', row.description ?? 'Compatibility row', row.outputLocation ?? '', row.outputs ?? '', ].join(','), ); await fs.writeFile(tempIndexDocsModuleHelpPath, [header, ...lines].join('\n'), 'utf8'); }; const runIndexDocsAuthorityValidation = async () => validateIndexDocsAuthoritySplitAndPrecedence({ sidecarPath: tempIndexDocsAuthoritySidecarPath, sourceXmlPath: tempIndexDocsAuthoritySourcePath, compatibilityCatalogPath: tempIndexDocsModuleHelpPath, sidecarSourcePath: deterministicIndexDocsAuthorityPaths.sidecar, sourceXmlSourcePath: deterministicIndexDocsAuthorityPaths.source, compatibilityCatalogSourcePath: deterministicIndexDocsAuthorityPaths.compatibility, compatibilityWorkflowFilePath: deterministicIndexDocsAuthorityPaths.workflowFile, }); const expectIndexDocsAuthorityValidationError = async ( rows, expectedCode, expectedFieldPath, testLabel, expectedSourcePath = deterministicIndexDocsAuthorityPaths.compatibility, ) => { await writeIndexDocsModuleHelpCsv(rows); try { await runIndexDocsAuthorityValidation(); assert(false, testLabel, 'Expected index-docs authority validation error but validation passed'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === expectedSourcePath, `${testLabel} returns expected source path`, `Expected ${expectedSourcePath}, got ${error.sourcePath}`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(expectedSourcePath), `${testLabel} includes deterministic message context`, ); } }; await fs.writeFile(tempIndexDocsAuthoritySidecarPath, yaml.stringify(validIndexDocsAuthoritySidecar), 'utf8'); await fs.writeFile(tempIndexDocsAuthoritySourcePath, '\n', 'utf8'); await writeIndexDocsModuleHelpCsv([ { workflowFile: deterministicIndexDocsAuthorityPaths.workflowFile, command: 'bmad-index-docs', name: 'Index Docs', }, ]); const indexDocsAuthorityValidation = await runIndexDocsAuthorityValidation(); assert( indexDocsAuthorityValidation.authoritativePresenceKey === 'capability:bmad-index-docs', 'Index-docs authority validation returns expected authoritative presence key', ); assert( Array.isArray(indexDocsAuthorityValidation.authoritativeRecords) && indexDocsAuthorityValidation.authoritativeRecords.length === 2, 'Index-docs authority validation returns sidecar and source authority records', ); const indexDocsSidecarRecord = indexDocsAuthorityValidation.authoritativeRecords.find( (record) => record.authoritySourceType === 'sidecar', ); const indexDocsSourceRecord = indexDocsAuthorityValidation.authoritativeRecords.find( (record) => record.authoritySourceType === 'source-xml', ); assert( indexDocsSidecarRecord && indexDocsSourceRecord && indexDocsSidecarRecord.authoritativePresenceKey === indexDocsSourceRecord.authoritativePresenceKey, 'Index-docs sidecar and source-xml records share one authoritative presence key', ); assert( indexDocsSidecarRecord && indexDocsSourceRecord && indexDocsSidecarRecord.authoritativePresenceKey === 'capability:bmad-index-docs' && indexDocsSourceRecord.authoritativePresenceKey === 'capability:bmad-index-docs', 'Index-docs authority records lock authoritative presence key to capability:bmad-index-docs', ); assert( indexDocsSidecarRecord && indexDocsSidecarRecord.authoritySourcePath === deterministicIndexDocsAuthorityPaths.sidecar, 'Index-docs metadata authority record preserves sidecar source path', ); assert( indexDocsSourceRecord && indexDocsSourceRecord.authoritySourcePath === deterministicIndexDocsAuthorityPaths.source, 'Index-docs source-body authority record preserves source XML path', ); await expectIndexDocsAuthorityValidationError( [ { workflowFile: deterministicIndexDocsAuthorityPaths.workflowFile, command: 'legacy-index-docs', name: 'Index Docs', }, ], INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH, 'command', 'Index-docs compatibility command mismatch', ); await expectIndexDocsAuthorityValidationError( [ { workflowFile: '_bmad/core/tasks/help.md', command: 'bmad-index-docs', name: 'Index Docs', }, ], INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_MISSING, 'workflow-file', 'Index-docs missing compatibility row', ); await expectIndexDocsAuthorityValidationError( [ { workflowFile: deterministicIndexDocsAuthorityPaths.workflowFile, command: 'bmad-index-docs', name: 'Index Docs', }, { workflowFile: '_bmad/core/tasks/another.xml', command: 'bmad-index-docs', name: 'Index Docs', }, ], INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.DUPLICATE_CANONICAL_COMMAND, 'command', 'Index-docs duplicate canonical command rows', ); await fs.writeFile( tempIndexDocsAuthoritySidecarPath, yaml.stringify({ ...validIndexDocsAuthoritySidecar, canonicalId: 'bmad-index-docs-renamed', }), 'utf8', ); await expectIndexDocsAuthorityValidationError( [ { workflowFile: deterministicIndexDocsAuthorityPaths.workflowFile, command: 'bmad-index-docs-renamed', name: 'Index Docs', }, ], INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH, 'canonicalId', 'Index-docs canonicalId drift fails deterministic authority validation', deterministicIndexDocsAuthorityPaths.sidecar, ); await fs.writeFile(tempIndexDocsAuthoritySidecarPath, yaml.stringify(validIndexDocsAuthoritySidecar), 'utf8'); } catch (error) { assert(false, 'Authority split and precedence suite setup', error.message); } finally { await fs.remove(tempAuthorityRoot); } console.log(''); // ============================================================ // Test 6: Installer Fail-Fast Pre-Generation // ============================================================ console.log(`${colors.yellow}Test Suite 6: Installer Fail-Fast Pre-Generation${colors.reset}\n`); const tempInstallerRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-installer-sidecar-failfast-')); try { // 6a: Existing help sidecar fail-fast behavior remains intact. { const installer = new Installer(); let shardDocValidationCalled = false; let indexDocsValidationCalled = false; let shardDocAuthorityValidationCalled = false; let indexDocsAuthorityValidationCalled = false; let helpAuthorityValidationCalled = false; let generateConfigsCalled = false; let manifestGenerationCalled = false; let helpCatalogGenerationCalled = false; let successResultCount = 0; installer.validateShardDocSidecarContractFile = async () => { shardDocValidationCalled = true; }; installer.validateIndexDocsSidecarContractFile = async () => { indexDocsValidationCalled = true; }; installer.validateHelpSidecarContractFile = async () => { const error = new Error(expectedUnsupportedMajorDetail); error.code = HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED; error.fieldPath = 'schemaVersion'; error.detail = expectedUnsupportedMajorDetail; throw error; }; installer.validateShardDocAuthoritySplitAndPrecedence = async () => { shardDocAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-shard-doc', }; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { indexDocsAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-index-docs', }; }; installer.validateHelpAuthoritySplitAndPrecedence = async () => { helpAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-help', }; }; installer.generateModuleConfigs = async () => { generateConfigsCalled = true; }; installer.mergeModuleHelpCatalogs = async () => { helpCatalogGenerationCalled = true; }; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests() { manifestGenerationCalled = true; return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; try { await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempInstallerRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: () => { successResultCount += 1; }, }); assert( false, 'Installer fail-fast blocks projection generation on help sidecar validation failure', 'Expected sidecar validation failure but configuration generation completed', ); } catch (error) { assert( error.code === HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, 'Installer fail-fast surfaces help sidecar validation error code', `Expected ${HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED}, got ${error.code}`, ); assert(shardDocValidationCalled, 'Installer runs shard-doc sidecar validation before help sidecar validation'); assert(indexDocsValidationCalled, 'Installer runs index-docs sidecar validation before help sidecar validation'); assert( !shardDocAuthorityValidationCalled && !indexDocsAuthorityValidationCalled && !helpAuthorityValidationCalled && !generateConfigsCalled && !manifestGenerationCalled && !helpCatalogGenerationCalled, 'Installer help fail-fast prevents downstream authority/config/manifest/help generation', ); assert( successResultCount === 0, 'Installer help fail-fast records no successful projection milestones', `Expected 0, got ${successResultCount}`, ); } } // 6b: Shard-doc fail-fast covers Shard-doc negative matrix classes. { const deterministicShardDocFailFastSourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; const shardDocFailureScenarios = [ { label: 'missing shard-doc sidecar file', code: SHARD_DOC_SIDECAR_ERROR_CODES.FILE_NOT_FOUND, fieldPath: '', detail: 'Expected shard-doc sidecar file was not found.', }, { label: 'malformed shard-doc sidecar YAML', code: SHARD_DOC_SIDECAR_ERROR_CODES.PARSE_FAILED, fieldPath: '', detail: 'YAML parse failure: malformed content', }, { label: 'missing shard-doc required field', code: SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING, fieldPath: 'canonicalId', detail: 'Missing required sidecar field "canonicalId".', }, { label: 'empty shard-doc required field', code: SHARD_DOC_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY, fieldPath: 'canonicalId', detail: 'Required sidecar field "canonicalId" must be a non-empty string.', }, { label: 'unsupported shard-doc sidecar major schema version', code: SHARD_DOC_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED, fieldPath: 'schemaVersion', detail: expectedUnsupportedMajorDetail, }, { label: 'shard-doc sourcePath basename mismatch', code: SHARD_DOC_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, fieldPath: 'sourcePath', detail: expectedBasenameMismatchDetail, }, ]; for (const scenario of shardDocFailureScenarios) { const installer = new Installer(); let indexDocsValidationCalled = false; let helpValidationCalled = false; let shardDocAuthorityValidationCalled = false; let indexDocsAuthorityValidationCalled = false; let helpAuthorityValidationCalled = false; let generateConfigsCalled = false; let manifestGenerationCalled = false; let helpCatalogGenerationCalled = false; let successResultCount = 0; installer.validateShardDocSidecarContractFile = async () => { const error = new Error(scenario.detail); error.code = scenario.code; error.fieldPath = scenario.fieldPath; error.sourcePath = deterministicShardDocFailFastSourcePath; error.detail = scenario.detail; throw error; }; installer.validateIndexDocsSidecarContractFile = async () => { indexDocsValidationCalled = true; }; installer.validateHelpSidecarContractFile = async () => { helpValidationCalled = true; }; installer.validateShardDocAuthoritySplitAndPrecedence = async () => { shardDocAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-shard-doc', }; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { indexDocsAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-index-docs', }; }; installer.validateHelpAuthoritySplitAndPrecedence = async () => { helpAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-help', }; }; installer.generateModuleConfigs = async () => { generateConfigsCalled = true; }; installer.mergeModuleHelpCatalogs = async () => { helpCatalogGenerationCalled = true; }; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests() { manifestGenerationCalled = true; return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; try { await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempInstallerRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: () => { successResultCount += 1; }, }); assert(false, `Installer fail-fast blocks projection generation on ${scenario.label}`); } catch (error) { assert(error.code === scenario.code, `Installer ${scenario.label} returns deterministic error code`); assert(error.fieldPath === scenario.fieldPath, `Installer ${scenario.label} returns deterministic field path`); assert( error.sourcePath === deterministicShardDocFailFastSourcePath, `Installer ${scenario.label} returns deterministic source path`, ); assert(!indexDocsValidationCalled, `Installer ${scenario.label} aborts before index-docs sidecar validation`); assert(!helpValidationCalled, `Installer ${scenario.label} aborts before help sidecar validation`); assert( !shardDocAuthorityValidationCalled && !indexDocsAuthorityValidationCalled && !helpAuthorityValidationCalled && !generateConfigsCalled && !manifestGenerationCalled && !helpCatalogGenerationCalled, `Installer ${scenario.label} prevents downstream authority/config/manifest/help generation`, ); assert(successResultCount === 0, `Installer ${scenario.label} records no successful projection milestones`); } } } // 6c: Shard-doc authority precedence conflict fails fast before help authority or generation. { const installer = new Installer(); let indexDocsAuthorityValidationCalled = false; let helpAuthorityValidationCalled = false; let generateConfigsCalled = false; let manifestGenerationCalled = false; let helpCatalogGenerationCalled = false; let successResultCount = 0; installer.validateShardDocSidecarContractFile = async () => {}; installer.validateIndexDocsSidecarContractFile = async () => {}; installer.validateHelpSidecarContractFile = async () => {}; installer.validateShardDocAuthoritySplitAndPrecedence = async () => { const error = new Error('Converted shard-doc compatibility command must match sidecar canonicalId'); error.code = SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH; error.fieldPath = 'command'; error.sourcePath = 'bmad-fork/src/core/module-help.csv'; throw error; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { indexDocsAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-index-docs', }; }; installer.validateHelpAuthoritySplitAndPrecedence = async () => { helpAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-help', }; }; installer.generateModuleConfigs = async () => { generateConfigsCalled = true; }; installer.mergeModuleHelpCatalogs = async () => { helpCatalogGenerationCalled = true; }; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests() { manifestGenerationCalled = true; return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; try { await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempInstallerRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: () => { successResultCount += 1; }, }); assert(false, 'Installer shard-doc authority mismatch fails fast pre-projection'); } catch (error) { assert( error.code === SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH, 'Installer shard-doc authority mismatch returns deterministic error code', ); assert(error.fieldPath === 'command', 'Installer shard-doc authority mismatch returns deterministic field path'); assert( error.sourcePath === 'bmad-fork/src/core/module-help.csv', 'Installer shard-doc authority mismatch returns deterministic source path', ); assert( !indexDocsAuthorityValidationCalled && !helpAuthorityValidationCalled && !generateConfigsCalled && !manifestGenerationCalled && !helpCatalogGenerationCalled, 'Installer shard-doc authority mismatch blocks downstream help authority/config/manifest/help generation', ); assert( successResultCount === 3, 'Installer shard-doc authority mismatch records only sidecar gate pass milestones before abort', `Expected 3, got ${successResultCount}`, ); } } // 6d: Shard-doc canonical drift fails fast before help authority or generation. { const installer = new Installer(); let indexDocsAuthorityValidationCalled = false; let helpAuthorityValidationCalled = false; let generateConfigsCalled = false; let manifestGenerationCalled = false; let helpCatalogGenerationCalled = false; let successResultCount = 0; installer.validateShardDocSidecarContractFile = async () => {}; installer.validateIndexDocsSidecarContractFile = async () => {}; installer.validateHelpSidecarContractFile = async () => {}; installer.validateShardDocAuthoritySplitAndPrecedence = async () => { const error = new Error('Converted shard-doc sidecar canonicalId must remain locked to bmad-shard-doc'); error.code = SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH; error.fieldPath = 'canonicalId'; error.sourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; throw error; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { indexDocsAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-index-docs', }; }; installer.validateHelpAuthoritySplitAndPrecedence = async () => { helpAuthorityValidationCalled = true; return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-help', }; }; installer.generateModuleConfigs = async () => { generateConfigsCalled = true; }; installer.mergeModuleHelpCatalogs = async () => { helpCatalogGenerationCalled = true; }; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests() { manifestGenerationCalled = true; return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; try { await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempInstallerRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: () => { successResultCount += 1; }, }); assert(false, 'Installer shard-doc canonical drift fails fast pre-projection'); } catch (error) { assert( error.code === SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH, 'Installer shard-doc canonical drift returns deterministic error code', ); assert(error.fieldPath === 'canonicalId', 'Installer shard-doc canonical drift returns deterministic field path'); assert( error.sourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', 'Installer shard-doc canonical drift returns deterministic source path', ); assert( !indexDocsAuthorityValidationCalled && !helpAuthorityValidationCalled && !generateConfigsCalled && !manifestGenerationCalled && !helpCatalogGenerationCalled, 'Installer shard-doc canonical drift blocks downstream help authority/config/manifest/help generation', ); assert( successResultCount === 3, 'Installer shard-doc canonical drift records only sidecar gate pass milestones before abort', `Expected 3, got ${successResultCount}`, ); } } // 6e: Valid sidecars preserve fail-fast ordering and allow generation path. { const installer = new Installer(); const executionOrder = []; const resultMilestones = []; installer.validateShardDocSidecarContractFile = async () => { executionOrder.push('shard-doc-sidecar'); }; installer.validateIndexDocsSidecarContractFile = async () => { executionOrder.push('index-docs-sidecar'); }; installer.validateHelpSidecarContractFile = async () => { executionOrder.push('help-sidecar'); }; installer.validateShardDocAuthoritySplitAndPrecedence = async () => { executionOrder.push('shard-doc-authority'); return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-shard-doc', }; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { executionOrder.push('index-docs-authority'); return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-index-docs', }; }; installer.validateHelpAuthoritySplitAndPrecedence = async () => { executionOrder.push('help-authority'); return { authoritativeRecords: [], authoritativePresenceKey: 'capability:bmad-help', }; }; installer.generateModuleConfigs = async () => { executionOrder.push('config-generation'); }; installer.mergeModuleHelpCatalogs = async () => { executionOrder.push('help-catalog-generation'); }; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests() { executionOrder.push('manifest-generation'); return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempInstallerRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: (name) => { resultMilestones.push(name); }, }); assert( executionOrder.join(' -> ') === 'shard-doc-sidecar -> index-docs-sidecar -> help-sidecar -> shard-doc-authority -> index-docs-authority -> help-authority -> config-generation -> manifest-generation -> help-catalog-generation', 'Installer valid sidecar path preserves fail-fast gate ordering and continues generation flow', `Observed order: ${executionOrder.join(' -> ')}`, ); assert( resultMilestones.includes('Shard-doc sidecar contract'), 'Installer valid sidecar path records explicit shard-doc sidecar gate pass milestone', ); assert( resultMilestones.includes('Index-docs sidecar contract'), 'Installer valid sidecar path records explicit index-docs sidecar gate pass milestone', ); assert( resultMilestones.includes('Shard-doc authority split'), 'Installer valid sidecar path records explicit shard-doc authority gate pass milestone', ); assert( resultMilestones.includes('Index-docs authority split'), 'Installer valid sidecar path records explicit index-docs authority gate pass milestone', ); } } catch (error) { assert(false, 'Installer fail-fast test setup', error.message); } finally { await fs.remove(tempInstallerRoot); } console.log(''); // ============================================================ // Test 7: Canonical Alias Normalization Core // ============================================================ console.log(`${colors.yellow}Test Suite 7: Canonical Alias Normalization Core${colors.reset}\n`); const deterministicAliasTableSourcePath = '_bmad/_config/canonical-aliases.csv'; const expectAliasNormalizationError = async ( operation, expectedCode, expectedFieldPath, expectedObservedValue, testLabel, expectedDetail = null, ) => { try { await Promise.resolve(operation()); assert(false, testLabel, 'Expected alias normalization error but operation succeeded'); } catch (error) { assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`); assert( error.fieldPath === expectedFieldPath, `${testLabel} returns expected field path`, `Expected ${expectedFieldPath}, got ${error.fieldPath}`, ); assert( error.sourcePath === deterministicAliasTableSourcePath, `${testLabel} returns expected source path`, `Expected ${deterministicAliasTableSourcePath}, got ${error.sourcePath}`, ); assert( error.observedValue === expectedObservedValue, `${testLabel} returns normalized offending value context`, `Expected "${expectedObservedValue}", got "${error.observedValue}"`, ); assert( typeof error.message === 'string' && error.message.includes(expectedCode) && error.message.includes(expectedFieldPath) && error.message.includes(deterministicAliasTableSourcePath), `${testLabel} includes deterministic message context`, ); if (expectedDetail !== null) { assert( error.detail === expectedDetail, `${testLabel} returns locked detail string`, `Expected "${expectedDetail}", got "${error.detail}"`, ); } } }; try { const canonicalTuple = normalizeRawIdentityToTuple(' BMAD-HELP ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); assert(canonicalTuple.rawIdentityHasLeadingSlash === false, 'Canonical tuple sets rawIdentityHasLeadingSlash=false'); assert(canonicalTuple.preAliasNormalizedValue === 'bmad-help', 'Canonical tuple computes preAliasNormalizedValue=bmad-help'); assert(canonicalTuple.normalizedRawIdentity === 'bmad-help', 'Canonical tuple computes normalizedRawIdentity'); const canonicalResolution = resolveAliasTupleFromRows(canonicalTuple, LOCKED_EXEMPLAR_ALIAS_ROWS, { sourcePath: deterministicAliasTableSourcePath, }); assert( canonicalResolution.aliasRowLocator === 'alias-row:bmad-help:canonical-id', 'Canonical tuple resolves to locked canonical-id row locator', ); assert(canonicalResolution.postAliasCanonicalId === 'bmad-help', 'Canonical tuple resolves to locked canonicalId'); const legacyResolution = await normalizeAndResolveExemplarAlias(' HELP ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); assert(legacyResolution.rawIdentityHasLeadingSlash === false, 'Legacy tuple sets rawIdentityHasLeadingSlash=false'); assert(legacyResolution.preAliasNormalizedValue === 'help', 'Legacy tuple computes preAliasNormalizedValue=help'); assert( legacyResolution.aliasRowLocator === 'alias-row:bmad-help:legacy-name', 'Legacy tuple resolves to locked legacy-name row locator', ); assert(legacyResolution.postAliasCanonicalId === 'bmad-help', 'Legacy tuple resolves to locked canonicalId'); const slashResolution = await normalizeAndResolveExemplarAlias(' /BMAD-HELP ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); assert(slashResolution.rawIdentityHasLeadingSlash === true, 'Slash tuple sets rawIdentityHasLeadingSlash=true'); assert(slashResolution.preAliasNormalizedValue === 'bmad-help', 'Slash tuple computes preAliasNormalizedValue=bmad-help'); assert( slashResolution.aliasRowLocator === 'alias-row:bmad-help:slash-command', 'Slash tuple resolves to locked slash-command row locator', ); assert(slashResolution.postAliasCanonicalId === 'bmad-help', 'Slash tuple resolves to locked canonicalId'); const tempAliasAuthorityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-alias-authority-')); const tempAliasSidecarPath = path.join(tempAliasAuthorityRoot, 'help.artifact.yaml'); const tempAliasSourcePath = path.join(tempAliasAuthorityRoot, 'help-source.md'); const tempAliasRuntimePath = path.join(tempAliasAuthorityRoot, 'help-runtime.md'); const tempAliasConfigDir = path.join(tempAliasAuthorityRoot, '_config'); const tempAuthorityAliasTablePath = path.join(tempAliasConfigDir, 'canonical-aliases.csv'); const aliasAuthorityPaths = { sidecar: 'bmad-fork/src/core/tasks/help.artifact.yaml', source: 'bmad-fork/src/core/tasks/help.md', runtime: '_bmad/core/tasks/help.md', }; const aliasFrontmatter = { name: 'help', description: 'Help command', canonicalId: 'help', dependencies: { requires: [], }, }; try { await fs.writeFile( tempAliasSidecarPath, yaml.stringify({ schemaVersion: 1, canonicalId: 'help', artifactType: 'task', module: 'core', sourcePath: aliasAuthorityPaths.source, displayName: 'help', description: 'Help command', dependencies: { requires: [], }, }), 'utf8', ); await fs.writeFile(tempAliasSourcePath, `---\n${yaml.stringify(aliasFrontmatter).trimEnd()}\n---\n\n# Help\n`, 'utf8'); await fs.writeFile(tempAliasRuntimePath, `---\n${yaml.stringify(aliasFrontmatter).trimEnd()}\n---\n\n# Help\n`, 'utf8'); const aliasAuthorityValidation = await validateHelpAuthoritySplitAndPrecedence({ sidecarPath: tempAliasSidecarPath, sourceMarkdownPath: tempAliasSourcePath, runtimeMarkdownPath: tempAliasRuntimePath, sidecarSourcePath: aliasAuthorityPaths.sidecar, sourceMarkdownSourcePath: aliasAuthorityPaths.source, runtimeMarkdownSourcePath: aliasAuthorityPaths.runtime, }); assert( aliasAuthorityValidation.canonicalId === 'bmad-help', 'Authority validation normalizes legacy canonical identity to locked canonicalId', ); assert( aliasAuthorityValidation.authoritativePresenceKey === 'capability:bmad-help', 'Authority validation emits canonical presence key after alias resolution', ); await fs.ensureDir(tempAliasConfigDir); await fs.writeFile( tempAuthorityAliasTablePath, [ 'rowIdentity,canonicalId,normalizedAliasValue,rawIdentityHasLeadingSlash', 'alias-row:bmad-help:legacy-name,bmad-help-csv,help,false', ].join('\n') + '\n', 'utf8', ); const csvBackedAuthorityValidation = await validateHelpAuthoritySplitAndPrecedence({ sidecarPath: tempAliasSidecarPath, sourceMarkdownPath: tempAliasSourcePath, runtimeMarkdownPath: tempAliasRuntimePath, sidecarSourcePath: aliasAuthorityPaths.sidecar, sourceMarkdownSourcePath: aliasAuthorityPaths.source, runtimeMarkdownSourcePath: aliasAuthorityPaths.runtime, bmadDir: tempAliasAuthorityRoot, }); assert( csvBackedAuthorityValidation.canonicalId === 'bmad-help-csv', 'Authority validation prefers canonical alias CSV when available', ); assert( csvBackedAuthorityValidation.authoritativePresenceKey === 'capability:bmad-help-csv', 'Authority validation derives presence key from CSV-resolved canonical identity', ); } finally { await fs.remove(tempAliasAuthorityRoot); } const collapsedWhitespaceTuple = normalizeRawIdentityToTuple(' bmad\t\thelp ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); assert( collapsedWhitespaceTuple.preAliasNormalizedValue === 'bmad help', 'Tuple normalization collapses internal whitespace runs deterministically', ); await expectAliasNormalizationError( () => normalizeRawIdentityToTuple(' \n\t ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.EMPTY_INPUT, 'canonicalId', '', 'Empty alias input', 'alias identity is empty after normalization', ); await expectAliasNormalizationError( () => normalizeRawIdentityToTuple('//bmad-help', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.MULTIPLE_LEADING_SLASHES, 'canonicalId', '//bmad-help', 'Alias input with multiple leading slashes', 'alias identity contains multiple leading slashes', ); await expectAliasNormalizationError( () => normalizeRawIdentityToTuple('/ ', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.EMPTY_PREALIAS, 'preAliasNormalizedValue', '/', 'Alias input with empty pre-alias value', 'alias preAliasNormalizedValue is empty after slash normalization', ); await expectAliasNormalizationError( () => normalizeAndResolveExemplarAlias('not-a-locked-alias', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'preAliasNormalizedValue', 'not-a-locked-alias|leadingSlash:false', 'Unresolved alias tuple', 'alias tuple did not resolve to any canonical alias row', ); const ambiguousAliasRows = [ { rowIdentity: 'alias-row:a', canonicalId: 'bmad-help', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: false, }, { rowIdentity: 'alias-row:b', canonicalId: 'legacy-help', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: false, }, ]; const ambiguousTuple = normalizeRawIdentityToTuple('help', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); await expectAliasNormalizationError( () => resolveAliasTupleFromRows(ambiguousTuple, ambiguousAliasRows, { sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'preAliasNormalizedValue', 'help|leadingSlash:false', 'Ambiguous alias tuple resolution', 'alias tuple resolved ambiguously to multiple canonical alias rows', ); const shardDocAliasRows = [ { rowIdentity: 'alias-row:bmad-shard-doc:canonical-id', canonicalId: 'bmad-shard-doc', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: false, }, { rowIdentity: 'alias-row:bmad-shard-doc:legacy-name', canonicalId: 'bmad-shard-doc', normalizedAliasValue: 'shard-doc', rawIdentityHasLeadingSlash: false, }, { rowIdentity: 'alias-row:bmad-shard-doc:slash-command', canonicalId: 'bmad-shard-doc', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: true, }, ]; const shardDocSlashResolution = await normalizeAndResolveExemplarAlias('/bmad-shard-doc', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, aliasRows: shardDocAliasRows, aliasTableSourcePath: deterministicAliasTableSourcePath, }); assert( shardDocSlashResolution.postAliasCanonicalId === 'bmad-shard-doc' && shardDocSlashResolution.aliasRowLocator === 'alias-row:bmad-shard-doc:slash-command', 'Alias resolver normalizes shard-doc slash-command tuple with explicit shard-doc alias rows', ); await expectAliasNormalizationError( () => normalizeAndResolveExemplarAlias('/bmad-shard-doc', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, aliasRows: LOCKED_EXEMPLAR_ALIAS_ROWS, aliasTableSourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'preAliasNormalizedValue', 'bmad-shard-doc|leadingSlash:true', 'Shard-doc alias tuple unresolved without shard-doc alias table rows', 'alias tuple did not resolve to any canonical alias row', ); const ambiguousShardDocRows = [ ...shardDocAliasRows, { rowIdentity: 'alias-row:bmad-shard-doc:slash-command:duplicate', canonicalId: 'bmad-shard-doc-alt', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: true, }, ]; await expectAliasNormalizationError( () => normalizeAndResolveExemplarAlias('/bmad-shard-doc', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, aliasRows: ambiguousShardDocRows, aliasTableSourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'preAliasNormalizedValue', 'bmad-shard-doc|leadingSlash:true', 'Shard-doc alias tuple ambiguous when duplicate shard-doc slash-command rows exist', 'alias tuple resolved ambiguously to multiple canonical alias rows', ); const tempAliasTableRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-canonical-alias-table-')); const tempAliasTablePath = path.join(tempAliasTableRoot, 'canonical-aliases.csv'); const csvRows = [ 'rowIdentity,canonicalId,normalizedAliasValue,rawIdentityHasLeadingSlash', 'alias-row:bmad-help:canonical-id,bmad-help,bmad-help,false', 'alias-row:bmad-help:legacy-name,bmad-help,help,false', 'alias-row:bmad-help:slash-command,bmad-help,bmad-help,true', ]; try { await fs.writeFile(tempAliasTablePath, `${csvRows.join('\n')}\n`, 'utf8'); const csvTuple = normalizeRawIdentityToTuple('/bmad-help', { fieldPath: 'canonicalId', sourcePath: deterministicAliasTableSourcePath, }); const csvResolution = await resolveAliasTupleUsingCanonicalAliasCsv(csvTuple, tempAliasTablePath, { sourcePath: deterministicAliasTableSourcePath, }); assert( csvResolution.aliasRowLocator === 'alias-row:bmad-help:slash-command', 'CSV-backed tuple resolution maps slash-command alias row locator', ); assert(csvResolution.postAliasCanonicalId === 'bmad-help', 'CSV-backed tuple resolution maps canonicalId'); const manifestGenerator = new ManifestGenerator(); const normalizedHelpAuthorityRecords = await manifestGenerator.normalizeHelpAuthorityRecords([ { recordType: 'metadata-authority', canonicalId: 'help', authoritativePresenceKey: 'capability:legacy-help', authoritySourceType: 'sidecar', authoritySourcePath: aliasAuthorityPaths.sidecar, sourcePath: aliasAuthorityPaths.source, }, ]); assert( normalizedHelpAuthorityRecords.length === 1 && normalizedHelpAuthorityRecords[0].canonicalId === 'bmad-help', 'Manifest generator normalizes legacy canonical identities using alias tuple resolution', ); assert( normalizedHelpAuthorityRecords.length === 1 && normalizedHelpAuthorityRecords[0].authoritativePresenceKey === 'capability:bmad-help', 'Manifest generator canonicalizes authoritative presence key from normalized canonicalId', ); await expectAliasNormalizationError( () => manifestGenerator.normalizeHelpAuthorityRecords([ { recordType: 'metadata-authority', canonicalId: 'not-a-locked-alias', authoritativePresenceKey: 'capability:not-a-locked-alias', authoritySourceType: 'sidecar', authoritySourcePath: aliasAuthorityPaths.sidecar, sourcePath: aliasAuthorityPaths.source, }, ]), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'preAliasNormalizedValue', 'not-a-locked-alias|leadingSlash:false', 'Manifest generator fails unresolved canonical identity normalization', 'alias tuple did not resolve to any canonical alias row', ); await expectAliasNormalizationError( () => resolveAliasTupleUsingCanonicalAliasCsv(csvTuple, path.join(tempAliasTableRoot, 'missing.csv'), { sourcePath: deterministicAliasTableSourcePath, }), HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED, 'aliasTablePath', path.join(tempAliasTableRoot, 'missing.csv'), 'CSV-backed alias resolution with missing table file', 'canonical alias table file was not found', ); } finally { await fs.remove(tempAliasTableRoot); } } catch (error) { assert(false, 'Canonical alias normalization suite setup', error.message); } console.log(''); // ============================================================ // Test 8: Additive Task Manifest Projection // ============================================================ console.log(`${colors.yellow}Test Suite 8: Additive Task Manifest Projection${colors.reset}\n`); const tempTaskManifestRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-task-manifest-')); try { const manifestGenerator = new ManifestGenerator(); manifestGenerator.bmadDir = tempTaskManifestRoot; manifestGenerator.bmadFolderName = '_bmad'; manifestGenerator.tasks = [ { name: 'help', displayName: 'help', description: 'Help command', module: 'core', path: 'core/tasks/help.md', standalone: true, }, { name: 'validate-workflow', displayName: 'validate-workflow', description: 'Validate workflow', module: 'core', path: 'core/tasks/validate-workflow.xml', standalone: true, }, { name: 'shard-doc', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', module: 'core', path: 'core/tasks/shard-doc.xml', standalone: true, }, { name: 'index-docs', displayName: 'Index Docs', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', module: 'core', path: 'core/tasks/index-docs.xml', standalone: true, }, ]; manifestGenerator.helpAuthorityRecords = [ { recordType: 'metadata-authority', canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; manifestGenerator.taskAuthorityRecords = [ ...manifestGenerator.helpAuthorityRecords, { recordType: 'metadata-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', }, { recordType: 'metadata-authority', canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', }, ]; const tempTaskManifestConfigDir = path.join(tempTaskManifestRoot, '_config'); await fs.ensureDir(tempTaskManifestConfigDir); await manifestGenerator.writeTaskManifest(tempTaskManifestConfigDir); const writtenTaskManifestRaw = await fs.readFile(path.join(tempTaskManifestConfigDir, 'task-manifest.csv'), 'utf8'); const writtenTaskManifestLines = writtenTaskManifestRaw.trim().split('\n'); const expectedHeader = 'name,displayName,description,module,path,standalone,legacyName,canonicalId,authoritySourceType,authoritySourcePath'; assert( writtenTaskManifestLines[0] === expectedHeader, 'Task manifest writes compatibility-prefix columns with locked canonical appended column order', ); const writtenTaskManifestRecords = csv.parse(writtenTaskManifestRaw, { columns: true, skip_empty_lines: true, trim: true, }); const helpTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'help'); const validateTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'validate-workflow'); const shardDocTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'shard-doc'); const indexDocsTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'index-docs'); assert(!!helpTaskRow, 'Task manifest includes exemplar help row'); assert(helpTaskRow && helpTaskRow.legacyName === 'help', 'Task manifest help row sets legacyName=help'); assert(helpTaskRow && helpTaskRow.canonicalId === 'bmad-help', 'Task manifest help row sets canonicalId=bmad-help'); assert(helpTaskRow && helpTaskRow.authoritySourceType === 'sidecar', 'Task manifest help row sets authoritySourceType=sidecar'); assert( helpTaskRow && helpTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Task manifest help row sets authoritySourcePath to sidecar source path', ); assert(!!validateTaskRow, 'Task manifest preserves non-exemplar rows'); assert( validateTaskRow && validateTaskRow.legacyName === 'validate-workflow', 'Task manifest non-exemplar rows remain additive-compatible with default legacyName', ); assert(!!shardDocTaskRow, 'Task manifest includes converted shard-doc row'); assert(shardDocTaskRow && shardDocTaskRow.legacyName === 'shard-doc', 'Task manifest shard-doc row sets legacyName=shard-doc'); assert( shardDocTaskRow && shardDocTaskRow.canonicalId === 'bmad-shard-doc', 'Task manifest shard-doc row sets canonicalId=bmad-shard-doc', ); assert( shardDocTaskRow && shardDocTaskRow.authoritySourceType === 'sidecar', 'Task manifest shard-doc row sets authoritySourceType=sidecar', ); assert( shardDocTaskRow && shardDocTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', 'Task manifest shard-doc row sets authoritySourcePath to shard-doc sidecar source path', ); assert(!!indexDocsTaskRow, 'Task manifest includes converted index-docs row'); assert(indexDocsTaskRow && indexDocsTaskRow.legacyName === 'index-docs', 'Task manifest index-docs row sets legacyName=index-docs'); assert( indexDocsTaskRow && indexDocsTaskRow.canonicalId === 'bmad-index-docs', 'Task manifest index-docs row sets canonicalId=bmad-index-docs', ); assert( indexDocsTaskRow && indexDocsTaskRow.authoritySourceType === 'sidecar', 'Task manifest index-docs row sets authoritySourceType=sidecar', ); assert( indexDocsTaskRow && indexDocsTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', 'Task manifest index-docs row sets authoritySourcePath to index-docs sidecar source path', ); await manifestGenerator.writeTaskManifest(tempTaskManifestConfigDir); const repeatedTaskManifestRaw = await fs.readFile(path.join(tempTaskManifestConfigDir, 'task-manifest.csv'), 'utf8'); assert( repeatedTaskManifestRaw === writtenTaskManifestRaw, 'Task manifest shard-doc canonical row values remain deterministic across repeated generation runs', ); let capturedAuthorityValidationOptions = null; let capturedShardDocAuthorityValidationOptions = null; let capturedIndexDocsAuthorityValidationOptions = null; let capturedManifestHelpAuthorityRecords = null; let capturedManifestTaskAuthorityRecords = null; let capturedInstalledFiles = null; const installer = new Installer(); installer.validateShardDocSidecarContractFile = async () => {}; installer.validateIndexDocsSidecarContractFile = async () => {}; installer.validateHelpSidecarContractFile = async () => {}; installer.validateShardDocAuthoritySplitAndPrecedence = async (options) => { capturedShardDocAuthorityValidationOptions = options; return { authoritativePresenceKey: 'capability:bmad-shard-doc', authoritativeRecords: [ { recordType: 'metadata-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', authoritySourcePath: options.sidecarSourcePath, sourcePath: options.sourceXmlSourcePath, }, { recordType: 'source-body-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'source-xml', authoritySourcePath: options.sourceXmlSourcePath, sourcePath: options.sourceXmlSourcePath, }, ], }; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async (options) => { capturedIndexDocsAuthorityValidationOptions = options; return { authoritativePresenceKey: 'capability:bmad-index-docs', authoritativeRecords: [ { recordType: 'metadata-authority', canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', authoritySourcePath: options.sidecarSourcePath, sourcePath: options.sourceXmlSourcePath, }, { recordType: 'source-body-authority', canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'source-xml', authoritySourcePath: options.sourceXmlSourcePath, sourcePath: options.sourceXmlSourcePath, }, ], }; }; installer.validateHelpAuthoritySplitAndPrecedence = async (options) => { capturedAuthorityValidationOptions = options; return { authoritativePresenceKey: 'capability:bmad-help', authoritativeRecords: [ { recordType: 'metadata-authority', canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: options.sidecarSourcePath, sourcePath: options.sourceMarkdownSourcePath, }, ], }; }; installer.generateModuleConfigs = async () => {}; installer.mergeModuleHelpCatalogs = async () => {}; installer.ManifestGenerator = class ManifestGeneratorStub { async generateManifests(_bmadDir, _selectedModules, _installedFiles, options = {}) { capturedInstalledFiles = _installedFiles; capturedManifestHelpAuthorityRecords = options.helpAuthorityRecords; capturedManifestTaskAuthorityRecords = options.taskAuthorityRecords; return { workflows: 0, agents: 0, tasks: 0, tools: 0, }; } }; await installer.runConfigurationGenerationTask({ message: () => {}, bmadDir: tempTaskManifestRoot, moduleConfigs: { core: {} }, config: { ides: [] }, allModules: ['core'], addResult: () => {}, }); assert( capturedAuthorityValidationOptions && capturedAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Installer passes locked sidecar source path to authority validation', ); assert( capturedAuthorityValidationOptions && capturedAuthorityValidationOptions.sourceMarkdownSourcePath === 'bmad-fork/src/core/tasks/help.md', 'Installer passes locked source-markdown path to authority validation', ); assert( capturedAuthorityValidationOptions && capturedAuthorityValidationOptions.runtimeMarkdownSourcePath === '_bmad/core/tasks/help.md', 'Installer passes locked runtime markdown path to authority validation', ); assert( capturedShardDocAuthorityValidationOptions && capturedShardDocAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', 'Installer passes locked shard-doc sidecar source path to shard-doc authority validation', ); assert( capturedShardDocAuthorityValidationOptions && capturedShardDocAuthorityValidationOptions.sourceXmlSourcePath === 'bmad-fork/src/core/tasks/shard-doc.xml', 'Installer passes locked shard-doc source XML path to shard-doc authority validation', ); assert( capturedShardDocAuthorityValidationOptions && capturedShardDocAuthorityValidationOptions.compatibilityCatalogSourcePath === 'bmad-fork/src/core/module-help.csv', 'Installer passes locked module-help source path to shard-doc authority validation', ); assert( capturedIndexDocsAuthorityValidationOptions && capturedIndexDocsAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', 'Installer passes locked index-docs sidecar source path to index-docs authority validation', ); assert( capturedIndexDocsAuthorityValidationOptions && capturedIndexDocsAuthorityValidationOptions.sourceXmlSourcePath === 'bmad-fork/src/core/tasks/index-docs.xml', 'Installer passes locked index-docs source XML path to index-docs authority validation', ); assert( capturedIndexDocsAuthorityValidationOptions && capturedIndexDocsAuthorityValidationOptions.compatibilityCatalogSourcePath === 'bmad-fork/src/core/module-help.csv', 'Installer passes locked module-help source path to index-docs authority validation', ); assert( Array.isArray(capturedManifestHelpAuthorityRecords) && capturedManifestHelpAuthorityRecords[0] && capturedManifestHelpAuthorityRecords[0].authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Installer passes sidecar authority path into manifest generation options', ); assert( Array.isArray(capturedManifestTaskAuthorityRecords) && capturedManifestTaskAuthorityRecords.some( (record) => record && record.canonicalId === 'bmad-shard-doc' && record.authoritySourceType === 'sidecar' && record.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', ), 'Installer passes shard-doc sidecar authority records into task-manifest projection options', ); assert( Array.isArray(capturedManifestTaskAuthorityRecords) && capturedManifestTaskAuthorityRecords.some( (record) => record && record.canonicalId === 'bmad-index-docs' && record.authoritySourceType === 'sidecar' && record.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', ), 'Installer passes index-docs sidecar authority records into task-manifest projection options', ); assert( Array.isArray(capturedInstalledFiles) && capturedInstalledFiles.some((filePath) => filePath.endsWith('/_config/canonical-aliases.csv')), 'Installer pre-registers canonical-aliases.csv for files-manifest tracking', ); } catch (error) { assert(false, 'Additive task manifest projection suite setup', error.message); } finally { await fs.remove(tempTaskManifestRoot); } console.log(''); // ============================================================ // Test 9: Canonical Alias Table Projection // ============================================================ console.log(`${colors.yellow}Test Suite 9: Canonical Alias Table Projection${colors.reset}\n`); const tempCanonicalAliasRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-canonical-alias-projection-')); try { const manifestGenerator = new ManifestGenerator(); manifestGenerator.bmadDir = tempCanonicalAliasRoot; manifestGenerator.bmadFolderName = '_bmad'; manifestGenerator.helpAuthorityRecords = [ { recordType: 'metadata-authority', canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; manifestGenerator.taskAuthorityRecords = [ ...manifestGenerator.helpAuthorityRecords, { recordType: 'metadata-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', }, { recordType: 'metadata-authority', canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', }, ]; const tempCanonicalAliasConfigDir = path.join(tempCanonicalAliasRoot, '_config'); await fs.ensureDir(tempCanonicalAliasConfigDir); const canonicalAliasPath = await manifestGenerator.writeCanonicalAliasManifest(tempCanonicalAliasConfigDir); const canonicalAliasRaw = await fs.readFile(canonicalAliasPath, 'utf8'); const canonicalAliasLines = canonicalAliasRaw.trim().split('\n'); const expectedCanonicalAliasHeader = 'canonicalId,alias,aliasType,authoritySourceType,authoritySourcePath,rowIdentity,normalizedAliasValue,rawIdentityHasLeadingSlash,resolutionEligibility'; assert( canonicalAliasLines[0] === expectedCanonicalAliasHeader, 'Canonical alias table writes locked compatibility-prefix plus tuple eligibility column order', ); const canonicalAliasRows = csv.parse(canonicalAliasRaw, { columns: true, skip_empty_lines: true, trim: true, }); assert(canonicalAliasRows.length === 9, 'Canonical alias table emits help + shard-doc + index-docs canonical alias exemplar rows'); assert( canonicalAliasRows.map((row) => row.aliasType).join(',') === 'canonical-id,legacy-name,slash-command,canonical-id,legacy-name,slash-command,canonical-id,legacy-name,slash-command', 'Canonical alias table preserves locked deterministic row ordering', ); const expectedRowsByIdentity = new Map([ [ 'alias-row:bmad-help:canonical-id', { canonicalId: 'bmad-help', alias: 'bmad-help', aliasType: 'canonical-id', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', }, ], [ 'alias-row:bmad-help:legacy-name', { canonicalId: 'bmad-help', alias: 'help', aliasType: 'legacy-name', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', }, ], [ 'alias-row:bmad-help:slash-command', { canonicalId: 'bmad-help', alias: '/bmad-help', aliasType: 'slash-command', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', }, ], [ 'alias-row:bmad-shard-doc:canonical-id', { canonicalId: 'bmad-shard-doc', alias: 'bmad-shard-doc', aliasType: 'canonical-id', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', }, ], [ 'alias-row:bmad-shard-doc:legacy-name', { canonicalId: 'bmad-shard-doc', alias: 'shard-doc', aliasType: 'legacy-name', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', normalizedAliasValue: 'shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', }, ], [ 'alias-row:bmad-shard-doc:slash-command', { canonicalId: 'bmad-shard-doc', alias: '/bmad-shard-doc', aliasType: 'slash-command', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', }, ], [ 'alias-row:bmad-index-docs:canonical-id', { canonicalId: 'bmad-index-docs', alias: 'bmad-index-docs', aliasType: 'canonical-id', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', }, ], [ 'alias-row:bmad-index-docs:legacy-name', { canonicalId: 'bmad-index-docs', alias: 'index-docs', aliasType: 'legacy-name', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', normalizedAliasValue: 'index-docs', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', }, ], [ 'alias-row:bmad-index-docs:slash-command', { canonicalId: 'bmad-index-docs', alias: '/bmad-index-docs', aliasType: 'slash-command', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', }, ], ]); for (const [rowIdentity, expectedRow] of expectedRowsByIdentity) { const matchingRows = canonicalAliasRows.filter((row) => row.rowIdentity === rowIdentity); assert(matchingRows.length === 1, `Canonical alias table emits exactly one ${rowIdentity} exemplar row`); const row = matchingRows[0]; assert( row && row.authoritySourceType === 'sidecar' && row.authoritySourcePath === expectedRow.authoritySourcePath, `${rowIdentity} exemplar row uses locked sidecar provenance`, ); assert(row && row.canonicalId === expectedRow.canonicalId, `${rowIdentity} exemplar row locks canonicalId contract`); assert(row && row.alias === expectedRow.alias, `${rowIdentity} exemplar row locks alias contract`); assert(row && row.aliasType === expectedRow.aliasType, `${rowIdentity} exemplar row locks aliasType contract`); assert(row && row.rowIdentity === rowIdentity, `${rowIdentity} exemplar row locks rowIdentity contract`); assert( row && row.normalizedAliasValue === expectedRow.normalizedAliasValue, `${rowIdentity} exemplar row locks normalizedAliasValue contract`, ); assert( row && row.rawIdentityHasLeadingSlash === expectedRow.rawIdentityHasLeadingSlash, `${rowIdentity} exemplar row locks rawIdentityHasLeadingSlash contract`, ); assert( row && row.resolutionEligibility === expectedRow.resolutionEligibility, `${rowIdentity} exemplar row locks resolutionEligibility contract`, ); } const validateLockedCanonicalAliasProjection = (rows) => { for (const [rowIdentity, expectedRow] of expectedRowsByIdentity) { const matchingRows = rows.filter((row) => row.rowIdentity === rowIdentity); if (matchingRows.length === 0) { return { valid: false, reason: `missing:${rowIdentity}` }; } if (matchingRows.length > 1) { return { valid: false, reason: `conflict:${rowIdentity}` }; } const row = matchingRows[0]; if ( row.canonicalId !== expectedRow.canonicalId || row.alias !== expectedRow.alias || row.aliasType !== expectedRow.aliasType || row.authoritySourceType !== 'sidecar' || row.authoritySourcePath !== expectedRow.authoritySourcePath || row.rowIdentity !== rowIdentity || row.normalizedAliasValue !== expectedRow.normalizedAliasValue || row.rawIdentityHasLeadingSlash !== expectedRow.rawIdentityHasLeadingSlash || row.resolutionEligibility !== expectedRow.resolutionEligibility ) { return { valid: false, reason: `conflict:${rowIdentity}` }; } } if (rows.length !== expectedRowsByIdentity.size) { return { valid: false, reason: 'conflict:extra-rows' }; } return { valid: true, reason: 'ok' }; }; const baselineProjectionValidation = validateLockedCanonicalAliasProjection(canonicalAliasRows); assert( baselineProjectionValidation.valid, 'Canonical alias projection validator passes when all required exemplar rows are present exactly once', baselineProjectionValidation.reason, ); const missingLegacyRows = canonicalAliasRows.filter((row) => row.rowIdentity !== 'alias-row:bmad-shard-doc:legacy-name'); const missingLegacyValidation = validateLockedCanonicalAliasProjection(missingLegacyRows); assert( !missingLegacyValidation.valid && missingLegacyValidation.reason === 'missing:alias-row:bmad-shard-doc:legacy-name', 'Canonical alias projection validator fails when required shard-doc legacy-name row is missing', ); const conflictingRows = [ ...canonicalAliasRows, { ...canonicalAliasRows.find((row) => row.rowIdentity === 'alias-row:bmad-help:slash-command'), }, ]; const conflictingValidation = validateLockedCanonicalAliasProjection(conflictingRows); assert( !conflictingValidation.valid && conflictingValidation.reason === 'conflict:alias-row:bmad-help:slash-command', 'Canonical alias projection validator fails when conflicting duplicate exemplar rows appear', ); const fallbackManifestGenerator = new ManifestGenerator(); fallbackManifestGenerator.bmadDir = tempCanonicalAliasRoot; fallbackManifestGenerator.bmadFolderName = '_bmad'; fallbackManifestGenerator.helpAuthorityRecords = []; fallbackManifestGenerator.taskAuthorityRecords = []; fallbackManifestGenerator.includeConvertedShardDocAliasRows = true; const fallbackCanonicalAliasPath = await fallbackManifestGenerator.writeCanonicalAliasManifest(tempCanonicalAliasConfigDir); const fallbackCanonicalAliasRaw = await fs.readFile(fallbackCanonicalAliasPath, 'utf8'); const fallbackCanonicalAliasRows = csv.parse(fallbackCanonicalAliasRaw, { columns: true, skip_empty_lines: true, trim: true, }); assert( fallbackCanonicalAliasRows.every((row) => { if (row.authoritySourceType !== 'sidecar') { return false; } if (row.canonicalId === 'bmad-help') { return row.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml'; } if (row.canonicalId === 'bmad-shard-doc') { return row.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; } return false; }), 'Canonical alias table falls back to locked sidecar provenance when authority records are unavailable', ); const tempGeneratedBmadDir = path.join(tempCanonicalAliasRoot, '_bmad'); await fs.ensureDir(tempGeneratedBmadDir); const manifestStats = await new ManifestGenerator().generateManifests( tempGeneratedBmadDir, [], [path.join(tempGeneratedBmadDir, '_config', 'canonical-aliases.csv')], { ides: [], preservedModules: [], helpAuthorityRecords: manifestGenerator.helpAuthorityRecords, taskAuthorityRecords: manifestGenerator.taskAuthorityRecords, }, ); assert( Array.isArray(manifestStats.manifestFiles) && manifestStats.manifestFiles.some((filePath) => filePath.endsWith('/_config/canonical-aliases.csv')), 'Manifest generation includes canonical-aliases.csv in output sequencing', ); const writtenFilesManifestRaw = await fs.readFile(path.join(tempGeneratedBmadDir, '_config', 'files-manifest.csv'), 'utf8'); assert( writtenFilesManifestRaw.includes('"_config/canonical-aliases.csv"'), 'Files manifest tracks canonical-aliases.csv when pre-registered by installer flow', ); } catch (error) { assert(false, 'Canonical alias projection suite setup', error.message); } finally { await fs.remove(tempCanonicalAliasRoot); } console.log(''); // ============================================================ // Test 10: Help Catalog Projection + Command Label Contract // ============================================================ console.log(`${colors.yellow}Test Suite 10: Help Catalog Projection + Command Label Contract${colors.reset}\n`); const tempHelpCatalogRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-catalog-projection-')); try { const installer = new Installer(); installer.helpAuthorityRecords = [ { recordType: 'metadata-authority', canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; const sidecarAwareExemplar = await buildSidecarAwareExemplarHelpRow({ helpAuthorityRecords: installer.helpAuthorityRecords, }); assert( sidecarAwareExemplar.commandValue === 'bmad-help', 'Sidecar-aware exemplar help row derives raw command from canonical identity', ); assert( sidecarAwareExemplar.displayedCommandLabel === '/bmad-help', 'Sidecar-aware exemplar help row renders displayed label with exactly one leading slash', ); assert( sidecarAwareExemplar.authoritySourcePath === EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH, 'Sidecar-aware exemplar help row locks authority source path to sidecar metadata file', ); const legacySidecarPath = path.join(tempHelpCatalogRoot, 'legacy-help.artifact.yaml'); await fs.writeFile( legacySidecarPath, yaml.stringify({ schemaVersion: 1, canonicalId: 'help', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Legacy exemplar alias canonical id', dependencies: { requires: [] }, }), 'utf8', ); const legacyIdentityExemplar = await buildSidecarAwareExemplarHelpRow({ sidecarPath: legacySidecarPath, helpAuthorityRecords: installer.helpAuthorityRecords, }); assert( legacyIdentityExemplar.commandValue === 'bmad-help', 'Sidecar-aware exemplar help row normalizes legacy sidecar canonicalId to locked canonical identity', ); await installer.mergeModuleHelpCatalogs(tempHelpCatalogRoot); const generatedHelpPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help.csv'); const generatedCommandLabelReportPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help-command-label-report.csv'); const generatedPipelineReportPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help-catalog-pipeline.csv'); const generatedHelpRaw = await fs.readFile(generatedHelpPath, 'utf8'); const generatedHelpLines = generatedHelpRaw.trim().split('\n'); const expectedHelpHeader = 'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs'; assert(generatedHelpLines[0] === expectedHelpHeader, 'Help catalog header remains additive-compatible for existing consumers'); const generatedHelpRows = csv.parse(generatedHelpRaw, { columns: true, skip_empty_lines: true, trim: true, }); const exemplarRows = generatedHelpRows.filter((row) => row.command === 'bmad-help'); const shardDocRows = generatedHelpRows.filter((row) => row.command === 'bmad-shard-doc'); const indexDocsRows = generatedHelpRows.filter((row) => row.command === 'bmad-index-docs'); assert(exemplarRows.length === 1, 'Help catalog emits exactly one exemplar raw command row for bmad-help'); assert( exemplarRows[0] && exemplarRows[0].name === 'bmad-help', 'Help catalog exemplar row preserves locked bmad-help workflow identity', ); assert(shardDocRows.length === 1, 'Help catalog emits exactly one shard-doc raw command row for bmad-shard-doc'); assert( shardDocRows[0] && shardDocRows[0]['workflow-file'] === '_bmad/core/tasks/shard-doc.xml', 'Help catalog shard-doc row preserves locked shard-doc workflow identity', ); assert(indexDocsRows.length === 1, 'Help catalog emits exactly one index-docs raw command row for bmad-index-docs'); assert( indexDocsRows[0] && indexDocsRows[0]['workflow-file'] === '_bmad/core/tasks/index-docs.xml', 'Help catalog index-docs row preserves locked index-docs workflow identity', ); const sidecarRaw = await fs.readFile(path.join(projectRoot, 'src', 'core', 'tasks', 'help.artifact.yaml'), 'utf8'); const sidecarData = yaml.parse(sidecarRaw); assert( exemplarRows[0] && exemplarRows[0].description === sidecarData.description, 'Help catalog exemplar row description is sourced from sidecar metadata', ); const commandLabelRows = installer.helpCatalogCommandLabelReportRows || []; const helpCommandLabelRow = commandLabelRows.find((row) => row.canonicalId === 'bmad-help'); const shardDocCommandLabelRow = commandLabelRows.find((row) => row.canonicalId === 'bmad-shard-doc'); const indexDocsCommandLabelRow = commandLabelRows.find((row) => row.canonicalId === 'bmad-index-docs'); assert(commandLabelRows.length === 3, 'Installer emits command-label report rows for help, shard-doc, and index-docs canonical ids'); assert( helpCommandLabelRow && helpCommandLabelRow.rawCommandValue === 'bmad-help' && helpCommandLabelRow.displayedCommandLabel === '/bmad-help', 'Command-label report locks raw and displayed command values for exemplar', ); assert( helpCommandLabelRow && helpCommandLabelRow.authoritySourceType === 'sidecar' && helpCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Command-label report includes sidecar provenance linkage', ); assert( shardDocCommandLabelRow && shardDocCommandLabelRow.rawCommandValue === 'bmad-shard-doc' && shardDocCommandLabelRow.displayedCommandLabel === '/bmad-shard-doc', 'Command-label report locks raw and displayed command values for shard-doc', ); assert( shardDocCommandLabelRow && shardDocCommandLabelRow.authoritySourceType === 'sidecar' && shardDocCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', 'Command-label report includes shard-doc sidecar provenance linkage', ); assert( indexDocsCommandLabelRow && indexDocsCommandLabelRow.rawCommandValue === 'bmad-index-docs' && indexDocsCommandLabelRow.displayedCommandLabel === '/bmad-index-docs', 'Command-label report locks raw and displayed command values for index-docs', ); assert( indexDocsCommandLabelRow && indexDocsCommandLabelRow.authoritySourceType === 'sidecar' && indexDocsCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', 'Command-label report includes index-docs sidecar provenance linkage', ); const generatedCommandLabelReportRaw = await fs.readFile(generatedCommandLabelReportPath, 'utf8'); const generatedCommandLabelReportRows = csv.parse(generatedCommandLabelReportRaw, { columns: true, skip_empty_lines: true, trim: true, }); const generatedHelpCommandLabelRow = generatedCommandLabelReportRows.find((row) => row.canonicalId === 'bmad-help'); const generatedShardDocCommandLabelRow = generatedCommandLabelReportRows.find((row) => row.canonicalId === 'bmad-shard-doc'); const generatedIndexDocsCommandLabelRow = generatedCommandLabelReportRows.find((row) => row.canonicalId === 'bmad-index-docs'); assert( generatedCommandLabelReportRows.length === 3 && generatedHelpCommandLabelRow && generatedHelpCommandLabelRow.displayedCommandLabel === '/bmad-help' && generatedHelpCommandLabelRow.rowCountForCanonicalId === '1' && generatedShardDocCommandLabelRow && generatedShardDocCommandLabelRow.displayedCommandLabel === '/bmad-shard-doc' && generatedShardDocCommandLabelRow.rowCountForCanonicalId === '1' && generatedIndexDocsCommandLabelRow && generatedIndexDocsCommandLabelRow.displayedCommandLabel === '/bmad-index-docs' && generatedIndexDocsCommandLabelRow.rowCountForCanonicalId === '1', 'Installer persists command-label report artifact with locked help, shard-doc, and index-docs label contract values', ); const baselineLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows); assert( baselineLabelContract.valid, 'Command-label validator passes when exactly one exemplar /bmad-help displayed label row exists', baselineLabelContract.reason, ); const baselineShardDocLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows, { canonicalId: 'bmad-shard-doc', displayedCommandLabel: '/bmad-shard-doc', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', }); assert( baselineShardDocLabelContract.valid, 'Command-label validator passes when exactly one /bmad-shard-doc displayed label row exists', baselineShardDocLabelContract.reason, ); const baselineIndexDocsLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows, { canonicalId: 'bmad-index-docs', displayedCommandLabel: '/bmad-index-docs', authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', }); assert( baselineIndexDocsLabelContract.valid, 'Command-label validator passes when exactly one /bmad-index-docs displayed label row exists', baselineIndexDocsLabelContract.reason, ); const commandDocsSourcePath = path.join(projectRoot, 'docs', 'reference', 'commands.md'); const commandDocsMarkdown = await fs.readFile(commandDocsSourcePath, 'utf8'); const commandDocConsistency = validateCommandDocSurfaceConsistency(commandDocsMarkdown, { sourcePath: 'docs/reference/commands.md', generatedSurfacePath: '_bmad/_config/bmad-help-command-label-report.csv', commandLabelRows, canonicalId: 'bmad-shard-doc', expectedDisplayedCommandLabel: '/bmad-shard-doc', disallowedAliasLabels: ['/shard-doc'], }); assert( commandDocConsistency.generatedCanonicalCommand === '/bmad-shard-doc', 'Command-doc consistency validator passes when generated shard-doc command matches command docs canonical label', ); const missingCanonicalCommandDocsMarkdown = commandDocsMarkdown.replace( '| `/bmad-shard-doc` | Split a large markdown file into smaller sections |', '| `/bmad-shard-doc-renamed` | Split a large markdown file into smaller sections |', ); try { validateCommandDocSurfaceConsistency(missingCanonicalCommandDocsMarkdown, { sourcePath: 'docs/reference/commands.md', generatedSurfacePath: '_bmad/_config/bmad-help-command-label-report.csv', commandLabelRows, canonicalId: 'bmad-shard-doc', expectedDisplayedCommandLabel: '/bmad-shard-doc', disallowedAliasLabels: ['/shard-doc'], }); assert(false, 'Command-doc consistency validator rejects missing canonical shard-doc command rows'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_CANONICAL_COMMAND_MISSING, 'Command-doc consistency validator emits deterministic diagnostics for missing canonical shard-doc command docs row', `Expected ${PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_CANONICAL_COMMAND_MISSING}, got ${error.code}`, ); } const aliasAmbiguousCommandDocsMarkdown = `${commandDocsMarkdown}\n| \`/shard-doc\` | Legacy alias |\n`; try { validateCommandDocSurfaceConsistency(aliasAmbiguousCommandDocsMarkdown, { sourcePath: 'docs/reference/commands.md', generatedSurfacePath: '_bmad/_config/bmad-help-command-label-report.csv', commandLabelRows, canonicalId: 'bmad-shard-doc', expectedDisplayedCommandLabel: '/bmad-shard-doc', disallowedAliasLabels: ['/shard-doc'], }); assert(false, 'Command-doc consistency validator rejects shard-doc alias ambiguity in command docs'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_ALIAS_AMBIGUOUS, 'Command-doc consistency validator emits deterministic diagnostics for shard-doc alias ambiguity in command docs', `Expected ${PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_ALIAS_AMBIGUOUS}, got ${error.code}`, ); } try { validateCommandDocSurfaceConsistency(commandDocsMarkdown, { sourcePath: 'docs/reference/commands.md', generatedSurfacePath: '_bmad/_config/bmad-help-command-label-report.csv', commandLabelRows: [ helpCommandLabelRow, { ...shardDocCommandLabelRow, displayedCommandLabel: '/shard-doc', }, ], canonicalId: 'bmad-shard-doc', expectedDisplayedCommandLabel: '/bmad-shard-doc', disallowedAliasLabels: ['/shard-doc'], }); assert(false, 'Command-doc consistency validator rejects generated shard-doc command-label drift'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_GENERATED_SURFACE_MISMATCH, 'Command-doc consistency validator emits deterministic diagnostics for generated shard-doc command-label drift', `Expected ${PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_GENERATED_SURFACE_MISMATCH}, got ${error.code}`, ); } const invalidLegacyLabelContract = evaluateExemplarCommandLabelReportRows([ { ...helpCommandLabelRow, displayedCommandLabel: 'help', }, ]); assert( !invalidLegacyLabelContract.valid && invalidLegacyLabelContract.reason === 'invalid-displayed-label:help', 'Command-label validator fails on alternate displayed label form "help"', ); const invalidSlashHelpLabelContract = evaluateExemplarCommandLabelReportRows([ { ...helpCommandLabelRow, displayedCommandLabel: '/help', }, ]); assert( !invalidSlashHelpLabelContract.valid && invalidSlashHelpLabelContract.reason === 'invalid-displayed-label:/help', 'Command-label validator fails on alternate displayed label form "/help"', ); const invalidShardDocLabelContract = evaluateExemplarCommandLabelReportRows( [ helpCommandLabelRow, { ...shardDocCommandLabelRow, displayedCommandLabel: '/shard-doc', }, ], { canonicalId: 'bmad-shard-doc', displayedCommandLabel: '/bmad-shard-doc', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', }, ); assert( !invalidShardDocLabelContract.valid && invalidShardDocLabelContract.reason === 'invalid-displayed-label:/shard-doc', 'Command-label validator fails on alternate shard-doc displayed label form "/shard-doc"', ); const pipelineRows = installer.helpCatalogPipelineRows || []; assert(pipelineRows.length === 2, 'Installer emits two stage rows for help catalog pipeline evidence linkage'); const installedStageRow = pipelineRows.find((row) => row.stage === 'installed-compatibility-row'); const mergedStageRow = pipelineRows.find((row) => row.stage === 'merged-config-row'); assert( installedStageRow && installedStageRow.issuingComponent === EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT && installedStageRow.commandAuthoritySourceType === 'sidecar' && installedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Installed compatibility stage row preserves sidecar command provenance and issuing component linkage', ); assert( mergedStageRow && mergedStageRow.issuingComponent === INSTALLER_HELP_CATALOG_MERGE_COMPONENT && mergedStageRow.commandAuthoritySourceType === 'sidecar' && mergedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Merged config stage row preserves sidecar command provenance and merge issuing component linkage', ); assert( pipelineRows.every((row) => row.status === 'PASS' && typeof row.issuingComponentBindingEvidence === 'string'), 'Pipeline rows include deterministic PASS status and non-empty issuing-component evidence linkage', ); const generatedPipelineReportRaw = await fs.readFile(generatedPipelineReportPath, 'utf8'); const generatedPipelineReportRows = csv.parse(generatedPipelineReportRaw, { columns: true, skip_empty_lines: true, trim: true, }); assert( generatedPipelineReportRows.length === 2 && generatedPipelineReportRows.every( (row) => row.commandAuthoritySourceType === 'sidecar' && row.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', ), 'Installer persists pipeline stage artifact with sidecar command provenance linkage for both stages', ); const tempAltLabelRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-catalog-alt-label-')); try { const moduleDir = path.join(tempAltLabelRoot, 'modx'); await fs.ensureDir(moduleDir); await fs.writeFile( path.join(moduleDir, 'module-help.csv'), [ 'module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs', 'modx,anytime,alt-help,AH,,_bmad/core/tasks/help.md,/help,false,,,Alt help label,,,', ].join('\n') + '\n', 'utf8', ); const alternateLabelInstaller = new Installer(); alternateLabelInstaller.helpAuthorityRecords = installer.helpAuthorityRecords; try { await alternateLabelInstaller.mergeModuleHelpCatalogs(tempAltLabelRoot); assert( false, 'Installer command-label contract rejects alternate rendered labels in merged help catalog', 'Expected command label contract failure for /help but merge succeeded', ); } catch (error) { assert( error.code === HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED, 'Installer command-label contract returns deterministic failure code for alternate labels', `Expected ${HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED}, got ${error.code}`, ); } } finally { await fs.remove(tempAltLabelRoot); } } catch (error) { assert(false, 'Help catalog projection suite setup', error.message); } finally { await fs.remove(tempHelpCatalogRoot); } console.log(''); // ============================================================ // Test 11: Export Projection from Sidecar Canonical ID // ============================================================ console.log(`${colors.yellow}Test Suite 11: Export Projection from Sidecar Canonical ID${colors.reset}\n`); const tempExportRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-projection-')); try { const codexSetup = new CodexSetup(); const skillsDir = path.join(tempExportRoot, '.agents', 'skills'); await fs.ensureDir(skillsDir); await fs.ensureDir(path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks')); await fs.writeFile( path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'bmad-help', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Help command', dependencies: { requires: [] }, }), 'utf8', ); await fs.writeFile( path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'bmad-shard-doc', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', dependencies: { requires: [] }, }), 'utf8', ); await fs.writeFile( path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks', 'index-docs.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'bmad-index-docs', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', displayName: 'Index Docs', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', dependencies: { requires: [] }, }), 'utf8', ); const exemplarTaskArtifact = { type: 'task', name: 'help', module: 'core', sourcePath: path.join(tempExportRoot, '_bmad', 'core', 'tasks', 'help.md'), relativePath: path.join('core', 'tasks', 'help.md'), content: '---\nname: help\ndescription: Help command\ncanonicalId: bmad-help\n---\n\n# help\n', }; const shardDocTaskArtifact = { type: 'task', name: 'shard-doc', module: 'core', sourcePath: path.join(tempExportRoot, '_bmad', 'core', 'tasks', 'shard-doc.xml'), relativePath: path.join('core', 'tasks', 'shard-doc.md'), content: 'Split markdown docs\n', }; const indexDocsTaskArtifact = { type: 'task', name: 'index-docs', module: 'core', sourcePath: path.join(tempExportRoot, '_bmad', 'core', 'tasks', 'index-docs.xml'), relativePath: path.join('core', 'tasks', 'index-docs.md'), content: 'Index docs\n', }; const writtenCount = await codexSetup.writeSkillArtifacts(skillsDir, [exemplarTaskArtifact], 'task', { projectDir: tempExportRoot, }); assert(writtenCount === 1, 'Codex export writes one exemplar skill artifact'); const exemplarSkillPath = path.join(skillsDir, 'bmad-help', 'SKILL.md'); assert(await fs.pathExists(exemplarSkillPath), 'Codex export derives exemplar skill path from sidecar canonical identity'); const exemplarSkillRaw = await fs.readFile(exemplarSkillPath, 'utf8'); const exemplarFrontmatterMatch = exemplarSkillRaw.match(/^---\r?\n([\s\S]*?)\r?\n---/); const exemplarFrontmatter = exemplarFrontmatterMatch ? yaml.parse(exemplarFrontmatterMatch[1]) : null; assert( exemplarFrontmatter && exemplarFrontmatter.name === 'bmad-help', 'Codex export frontmatter sets required name from sidecar canonical identity', ); assert( exemplarFrontmatter && Object.keys(exemplarFrontmatter).sort().join(',') === 'description,name', 'Codex export frontmatter remains constrained to required name plus optional description', ); const exportDerivationRecord = codexSetup.exportDerivationRecords.find((row) => row.exportPath === '.agents/skills/bmad-help/SKILL.md'); assert( exportDerivationRecord && exportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && exportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Codex export records exemplar derivation source metadata from sidecar canonical-id', ); const shardDocWrittenCount = await codexSetup.writeSkillArtifacts(skillsDir, [shardDocTaskArtifact], 'task', { projectDir: tempExportRoot, }); assert(shardDocWrittenCount === 1, 'Codex export writes one shard-doc converted skill artifact'); const shardDocSkillPath = path.join(skillsDir, 'bmad-shard-doc', 'SKILL.md'); assert(await fs.pathExists(shardDocSkillPath), 'Codex export derives shard-doc skill path from sidecar canonical identity'); const shardDocSkillRaw = await fs.readFile(shardDocSkillPath, 'utf8'); const shardDocFrontmatterMatch = shardDocSkillRaw.match(/^---\r?\n([\s\S]*?)\r?\n---/); const shardDocFrontmatter = shardDocFrontmatterMatch ? yaml.parse(shardDocFrontmatterMatch[1]) : null; assert( shardDocFrontmatter && shardDocFrontmatter.name === 'bmad-shard-doc', 'Codex export frontmatter sets shard-doc required name from sidecar canonical identity', ); const shardDocExportDerivationRecord = codexSetup.exportDerivationRecords.find( (row) => row.exportPath === '.agents/skills/bmad-shard-doc/SKILL.md', ); assert( shardDocExportDerivationRecord && shardDocExportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && shardDocExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml' && shardDocExportDerivationRecord.sourcePath === 'bmad-fork/src/core/tasks/shard-doc.xml', 'Codex export records shard-doc sidecar-canonical derivation metadata and source path', ); const indexDocsWrittenCount = await codexSetup.writeSkillArtifacts(skillsDir, [indexDocsTaskArtifact], 'task', { projectDir: tempExportRoot, }); assert(indexDocsWrittenCount === 1, 'Codex export writes one index-docs converted skill artifact'); const indexDocsSkillPath = path.join(skillsDir, 'bmad-index-docs', 'SKILL.md'); assert(await fs.pathExists(indexDocsSkillPath), 'Codex export derives index-docs skill path from sidecar canonical identity'); const indexDocsSkillRaw = await fs.readFile(indexDocsSkillPath, 'utf8'); const indexDocsFrontmatterMatch = indexDocsSkillRaw.match(/^---\r?\n([\s\S]*?)\r?\n---/); const indexDocsFrontmatter = indexDocsFrontmatterMatch ? yaml.parse(indexDocsFrontmatterMatch[1]) : null; assert( indexDocsFrontmatter && indexDocsFrontmatter.name === 'bmad-index-docs', 'Codex export frontmatter sets index-docs required name from sidecar canonical identity', ); const indexDocsExportDerivationRecord = codexSetup.exportDerivationRecords.find( (row) => row.exportPath === '.agents/skills/bmad-index-docs/SKILL.md', ); assert( indexDocsExportDerivationRecord && indexDocsExportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && indexDocsExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml' && indexDocsExportDerivationRecord.sourcePath === 'bmad-fork/src/core/tasks/index-docs.xml', 'Codex export records index-docs sidecar-canonical derivation metadata and source path', ); const duplicateExportSetup = new CodexSetup(); const duplicateSkillDir = path.join(tempExportRoot, '.agents', 'skills-duplicate-check'); await fs.ensureDir(duplicateSkillDir); try { await duplicateExportSetup.writeSkillArtifacts( duplicateSkillDir, [ shardDocTaskArtifact, { ...shardDocTaskArtifact, content: 'Duplicate shard-doc export artifact\n', }, ], 'task', { projectDir: tempExportRoot, }, ); assert( false, 'Codex export rejects duplicate shard-doc canonical-id skill export surfaces', 'Expected duplicate export-surface failure but export succeeded', ); } catch (error) { assert( error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.DUPLICATE_EXPORT_SURFACE, 'Codex export duplicate shard-doc canonical-id rejection returns deterministic failure code', `Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.DUPLICATE_EXPORT_SURFACE}, got ${error.code}`, ); } const tempSubmoduleRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-submodule-root-')); try { const submoduleRootSetup = new CodexSetup(); const submoduleSkillsDir = path.join(tempSubmoduleRoot, '.agents', 'skills'); await fs.ensureDir(submoduleSkillsDir); await fs.ensureDir(path.join(tempSubmoduleRoot, 'src', 'core', 'tasks')); await fs.writeFile( path.join(tempSubmoduleRoot, 'src', 'core', 'tasks', 'help.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'bmad-help', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Help command', dependencies: { requires: [] }, }), 'utf8', ); await submoduleRootSetup.writeSkillArtifacts(submoduleSkillsDir, [exemplarTaskArtifact], 'task', { projectDir: tempSubmoduleRoot, }); const submoduleExportDerivationRecord = submoduleRootSetup.exportDerivationRecords.find( (row) => row.exportPath === '.agents/skills/bmad-help/SKILL.md', ); assert( submoduleExportDerivationRecord && submoduleExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', 'Codex export locks exemplar derivation source-path contract when running from submodule root', ); } finally { await fs.remove(tempSubmoduleRoot); } const tempNoSidecarRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-missing-sidecar-')); try { const noSidecarSetup = new CodexSetup(); const noSidecarSkillDir = path.join(tempNoSidecarRoot, '.agents', 'skills'); await fs.ensureDir(noSidecarSkillDir); try { await noSidecarSetup.writeSkillArtifacts(noSidecarSkillDir, [exemplarTaskArtifact], 'task', { projectDir: tempNoSidecarRoot, }); assert( false, 'Codex export fails when exemplar sidecar metadata is missing', 'Expected sidecar file-not-found failure but export succeeded', ); } catch (error) { assert( error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, 'Codex export missing sidecar failure returns deterministic error code', `Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND}, got ${error.code}`, ); } } finally { await fs.remove(tempNoSidecarRoot); } const tempInferenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-no-inference-')); try { const noInferenceSetup = new CodexSetup(); const noInferenceSkillDir = path.join(tempInferenceRoot, '.agents', 'skills'); await fs.ensureDir(noInferenceSkillDir); await fs.ensureDir(path.join(tempInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks')); await fs.writeFile( path.join(tempInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'nonexistent-help-id', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Help command', dependencies: { requires: [] }, }), 'utf8', ); try { await noInferenceSetup.writeSkillArtifacts(noInferenceSkillDir, [exemplarTaskArtifact], 'task', { projectDir: tempInferenceRoot, }); assert( false, 'Codex export rejects path-inferred exemplar id when sidecar canonical-id derivation is unresolved', 'Expected canonical-id derivation failure but export succeeded', ); } catch (error) { assert( error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED, 'Codex export unresolved canonical-id derivation returns deterministic failure code', `Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED}, got ${error.code}`, ); } } finally { await fs.remove(tempInferenceRoot); } const tempShardDocInferenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-no-shard-doc-inference-')); try { const noShardDocInferenceSetup = new CodexSetup(); const noShardDocInferenceSkillDir = path.join(tempShardDocInferenceRoot, '.agents', 'skills'); await fs.ensureDir(noShardDocInferenceSkillDir); await fs.ensureDir(path.join(tempShardDocInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks')); await fs.writeFile( path.join(tempShardDocInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'nonexistent-shard-doc-id', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', dependencies: { requires: [] }, }), 'utf8', ); try { await noShardDocInferenceSetup.writeSkillArtifacts(noShardDocInferenceSkillDir, [shardDocTaskArtifact], 'task', { projectDir: tempShardDocInferenceRoot, }); assert( false, 'Codex export rejects path-inferred shard-doc id when sidecar canonical-id derivation is unresolved', 'Expected shard-doc canonical-id derivation failure but export succeeded', ); } catch (error) { assert( error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED, 'Codex export unresolved shard-doc canonical-id derivation returns deterministic failure code', `Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED}, got ${error.code}`, ); } } finally { await fs.remove(tempShardDocInferenceRoot); } const tempIndexDocsInferenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-no-index-docs-inference-')); try { const noIndexDocsInferenceSetup = new CodexSetup(); const noIndexDocsInferenceSkillDir = path.join(tempIndexDocsInferenceRoot, '.agents', 'skills'); await fs.ensureDir(noIndexDocsInferenceSkillDir); await fs.ensureDir(path.join(tempIndexDocsInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks')); await fs.writeFile( path.join(tempIndexDocsInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks', 'index-docs.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'nonexistent-index-docs-id', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', displayName: 'Index Docs', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', dependencies: { requires: [] }, }), 'utf8', ); try { await noIndexDocsInferenceSetup.writeSkillArtifacts(noIndexDocsInferenceSkillDir, [indexDocsTaskArtifact], 'task', { projectDir: tempIndexDocsInferenceRoot, }); assert( false, 'Codex export rejects path-inferred index-docs id when sidecar canonical-id derivation is unresolved', 'Expected index-docs canonical-id derivation failure but export succeeded', ); } catch (error) { assert( error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED, 'Codex export unresolved index-docs canonical-id derivation returns deterministic failure code', `Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED}, got ${error.code}`, ); } } finally { await fs.remove(tempIndexDocsInferenceRoot); } const compatibilitySetup = new CodexSetup(); const compatibilityIdentity = await compatibilitySetup.resolveSkillIdentityFromArtifact( { type: 'workflow-command', name: 'create-story', module: 'bmm', relativePath: path.join('bmm', 'workflows', 'create-story.md'), }, tempExportRoot, ); assert( compatibilityIdentity.skillName === 'bmad-bmm-create-story' && compatibilityIdentity.exportIdDerivationSourceType === 'path-derived', 'Codex export preserves non-exemplar path-derived skill identity behavior', ); } catch (error) { assert(false, 'Export projection suite setup', error.message); } finally { await fs.remove(tempExportRoot); } console.log(''); // ============================================================ // Test 12: QA Agent Compilation // ============================================================ console.log(`${colors.yellow}Test Suite 12: QA Agent Compilation${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/qa.agent.yaml'); const tempOutput = path.join(__dirname, 'temp-qa-agent.md'); try { const result = await builder.buildAgent(qaAgentPath, null, tempOutput, { includeMetadata: true }); const compiled = await fs.readFile(tempOutput, 'utf8'); assert(compiled.includes('QA Engineer'), 'QA agent compilation includes agent title'); assert(compiled.includes('qa-generate-e2e-tests'), 'QA agent menu includes automate workflow'); // Cleanup await fs.remove(tempOutput); } catch (error) { assert(false, 'QA agent compiles successfully', error.message); } } catch (error) { assert(false, 'QA compilation test setup', error.message); } console.log(''); // ============================================================ // Test 13: Projection Consumer Compatibility Contracts // ============================================================ console.log(`${colors.yellow}Test Suite 13: Projection Consumer Compatibility${colors.reset}\n`); const tempCompatibilityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-projection-compatibility-')); try { const tempCompatibilityConfigDir = path.join(tempCompatibilityRoot, '_config'); await fs.ensureDir(tempCompatibilityConfigDir); const buildCsvLine = (columns, row) => columns .map((column) => { const value = String(row[column] ?? ''); return value.includes(',') ? `"${value.replaceAll('"', '""')}"` : value; }) .join(','); const taskManifestColumns = [ ...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS, 'futureAdditiveField', ]; const validTaskRows = [ { name: 'help', displayName: 'help', description: 'Help command', module: 'core', path: '{project-root}/_bmad/core/tasks/help.md', standalone: 'true', legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', futureAdditiveField: 'canonical-additive', }, { name: 'create-story', displayName: 'Create Story', description: 'Create a dedicated story file', module: 'bmm', path: '{project-root}/_bmad/bmm/workflows/2-creation/create-story/workflow.yaml', standalone: 'true', legacyName: 'create-story', canonicalId: '', authoritySourceType: '', authoritySourcePath: '', futureAdditiveField: 'canonical-additive', }, ]; const validTaskManifestCsv = [taskManifestColumns.join(','), ...validTaskRows.map((row) => buildCsvLine(taskManifestColumns, row))].join('\n') + '\n'; await fs.writeFile(path.join(tempCompatibilityConfigDir, 'task-manifest.csv'), validTaskManifestCsv, 'utf8'); const validatedTaskSurface = validateTaskManifestCompatibilitySurface(validTaskManifestCsv, { sourcePath: '_bmad/_config/task-manifest.csv', }); assert( validatedTaskSurface.headerColumns[0] === 'name' && validatedTaskSurface.headerColumns[TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length] === 'legacyName', 'Task-manifest compatibility validator enforces locked prefix plus additive canonical ordering', ); assert( validatedTaskSurface.headerColumns.at(-1) === 'futureAdditiveField', 'Task-manifest compatibility validator allows additive columns appended after locked canonical columns', ); validateTaskManifestLoaderEntries(validatedTaskSurface.rows, { sourcePath: '_bmad/_config/task-manifest.csv', headerColumns: validatedTaskSurface.headerColumns, }); assert(true, 'Task-manifest loader compatibility validator accepts known loader columns with additive fields'); const taskToolGenerator = new TaskToolCommandGenerator(); const loadedTaskRows = await taskToolGenerator.loadTaskManifest(tempCompatibilityRoot); assert( Array.isArray(loadedTaskRows) && loadedTaskRows.length === 2 && loadedTaskRows[0].name === 'help' && loadedTaskRows[1].name === 'create-story', 'Task-manifest loader remains parseable when additive columns are present', ); const legacyTaskManifestColumns = [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS]; const legacyTaskManifestCsv = [legacyTaskManifestColumns.join(','), buildCsvLine(legacyTaskManifestColumns, validTaskRows[0])].join('\n') + '\n'; const legacyTaskSurface = validateTaskManifestCompatibilitySurface(legacyTaskManifestCsv, { sourcePath: '_bmad/_config/task-manifest.csv', allowLegacyPrefixOnly: true, }); assert( legacyTaskSurface.isLegacyPrefixOnlyHeader === true, 'Task-manifest compatibility validator supports legacy prefix-only headers during migration reads', ); try { validateTaskManifestCompatibilitySurface(legacyTaskManifestCsv, { sourcePath: '_bmad/_config/task-manifest.csv', }); assert(false, 'Task-manifest strict validator rejects legacy prefix-only header without migration mode'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_CANONICAL_MISMATCH, 'Task-manifest strict validator emits deterministic canonical mismatch error for legacy prefix-only headers', ); } const reorderedTaskManifestColumns = [...taskManifestColumns]; [reorderedTaskManifestColumns[0], reorderedTaskManifestColumns[1]] = [reorderedTaskManifestColumns[1], reorderedTaskManifestColumns[0]]; const invalidTaskManifestCsv = [reorderedTaskManifestColumns.join(','), buildCsvLine(reorderedTaskManifestColumns, validTaskRows[0])].join('\n') + '\n'; try { validateTaskManifestCompatibilitySurface(invalidTaskManifestCsv, { sourcePath: '_bmad/_config/task-manifest.csv', }); assert(false, 'Task-manifest validator rejects non-additive reordered compatibility-prefix headers'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_PREFIX_MISMATCH && error.fieldPath === 'header[0]', 'Task-manifest validator emits deterministic diagnostics for reordered compatibility-prefix headers', ); } const helpCatalogColumns = [ ...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS, 'futureAdditiveField', ]; const validHelpRows = [ { module: 'core', phase: 'anytime', name: 'bmad-help', code: 'BH', sequence: '', 'workflow-file': '_bmad/core/tasks/help.md', command: 'bmad-help', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Help command', 'output-location': '', outputs: '', futureAdditiveField: 'canonical-additive', }, { module: 'core', phase: 'anytime', name: 'Shard Document', code: 'SD', sequence: '', 'workflow-file': '_bmad/core/tasks/shard-doc.xml', command: 'bmad-shard-doc', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Shard document command', 'output-location': '', outputs: '', futureAdditiveField: 'canonical-additive', }, { module: 'core', phase: 'anytime', name: 'Index Docs', code: 'ID', sequence: '', 'workflow-file': '_bmad/core/tasks/index-docs.xml', command: 'bmad-index-docs', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Index docs command', 'output-location': '', outputs: '', futureAdditiveField: 'canonical-additive', }, { module: 'bmm', phase: 'planning', name: 'create-story', code: 'CS', sequence: '', 'workflow-file': '_bmad/bmm/workflows/2-creation/create-story/workflow.yaml', command: 'bmad-bmm-create-story', required: 'false', 'agent-name': 'sm', 'agent-command': 'bmad:agent:sm', 'agent-display-name': 'Scrum Master', 'agent-title': 'SM', options: '', description: 'Create next story', 'output-location': '', outputs: '', futureAdditiveField: 'canonical-additive', }, ]; const validHelpCatalogCsv = [helpCatalogColumns.join(','), ...validHelpRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n'; await fs.writeFile(path.join(tempCompatibilityConfigDir, 'bmad-help.csv'), validHelpCatalogCsv, 'utf8'); const validatedHelpSurface = validateHelpCatalogCompatibilitySurface(validHelpCatalogCsv, { sourcePath: '_bmad/_config/bmad-help.csv', }); assert( validatedHelpSurface.headerColumns[5] === 'workflow-file' && validatedHelpSurface.headerColumns[6] === 'command', 'Help-catalog compatibility validator preserves workflow-file and command compatibility columns', ); assert( validatedHelpSurface.headerColumns.at(-1) === 'futureAdditiveField', 'Help-catalog compatibility validator allows additive columns appended after locked canonical columns', ); validateHelpCatalogLoaderEntries(validatedHelpSurface.rows, { sourcePath: '_bmad/_config/bmad-help.csv', headerColumns: validatedHelpSurface.headerColumns, }); validateGithubCopilotHelpLoaderEntries(validatedHelpSurface.rows, { sourcePath: '_bmad/_config/bmad-help.csv', headerColumns: validatedHelpSurface.headerColumns, }); assert(true, 'Help-catalog and GitHub Copilot loader compatibility validators accept stable command/workflow-file contracts'); const githubCopilotSetup = new GitHubCopilotSetup(); const loadedHelpRows = await githubCopilotSetup.loadBmadHelp(tempCompatibilityRoot); assert( Array.isArray(loadedHelpRows) && loadedHelpRows.length === 4 && loadedHelpRows.some((row) => row['workflow-file'] === '_bmad/core/tasks/help.md' && row.command === 'bmad-help') && loadedHelpRows.some((row) => row['workflow-file'] === '_bmad/core/tasks/shard-doc.xml' && row.command === 'bmad-shard-doc') && loadedHelpRows.some((row) => row['workflow-file'] === '_bmad/core/tasks/index-docs.xml' && row.command === 'bmad-index-docs'), 'GitHub Copilot help loader remains parseable with additive help-catalog columns', ); const reorderedHelpCatalogColumns = [...helpCatalogColumns]; [reorderedHelpCatalogColumns[5], reorderedHelpCatalogColumns[6]] = [reorderedHelpCatalogColumns[6], reorderedHelpCatalogColumns[5]]; const invalidHelpCatalogCsv = [reorderedHelpCatalogColumns.join(','), buildCsvLine(reorderedHelpCatalogColumns, validHelpRows[0])].join('\n') + '\n'; try { validateHelpCatalogCompatibilitySurface(invalidHelpCatalogCsv, { sourcePath: '_bmad/_config/bmad-help.csv', }); assert(false, 'Help-catalog validator rejects non-additive reordered compatibility headers'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_HEADER_PREFIX_MISMATCH && error.fieldPath === 'header[5]', 'Help-catalog validator emits deterministic diagnostics for reordered compatibility headers', ); } const missingShardDocRows = validHelpRows.filter((row) => row.command !== 'bmad-shard-doc'); const missingShardDocCsv = [helpCatalogColumns.join(','), ...missingShardDocRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n'; try { validateHelpCatalogCompatibilitySurface(missingShardDocCsv, { sourcePath: '_bmad/_config/bmad-help.csv', }); assert(false, 'Help-catalog validator rejects missing shard-doc canonical command rows'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED && error.fieldPath === 'rows[*].command' && error.observedValue === '0', 'Help-catalog validator emits deterministic diagnostics for missing shard-doc canonical command rows', ); } const missingIndexDocsRows = validHelpRows.filter((row) => row.command !== 'bmad-index-docs'); const missingIndexDocsCsv = [helpCatalogColumns.join(','), ...missingIndexDocsRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n'; try { validateHelpCatalogCompatibilitySurface(missingIndexDocsCsv, { sourcePath: '_bmad/_config/bmad-help.csv', }); assert(false, 'Help-catalog validator rejects missing index-docs canonical command rows'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_INDEX_DOCS_ROW_CONTRACT_FAILED && error.fieldPath === 'rows[*].command' && error.observedValue === '0', 'Help-catalog validator emits deterministic diagnostics for missing index-docs canonical command rows', ); } const shardDocBaselineRow = validHelpRows.find((row) => row.command === 'bmad-shard-doc'); const duplicateShardDocCsv = [ helpCatalogColumns.join(','), ...[...validHelpRows, { ...shardDocBaselineRow, name: 'Shard Document Duplicate' }].map((row) => buildCsvLine(helpCatalogColumns, row), ), ].join('\n') + '\n'; try { validateHelpCatalogCompatibilitySurface(duplicateShardDocCsv, { sourcePath: '_bmad/_config/bmad-help.csv', }); assert(false, 'Help-catalog validator rejects duplicate shard-doc canonical command rows'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED && error.fieldPath === 'rows[*].command' && error.observedValue === '2', 'Help-catalog validator emits deterministic diagnostics for duplicate shard-doc canonical command rows', ); } const missingWorkflowFileRows = [ { ...validHelpRows[0], 'workflow-file': '', command: 'bmad-help', }, ]; const missingWorkflowFileCsv = [helpCatalogColumns.join(','), ...missingWorkflowFileRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n'; await fs.writeFile(path.join(tempCompatibilityConfigDir, 'bmad-help.csv'), missingWorkflowFileCsv, 'utf8'); try { await githubCopilotSetup.loadBmadHelp(tempCompatibilityRoot); assert(false, 'GitHub Copilot help loader rejects rows that drop workflow-file while keeping command values'); } catch (error) { assert( error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.GITHUB_COPILOT_WORKFLOW_FILE_MISSING && error.fieldPath === 'rows[0].workflow-file', 'GitHub Copilot help loader emits deterministic diagnostics for missing workflow-file compatibility breaks', ); } } catch (error) { assert(false, 'Projection compatibility suite setup', error.message); } finally { await fs.remove(tempCompatibilityRoot); } console.log(''); // ============================================================ // Test 14: Deterministic Validation Artifact Suite // ============================================================ console.log(`${colors.yellow}Test Suite 14: Deterministic Validation Artifact Suite${colors.reset}\n`); const tempValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-validation-suite-')); try { const tempProjectRoot = tempValidationHarnessRoot; const tempBmadDir = path.join(tempProjectRoot, '_bmad'); const tempConfigDir = path.join(tempBmadDir, '_config'); const tempSourceTasksDir = path.join(tempProjectRoot, 'bmad-fork', 'src', 'core', 'tasks'); const tempSkillDir = path.join(tempProjectRoot, '.agents', 'skills', 'bmad-help'); await fs.ensureDir(tempConfigDir); await fs.ensureDir(path.join(tempBmadDir, 'core', 'tasks')); await fs.ensureDir(path.join(tempBmadDir, 'core')); await fs.ensureDir(tempSourceTasksDir); await fs.ensureDir(tempSkillDir); const writeCsv = async (filePath, columns, rows) => { const buildCsvLine = (values) => values .map((value) => { const text = String(value ?? ''); return text.includes(',') || text.includes('"') ? `"${text.replaceAll('"', '""')}"` : text; }) .join(','); const lines = [columns.join(','), ...rows.map((row) => buildCsvLine(columns.map((column) => row[column] ?? '')))]; await fs.writeFile(filePath, `${lines.join('\n')}\n`, 'utf8'); }; const sidecarFixture = { schemaVersion: 1, canonicalId: 'bmad-help', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/help.md', displayName: 'help', description: 'Help command', dependencies: { requires: [], }, }; await fs.writeFile(path.join(tempSourceTasksDir, 'help.artifact.yaml'), yaml.stringify(sidecarFixture), 'utf8'); await fs.writeFile( path.join(tempSourceTasksDir, 'help.md'), `---\n${yaml .stringify({ name: 'help', description: 'Help command', canonicalId: 'bmad-help', dependencies: { requires: [] }, }) .trimEnd()}\n---\n\n# Source Help\n`, 'utf8', ); await fs.writeFile( path.join(tempBmadDir, 'core', 'tasks', 'help.md'), `---\n${yaml .stringify({ name: 'help', description: 'Help command', canonicalId: 'bmad-help', dependencies: { requires: [] }, }) .trimEnd()}\n---\n\n# Runtime Help\n`, 'utf8', ); await fs.writeFile( path.join(tempSkillDir, 'SKILL.md'), `---\n${yaml.stringify({ name: 'bmad-help', description: 'Help command' }).trimEnd()}\n---\n\n# Skill\n`, 'utf8', ); await writeCsv( path.join(tempConfigDir, 'task-manifest.csv'), [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS], [ { name: 'help', displayName: 'help', description: 'Help command', module: 'core', path: '_bmad/core/tasks/help.md', standalone: 'true', legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', }, ], ); await writeCsv( path.join(tempConfigDir, 'canonical-aliases.csv'), [ 'canonicalId', 'alias', 'aliasType', 'authoritySourceType', 'authoritySourcePath', 'rowIdentity', 'normalizedAliasValue', 'rawIdentityHasLeadingSlash', 'resolutionEligibility', ], [ { canonicalId: 'bmad-help', alias: 'bmad-help', aliasType: 'canonical-id', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', rowIdentity: 'alias-row:bmad-help:canonical-id', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', }, { canonicalId: 'bmad-help', alias: 'help', aliasType: 'legacy-name', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', rowIdentity: 'alias-row:bmad-help:legacy-name', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', }, { canonicalId: 'bmad-help', alias: '/bmad-help', aliasType: 'slash-command', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', rowIdentity: 'alias-row:bmad-help:slash-command', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', }, ], ); await writeCsv( path.join(tempConfigDir, 'bmad-help.csv'), [...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS], [ { module: 'core', phase: 'anytime', name: 'bmad-help', code: 'BH', sequence: '', 'workflow-file': '_bmad/core/tasks/help.md', command: 'bmad-help', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Help command', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Shard Document', code: 'SD', sequence: '', 'workflow-file': '_bmad/core/tasks/shard-doc.xml', command: 'bmad-shard-doc', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Split large markdown documents into smaller files by section with an index.', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Index Docs', code: 'ID', sequence: '', 'workflow-file': '_bmad/core/tasks/index-docs.xml', command: 'bmad-index-docs', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', 'output-location': '', outputs: '', }, ], ); await writeCsv( path.join(tempBmadDir, 'core', 'module-help.csv'), [ 'module', 'phase', 'name', 'code', 'sequence', 'workflow-file', 'command', 'required', 'agent', 'options', 'description', 'output-location', 'outputs', ], [ { module: 'core', phase: 'anytime', name: 'bmad-help', code: 'BH', sequence: '', 'workflow-file': '_bmad/core/tasks/help.md', command: 'bmad-help', required: 'false', agent: '', options: '', description: 'Help command', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Shard Document', code: 'SD', sequence: '', 'workflow-file': '_bmad/core/tasks/shard-doc.xml', command: 'bmad-shard-doc', required: 'false', agent: '', options: '', description: 'Split large markdown documents into smaller files by section with an index.', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Index Docs', code: 'ID', sequence: '', 'workflow-file': '_bmad/core/tasks/index-docs.xml', command: 'bmad-index-docs', required: 'false', agent: '', options: '', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', 'output-location': '', outputs: '', }, ], ); await writeCsv( path.join(tempConfigDir, 'bmad-help-catalog-pipeline.csv'), [ 'stage', 'artifactPath', 'rowIdentity', 'canonicalId', 'sourcePath', 'rowCountForStageCanonicalId', 'commandValue', 'expectedCommandValue', 'descriptionValue', 'expectedDescriptionValue', 'descriptionAuthoritySourceType', 'descriptionAuthoritySourcePath', 'commandAuthoritySourceType', 'commandAuthoritySourcePath', 'issuerOwnerClass', 'issuingComponent', 'issuingComponentBindingEvidence', 'stageStatus', 'status', ], [ { stage: 'installed-compatibility-row', artifactPath: '_bmad/core/module-help.csv', rowIdentity: 'module-help-row:bmad-help', canonicalId: 'bmad-help', sourcePath: 'bmad-fork/src/core/tasks/help.md', rowCountForStageCanonicalId: '1', commandValue: 'bmad-help', expectedCommandValue: 'bmad-help', descriptionValue: 'Help command', expectedDescriptionValue: 'Help command', descriptionAuthoritySourceType: 'sidecar', descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', commandAuthoritySourceType: 'sidecar', commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', issuerOwnerClass: 'installer', issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/help-catalog-generator.js::buildSidecarAwareExemplarHelpRow()', issuingComponentBindingEvidence: 'deterministic', stageStatus: 'PASS', status: 'PASS', }, { stage: 'merged-config-row', artifactPath: '_bmad/_config/bmad-help.csv', rowIdentity: 'merged-help-row:bmad-help', canonicalId: 'bmad-help', sourcePath: 'bmad-fork/src/core/tasks/help.md', rowCountForStageCanonicalId: '1', commandValue: 'bmad-help', expectedCommandValue: 'bmad-help', descriptionValue: 'Help command', expectedDescriptionValue: 'Help command', descriptionAuthoritySourceType: 'sidecar', descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', commandAuthoritySourceType: 'sidecar', commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', issuerOwnerClass: 'installer', issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/installer.js::mergeModuleHelpCatalogs()', issuingComponentBindingEvidence: 'deterministic', stageStatus: 'PASS', status: 'PASS', }, ], ); await writeCsv( path.join(tempConfigDir, 'bmad-help-command-label-report.csv'), [ 'surface', 'canonicalId', 'rawCommandValue', 'displayedCommandLabel', 'normalizedDisplayedLabel', 'rowCountForCanonicalId', 'authoritySourceType', 'authoritySourcePath', 'status', 'failureReason', ], [ { surface: '_bmad/_config/bmad-help.csv', canonicalId: 'bmad-help', rawCommandValue: 'bmad-help', displayedCommandLabel: '/bmad-help', normalizedDisplayedLabel: '/bmad-help', rowCountForCanonicalId: '1', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', status: 'PASS', failureReason: '', }, ], ); const harness = new HelpValidationHarness(); const firstRun = await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); assert( firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === HELP_VALIDATION_ARTIFACT_REGISTRY.length, 'Help validation harness generates and validates all required artifacts', ); const artifactPathsById = new Map( HELP_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [ artifact.artifactId, path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath), ]), ); for (const [artifactId, artifactPath] of artifactPathsById.entries()) { assert(await fs.pathExists(artifactPath), `Help validation harness outputs artifact ${artifactId}`); } const artifactThreeBaselineRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), { columns: true, skip_empty_lines: true, }); const manifestProvenanceRow = artifactThreeBaselineRows.find((row) => row.artifactPath === '_bmad/_config/task-manifest.csv'); let manifestReplayEvidence = null; try { manifestReplayEvidence = JSON.parse(String(manifestProvenanceRow?.issuingComponentBindingEvidence || '')); } catch { manifestReplayEvidence = null; } assert( manifestReplayEvidence && manifestReplayEvidence.evidenceVersion === 1 && manifestReplayEvidence.observationMethod === 'validator-observed-baseline-plus-isolated-single-component-perturbation' && typeof manifestReplayEvidence.baselineArtifactSha256 === 'string' && manifestReplayEvidence.baselineArtifactSha256.length === 64 && typeof manifestReplayEvidence.mutatedArtifactSha256 === 'string' && manifestReplayEvidence.mutatedArtifactSha256.length === 64 && manifestReplayEvidence.baselineArtifactSha256 !== manifestReplayEvidence.mutatedArtifactSha256 && manifestReplayEvidence.perturbationApplied === true && Number(manifestReplayEvidence.baselineTargetRowCount) > Number(manifestReplayEvidence.mutatedTargetRowCount) && manifestReplayEvidence.targetedRowLocator === manifestProvenanceRow.rowIdentity, 'Help validation harness emits validator-observed replay evidence with baseline/perturbation impact', ); const firstArtifactContents = new Map(); for (const [artifactId, artifactPath] of artifactPathsById.entries()) { firstArtifactContents.set(artifactId, await fs.readFile(artifactPath, 'utf8')); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); let deterministicOutputs = true; for (const [artifactId, artifactPath] of artifactPathsById.entries()) { const rerunContent = await fs.readFile(artifactPath, 'utf8'); if (rerunContent !== firstArtifactContents.get(artifactId)) { deterministicOutputs = false; break; } } assert(deterministicOutputs, 'Help validation harness outputs are byte-stable across unchanged repeated runs'); await fs.remove(path.join(tempSkillDir, 'SKILL.md')); const noIdeInstaller = new Installer(); noIdeInstaller.codexExportDerivationRecords = []; const noIdeValidationOptions = await noIdeInstaller.buildHelpValidationOptions({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, }); assert( noIdeValidationOptions.requireExportSkillProjection === false, 'Installer help validation options disable export-surface requirement for no-IDE/non-Codex flow', ); const noIdeRun = await harness.generateAndValidate({ ...noIdeValidationOptions, sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); assert( noIdeRun.terminalStatus === 'PASS', 'Help validation harness remains terminal-PASS for no-IDE/non-Codex flow when core projection surfaces are present', ); const noIdeStandaloneValidation = await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot, bmadFolderName: '_bmad', }); assert( noIdeStandaloneValidation.status === 'PASS', 'Help validation harness infers no-IDE export prerequisite context during standalone validation when options are omitted', ); try { await harness.buildObservedBindingEvidence({ artifactPath: '_bmad/_config/task-manifest.csv', absolutePath: path.join(tempBmadDir, '_config', 'task-manifest.csv'), componentPath: 'bmad-fork/tools/cli/installers/lib/core/manifest-generator.js', rowIdentity: 'issued-artifact:missing-claim-row', optionalSurface: false, runtimeFolder: '_bmad', }); assert(false, 'Help replay evidence generation rejects unmapped claimed rowIdentity'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, 'Help replay evidence generation emits deterministic missing-claimed-rowIdentity error code', ); } await fs.writeFile( path.join(tempSkillDir, 'SKILL.md'), `---\n${yaml.stringify({ name: 'bmad-help', description: 'Help command' }).trimEnd()}\n---\n\n# Skill\n`, 'utf8', ); await fs.remove(path.join(tempConfigDir, 'task-manifest.csv')); try { await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); assert(false, 'Help validation harness fails when required projection input surfaces are missing'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, 'Help validation harness emits deterministic missing-input-surface error code', ); } await writeCsv( path.join(tempConfigDir, 'task-manifest.csv'), [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS], [ { name: 'help', displayName: 'help', description: 'Help command', module: 'core', path: '_bmad/core/tasks/help.md', standalone: 'true', legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', }, ], ); await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); await fs.remove(artifactPathsById.get(14)); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness fails when a required artifact is missing'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, 'Help validation harness emits deterministic missing-artifact error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactTwoPath = artifactPathsById.get(2); const artifactTwoContent = await fs.readFile(artifactTwoPath, 'utf8'); const artifactTwoLines = artifactTwoContent.split('\n'); artifactTwoLines[0] = artifactTwoLines[0].replace('surface', 'brokenSurface'); await fs.writeFile(artifactTwoPath, artifactTwoLines.join('\n'), 'utf8'); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects schema/header drift'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, 'Help validation harness emits deterministic schema-mismatch error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactNinePath = artifactPathsById.get(9); const artifactNineHeader = (await fs.readFile(artifactNinePath, 'utf8')).split('\n')[0]; await fs.writeFile(artifactNinePath, `${artifactNineHeader}\n`, 'utf8'); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects header-only required-identity artifacts'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, 'Help validation harness emits deterministic missing-row error code for header-only artifacts', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactThreePath = artifactPathsById.get(3); const artifactThreeContent = await fs.readFile(artifactThreePath, 'utf8'); const artifactThreeRows = csv.parse(artifactThreeContent, { columns: true, skip_empty_lines: true, }); artifactThreeRows[0].rowIdentity = ''; await writeCsv( artifactThreePath, [ 'rowIdentity', 'artifactPath', 'canonicalId', 'issuerOwnerClass', 'evidenceIssuerComponent', 'evidenceMethod', 'issuingComponent', 'issuingComponentBindingBasis', 'issuingComponentBindingEvidence', 'claimScope', 'status', ], artifactThreeRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects missing required row identity values'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING, 'Help validation harness emits deterministic row-identity error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactFourPath = artifactPathsById.get(4); const artifactFourRows = csv.parse(await fs.readFile(artifactFourPath, 'utf8'), { columns: true, skip_empty_lines: true, }); artifactFourRows[0].issuedArtifactEvidenceRowIdentity = ''; await writeCsv( artifactFourPath, [ 'surface', 'sourcePath', 'legacyName', 'canonicalId', 'displayName', 'normalizedCapabilityKey', 'authoritySourceType', 'authoritySourcePath', 'issuerOwnerClass', 'issuingComponent', 'issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity', 'issuingComponentBindingEvidence', 'status', ], artifactFourRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects PASS rows missing required evidence-link fields'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING, 'Help validation harness emits deterministic evidence-link error code for missing row identity link', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactNineTamperedRows = csv.parse(await fs.readFile(artifactPathsById.get(9), 'utf8'), { columns: true, skip_empty_lines: true, }); artifactNineTamperedRows[0].issuingComponent = 'self-attested-generator-component'; await writeCsv( artifactPathsById.get(9), [ 'stage', 'artifactPath', 'rowIdentity', 'canonicalId', 'sourcePath', 'rowCountForStageCanonicalId', 'commandValue', 'expectedCommandValue', 'descriptionValue', 'expectedDescriptionValue', 'descriptionAuthoritySourceType', 'descriptionAuthoritySourcePath', 'commandAuthoritySourceType', 'commandAuthoritySourcePath', 'issuerOwnerClass', 'issuingComponent', 'issuedArtifactEvidencePath', 'issuedArtifactEvidenceRowIdentity', 'issuingComponentBindingEvidence', 'stageStatus', 'status', ], artifactNineTamperedRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects self-attested issuer claims that diverge from validator evidence'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM, 'Help validation harness emits deterministic self-attested issuer-claim rejection code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'), sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), }); const artifactThreeTamperedRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), { columns: true, skip_empty_lines: true, }); artifactThreeTamperedRows[0].issuingComponentBindingEvidence = '{"broken":true}'; await writeCsv( artifactPathsById.get(3), [ 'rowIdentity', 'artifactPath', 'canonicalId', 'issuerOwnerClass', 'evidenceIssuerComponent', 'evidenceMethod', 'issuingComponent', 'issuingComponentBindingBasis', 'issuingComponentBindingEvidence', 'claimScope', 'status', ], artifactThreeTamperedRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Help validation harness rejects malformed replay-evidence payloads'); } catch (error) { assert( error.code === HELP_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, 'Help validation harness emits deterministic replay-evidence validation error code', ); } } catch (error) { assert(false, 'Deterministic validation artifact suite setup', error.message); } finally { await fs.remove(tempValidationHarnessRoot); } console.log(''); // ============================================================ // Test 15: Shard-doc Validation Artifact Suite // ============================================================ console.log(`${colors.yellow}Test Suite 15: Shard-doc Validation Artifact Suite${colors.reset}\n`); const tempShardDocValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-shard-doc-validation-suite-')); try { const tempProjectRoot = tempShardDocValidationHarnessRoot; const tempBmadDir = path.join(tempProjectRoot, '_bmad'); const tempConfigDir = path.join(tempBmadDir, '_config'); const tempSourceTasksDir = path.join(tempProjectRoot, 'bmad-fork', 'src', 'core', 'tasks'); const commandLabelReportPath = path.join(tempConfigDir, 'bmad-help-command-label-report.csv'); await fs.ensureDir(tempConfigDir); await fs.ensureDir(tempSourceTasksDir); const writeCsv = async (filePath, columns, rows) => { const buildCsvLine = (values) => values .map((value) => { const text = String(value ?? ''); return text.includes(',') || text.includes('"') ? `"${text.replaceAll('"', '""')}"` : text; }) .join(','); const lines = [columns.join(','), ...rows.map((row) => buildCsvLine(columns.map((column) => row[column] ?? '')))]; await fs.writeFile(filePath, `${lines.join('\n')}\n`, 'utf8'); }; const commandLabelReportColumns = [ 'surface', 'canonicalId', 'rawCommandValue', 'displayedCommandLabel', 'normalizedDisplayedLabel', 'rowCountForCanonicalId', 'authoritySourceType', 'authoritySourcePath', 'status', 'failureReason', ]; const commandLabelReportRows = [ { surface: '_bmad/_config/bmad-help.csv', canonicalId: 'bmad-shard-doc', rawCommandValue: 'bmad-shard-doc', displayedCommandLabel: '/bmad-shard-doc', normalizedDisplayedLabel: '/bmad-shard-doc', rowCountForCanonicalId: '1', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', status: 'PASS', failureReason: '', }, ]; await fs.writeFile( path.join(tempSourceTasksDir, 'shard-doc.artifact.yaml'), yaml.stringify({ schemaVersion: 1, canonicalId: 'bmad-shard-doc', artifactType: 'task', module: 'core', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', dependencies: { requires: [], }, }), 'utf8', ); await fs.writeFile( path.join(tempSourceTasksDir, 'shard-doc.xml'), 'Split markdown docs\n', 'utf8', ); await writeCsv( path.join(tempConfigDir, 'task-manifest.csv'), [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_CANONICAL_ADDITIVE_COLUMNS], [ { name: 'shard-doc', displayName: 'Shard Document', description: 'Split large markdown documents into smaller files by section with an index.', module: 'core', path: '_bmad/core/tasks/shard-doc.xml', standalone: 'true', legacyName: 'shard-doc', canonicalId: 'bmad-shard-doc', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', }, ], ); await writeCsv( path.join(tempConfigDir, 'bmad-help.csv'), [...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_CANONICAL_ADDITIVE_COLUMNS], [ { module: 'core', phase: 'anytime', name: 'Help', code: 'BH', sequence: '', 'workflow-file': '_bmad/core/tasks/help.md', command: 'bmad-help', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Show BMAD help and available resources.', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Shard Document', code: 'SD', sequence: '', 'workflow-file': '_bmad/core/tasks/shard-doc.xml', command: 'bmad-shard-doc', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Split large markdown documents into smaller files by section with an index.', 'output-location': '', outputs: '', }, { module: 'core', phase: 'anytime', name: 'Index Docs', code: 'ID', sequence: '', 'workflow-file': '_bmad/core/tasks/index-docs.xml', command: 'bmad-index-docs', required: 'false', 'agent-name': '', 'agent-command': '', 'agent-display-name': '', 'agent-title': '', options: '', description: 'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.', 'output-location': '', outputs: '', }, ], ); await writeCsv( path.join(tempConfigDir, 'canonical-aliases.csv'), [ 'canonicalId', 'alias', 'aliasType', 'authoritySourceType', 'authoritySourcePath', 'rowIdentity', 'normalizedAliasValue', 'rawIdentityHasLeadingSlash', 'resolutionEligibility', ], [ { canonicalId: 'bmad-shard-doc', alias: 'bmad-shard-doc', aliasType: 'canonical-id', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', rowIdentity: 'alias-row:bmad-shard-doc:canonical-id', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', }, { canonicalId: 'bmad-shard-doc', alias: 'shard-doc', aliasType: 'legacy-name', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', rowIdentity: 'alias-row:bmad-shard-doc:legacy-name', normalizedAliasValue: 'shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', }, { canonicalId: 'bmad-shard-doc', alias: '/bmad-shard-doc', aliasType: 'slash-command', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', rowIdentity: 'alias-row:bmad-shard-doc:slash-command', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', }, ], ); await writeCsv(commandLabelReportPath, commandLabelReportColumns, commandLabelReportRows); const authorityRecords = [ { recordType: 'metadata-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', }, { recordType: 'source-body-authority', canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'source-xml', authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', }, ]; const harness = new ShardDocValidationHarness(); const firstRun = await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); assert( firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY.length, 'Shard-doc validation harness generates and validates all required artifacts', ); const artifactPathsById = new Map( SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [ artifact.artifactId, path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath), ]), ); for (const [artifactId, artifactPath] of artifactPathsById.entries()) { assert(await fs.pathExists(artifactPath), `Shard-doc validation harness outputs artifact ${artifactId}`); } const firstArtifactContents = new Map(); for (const [artifactId, artifactPath] of artifactPathsById.entries()) { firstArtifactContents.set(artifactId, await fs.readFile(artifactPath, 'utf8')); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); let deterministicOutputs = true; for (const [artifactId, artifactPath] of artifactPathsById.entries()) { const rerunContent = await fs.readFile(artifactPath, 'utf8'); if (rerunContent !== firstArtifactContents.get(artifactId)) { deterministicOutputs = false; break; } } assert(deterministicOutputs, 'Shard-doc validation harness outputs are byte-stable across unchanged repeated runs'); try { await harness.executeIsolatedReplay({ artifactPath: '_bmad/_config/task-manifest.csv', componentPath: 'bmad-fork/tools/cli/installers/lib/core/manifest-generator.js', rowIdentity: '', runtimeFolder: '_bmad', }); assert(false, 'Shard-doc replay evidence generation rejects missing claimed rowIdentity'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, 'Shard-doc replay evidence generation emits deterministic missing-claimed-rowIdentity error code', ); } try { await harness.executeIsolatedReplay({ artifactPath: '_bmad/_config/task-manifest.csv', componentPath: 'bmad-fork/tools/cli/installers/lib/core/installer.js::mergeModuleHelpCatalogs()', rowIdentity: 'issued-artifact:_bmad-_config-task-manifest.csv', runtimeFolder: '_bmad', }); assert(false, 'Shard-doc replay evidence generation rejects issuing-component contract mismatch'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID, 'Shard-doc replay evidence generation emits deterministic issuing-component contract mismatch code', ); } const artifactElevenPath = artifactPathsById.get(11); const artifactElevenRows = csv.parse(await fs.readFile(artifactElevenPath, 'utf8'), { columns: true, skip_empty_lines: true, }); artifactElevenRows[0].baselineArtifactSha256 = 'not-a-sha'; await writeCsv(artifactElevenPath, SHARD_DOC_VALIDATION_ARTIFACT_REGISTRY[10].columns, artifactElevenRows); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Shard-doc validation harness rejects malformed replay-evidence payloads'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REPLAY_EVIDENCE_INVALID, 'Shard-doc validation harness emits deterministic replay-evidence validation error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); await fs.remove(artifactPathsById.get(8)); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Shard-doc validation harness fails when a required artifact is missing'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, 'Shard-doc validation harness emits deterministic missing-artifact error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); await fs.remove(commandLabelReportPath); try { await harness.generateValidationArtifacts({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); assert(false, 'Shard-doc validation harness rejects missing command-label report input surface'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING, 'Shard-doc validation harness emits deterministic missing-input-surface error code', ); } await writeCsv(commandLabelReportPath, commandLabelReportColumns, commandLabelReportRows); const artifactSixPath = artifactPathsById.get(6); const artifactSixLines = (await fs.readFile(artifactSixPath, 'utf8')).split('\n'); artifactSixLines[0] = artifactSixLines[0].replace('canonicalId', 'brokenCanonicalId'); await fs.writeFile(artifactSixPath, artifactSixLines.join('\n'), 'utf8'); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Shard-doc validation harness rejects schema/header drift'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH, 'Shard-doc validation harness emits deterministic schema-mismatch error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); const artifactEightPath = artifactPathsById.get(8); const artifactEightRows = csv.parse(await fs.readFile(artifactEightPath, 'utf8'), { columns: true, skip_empty_lines: true, }); const artifactSixInventoryRow = artifactEightRows.find((row) => row.artifactId === '6'); if (artifactSixInventoryRow) { artifactSixInventoryRow.artifactPath = 'validation/shard-doc/drifted-command-label-report.csv'; } await writeCsv( artifactEightPath, ['rowIdentity', 'artifactId', 'artifactPath', 'artifactType', 'required', 'rowCount', 'exists', 'schemaVersion', 'status'], artifactEightRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Shard-doc validation harness rejects inventory deterministic-identifier drift'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, 'Shard-doc validation harness emits deterministic inventory-row validation error code', ); } await harness.generateAndValidate({ projectDir: tempProjectRoot, bmadDir: tempBmadDir, bmadFolderName: '_bmad', shardDocAuthorityRecords: authorityRecords, }); const artifactTwoPath = artifactPathsById.get(2); const artifactTwoRows = csv.parse(await fs.readFile(artifactTwoPath, 'utf8'), { columns: true, skip_empty_lines: true, }); const filteredAuthorityRows = artifactTwoRows.filter((row) => row.recordType !== 'source-body-authority'); await writeCsv( artifactTwoPath, ['rowIdentity', 'recordType', 'canonicalId', 'authoritativePresenceKey', 'authoritySourceType', 'authoritySourcePath', 'status'], filteredAuthorityRows, ); try { await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot }); assert(false, 'Shard-doc validation harness rejects missing source-body authority records'); } catch (error) { assert( error.code === SHARD_DOC_VALIDATION_ERROR_CODES.REQUIRED_ROW_MISSING, 'Shard-doc validation harness emits deterministic missing-row error code', ); } } catch (error) { assert(false, 'Shard-doc validation artifact suite setup', error.message); } finally { await fs.remove(tempShardDocValidationHarnessRoot); } console.log(''); // ============================================================ // Summary // ============================================================ console.log(`${colors.cyan}========================================`); console.log('Test Results:'); console.log(` Passed: ${colors.green}${passed}${colors.reset}`); console.log(` Failed: ${colors.red}${failed}${colors.reset}`); console.log(`========================================${colors.reset}\n`); if (failed === 0) { console.log(`${colors.green}✨ All installation component tests passed!${colors.reset}\n`); process.exit(0); } else { console.log(`${colors.red}❌ Some installation component tests failed${colors.reset}\n`); process.exit(1); } } // Run tests runTests().catch((error) => { console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message); console.error(error.stack); process.exit(1); });