diff --git a/test/test-installation-components.js b/test/test-installation-components.js index beb6c2551..e7433d571 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -36,6 +36,8 @@ const { SHARD_DOC_SIDECAR_ERROR_CODES, INDEX_DOCS_SIDECAR_REQUIRED_FIELDS, INDEX_DOCS_SIDECAR_ERROR_CODES, + SKILL_METADATA_RESOLUTION_ERROR_CODES, + resolveSkillMetadataAuthority, validateHelpSidecarContractFile, validateShardDocSidecarContractFile, validateIndexDocsSidecarContractFile, @@ -255,7 +257,7 @@ async function runTests() { 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 deterministicSourcePath = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; const expectedUnsupportedMajorDetail = 'sidecar schema major version is unsupported'; const expectedBasenameMismatchDetail = 'sidecar basename does not match sourcePath basename'; @@ -434,7 +436,7 @@ async function runTests() { 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 deterministicShardDocSourcePath = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; const writeTempShardDocSidecar = async (data) => { await fs.writeFile(tempShardDocSidecarPath, yaml.stringify(data), 'utf8'); @@ -631,7 +633,7 @@ async function runTests() { 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 deterministicIndexDocsSourcePath = 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml'; const writeTempIndexDocsSidecar = async (data) => { await fs.writeFile(tempIndexDocsSidecarPath, yaml.stringify(data), 'utf8'); @@ -803,6 +805,140 @@ async function runTests() { console.log(''); + // ============================================================ + // Test 4d: Skill Metadata Filename Authority Resolution + // ============================================================ + console.log(`${colors.yellow}Test Suite 4d: Skill Metadata Filename Authority Resolution${colors.reset}\n`); + try { + const convertedCapabilitySources = [ + { label: 'help', sourceFilename: 'help.md', artifactFilename: 'help.artifact.yaml' }, + { label: 'shard-doc', sourceFilename: 'shard-doc.xml', artifactFilename: 'shard-doc.artifact.yaml' }, + { label: 'index-docs', sourceFilename: 'index-docs.xml', artifactFilename: 'index-docs.artifact.yaml' }, + ]; + + const withResolverWorkspace = async (sourceFilename, callback) => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), `bmad-metadata-authority-${sourceFilename.replaceAll(/\W+/g, '-')}-`)); + try { + const tasksDir = path.join(tempRoot, 'src', 'core', 'tasks'); + await fs.ensureDir(tasksDir); + + const sourcePath = path.join(tasksDir, sourceFilename); + await fs.writeFile(sourcePath, '# source\n', 'utf8'); + + const sourceStem = path.basename(sourceFilename, path.extname(sourceFilename)); + const skillDir = path.join(tasksDir, sourceStem); + await fs.ensureDir(skillDir); + + await callback({ + tempRoot, + tasksDir, + sourcePath, + skillDir, + }); + } finally { + await fs.remove(tempRoot); + } + }; + + for (const sourceConfig of convertedCapabilitySources) { + const { label, sourceFilename, artifactFilename } = sourceConfig; + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath, skillDir }) => { + await fs.writeFile(path.join(skillDir, 'skill-manifest.yaml'), 'canonicalId: canonical\n', 'utf8'); + await fs.writeFile(path.join(skillDir, 'bmad-config.yaml'), 'canonicalId: bmad-config\n', 'utf8'); + await fs.writeFile(path.join(skillDir, 'manifest.yaml'), 'canonicalId: manifest\n', 'utf8'); + await fs.writeFile(path.join(tasksDir, artifactFilename), 'canonicalId: artifact\n', 'utf8'); + + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert( + resolution.resolvedFilename === 'skill-manifest.yaml' && resolution.derivationMode === 'canonical', + `${label} resolver prioritizes per-skill canonical skill-manifest.yaml over legacy metadata files`, + ); + }); + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath, skillDir }) => { + await fs.writeFile(path.join(skillDir, 'bmad-config.yaml'), 'canonicalId: bmad-config\n', 'utf8'); + await fs.writeFile(path.join(skillDir, 'manifest.yaml'), 'canonicalId: manifest\n', 'utf8'); + await fs.writeFile(path.join(tasksDir, artifactFilename), 'canonicalId: artifact\n', 'utf8'); + + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert( + resolution.resolvedFilename === 'bmad-config.yaml' && resolution.derivationMode === 'legacy-fallback', + `${label} resolver falls back to bmad-config.yaml before manifest.yaml and *.artifact.yaml`, + ); + }); + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath, skillDir }) => { + await fs.writeFile(path.join(skillDir, 'manifest.yaml'), 'canonicalId: manifest\n', 'utf8'); + await fs.writeFile(path.join(tasksDir, artifactFilename), 'canonicalId: artifact\n', 'utf8'); + + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert( + resolution.resolvedFilename === 'manifest.yaml' && resolution.derivationMode === 'legacy-fallback', + `${label} resolver falls back to manifest.yaml before *.artifact.yaml`, + ); + }); + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath }) => { + await fs.writeFile(path.join(tasksDir, artifactFilename), 'canonicalId: artifact\n', 'utf8'); + + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert( + resolution.resolvedFilename === artifactFilename && resolution.derivationMode === 'legacy-fallback', + `${label} resolver supports capability-scoped *.artifact.yaml fallback`, + ); + }); + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath }) => { + await fs.writeFile(path.join(tasksDir, 'skill-manifest.yaml'), 'canonicalId: root-canonical\n', 'utf8'); + await fs.writeFile(path.join(tasksDir, artifactFilename), 'canonicalId: artifact\n', 'utf8'); + + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert( + resolution.resolvedFilename === artifactFilename, + `${label} resolver does not treat root task-folder skill-manifest.yaml as per-skill canonical authority`, + ); + }); + + await withResolverWorkspace(sourceFilename, async ({ tempRoot, tasksDir, sourcePath, skillDir }) => { + await fs.writeFile(path.join(tasksDir, 'bmad-config.yaml'), 'canonicalId: root-bmad-config\n', 'utf8'); + await fs.writeFile(path.join(skillDir, 'bmad-config.yaml'), 'canonicalId: skill-bmad-config\n', 'utf8'); + + try { + await resolveSkillMetadataAuthority({ + sourceFilePath: sourcePath, + projectRoot: tempRoot, + }); + assert(false, `${label} resolver rejects ambiguous bmad-config.yaml coexistence across legacy locations`); + } catch (error) { + assert( + error.code === SKILL_METADATA_RESOLUTION_ERROR_CODES.AMBIGUOUS_MATCH, + `${label} resolver emits deterministic ambiguity code for bmad-config.yaml coexistence`, + ); + } + }); + } + } catch (error) { + assert(false, 'Skill metadata filename authority resolver suite setup', error.message); + } + + console.log(''); + // ============================================================ // Test 5: Authority Split and Frontmatter Precedence // ============================================================ @@ -814,7 +950,7 @@ async function runTests() { const tempAuthorityRuntimePath = path.join(tempAuthorityRoot, 'help-runtime.md'); const deterministicAuthorityPaths = { - sidecar: 'bmad-fork/src/core/tasks/help.artifact.yaml', + sidecar: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', source: 'bmad-fork/src/core/tasks/help.md', runtime: '_bmad/core/tasks/help.md', }; @@ -1000,7 +1136,7 @@ async function runTests() { const tempShardDocModuleHelpPath = path.join(tempAuthorityRoot, 'module-help.csv'); const deterministicShardDocAuthorityPaths = { - sidecar: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + sidecar: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.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', @@ -1211,7 +1347,7 @@ async function runTests() { const tempIndexDocsModuleHelpPath = path.join(tempAuthorityRoot, 'index-docs-module-help.csv'); const deterministicIndexDocsAuthorityPaths = { - sidecar: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + sidecar: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.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', @@ -1546,7 +1682,7 @@ async function runTests() { // 6b: Shard-doc fail-fast covers Shard-doc negative matrix classes. { - const deterministicShardDocFailFastSourcePath = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; + const deterministicShardDocFailFastSourcePath = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; const shardDocFailureScenarios = [ { label: 'missing shard-doc sidecar file', @@ -1793,7 +1929,7 @@ async function runTests() { 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'; + error.sourcePath = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; throw error; }; installer.validateIndexDocsAuthoritySplitAndPrecedence = async () => { @@ -1847,7 +1983,7 @@ async function runTests() { ); 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', + error.sourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', 'Installer shard-doc canonical drift returns deterministic source path', ); assert( @@ -2064,7 +2200,7 @@ async function runTests() { 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', + sidecar: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', source: 'bmad-fork/src/core/tasks/help.md', runtime: '_bmad/core/tasks/help.md', }; @@ -2443,7 +2579,7 @@ async function runTests() { canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; @@ -2454,7 +2590,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', }, { @@ -2462,7 +2598,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', }, ]; @@ -2495,7 +2631,7 @@ async function runTests() { 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', + helpTaskRow && helpTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Task manifest help row sets authoritySourcePath to sidecar source path', ); @@ -2515,7 +2651,7 @@ async function runTests() { 'Task manifest shard-doc row sets authoritySourceType=sidecar', ); assert( - shardDocTaskRow && shardDocTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + shardDocTaskRow && shardDocTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', 'Task manifest shard-doc row sets authoritySourcePath to shard-doc sidecar source path', ); assert(!!indexDocsTaskRow, 'Task manifest includes converted index-docs row'); @@ -2529,7 +2665,7 @@ async function runTests() { 'Task manifest index-docs row sets authoritySourceType=sidecar', ); assert( - indexDocsTaskRow && indexDocsTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + indexDocsTaskRow && indexDocsTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', 'Task manifest index-docs row sets authoritySourcePath to index-docs sidecar source path', ); @@ -2642,7 +2778,7 @@ async function runTests() { assert( capturedAuthorityValidationOptions && - capturedAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + capturedAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Installer passes locked sidecar source path to authority validation', ); assert( @@ -2656,7 +2792,7 @@ async function runTests() { ); assert( capturedShardDocAuthorityValidationOptions && - capturedShardDocAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + capturedShardDocAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', 'Installer passes locked shard-doc sidecar source path to shard-doc authority validation', ); assert( @@ -2671,7 +2807,7 @@ async function runTests() { ); assert( capturedIndexDocsAuthorityValidationOptions && - capturedIndexDocsAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + capturedIndexDocsAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', 'Installer passes locked index-docs sidecar source path to index-docs authority validation', ); assert( @@ -2687,7 +2823,7 @@ async function runTests() { assert( Array.isArray(capturedManifestHelpAuthorityRecords) && capturedManifestHelpAuthorityRecords[0] && - capturedManifestHelpAuthorityRecords[0].authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + capturedManifestHelpAuthorityRecords[0].authoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Installer passes sidecar authority path into manifest generation options', ); assert( @@ -2697,7 +2833,7 @@ async function runTests() { record && record.canonicalId === 'bmad-shard-doc' && record.authoritySourceType === 'sidecar' && - record.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + record.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', ), 'Installer passes shard-doc sidecar authority records into task-manifest projection options', ); @@ -2708,7 +2844,7 @@ async function runTests() { record && record.canonicalId === 'bmad-index-docs' && record.authoritySourceType === 'sidecar' && - record.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + record.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', ), 'Installer passes index-docs sidecar authority records into task-manifest projection options', ); @@ -2741,7 +2877,7 @@ async function runTests() { canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; @@ -2752,7 +2888,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/shard-doc.xml', }, { @@ -2760,7 +2896,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/index-docs.xml', }, ]; @@ -2797,7 +2933,7 @@ async function runTests() { canonicalId: 'bmad-help', alias: 'bmad-help', aliasType: 'canonical-id', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', @@ -2809,7 +2945,7 @@ async function runTests() { canonicalId: 'bmad-help', alias: 'help', aliasType: 'legacy-name', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', @@ -2821,7 +2957,7 @@ async function runTests() { canonicalId: 'bmad-help', alias: '/bmad-help', aliasType: 'slash-command', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', @@ -2833,7 +2969,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', alias: 'bmad-shard-doc', aliasType: 'canonical-id', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', @@ -2845,7 +2981,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', alias: 'shard-doc', aliasType: 'legacy-name', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', normalizedAliasValue: 'shard-doc', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', @@ -2857,7 +2993,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', alias: '/bmad-shard-doc', aliasType: 'slash-command', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', @@ -2869,7 +3005,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', alias: 'bmad-index-docs', aliasType: 'canonical-id', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'canonical-id-only', @@ -2881,7 +3017,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', alias: 'index-docs', aliasType: 'legacy-name', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', normalizedAliasValue: 'index-docs', rawIdentityHasLeadingSlash: 'false', resolutionEligibility: 'legacy-name-only', @@ -2893,7 +3029,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', alias: '/bmad-index-docs', aliasType: 'slash-command', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'true', resolutionEligibility: 'slash-command-only', @@ -3006,10 +3142,10 @@ async function runTests() { return false; } if (row.canonicalId === 'bmad-help') { - return row.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml'; + return row.authoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; } if (row.canonicalId === 'bmad-shard-doc') { - return row.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; + return row.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; } return false; }), @@ -3063,7 +3199,7 @@ async function runTests() { canonicalId: 'bmad-help', authoritativePresenceKey: 'capability:bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', sourcePath: 'bmad-fork/src/core/tasks/help.md', }, ]; @@ -3165,7 +3301,7 @@ async function runTests() { assert( helpCommandLabelRow && helpCommandLabelRow.authoritySourceType === 'sidecar' && - helpCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + helpCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Command-label report includes sidecar provenance linkage', ); assert( @@ -3177,7 +3313,7 @@ async function runTests() { assert( shardDocCommandLabelRow && shardDocCommandLabelRow.authoritySourceType === 'sidecar' && - shardDocCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + shardDocCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', 'Command-label report includes shard-doc sidecar provenance linkage', ); assert( @@ -3189,7 +3325,7 @@ async function runTests() { assert( indexDocsCommandLabelRow && indexDocsCommandLabelRow.authoritySourceType === 'sidecar' && - indexDocsCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + indexDocsCommandLabelRow.authoritySourcePath === 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', 'Command-label report includes index-docs sidecar provenance linkage', ); const generatedCommandLabelReportRaw = await fs.readFile(generatedCommandLabelReportPath, 'utf8'); @@ -3224,7 +3360,7 @@ async function runTests() { const baselineShardDocLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows, { canonicalId: 'bmad-shard-doc', displayedCommandLabel: '/bmad-shard-doc', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', }); assert( baselineShardDocLabelContract.valid, @@ -3234,7 +3370,7 @@ async function runTests() { const baselineIndexDocsLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows, { canonicalId: 'bmad-index-docs', displayedCommandLabel: '/bmad-index-docs', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', }); assert( baselineIndexDocsLabelContract.valid, @@ -3355,7 +3491,7 @@ async function runTests() { { canonicalId: 'bmad-shard-doc', displayedCommandLabel: '/bmad-shard-doc', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', }, ); assert( @@ -3372,14 +3508,14 @@ async function runTests() { installedStageRow && installedStageRow.issuingComponent === EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT && installedStageRow.commandAuthoritySourceType === 'sidecar' && - installedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + installedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.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', + mergedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Merged config stage row preserves sidecar command provenance and merge issuing component linkage', ); assert( @@ -3397,7 +3533,7 @@ async function runTests() { generatedPipelineReportRows.every( (row) => row.commandAuthoritySourceType === 'sidecar' && - row.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + row.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', ), 'Installer persists pipeline stage artifact with sidecar command provenance linkage for both stages', ); @@ -3546,7 +3682,7 @@ async function runTests() { assert( exportDerivationRecord && exportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && - exportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + exportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Codex export records exemplar derivation source metadata from sidecar canonical-id', ); @@ -3572,7 +3708,7 @@ async function runTests() { assert( shardDocExportDerivationRecord && shardDocExportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && - shardDocExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml' && + shardDocExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml' && shardDocExportDerivationRecord.sourcePath === 'bmad-fork/src/core/tasks/shard-doc.xml', 'Codex export records shard-doc sidecar-canonical derivation metadata and source path', ); @@ -3599,7 +3735,7 @@ async function runTests() { assert( indexDocsExportDerivationRecord && indexDocsExportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE && - indexDocsExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/index-docs.artifact.yaml' && + indexDocsExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml' && indexDocsExportDerivationRecord.sourcePath === 'bmad-fork/src/core/tasks/index-docs.xml', 'Codex export records index-docs sidecar-canonical derivation metadata and source path', ); @@ -3665,7 +3801,7 @@ async function runTests() { ); assert( submoduleExportDerivationRecord && - submoduleExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml', + submoduleExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', 'Codex export locks exemplar derivation source-path contract when running from submodule root', ); } finally { @@ -3907,7 +4043,7 @@ async function runTests() { legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', futureAdditiveField: 'canonical-additive', }, { @@ -4304,7 +4440,7 @@ async function runTests() { legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', }, ], ); @@ -4327,7 +4463,7 @@ async function runTests() { alias: 'bmad-help', aliasType: 'canonical-id', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-help:canonical-id', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'false', @@ -4338,7 +4474,7 @@ async function runTests() { alias: 'help', aliasType: 'legacy-name', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-help:legacy-name', normalizedAliasValue: 'help', rawIdentityHasLeadingSlash: 'false', @@ -4349,7 +4485,7 @@ async function runTests() { alias: '/bmad-help', aliasType: 'slash-command', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-help:slash-command', normalizedAliasValue: 'bmad-help', rawIdentityHasLeadingSlash: 'true', @@ -4520,9 +4656,9 @@ async function runTests() { descriptionValue: 'Help command', expectedDescriptionValue: 'Help command', descriptionAuthoritySourceType: 'sidecar', - descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', commandAuthoritySourceType: 'sidecar', - commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', issuerOwnerClass: 'installer', issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/help-catalog-generator.js::buildSidecarAwareExemplarHelpRow()', issuingComponentBindingEvidence: 'deterministic', @@ -4541,9 +4677,9 @@ async function runTests() { descriptionValue: 'Help command', expectedDescriptionValue: 'Help command', descriptionAuthoritySourceType: 'sidecar', - descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', commandAuthoritySourceType: 'sidecar', - commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', issuerOwnerClass: 'installer', issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/installer.js::mergeModuleHelpCatalogs()', issuingComponentBindingEvidence: 'deterministic', @@ -4575,7 +4711,7 @@ async function runTests() { normalizedDisplayedLabel: '/bmad-help', rowCountForCanonicalId: '1', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', status: 'PASS', failureReason: '', }, @@ -4734,7 +4870,7 @@ async function runTests() { legacyName: 'help', canonicalId: 'bmad-help', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/help/skill-manifest.yaml', }, ], ); @@ -4976,6 +5112,24 @@ async function runTests() { 'Help validation harness emits deterministic replay-evidence validation error code', ); } + + await fs.writeFile(path.join(tempSourceTasksDir, 'bmad-config.yaml'), 'canonicalId: root-bmad-config\n', 'utf8'); + await fs.ensureDir(path.join(tempSourceTasksDir, 'help')); + await fs.writeFile(path.join(tempSourceTasksDir, 'help', 'bmad-config.yaml'), 'canonicalId: help-bmad-config\n', 'utf8'); + try { + await harness.generateValidationArtifacts({ + projectDir: tempProjectRoot, + bmadDir: tempBmadDir, + bmadFolderName: '_bmad', + sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'), + }); + assert(false, 'Help validation harness normalizes metadata-resolution ambiguity into harness-native deterministic error'); + } catch (error) { + assert( + error.code === HELP_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + 'Help validation harness emits deterministic metadata-resolution error code', + ); + } } catch (error) { assert(false, 'Deterministic validation artifact suite setup', error.message); } finally { @@ -5033,7 +5187,7 @@ async function runTests() { normalizedDisplayedLabel: '/bmad-shard-doc', rowCountForCanonicalId: '1', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', status: 'PASS', failureReason: '', }, @@ -5075,7 +5229,7 @@ async function runTests() { legacyName: 'shard-doc', canonicalId: 'bmad-shard-doc', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', }, ], ); @@ -5159,7 +5313,7 @@ async function runTests() { alias: 'bmad-shard-doc', aliasType: 'canonical-id', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-shard-doc:canonical-id', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'false', @@ -5170,7 +5324,7 @@ async function runTests() { alias: 'shard-doc', aliasType: 'legacy-name', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-shard-doc:legacy-name', normalizedAliasValue: 'shard-doc', rawIdentityHasLeadingSlash: 'false', @@ -5181,7 +5335,7 @@ async function runTests() { alias: '/bmad-shard-doc', aliasType: 'slash-command', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-shard-doc:slash-command', normalizedAliasValue: 'bmad-shard-doc', rawIdentityHasLeadingSlash: 'true', @@ -5197,7 +5351,7 @@ async function runTests() { canonicalId: 'bmad-shard-doc', authoritativePresenceKey: 'capability:bmad-shard-doc', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml', }, { recordType: 'source-body-authority', @@ -5412,6 +5566,24 @@ async function runTests() { 'Shard-doc validation harness emits deterministic missing-row error code', ); } + + await fs.writeFile(path.join(tempSourceTasksDir, 'bmad-config.yaml'), 'canonicalId: root-bmad-config\n', 'utf8'); + await fs.ensureDir(path.join(tempSourceTasksDir, 'shard-doc')); + await fs.writeFile(path.join(tempSourceTasksDir, 'shard-doc', 'bmad-config.yaml'), 'canonicalId: shard-doc-bmad-config\n', 'utf8'); + try { + await harness.generateValidationArtifacts({ + projectDir: tempProjectRoot, + bmadDir: tempBmadDir, + bmadFolderName: '_bmad', + sourceXmlPath: path.join(tempSourceTasksDir, 'shard-doc.xml'), + }); + assert(false, 'Shard-doc validation harness normalizes metadata-resolution ambiguity into harness-native deterministic error'); + } catch (error) { + assert( + error.code === SHARD_DOC_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + 'Shard-doc validation harness emits deterministic metadata-resolution error code', + ); + } } catch (error) { assert(false, 'Shard-doc validation artifact suite setup', error.message); } finally { @@ -5468,7 +5640,7 @@ async function runTests() { normalizedDisplayedLabel: '/bmad-index-docs', rowCountForCanonicalId: '1', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', status: 'PASS', failureReason: '', }, @@ -5512,7 +5684,7 @@ async function runTests() { legacyName: 'index-docs', canonicalId: 'bmad-index-docs', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', }, ], ); @@ -5596,7 +5768,7 @@ async function runTests() { alias: 'bmad-index-docs', aliasType: 'canonical-id', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-index-docs:canonical-id', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'false', @@ -5607,7 +5779,7 @@ async function runTests() { alias: 'index-docs', aliasType: 'legacy-name', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-index-docs:legacy-name', normalizedAliasValue: 'index-docs', rawIdentityHasLeadingSlash: 'false', @@ -5618,7 +5790,7 @@ async function runTests() { alias: '/bmad-index-docs', aliasType: 'slash-command', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', rowIdentity: 'alias-row:bmad-index-docs:slash-command', normalizedAliasValue: 'bmad-index-docs', rawIdentityHasLeadingSlash: 'true', @@ -5634,7 +5806,7 @@ async function runTests() { canonicalId: 'bmad-index-docs', authoritativePresenceKey: 'capability:bmad-index-docs', authoritySourceType: 'sidecar', - authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs.artifact.yaml', + authoritySourcePath: 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml', }, { recordType: 'source-body-authority', @@ -5849,6 +6021,24 @@ async function runTests() { 'Index-docs validation harness emits deterministic missing-row error code', ); } + + await fs.writeFile(path.join(tempSourceTasksDir, 'bmad-config.yaml'), 'canonicalId: root-bmad-config\n', 'utf8'); + await fs.ensureDir(path.join(tempSourceTasksDir, 'index-docs')); + await fs.writeFile(path.join(tempSourceTasksDir, 'index-docs', 'bmad-config.yaml'), 'canonicalId: index-docs-bmad-config\n', 'utf8'); + try { + await harness.generateValidationArtifacts({ + projectDir: tempProjectRoot, + bmadDir: tempBmadDir, + bmadFolderName: '_bmad', + sourceXmlPath: path.join(tempSourceTasksDir, 'index-docs.xml'), + }); + assert(false, 'Index-docs validation harness normalizes metadata-resolution ambiguity into harness-native deterministic error'); + } catch (error) { + assert( + error.code === INDEX_DOCS_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + 'Index-docs validation harness emits deterministic metadata-resolution error code', + ); + } } catch (error) { assert(false, 'Index-docs validation artifact suite setup', error.message); } finally { diff --git a/tools/cli/installers/lib/core/help-authority-validator.js b/tools/cli/installers/lib/core/help-authority-validator.js index b5a3b02a0..08dc58129 100644 --- a/tools/cli/installers/lib/core/help-authority-validator.js +++ b/tools/cli/installers/lib/core/help-authority-validator.js @@ -3,9 +3,11 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const { getProjectRoot, getSourcePath } = require('../../../lib/project-root'); const { normalizeAndResolveExemplarAlias } = require('./help-alias-normalizer'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const HELP_AUTHORITY_VALIDATION_ERROR_CODES = Object.freeze({ SIDECAR_FILE_NOT_FOUND: 'ERR_HELP_AUTHORITY_SIDECAR_FILE_NOT_FOUND', + SIDECAR_FILENAME_AMBIGUOUS: 'ERR_HELP_AUTHORITY_SIDECAR_FILENAME_AMBIGUOUS', SIDECAR_PARSE_FAILED: 'ERR_HELP_AUTHORITY_SIDECAR_PARSE_FAILED', SIDECAR_INVALID_METADATA: 'ERR_HELP_AUTHORITY_SIDECAR_INVALID_METADATA', MARKDOWN_FILE_NOT_FOUND: 'ERR_HELP_AUTHORITY_MARKDOWN_FILE_NOT_FOUND', @@ -277,17 +279,37 @@ function buildHelpAuthorityRecords({ canonicalId, sidecarSourcePath, sourceMarkd } async function validateHelpAuthoritySplitAndPrecedence(options = {}) { - const sidecarPath = options.sidecarPath || getSourcePath('core', 'tasks', 'help.artifact.yaml'); const sourceMarkdownPath = options.sourceMarkdownPath || getSourcePath('core', 'tasks', 'help.md'); const runtimeMarkdownPath = options.runtimeMarkdownPath || ''; - const sidecarSourcePath = normalizeSourcePath(options.sidecarSourcePath || toProjectRelativePath(sidecarPath)); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceMarkdownPath, + metadataPath: options.sidecarPath || '', + metadataSourcePath: options.sidecarSourcePath || '', + ambiguousErrorCode: HELP_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + }); + } catch (error) { + throw new HelpAuthorityValidationError({ + code: error.code || HELP_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + detail: error.detail || error.message, + fieldPath: error.fieldPath || '', + sourcePath: normalizeSourcePath(error.sourcePath || toProjectRelativePath(sourceMarkdownPath)), + }); + } + + const sidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + + const sidecarSourcePath = normalizeSourcePath( + options.sidecarSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath || resolvedMetadataAuthority.resolvedSourcePath, + ); const sourceMarkdownSourcePath = normalizeSourcePath(options.sourceMarkdownSourcePath || toProjectRelativePath(sourceMarkdownPath)); const runtimeMarkdownSourcePath = normalizeSourcePath( options.runtimeMarkdownSourcePath || (runtimeMarkdownPath ? toProjectRelativePath(runtimeMarkdownPath) : ''), ); - if (!(await fs.pathExists(sidecarPath))) { + if (!sidecarPath || !(await fs.pathExists(sidecarPath))) { throw new HelpAuthorityValidationError({ code: HELP_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, detail: 'Expected sidecar metadata file was not found', @@ -359,6 +381,13 @@ async function validateHelpAuthoritySplitAndPrecedence(options = {}) { authoritativePresenceKey: `capability:${canonicalId}`, authoritativeRecords, checkedSurfaces, + metadataAuthority: { + resolvedPath: normalizeSourcePath(resolvedMetadataAuthority.resolvedSourcePath || sidecarSourcePath), + resolvedFilename: normalizeSourcePath(resolvedMetadataAuthority.resolvedFilename || ''), + canonicalTargetFilename: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetFilename || 'skill-manifest.yaml'), + canonicalTargetPath: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetSourcePath || sidecarSourcePath), + derivationMode: normalizeSourcePath(resolvedMetadataAuthority.derivationMode || ''), + }, }; } diff --git a/tools/cli/installers/lib/core/help-catalog-generator.js b/tools/cli/installers/lib/core/help-catalog-generator.js index 6085ac26c..58ae25ce9 100644 --- a/tools/cli/installers/lib/core/help-catalog-generator.js +++ b/tools/cli/installers/lib/core/help-catalog-generator.js @@ -3,9 +3,10 @@ const path = require('node:path'); const yaml = require('yaml'); const { getSourcePath, getProjectRoot } = require('../../../lib/project-root'); const { normalizeAndResolveExemplarAlias } = require('./help-alias-normalizer'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const EXEMPLAR_HELP_CATALOG_CANONICAL_ID = 'bmad-help'; -const EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml'; +const EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; const EXEMPLAR_HELP_CATALOG_SOURCE_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md'; const EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/help-catalog-generator.js::buildSidecarAwareExemplarHelpRow()'; @@ -13,6 +14,7 @@ const INSTALLER_HELP_CATALOG_MERGE_COMPONENT = 'bmad-fork/tools/cli/installers/l const HELP_CATALOG_GENERATION_ERROR_CODES = Object.freeze({ SIDECAR_FILE_NOT_FOUND: 'ERR_HELP_CATALOG_SIDECAR_FILE_NOT_FOUND', + SIDECAR_FILENAME_AMBIGUOUS: 'ERR_HELP_CATALOG_SIDECAR_FILENAME_AMBIGUOUS', SIDECAR_PARSE_FAILED: 'ERR_HELP_CATALOG_SIDECAR_PARSE_FAILED', SIDECAR_INVALID_METADATA: 'ERR_HELP_CATALOG_SIDECAR_INVALID_METADATA', CANONICAL_ID_MISMATCH: 'ERR_HELP_CATALOG_CANONICAL_ID_MISMATCH', @@ -71,9 +73,29 @@ function createGenerationError(code, fieldPath, sourcePath, detail, observedValu }); } -async function loadExemplarHelpSidecar(sidecarPath = getSourcePath('core', 'tasks', 'help.artifact.yaml')) { - const sourcePath = normalizeSourcePath(toProjectRelativePath(sidecarPath)); - if (!(await fs.pathExists(sidecarPath))) { +async function loadExemplarHelpSidecar(sidecarPath = '') { + const sourceMarkdownPath = getSourcePath('core', 'tasks', 'help.md'); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceMarkdownPath, + metadataPath: sidecarPath, + ambiguousErrorCode: HELP_CATALOG_GENERATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + }); + } catch (error) { + createGenerationError( + error.code || HELP_CATALOG_GENERATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + error.fieldPath || '', + normalizeSourcePath(error.sourcePath || toProjectRelativePath(sourceMarkdownPath)), + error.detail || error.message, + ); + } + + const resolvedMetadataPath = resolvedMetadataAuthority.resolvedAbsolutePath; + const sourcePath = normalizeSourcePath( + resolvedMetadataAuthority.canonicalTargetSourcePath || resolvedMetadataAuthority.resolvedSourcePath, + ); + if (!resolvedMetadataPath || !(await fs.pathExists(resolvedMetadataPath))) { createGenerationError( HELP_CATALOG_GENERATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, '', @@ -84,7 +106,7 @@ async function loadExemplarHelpSidecar(sidecarPath = getSourcePath('core', 'task let sidecarData; try { - sidecarData = yaml.parse(await fs.readFile(sidecarPath, 'utf8')); + sidecarData = yaml.parse(await fs.readFile(resolvedMetadataPath, 'utf8')); } catch (error) { createGenerationError( HELP_CATALOG_GENERATION_ERROR_CODES.SIDECAR_PARSE_FAILED, @@ -128,6 +150,9 @@ async function loadExemplarHelpSidecar(sidecarPath = getSourcePath('core', 'task displayName, description, sourcePath, + resolvedFilename: normalizeSourcePath(resolvedMetadataAuthority.resolvedFilename || ''), + canonicalTargetFilename: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetFilename || 'skill-manifest.yaml'), + derivationMode: normalizeSourcePath(resolvedMetadataAuthority.derivationMode || ''), }; } diff --git a/tools/cli/installers/lib/core/help-validation-harness.js b/tools/cli/installers/lib/core/help-validation-harness.js index 494072cc9..d75c9858a 100644 --- a/tools/cli/installers/lib/core/help-validation-harness.js +++ b/tools/cli/installers/lib/core/help-validation-harness.js @@ -5,7 +5,11 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { getSourcePath } = require('../../../lib/project-root'); -const { validateHelpSidecarContractFile, HELP_SIDECAR_ERROR_CODES } = require('./sidecar-contract-validator'); +const { + validateHelpSidecarContractFile, + HELP_SIDECAR_ERROR_CODES, + resolveSkillMetadataAuthority, +} = require('./sidecar-contract-validator'); const { validateHelpAuthoritySplitAndPrecedence, HELP_FRONTMATTER_MISMATCH_ERROR_CODES } = require('./help-authority-validator'); const { ManifestGenerator } = require('./manifest-generator'); const { buildSidecarAwareExemplarHelpRow } = require('./help-catalog-generator'); @@ -13,6 +17,7 @@ const { CodexSetup } = require('../ide/codex'); const HELP_VALIDATION_ERROR_CODES = Object.freeze({ REQUIRED_ARTIFACT_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ARTIFACT_MISSING', + METADATA_RESOLUTION_FAILED: 'ERR_HELP_VALIDATION_METADATA_RESOLUTION_FAILED', CSV_SCHEMA_MISMATCH: 'ERR_HELP_VALIDATION_CSV_SCHEMA_MISMATCH', REQUIRED_ROW_IDENTITY_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_ROW_IDENTITY_MISSING', REQUIRED_EVIDENCE_LINK_MISSING: 'ERR_HELP_VALIDATION_REQUIRED_EVIDENCE_LINK_MISSING', @@ -25,7 +30,7 @@ const HELP_VALIDATION_ERROR_CODES = Object.freeze({ DECISION_RECORD_PARSE_FAILED: 'ERR_HELP_VALIDATION_DECISION_RECORD_PARSE_FAILED', }); -const SIDEcar_AUTHORITY_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml'; +const SIDEcar_AUTHORITY_SOURCE_PATH = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; const SOURCE_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md'; const EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/help-validation-harness.js'; @@ -536,16 +541,9 @@ class HelpValidationHarness { }; } - resolveSourceArtifactPaths(options = {}) { + async resolveSourceArtifactPaths(options = {}) { const projectDir = path.resolve(options.projectDir || process.cwd()); - const sidecarCandidates = [ - options.sidecarPath, - path.join(projectDir, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'), - path.join(projectDir, 'src', 'core', 'tasks', 'help.artifact.yaml'), - getSourcePath('core', 'tasks', 'help.artifact.yaml'), - ].filter(Boolean); - const sourceMarkdownCandidates = [ options.sourceMarkdownPath, path.join(projectDir, 'bmad-fork', 'src', 'core', 'tasks', 'help.md'), @@ -562,12 +560,33 @@ class HelpValidationHarness { return candidates[0]; }; - return Promise.all([resolveExistingPath(sidecarCandidates), resolveExistingPath(sourceMarkdownCandidates)]).then( - ([sidecarPath, sourceMarkdownPath]) => ({ - sidecarPath, - sourceMarkdownPath, - }), - ); + const sourceMarkdownPath = await resolveExistingPath(sourceMarkdownCandidates); + + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceMarkdownPath, + metadataPath: options.sidecarPath || '', + projectRoot: projectDir, + ambiguousErrorCode: HELP_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + }); + } catch (error) { + throw new HelpValidationHarnessError({ + code: HELP_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + detail: error.detail || error.message || 'metadata authority resolution failed', + artifactId: 1, + fieldPath: normalizeValue(error.fieldPath || ''), + sourcePath: normalizePath(error.sourcePath || SIDEcar_AUTHORITY_SOURCE_PATH), + observedValue: normalizeValue(error.code || ''), + expectedValue: 'unambiguous metadata authority candidate', + }); + } + + return { + sidecarPath: resolvedMetadataAuthority.resolvedAbsolutePath || options.sidecarPath || '', + sourceMarkdownPath, + metadataAuthority: resolvedMetadataAuthority, + }; } async readSidecarMetadata(sidecarPath) { diff --git a/tools/cli/installers/lib/core/index-docs-authority-validator.js b/tools/cli/installers/lib/core/index-docs-authority-validator.js index 242bd5b53..ebd07c52e 100644 --- a/tools/cli/installers/lib/core/index-docs-authority-validator.js +++ b/tools/cli/installers/lib/core/index-docs-authority-validator.js @@ -3,9 +3,11 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { getProjectRoot, getSourcePath } = require('../../../lib/project-root'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES = Object.freeze({ SIDECAR_FILE_NOT_FOUND: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_FILE_NOT_FOUND', + SIDECAR_FILENAME_AMBIGUOUS: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_FILENAME_AMBIGUOUS', SIDECAR_PARSE_FAILED: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_PARSE_FAILED', SIDECAR_INVALID_METADATA: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_INVALID_METADATA', SIDECAR_CANONICAL_ID_MISMATCH: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_CANONICAL_ID_MISMATCH', @@ -249,18 +251,38 @@ function buildIndexDocsAuthorityRecords({ canonicalId, sidecarSourcePath, source } async function validateIndexDocsAuthoritySplitAndPrecedence(options = {}) { - const sidecarPath = options.sidecarPath || getSourcePath('core', 'tasks', 'index-docs.artifact.yaml'); const sourceXmlPath = options.sourceXmlPath || getSourcePath('core', 'tasks', 'index-docs.xml'); const compatibilityCatalogPath = options.compatibilityCatalogPath || getSourcePath('core', 'module-help.csv'); const compatibilityWorkflowFilePath = options.compatibilityWorkflowFilePath || '_bmad/core/tasks/index-docs.xml'; - const sidecarSourcePath = normalizeSourcePath(options.sidecarSourcePath || toProjectRelativePath(sidecarPath)); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceXmlPath, + metadataPath: options.sidecarPath || '', + metadataSourcePath: options.sidecarSourcePath || '', + ambiguousErrorCode: INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + }); + } catch (error) { + createValidationError( + error.code || INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + error.detail || error.message, + error.fieldPath || '', + normalizeSourcePath(error.sourcePath || toProjectRelativePath(sourceXmlPath)), + ); + } + + const sidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + + const sidecarSourcePath = normalizeSourcePath( + options.sidecarSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath || resolvedMetadataAuthority.resolvedSourcePath, + ); const sourceXmlSourcePath = normalizeSourcePath(options.sourceXmlSourcePath || toProjectRelativePath(sourceXmlPath)); const compatibilityCatalogSourcePath = normalizeSourcePath( options.compatibilityCatalogSourcePath || toProjectRelativePath(compatibilityCatalogPath), ); - if (!(await fs.pathExists(sidecarPath))) { + if (!sidecarPath || !(await fs.pathExists(sidecarPath))) { createValidationError( INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, 'Expected index-docs sidecar metadata file was not found', @@ -320,6 +342,13 @@ async function validateIndexDocsAuthoritySplitAndPrecedence(options = {}) { canonicalId, authoritativePresenceKey: INDEX_DOCS_LOCKED_AUTHORITATIVE_PRESENCE_KEY, authoritativeRecords, + metadataAuthority: { + resolvedPath: normalizeSourcePath(resolvedMetadataAuthority.resolvedSourcePath || sidecarSourcePath), + resolvedFilename: normalizeSourcePath(resolvedMetadataAuthority.resolvedFilename || ''), + canonicalTargetFilename: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetFilename || 'skill-manifest.yaml'), + canonicalTargetPath: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetSourcePath || sidecarSourcePath), + derivationMode: normalizeSourcePath(resolvedMetadataAuthority.derivationMode || ''), + }, }; } diff --git a/tools/cli/installers/lib/core/index-docs-validation-harness.js b/tools/cli/installers/lib/core/index-docs-validation-harness.js index 42c4c7738..37ce04c2e 100644 --- a/tools/cli/installers/lib/core/index-docs-validation-harness.js +++ b/tools/cli/installers/lib/core/index-docs-validation-harness.js @@ -5,6 +5,7 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { getSourcePath } = require('../../../lib/project-root'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const { normalizeDisplayedCommandLabel } = require('./help-catalog-generator'); const { ManifestGenerator } = require('./manifest-generator'); const { @@ -14,12 +15,13 @@ const { validateGithubCopilotHelpLoaderEntries, } = require('./projection-compatibility-validator'); -const INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml'; +const INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml'; const INDEX_DOCS_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml'; const INDEX_DOCS_EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/index-docs-validation-harness.js'; const INDEX_DOCS_VALIDATION_ERROR_CODES = Object.freeze({ REQUIRED_ARTIFACT_MISSING: 'ERR_INDEX_DOCS_VALIDATION_REQUIRED_ARTIFACT_MISSING', + METADATA_RESOLUTION_FAILED: 'ERR_INDEX_DOCS_VALIDATION_METADATA_RESOLUTION_FAILED', CSV_SCHEMA_MISMATCH: 'ERR_INDEX_DOCS_VALIDATION_CSV_SCHEMA_MISMATCH', REQUIRED_ROW_MISSING: 'ERR_INDEX_DOCS_VALIDATION_REQUIRED_ROW_MISSING', YAML_SCHEMA_MISMATCH: 'ERR_INDEX_DOCS_VALIDATION_YAML_SCHEMA_MISMATCH', @@ -889,16 +891,34 @@ class IndexDocsValidationHarness { const runtimeFolder = normalizeValue(options.bmadFolderName || '_bmad'); const bmadDir = path.resolve(options.bmadDir || path.join(outputPaths.projectDir, runtimeFolder)); const artifactPaths = this.buildArtifactPathsMap(outputPaths); - const sidecarPath = - options.sidecarPath || - ((await fs.pathExists(path.join(outputPaths.projectDir, INDEX_DOCS_SIDECAR_SOURCE_PATH))) - ? path.join(outputPaths.projectDir, INDEX_DOCS_SIDECAR_SOURCE_PATH) - : getSourcePath('core', 'tasks', 'index-docs.artifact.yaml')); const sourceXmlPath = options.sourceXmlPath || ((await fs.pathExists(path.join(outputPaths.projectDir, INDEX_DOCS_SOURCE_XML_SOURCE_PATH))) ? path.join(outputPaths.projectDir, INDEX_DOCS_SOURCE_XML_SOURCE_PATH) : getSourcePath('core', 'tasks', 'index-docs.xml')); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceXmlPath, + metadataPath: options.sidecarPath || '', + projectRoot: outputPaths.projectDir, + ambiguousErrorCode: INDEX_DOCS_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + }); + } catch (error) { + throw new IndexDocsValidationHarnessError({ + code: INDEX_DOCS_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + detail: error.detail || error.message || 'metadata authority resolution failed', + artifactId: 1, + fieldPath: normalizeValue(error.fieldPath || ''), + sourcePath: normalizePath(error.sourcePath || INDEX_DOCS_SIDECAR_SOURCE_PATH), + observedValue: normalizeValue(error.code || ''), + expectedValue: 'unambiguous metadata authority candidate', + }); + } + const sidecarPath = + resolvedMetadataAuthority.resolvedAbsolutePath || + options.sidecarPath || + path.join(path.dirname(sourceXmlPath), path.basename(sourceXmlPath, path.extname(sourceXmlPath)), 'skill-manifest.yaml'); await fs.ensureDir(outputPaths.validationRoot); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index e0fd7e328..0e204a19e 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -36,13 +36,13 @@ const { CustomHandler } = require('../custom/handler'); const prompts = require('../../../lib/prompts'); const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); -const EXEMPLAR_HELP_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml'; +const EXEMPLAR_HELP_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; const EXEMPLAR_HELP_SOURCE_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md'; -const EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; +const EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; const EXEMPLAR_SHARD_DOC_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml'; const EXEMPLAR_SHARD_DOC_COMPATIBILITY_CATALOG_SOURCE_PATH = 'bmad-fork/src/core/module-help.csv'; const EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH = '_bmad/core/tasks/shard-doc.xml'; -const EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml'; +const EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml'; const EXEMPLAR_INDEX_DOCS_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml'; const EXEMPLAR_INDEX_DOCS_COMPATIBILITY_CATALOG_SOURCE_PATH = 'bmad-fork/src/core/module-help.csv'; const EXEMPLAR_INDEX_DOCS_WORKFLOW_FILE_PATH = '_bmad/core/tasks/index-docs.xml'; diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 8fcefbb53..54938492f 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -14,9 +14,9 @@ const { validateTaskManifestCompatibilitySurface } = require('./projection-compa // Load package.json for version info const packageJson = require('../../../../../package.json'); -const DEFAULT_EXEMPLAR_HELP_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml'; -const DEFAULT_EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; -const DEFAULT_EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml'; +const DEFAULT_EXEMPLAR_HELP_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; +const DEFAULT_EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; +const DEFAULT_EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml'; const CANONICAL_ALIAS_TABLE_COLUMNS = Object.freeze([ 'canonicalId', 'alias', diff --git a/tools/cli/installers/lib/core/shard-doc-authority-validator.js b/tools/cli/installers/lib/core/shard-doc-authority-validator.js index a6f3a8ef1..982d9744a 100644 --- a/tools/cli/installers/lib/core/shard-doc-authority-validator.js +++ b/tools/cli/installers/lib/core/shard-doc-authority-validator.js @@ -3,9 +3,11 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { getProjectRoot, getSourcePath } = require('../../../lib/project-root'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES = Object.freeze({ SIDECAR_FILE_NOT_FOUND: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_FILE_NOT_FOUND', + SIDECAR_FILENAME_AMBIGUOUS: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_FILENAME_AMBIGUOUS', SIDECAR_PARSE_FAILED: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_PARSE_FAILED', SIDECAR_INVALID_METADATA: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_INVALID_METADATA', SIDECAR_CANONICAL_ID_MISMATCH: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_CANONICAL_ID_MISMATCH', @@ -249,18 +251,38 @@ function buildShardDocAuthorityRecords({ canonicalId, sidecarSourcePath, sourceX } async function validateShardDocAuthoritySplitAndPrecedence(options = {}) { - const sidecarPath = options.sidecarPath || getSourcePath('core', 'tasks', 'shard-doc.artifact.yaml'); const sourceXmlPath = options.sourceXmlPath || getSourcePath('core', 'tasks', 'shard-doc.xml'); const compatibilityCatalogPath = options.compatibilityCatalogPath || getSourcePath('core', 'module-help.csv'); const compatibilityWorkflowFilePath = options.compatibilityWorkflowFilePath || '_bmad/core/tasks/shard-doc.xml'; - const sidecarSourcePath = normalizeSourcePath(options.sidecarSourcePath || toProjectRelativePath(sidecarPath)); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceXmlPath, + metadataPath: options.sidecarPath || '', + metadataSourcePath: options.sidecarSourcePath || '', + ambiguousErrorCode: SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + }); + } catch (error) { + createValidationError( + error.code || SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + error.detail || error.message, + error.fieldPath || '', + normalizeSourcePath(error.sourcePath || toProjectRelativePath(sourceXmlPath)), + ); + } + + const sidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + + const sidecarSourcePath = normalizeSourcePath( + options.sidecarSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath || resolvedMetadataAuthority.resolvedSourcePath, + ); const sourceXmlSourcePath = normalizeSourcePath(options.sourceXmlSourcePath || toProjectRelativePath(sourceXmlPath)); const compatibilityCatalogSourcePath = normalizeSourcePath( options.compatibilityCatalogSourcePath || toProjectRelativePath(compatibilityCatalogPath), ); - if (!(await fs.pathExists(sidecarPath))) { + if (!sidecarPath || !(await fs.pathExists(sidecarPath))) { createValidationError( SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, 'Expected shard-doc sidecar metadata file was not found', @@ -322,6 +344,13 @@ async function validateShardDocAuthoritySplitAndPrecedence(options = {}) { authoritativePresenceKey: SHARD_DOC_LOCKED_AUTHORITATIVE_PRESENCE_KEY, authoritativeRecords, checkedSurfaces: [sourceXmlSourcePath, compatibilityCatalogSourcePath], + metadataAuthority: { + resolvedPath: normalizeSourcePath(resolvedMetadataAuthority.resolvedSourcePath || sidecarSourcePath), + resolvedFilename: normalizeSourcePath(resolvedMetadataAuthority.resolvedFilename || ''), + canonicalTargetFilename: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetFilename || 'skill-manifest.yaml'), + canonicalTargetPath: normalizeSourcePath(resolvedMetadataAuthority.canonicalTargetSourcePath || sidecarSourcePath), + derivationMode: normalizeSourcePath(resolvedMetadataAuthority.derivationMode || ''), + }, }; } diff --git a/tools/cli/installers/lib/core/shard-doc-validation-harness.js b/tools/cli/installers/lib/core/shard-doc-validation-harness.js index 6dab6d973..665a1934b 100644 --- a/tools/cli/installers/lib/core/shard-doc-validation-harness.js +++ b/tools/cli/installers/lib/core/shard-doc-validation-harness.js @@ -5,6 +5,7 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const csv = require('csv-parse/sync'); const { getSourcePath } = require('../../../lib/project-root'); +const { resolveSkillMetadataAuthority } = require('./sidecar-contract-validator'); const { normalizeDisplayedCommandLabel } = require('./help-catalog-generator'); const { ManifestGenerator } = require('./manifest-generator'); const { @@ -14,12 +15,13 @@ const { validateGithubCopilotHelpLoaderEntries, } = require('./projection-compatibility-validator'); -const SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; +const SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; const SHARD_DOC_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml'; const SHARD_DOC_EVIDENCE_ISSUER_COMPONENT = 'bmad-fork/tools/cli/installers/lib/core/shard-doc-validation-harness.js'; const SHARD_DOC_VALIDATION_ERROR_CODES = Object.freeze({ REQUIRED_ARTIFACT_MISSING: 'ERR_SHARD_DOC_VALIDATION_REQUIRED_ARTIFACT_MISSING', + METADATA_RESOLUTION_FAILED: 'ERR_SHARD_DOC_VALIDATION_METADATA_RESOLUTION_FAILED', CSV_SCHEMA_MISMATCH: 'ERR_SHARD_DOC_VALIDATION_CSV_SCHEMA_MISMATCH', REQUIRED_ROW_MISSING: 'ERR_SHARD_DOC_VALIDATION_REQUIRED_ROW_MISSING', YAML_SCHEMA_MISMATCH: 'ERR_SHARD_DOC_VALIDATION_YAML_SCHEMA_MISMATCH', @@ -888,16 +890,34 @@ class ShardDocValidationHarness { const runtimeFolder = normalizeValue(options.bmadFolderName || '_bmad'); const bmadDir = path.resolve(options.bmadDir || path.join(outputPaths.projectDir, runtimeFolder)); const artifactPaths = this.buildArtifactPathsMap(outputPaths); - const sidecarPath = - options.sidecarPath || - ((await fs.pathExists(path.join(outputPaths.projectDir, SHARD_DOC_SIDECAR_SOURCE_PATH))) - ? path.join(outputPaths.projectDir, SHARD_DOC_SIDECAR_SOURCE_PATH) - : getSourcePath('core', 'tasks', 'shard-doc.artifact.yaml')); const sourceXmlPath = options.sourceXmlPath || ((await fs.pathExists(path.join(outputPaths.projectDir, SHARD_DOC_SOURCE_XML_SOURCE_PATH))) ? path.join(outputPaths.projectDir, SHARD_DOC_SOURCE_XML_SOURCE_PATH) : getSourcePath('core', 'tasks', 'shard-doc.xml')); + let resolvedMetadataAuthority; + try { + resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceXmlPath, + metadataPath: options.sidecarPath || '', + projectRoot: outputPaths.projectDir, + ambiguousErrorCode: SHARD_DOC_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + }); + } catch (error) { + throw new ShardDocValidationHarnessError({ + code: SHARD_DOC_VALIDATION_ERROR_CODES.METADATA_RESOLUTION_FAILED, + detail: error.detail || error.message || 'metadata authority resolution failed', + artifactId: 1, + fieldPath: normalizeValue(error.fieldPath || ''), + sourcePath: normalizePath(error.sourcePath || SHARD_DOC_SIDECAR_SOURCE_PATH), + observedValue: normalizeValue(error.code || ''), + expectedValue: 'unambiguous metadata authority candidate', + }); + } + const sidecarPath = + resolvedMetadataAuthority.resolvedAbsolutePath || + options.sidecarPath || + path.join(path.dirname(sourceXmlPath), path.basename(sourceXmlPath, path.extname(sourceXmlPath)), 'skill-manifest.yaml'); await fs.ensureDir(outputPaths.validationRoot); diff --git a/tools/cli/installers/lib/core/sidecar-contract-validator.js b/tools/cli/installers/lib/core/sidecar-contract-validator.js index ebdc5f6f2..e8b056cd2 100644 --- a/tools/cli/installers/lib/core/sidecar-contract-validator.js +++ b/tools/cli/installers/lib/core/sidecar-contract-validator.js @@ -30,6 +30,7 @@ const HELP_SIDECAR_ERROR_CODES = Object.freeze({ DEPENDENCIES_REQUIRES_NOT_EMPTY: 'ERR_HELP_SIDECAR_DEPENDENCIES_REQUIRES_NOT_EMPTY', MAJOR_VERSION_UNSUPPORTED: 'ERR_SIDECAR_MAJOR_VERSION_UNSUPPORTED', SOURCEPATH_BASENAME_MISMATCH: 'ERR_SIDECAR_SOURCEPATH_BASENAME_MISMATCH', + METADATA_FILENAME_AMBIGUOUS: 'ERR_HELP_SIDECAR_METADATA_FILENAME_AMBIGUOUS', }); const SHARD_DOC_SIDECAR_ERROR_CODES = Object.freeze({ @@ -45,6 +46,7 @@ const SHARD_DOC_SIDECAR_ERROR_CODES = Object.freeze({ DEPENDENCIES_REQUIRES_NOT_EMPTY: 'ERR_SHARD_DOC_SIDECAR_DEPENDENCIES_REQUIRES_NOT_EMPTY', MAJOR_VERSION_UNSUPPORTED: 'ERR_SHARD_DOC_SIDECAR_MAJOR_VERSION_UNSUPPORTED', SOURCEPATH_BASENAME_MISMATCH: 'ERR_SHARD_DOC_SIDECAR_SOURCEPATH_BASENAME_MISMATCH', + METADATA_FILENAME_AMBIGUOUS: 'ERR_SHARD_DOC_SIDECAR_METADATA_FILENAME_AMBIGUOUS', }); const INDEX_DOCS_SIDECAR_ERROR_CODES = Object.freeze({ @@ -60,11 +62,21 @@ const INDEX_DOCS_SIDECAR_ERROR_CODES = Object.freeze({ DEPENDENCIES_REQUIRES_NOT_EMPTY: 'ERR_INDEX_DOCS_SIDECAR_DEPENDENCIES_REQUIRES_NOT_EMPTY', MAJOR_VERSION_UNSUPPORTED: 'ERR_INDEX_DOCS_SIDECAR_MAJOR_VERSION_UNSUPPORTED', SOURCEPATH_BASENAME_MISMATCH: 'ERR_INDEX_DOCS_SIDECAR_SOURCEPATH_BASENAME_MISMATCH', + METADATA_FILENAME_AMBIGUOUS: 'ERR_INDEX_DOCS_SIDECAR_METADATA_FILENAME_AMBIGUOUS', }); const HELP_EXEMPLAR_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md'; const SHARD_DOC_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml'; const INDEX_DOCS_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml'; +const SKILL_METADATA_CANONICAL_FILENAME = 'skill-manifest.yaml'; +const SKILL_METADATA_LEGACY_FILENAMES = Object.freeze(['bmad-config.yaml', 'manifest.yaml']); +const SKILL_METADATA_DERIVATION_MODES = Object.freeze({ + CANONICAL: 'canonical', + LEGACY_FALLBACK: 'legacy-fallback', +}); +const SKILL_METADATA_RESOLUTION_ERROR_CODES = Object.freeze({ + AMBIGUOUS_MATCH: 'ERR_SKILL_METADATA_FILENAME_AMBIGUOUS', +}); const SIDECAR_SUPPORTED_SCHEMA_MAJOR = 1; class SidecarContractError extends Error { @@ -85,8 +97,7 @@ function normalizeSourcePath(value) { return String(value).replaceAll('\\', '/'); } -function toProjectRelativePath(filePath) { - const projectRoot = getProjectRoot(); +function toProjectRelativePath(filePath, projectRoot = getProjectRoot()) { const relative = path.relative(projectRoot, filePath); if (!relative || relative.startsWith('..')) { @@ -96,6 +107,17 @@ function toProjectRelativePath(filePath) { return normalizeSourcePath(relative); } +function dedupeAndSort(values) { + const normalized = new Set(); + for (const value of values || []) { + const text = normalizeSourcePath(value).trim(); + if (text.length > 0) { + normalized.add(text); + } + } + return [...normalized].sort((left, right) => left.localeCompare(right)); +} + function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } @@ -120,7 +142,169 @@ function parseSchemaMajorVersion(value) { return null; } -function getExpectedSidecarBasenameFromSourcePath(sourcePathValue) { +function classifyMetadataFilename(filename) { + const normalizedFilename = String(filename || '') + .trim() + .toLowerCase(); + if (normalizedFilename === SKILL_METADATA_CANONICAL_FILENAME) { + return SKILL_METADATA_DERIVATION_MODES.CANONICAL; + } + if (SKILL_METADATA_LEGACY_FILENAMES.includes(normalizedFilename) || normalizedFilename.endsWith('.artifact.yaml')) { + return SKILL_METADATA_DERIVATION_MODES.LEGACY_FALLBACK; + } + return SKILL_METADATA_DERIVATION_MODES.LEGACY_FALLBACK; +} + +function getMetadataStemFromSourcePath(sourcePathValue) { + const normalizedSourcePath = normalizeSourcePath(sourcePathValue).trim(); + if (!normalizedSourcePath) return ''; + + const sourceBasename = path.posix.basename(normalizedSourcePath); + if (!sourceBasename) return ''; + + const sourceExt = path.posix.extname(sourceBasename); + const baseWithoutExt = sourceExt ? sourceBasename.slice(0, -sourceExt.length) : sourceBasename; + return baseWithoutExt.trim(); +} + +function buildSkillMetadataResolutionPlan({ sourceFilePath, projectRoot = getProjectRoot() }) { + const absoluteSourceFilePath = path.resolve(sourceFilePath); + const sourceDirAbsolutePath = path.dirname(absoluteSourceFilePath); + const metadataStem = getMetadataStemFromSourcePath(absoluteSourceFilePath); + const skillFolderAbsolutePath = path.join(sourceDirAbsolutePath, metadataStem); + const canonicalTargetAbsolutePath = path.join(skillFolderAbsolutePath, SKILL_METADATA_CANONICAL_FILENAME); + + const candidateGroups = [ + { + precedenceToken: SKILL_METADATA_CANONICAL_FILENAME, + derivationMode: SKILL_METADATA_DERIVATION_MODES.CANONICAL, + // Canonical authority is per-skill only; root task-folder canonical files are not eligible. + explicitCandidates: [canonicalTargetAbsolutePath], + wildcardDirectories: [], + }, + { + precedenceToken: 'bmad-config.yaml', + derivationMode: SKILL_METADATA_DERIVATION_MODES.LEGACY_FALLBACK, + explicitCandidates: [path.join(skillFolderAbsolutePath, 'bmad-config.yaml'), path.join(sourceDirAbsolutePath, 'bmad-config.yaml')], + wildcardDirectories: [], + }, + { + precedenceToken: 'manifest.yaml', + derivationMode: SKILL_METADATA_DERIVATION_MODES.LEGACY_FALLBACK, + explicitCandidates: [path.join(skillFolderAbsolutePath, 'manifest.yaml'), path.join(sourceDirAbsolutePath, 'manifest.yaml')], + wildcardDirectories: [], + }, + { + precedenceToken: `${metadataStem}.artifact.yaml`, + derivationMode: SKILL_METADATA_DERIVATION_MODES.LEGACY_FALLBACK, + explicitCandidates: [ + path.join(sourceDirAbsolutePath, `${metadataStem}.artifact.yaml`), + path.join(skillFolderAbsolutePath, `${metadataStem}.artifact.yaml`), + ], + wildcardDirectories: [], + }, + ]; + + return { + metadataStem, + canonicalTargetAbsolutePath, + canonicalTargetSourcePath: toProjectRelativePath(canonicalTargetAbsolutePath, projectRoot), + candidateGroups, + }; +} + +async function resolveCandidateGroupMatches(group = {}) { + const explicitMatches = []; + for (const candidatePath of group.explicitCandidates || []) { + if (await fs.pathExists(candidatePath)) { + explicitMatches.push(path.resolve(candidatePath)); + } + } + + const wildcardMatches = []; + for (const wildcardDirectory of group.wildcardDirectories || []) { + if (!(await fs.pathExists(wildcardDirectory))) { + continue; + } + const directoryEntries = await fs.readdir(wildcardDirectory, { withFileTypes: true }); + for (const entry of directoryEntries) { + if (!entry.isFile()) continue; + const filename = String(entry.name || '').trim(); + if (!filename.toLowerCase().endsWith('.artifact.yaml')) continue; + wildcardMatches.push(path.join(wildcardDirectory, filename)); + } + } + + return dedupeAndSort([...explicitMatches, ...wildcardMatches]); +} + +async function resolveSkillMetadataAuthority({ + sourceFilePath, + metadataPath = '', + metadataSourcePath = '', + projectRoot = getProjectRoot(), + ambiguousErrorCode = SKILL_METADATA_RESOLUTION_ERROR_CODES.AMBIGUOUS_MATCH, +}) { + const resolutionPlan = buildSkillMetadataResolutionPlan({ + sourceFilePath, + projectRoot, + }); + + const resolvedMetadataPath = String(metadataPath || '').trim(); + if (resolvedMetadataPath.length > 0) { + const resolvedAbsolutePath = path.resolve(resolvedMetadataPath); + const resolvedFilename = path.posix.basename(normalizeSourcePath(resolvedAbsolutePath)); + return { + resolvedAbsolutePath, + resolvedSourcePath: normalizeSourcePath(metadataSourcePath || toProjectRelativePath(resolvedAbsolutePath, projectRoot)), + resolvedFilename, + canonicalTargetFilename: SKILL_METADATA_CANONICAL_FILENAME, + canonicalTargetSourcePath: resolutionPlan.canonicalTargetSourcePath, + derivationMode: classifyMetadataFilename(resolvedFilename), + precedenceToken: resolvedFilename, + }; + } + + for (const group of resolutionPlan.candidateGroups) { + const matches = await resolveCandidateGroupMatches(group); + if (matches.length === 0) { + continue; + } + + if (matches.length > 1) { + throw new SidecarContractError({ + code: ambiguousErrorCode, + detail: `metadata filename resolution is ambiguous for precedence "${group.precedenceToken}": ${matches.join('|')}`, + fieldPath: '', + sourcePath: resolutionPlan.canonicalTargetSourcePath, + }); + } + + const resolvedAbsolutePath = matches[0]; + const resolvedFilename = path.posix.basename(normalizeSourcePath(resolvedAbsolutePath)); + return { + resolvedAbsolutePath, + resolvedSourcePath: normalizeSourcePath(toProjectRelativePath(resolvedAbsolutePath, projectRoot)), + resolvedFilename, + canonicalTargetFilename: SKILL_METADATA_CANONICAL_FILENAME, + canonicalTargetSourcePath: resolutionPlan.canonicalTargetSourcePath, + derivationMode: group.derivationMode, + precedenceToken: group.precedenceToken, + }; + } + + return { + resolvedAbsolutePath: '', + resolvedSourcePath: '', + resolvedFilename: '', + canonicalTargetFilename: SKILL_METADATA_CANONICAL_FILENAME, + canonicalTargetSourcePath: resolutionPlan.canonicalTargetSourcePath, + derivationMode: '', + precedenceToken: '', + }; +} + +function getExpectedLegacyArtifactBasenameFromSourcePath(sourcePathValue) { const normalized = normalizeSourcePath(sourcePathValue).trim(); if (!normalized) return ''; @@ -218,11 +402,15 @@ function validateSidecarContractData(sidecarData, options) { } const normalizedDeclaredSourcePath = normalizeSourcePath(sidecarData.sourcePath); - const sidecarBasename = path.posix.basename(sourcePath); - const expectedSidecarBasename = getExpectedSidecarBasenameFromSourcePath(normalizedDeclaredSourcePath); + const sidecarBasename = path.posix.basename(normalizeSourcePath(sourcePath)).toLowerCase(); + const expectedLegacyArtifactBasename = getExpectedLegacyArtifactBasenameFromSourcePath(normalizedDeclaredSourcePath).toLowerCase(); + const allowedMetadataBasenames = new Set([SKILL_METADATA_CANONICAL_FILENAME, ...SKILL_METADATA_LEGACY_FILENAMES]); + if (expectedLegacyArtifactBasename.length > 0) { + allowedMetadataBasenames.add(expectedLegacyArtifactBasename); + } const sourcePathMismatch = normalizedDeclaredSourcePath !== expectedCanonicalSourcePath; - const basenameMismatch = !expectedSidecarBasename || sidecarBasename !== expectedSidecarBasename; + const basenameMismatch = !allowedMetadataBasenames.has(sidecarBasename); if (sourcePathMismatch || basenameMismatch) { createValidationError( @@ -235,7 +423,7 @@ function validateSidecarContractData(sidecarData, options) { } function validateHelpSidecarContractData(sidecarData, options = {}) { - const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/help.artifact.yaml'); + const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/help/skill-manifest.yaml'); validateSidecarContractData(sidecarData, { sourcePath, requiredFields: HELP_SIDECAR_REQUIRED_FIELDS, @@ -255,7 +443,7 @@ function validateHelpSidecarContractData(sidecarData, options = {}) { } function validateShardDocSidecarContractData(sidecarData, options = {}) { - const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/shard-doc.artifact.yaml'); + const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/shard-doc/skill-manifest.yaml'); validateSidecarContractData(sidecarData, { sourcePath, requiredFields: SHARD_DOC_SIDECAR_REQUIRED_FIELDS, @@ -275,7 +463,7 @@ function validateShardDocSidecarContractData(sidecarData, options = {}) { } function validateIndexDocsSidecarContractData(sidecarData, options = {}) { - const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/index-docs.artifact.yaml'); + const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/index-docs/skill-manifest.yaml'); validateSidecarContractData(sidecarData, { sourcePath, requiredFields: INDEX_DOCS_SIDECAR_REQUIRED_FIELDS, @@ -294,10 +482,20 @@ function validateIndexDocsSidecarContractData(sidecarData, options = {}) { }); } -async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core', 'tasks', 'help.artifact.yaml'), options = {}) { - const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath)); +async function validateHelpSidecarContractFile(sidecarPath = '', options = {}) { + const sourceFilePath = options.sourceFilePath || getSourcePath('core', 'tasks', 'help.md'); + const resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath, + metadataPath: sidecarPath, + metadataSourcePath: options.errorSourcePath, + ambiguousErrorCode: HELP_SIDECAR_ERROR_CODES.METADATA_FILENAME_AMBIGUOUS, + }); + const resolvedSidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + const normalizedSourcePath = normalizeSourcePath( + options.errorSourcePath || resolvedMetadataAuthority.resolvedSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath, + ); - if (!(await fs.pathExists(sidecarPath))) { + if (!resolvedSidecarPath || !(await fs.pathExists(resolvedSidecarPath))) { createValidationError( HELP_SIDECAR_ERROR_CODES.FILE_NOT_FOUND, '', @@ -308,7 +506,7 @@ async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core let parsedSidecar; try { - const sidecarRaw = await fs.readFile(sidecarPath, 'utf8'); + const sidecarRaw = await fs.readFile(resolvedSidecarPath, 'utf8'); parsedSidecar = yaml.parse(sidecarRaw); } catch (error) { createValidationError( @@ -320,12 +518,23 @@ async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core } validateHelpSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath }); + return resolvedMetadataAuthority; } -async function validateShardDocSidecarContractFile(sidecarPath = getSourcePath('core', 'tasks', 'shard-doc.artifact.yaml'), options = {}) { - const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath)); +async function validateShardDocSidecarContractFile(sidecarPath = '', options = {}) { + const sourceFilePath = options.sourceFilePath || getSourcePath('core', 'tasks', 'shard-doc.xml'); + const resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath, + metadataPath: sidecarPath, + metadataSourcePath: options.errorSourcePath, + ambiguousErrorCode: SHARD_DOC_SIDECAR_ERROR_CODES.METADATA_FILENAME_AMBIGUOUS, + }); + const resolvedSidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + const normalizedSourcePath = normalizeSourcePath( + options.errorSourcePath || resolvedMetadataAuthority.resolvedSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath, + ); - if (!(await fs.pathExists(sidecarPath))) { + if (!resolvedSidecarPath || !(await fs.pathExists(resolvedSidecarPath))) { createValidationError( SHARD_DOC_SIDECAR_ERROR_CODES.FILE_NOT_FOUND, '', @@ -336,7 +545,7 @@ async function validateShardDocSidecarContractFile(sidecarPath = getSourcePath(' let parsedSidecar; try { - const sidecarRaw = await fs.readFile(sidecarPath, 'utf8'); + const sidecarRaw = await fs.readFile(resolvedSidecarPath, 'utf8'); parsedSidecar = yaml.parse(sidecarRaw); } catch (error) { createValidationError( @@ -348,15 +557,23 @@ async function validateShardDocSidecarContractFile(sidecarPath = getSourcePath(' } validateShardDocSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath }); + return resolvedMetadataAuthority; } -async function validateIndexDocsSidecarContractFile( - sidecarPath = getSourcePath('core', 'tasks', 'index-docs.artifact.yaml'), - options = {}, -) { - const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath)); +async function validateIndexDocsSidecarContractFile(sidecarPath = '', options = {}) { + const sourceFilePath = options.sourceFilePath || getSourcePath('core', 'tasks', 'index-docs.xml'); + const resolvedMetadataAuthority = await resolveSkillMetadataAuthority({ + sourceFilePath, + metadataPath: sidecarPath, + metadataSourcePath: options.errorSourcePath, + ambiguousErrorCode: INDEX_DOCS_SIDECAR_ERROR_CODES.METADATA_FILENAME_AMBIGUOUS, + }); + const resolvedSidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + const normalizedSourcePath = normalizeSourcePath( + options.errorSourcePath || resolvedMetadataAuthority.resolvedSourcePath || resolvedMetadataAuthority.canonicalTargetSourcePath, + ); - if (!(await fs.pathExists(sidecarPath))) { + if (!resolvedSidecarPath || !(await fs.pathExists(resolvedSidecarPath))) { createValidationError( INDEX_DOCS_SIDECAR_ERROR_CODES.FILE_NOT_FOUND, '', @@ -367,7 +584,7 @@ async function validateIndexDocsSidecarContractFile( let parsedSidecar; try { - const sidecarRaw = await fs.readFile(sidecarPath, 'utf8'); + const sidecarRaw = await fs.readFile(resolvedSidecarPath, 'utf8'); parsedSidecar = yaml.parse(sidecarRaw); } catch (error) { createValidationError( @@ -379,6 +596,7 @@ async function validateIndexDocsSidecarContractFile( } validateIndexDocsSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath }); + return resolvedMetadataAuthority; } module.exports = { @@ -388,7 +606,12 @@ module.exports = { HELP_SIDECAR_ERROR_CODES, SHARD_DOC_SIDECAR_ERROR_CODES, INDEX_DOCS_SIDECAR_ERROR_CODES, + SKILL_METADATA_CANONICAL_FILENAME, + SKILL_METADATA_DERIVATION_MODES, + SKILL_METADATA_LEGACY_FILENAMES, + SKILL_METADATA_RESOLUTION_ERROR_CODES, SidecarContractError, + resolveSkillMetadataAuthority, validateHelpSidecarContractData, validateHelpSidecarContractFile, validateShardDocSidecarContractData, diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index f5e102fbc..da087c0b4 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -9,10 +9,12 @@ const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generat const { getTasksFromBmad } = require('./shared/bmad-artifacts'); const { toDashPath, customAgentDashName } = require('./shared/path-utils'); const { normalizeAndResolveExemplarAlias } = require('../core/help-alias-normalizer'); +const { resolveSkillMetadataAuthority } = require('../core/sidecar-contract-validator'); const prompts = require('../../../lib/prompts'); const CODEX_EXPORT_DERIVATION_ERROR_CODES = Object.freeze({ SIDECAR_FILE_NOT_FOUND: 'ERR_CODEX_EXPORT_SIDECAR_FILE_NOT_FOUND', + SIDECAR_FILENAME_AMBIGUOUS: 'ERR_CODEX_EXPORT_SIDECAR_FILENAME_AMBIGUOUS', SIDECAR_PARSE_FAILED: 'ERR_CODEX_EXPORT_SIDECAR_PARSE_FAILED', CANONICAL_ID_MISSING: 'ERR_CODEX_EXPORT_CANONICAL_ID_MISSING', CANONICAL_ID_DERIVATION_FAILED: 'ERR_CODEX_EXPORT_CANONICAL_ID_DERIVATION_FAILED', @@ -22,9 +24,9 @@ const CODEX_EXPORT_DERIVATION_ERROR_CODES = Object.freeze({ const EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md'; const EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml'; const EXEMPLAR_INDEX_DOCS_TASK_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml'; -const EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml'; -const EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml'; -const EXEMPLAR_INDEX_DOCS_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml'; +const EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/help/skill-manifest.yaml'; +const EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc/skill-manifest.yaml'; +const EXEMPLAR_INDEX_DOCS_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs/skill-manifest.yaml'; const EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE = 'sidecar-canonical-id'; const SHARD_DOC_EXPORT_ALIAS_ROWS = Object.freeze([ Object.freeze({ @@ -71,12 +73,12 @@ const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({ taskSourcePath: EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH, sourcePathSuffix: '/core/tasks/help.md', sidecarSourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH, - sidecarSourceCandidates: Object.freeze([ + sourceFileCandidates: Object.freeze([ Object.freeze({ - segments: ['bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'], + segments: ['bmad-fork', 'src', 'core', 'tasks', 'help.md'], }), Object.freeze({ - segments: ['src', 'core', 'tasks', 'help.artifact.yaml'], + segments: ['src', 'core', 'tasks', 'help.md'], }), ]), }), @@ -85,12 +87,12 @@ const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({ sourcePathSuffix: '/core/tasks/shard-doc.xml', sidecarSourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH, aliasRows: SHARD_DOC_EXPORT_ALIAS_ROWS, - sidecarSourceCandidates: Object.freeze([ + sourceFileCandidates: Object.freeze([ Object.freeze({ - segments: ['bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'], + segments: ['bmad-fork', 'src', 'core', 'tasks', 'shard-doc.xml'], }), Object.freeze({ - segments: ['src', 'core', 'tasks', 'shard-doc.artifact.yaml'], + segments: ['src', 'core', 'tasks', 'shard-doc.xml'], }), ]), }), @@ -99,12 +101,12 @@ const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({ sourcePathSuffix: '/core/tasks/index-docs.xml', sidecarSourcePath: EXEMPLAR_INDEX_DOCS_SIDECAR_CONTRACT_SOURCE_PATH, aliasRows: INDEX_DOCS_EXPORT_ALIAS_ROWS, - sidecarSourceCandidates: Object.freeze([ + sourceFileCandidates: Object.freeze([ Object.freeze({ - segments: ['bmad-fork', 'src', 'core', 'tasks', 'index-docs.artifact.yaml'], + segments: ['bmad-fork', 'src', 'core', 'tasks', 'index-docs.xml'], }), Object.freeze({ - segments: ['src', 'core', 'tasks', 'index-docs.artifact.yaml'], + segments: ['src', 'core', 'tasks', 'index-docs.xml'], }), ]), }), @@ -375,58 +377,96 @@ class CodexSetup extends BaseIdeSetup { } async loadConvertedTaskSidecar(projectDir, exportTarget) { - for (const candidate of exportTarget.sidecarSourceCandidates) { - const sidecarPath = path.join(projectDir, ...candidate.segments); - if (await fs.pathExists(sidecarPath)) { - let sidecarData; - try { - sidecarData = yaml.parse(await fs.readFile(sidecarPath, 'utf8')); - } catch (error) { - this.throwExportDerivationError({ - code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED, - detail: `YAML parse failure: ${error.message}`, - fieldPath: '', - sourcePath: exportTarget.sidecarSourcePath, - observedValue: '', - cause: error, - }); - } + const sourceCandidates = (exportTarget.sourceFileCandidates || []).map((candidate) => path.join(projectDir, ...candidate.segments)); + if (sourceCandidates.length === 0) { + this.throwExportDerivationError({ + code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, + detail: 'expected exemplar metadata source candidates are missing', + fieldPath: '', + sourcePath: exportTarget.sidecarSourcePath, + observedValue: projectDir, + }); + } - if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) { - this.throwExportDerivationError({ - code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED, - detail: 'sidecar root must be a YAML mapping object', - fieldPath: '', - sourcePath: exportTarget.sidecarSourcePath, - observedValue: typeof sidecarData, - }); + let resolvedMetadataAuthority = null; + for (const sourceCandidate of sourceCandidates) { + try { + const resolution = await resolveSkillMetadataAuthority({ + sourceFilePath: sourceCandidate, + projectRoot: projectDir, + ambiguousErrorCode: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + }); + if (!resolvedMetadataAuthority) { + resolvedMetadataAuthority = resolution; } - - const canonicalId = String(sidecarData.canonicalId || '').trim(); - if (canonicalId.length === 0) { - this.throwExportDerivationError({ - code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_MISSING, - detail: 'sidecar canonicalId is required for exemplar export derivation', - fieldPath: 'canonicalId', - sourcePath: exportTarget.sidecarSourcePath, - observedValue: canonicalId, - }); + if (resolution.resolvedAbsolutePath && (await fs.pathExists(resolution.resolvedAbsolutePath))) { + resolvedMetadataAuthority = resolution; + break; } - - return { - canonicalId, + } catch (error) { + this.throwExportDerivationError({ + code: error.code || CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILENAME_AMBIGUOUS, + detail: error.detail || error.message, + fieldPath: error.fieldPath || '', sourcePath: exportTarget.sidecarSourcePath, - }; + observedValue: error.sourcePath || projectDir, + cause: error, + }); } } - this.throwExportDerivationError({ - code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, - detail: 'expected exemplar sidecar metadata file was not found', - fieldPath: '', + const sidecarPath = resolvedMetadataAuthority.resolvedAbsolutePath; + if (!sidecarPath || !(await fs.pathExists(sidecarPath))) { + this.throwExportDerivationError({ + code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND, + detail: 'expected exemplar sidecar metadata file was not found', + fieldPath: '', + sourcePath: exportTarget.sidecarSourcePath, + observedValue: projectDir, + }); + } + + let sidecarData; + try { + sidecarData = yaml.parse(await fs.readFile(sidecarPath, 'utf8')); + } catch (error) { + this.throwExportDerivationError({ + code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED, + detail: `YAML parse failure: ${error.message}`, + fieldPath: '', + sourcePath: exportTarget.sidecarSourcePath, + observedValue: '', + cause: error, + }); + } + + if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) { + this.throwExportDerivationError({ + code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED, + detail: 'sidecar root must be a YAML mapping object', + fieldPath: '', + sourcePath: exportTarget.sidecarSourcePath, + observedValue: typeof sidecarData, + }); + } + + const canonicalId = String(sidecarData.canonicalId || '').trim(); + if (canonicalId.length === 0) { + this.throwExportDerivationError({ + code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_MISSING, + detail: 'sidecar canonicalId is required for exemplar export derivation', + fieldPath: 'canonicalId', + sourcePath: exportTarget.sidecarSourcePath, + observedValue: canonicalId, + }); + } + + return { + canonicalId, sourcePath: exportTarget.sidecarSourcePath, - observedValue: projectDir, - }); + resolvedFilename: String(resolvedMetadataAuthority.resolvedFilename || ''), + derivationMode: String(resolvedMetadataAuthority.derivationMode || ''), + }; } async resolveSkillIdentityFromArtifact(artifact, projectDir) {