feat(installer): harden skill metadata authority and validation

This commit is contained in:
Dicky Moore 2026-03-05 19:47:48 +00:00
parent b298ff456d
commit 239bc9d206
12 changed files with 825 additions and 201 deletions

View File

@ -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 {

View File

@ -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 || '<file>',
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 || ''),
},
};
}

View File

@ -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 || '<file>',
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,
'<file>',
@ -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 || ''),
};
}

View File

@ -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 || '<file>'),
sourcePath: normalizePath(error.sourcePath || SIDEcar_AUTHORITY_SOURCE_PATH),
observedValue: normalizeValue(error.code || '<resolution-error>'),
expectedValue: 'unambiguous metadata authority candidate',
});
}
return {
sidecarPath: resolvedMetadataAuthority.resolvedAbsolutePath || options.sidecarPath || '',
sourceMarkdownPath,
metadataAuthority: resolvedMetadataAuthority,
};
}
async readSidecarMetadata(sidecarPath) {

View File

@ -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 || '<file>',
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 || ''),
},
};
}

View File

@ -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 || '<file>'),
sourcePath: normalizePath(error.sourcePath || INDEX_DOCS_SIDECAR_SOURCE_PATH),
observedValue: normalizeValue(error.code || '<resolution-error>'),
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);

View File

@ -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';

View File

@ -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',

View File

@ -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 || '<file>',
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 || ''),
},
};
}

View File

@ -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 || '<file>'),
sourcePath: normalizePath(error.sourcePath || SHARD_DOC_SIDECAR_SOURCE_PATH),
observedValue: normalizeValue(error.code || '<resolution-error>'),
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);

View File

@ -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: '<file>',
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,
'<file>',
@ -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,
'<file>',
@ -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,
'<file>',
@ -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,

View File

@ -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: '<document>',
sourcePath: exportTarget.sidecarSourcePath,
observedValue: '<parse-error>',
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: '<file>',
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: '<document>',
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 || '<file>',
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: '<file>',
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: '<file>',
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: '<document>',
sourcePath: exportTarget.sidecarSourcePath,
observedValue: '<parse-error>',
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: '<document>',
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) {