feat(installer): complete wave-2 shard-doc parity and validation gates
This commit is contained in:
parent
51a73e28bd
commit
d24ef0633f
|
|
@ -0,0 +1,9 @@
|
|||
schemaVersion: 1
|
||||
canonicalId: bmad-shard-doc
|
||||
artifactType: task
|
||||
module: core
|
||||
sourcePath: bmad-fork/src/core/tasks/shard-doc.xml
|
||||
displayName: Shard Document
|
||||
description: "Split large markdown documents into smaller files by section with an index."
|
||||
dependencies:
|
||||
requires: []
|
||||
9
test/fixtures/wave-2/sidecar-negative/basename-path-mismatch/shard-doc.artifact.yaml
vendored
Normal file
9
test/fixtures/wave-2/sidecar-negative/basename-path-mismatch/shard-doc.artifact.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
schemaVersion: 1
|
||||
canonicalId: bmad-shard-doc
|
||||
artifactType: task
|
||||
module: core
|
||||
sourcePath: bmad-fork/src/core/tasks/not-shard-doc.xml
|
||||
displayName: Shard Document
|
||||
description: "Split large markdown documents into smaller files by section with an index."
|
||||
dependencies:
|
||||
requires: []
|
||||
9
test/fixtures/wave-2/sidecar-negative/unknown-major-version/shard-doc.artifact.yaml
vendored
Normal file
9
test/fixtures/wave-2/sidecar-negative/unknown-major-version/shard-doc.artifact.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
schemaVersion: 2
|
||||
canonicalId: bmad-shard-doc
|
||||
artifactType: task
|
||||
module: core
|
||||
sourcePath: bmad-fork/src/core/tasks/shard-doc.xml
|
||||
displayName: Shard Document
|
||||
description: "Split large markdown documents into smaller files by section with an index."
|
||||
dependencies:
|
||||
requires: []
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -169,6 +169,8 @@ function resolveCanonicalIdFromAuthorityRecords(helpAuthorityRecords = []) {
|
|||
function evaluateExemplarCommandLabelReportRows(rows, options = {}) {
|
||||
const expectedCanonicalId = frontmatterMatchValue(options.canonicalId || EXEMPLAR_HELP_CATALOG_CANONICAL_ID);
|
||||
const expectedDisplayedLabel = frontmatterMatchValue(options.displayedCommandLabel || `/${expectedCanonicalId}`);
|
||||
const expectedAuthoritySourceType = frontmatterMatchValue(options.authoritySourceType || 'sidecar');
|
||||
const expectedAuthoritySourcePath = frontmatterMatchValue(options.authoritySourcePath || EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH);
|
||||
const normalizedExpectedDisplayedLabel = normalizeDisplayedCommandLabel(expectedDisplayedLabel);
|
||||
|
||||
const targetRows = (Array.isArray(rows) ? rows : []).filter(
|
||||
|
|
@ -200,11 +202,14 @@ function evaluateExemplarCommandLabelReportRows(rows, options = {}) {
|
|||
return { valid: false, reason: `invalid-row-count-for-canonical-id:${String(row.rowCountForCanonicalId ?? '<empty>')}` };
|
||||
}
|
||||
|
||||
if (frontmatterMatchValue(row.authoritySourceType) !== 'sidecar') {
|
||||
return { valid: false, reason: `invalid-authority-source-type:${frontmatterMatchValue(row.authoritySourceType) || '<empty>'}` };
|
||||
if (frontmatterMatchValue(row.authoritySourceType) !== expectedAuthoritySourceType) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `invalid-authority-source-type:${frontmatterMatchValue(row.authoritySourceType) || '<empty>'}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (frontmatterMatchValue(row.authoritySourcePath) !== EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH) {
|
||||
if (frontmatterMatchValue(row.authoritySourcePath) !== expectedAuthoritySourcePath) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `invalid-authority-source-path:${frontmatterMatchValue(row.authoritySourcePath) || '<empty>'}`,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ const { Config } = require('../../../lib/config');
|
|||
const { XmlHandler } = require('../../../lib/xml-handler');
|
||||
const { DependencyResolver } = require('./dependency-resolver');
|
||||
const { ConfigCollector } = require('./config-collector');
|
||||
const { validateHelpSidecarContractFile } = require('./sidecar-contract-validator');
|
||||
const { validateHelpSidecarContractFile, validateShardDocSidecarContractFile } = require('./sidecar-contract-validator');
|
||||
const { validateHelpAuthoritySplitAndPrecedence } = require('./help-authority-validator');
|
||||
const { validateShardDocAuthoritySplitAndPrecedence } = require('./shard-doc-authority-validator');
|
||||
const {
|
||||
HELP_CATALOG_GENERATION_ERROR_CODES,
|
||||
buildSidecarAwareExemplarHelpRow,
|
||||
|
|
@ -30,6 +31,10 @@ 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_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_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';
|
||||
|
||||
class Installer {
|
||||
constructor() {
|
||||
|
|
@ -44,7 +49,9 @@ class Installer {
|
|||
this.configCollector = new ConfigCollector();
|
||||
this.ideConfigManager = new IdeConfigManager();
|
||||
this.validateHelpSidecarContractFile = validateHelpSidecarContractFile;
|
||||
this.validateShardDocSidecarContractFile = validateShardDocSidecarContractFile;
|
||||
this.validateHelpAuthoritySplitAndPrecedence = validateHelpAuthoritySplitAndPrecedence;
|
||||
this.validateShardDocAuthoritySplitAndPrecedence = validateShardDocAuthoritySplitAndPrecedence;
|
||||
this.ManifestGenerator = ManifestGenerator;
|
||||
this.installedFiles = new Set(); // Track all installed files
|
||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||
|
|
@ -56,12 +63,27 @@ class Installer {
|
|||
}
|
||||
|
||||
async runConfigurationGenerationTask({ message, bmadDir, moduleConfigs, config, allModules, addResult }) {
|
||||
// Validate exemplar sidecar contract before generating projections/manifests.
|
||||
// Validate converted-capability sidecar contracts before generating projections/manifests.
|
||||
// Fail-fast here prevents downstream artifacts from being produced on invalid metadata.
|
||||
message('Validating shard-doc sidecar contract...');
|
||||
await this.validateShardDocSidecarContractFile();
|
||||
|
||||
message('Validating exemplar sidecar contract...');
|
||||
await this.validateHelpSidecarContractFile();
|
||||
|
||||
addResult('Shard-doc sidecar contract', 'ok', 'validated');
|
||||
addResult('Sidecar contract', 'ok', 'validated');
|
||||
|
||||
message('Validating shard-doc authority split and XML precedence...');
|
||||
const shardDocAuthorityValidation = await this.validateShardDocAuthoritySplitAndPrecedence({
|
||||
sidecarSourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
||||
sourceXmlSourcePath: EXEMPLAR_SHARD_DOC_SOURCE_XML_SOURCE_PATH,
|
||||
compatibilityCatalogSourcePath: EXEMPLAR_SHARD_DOC_COMPATIBILITY_CATALOG_SOURCE_PATH,
|
||||
compatibilityWorkflowFilePath: EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH,
|
||||
});
|
||||
this.shardDocAuthorityRecords = shardDocAuthorityValidation.authoritativeRecords;
|
||||
addResult('Shard-doc authority split', 'ok', shardDocAuthorityValidation.authoritativePresenceKey);
|
||||
|
||||
message('Validating authority split and frontmatter precedence...');
|
||||
const helpAuthorityValidation = await this.validateHelpAuthoritySplitAndPrecedence({
|
||||
bmadDir,
|
||||
|
|
@ -109,6 +131,7 @@ class Installer {
|
|||
ides: config.ides || [],
|
||||
preservedModules: modulesForCsvPreserve,
|
||||
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
||||
taskAuthorityRecords: [...(this.helpAuthorityRecords || []), ...(this.shardDocAuthorityRecords || [])],
|
||||
});
|
||||
|
||||
addResult(
|
||||
|
|
@ -1780,6 +1803,38 @@ class Installer {
|
|||
/**
|
||||
* Private: Create directory structure
|
||||
*/
|
||||
resolveCanonicalIdFromAuthorityRecords({ authorityRecords, authoritySourcePath, fallbackCanonicalId }) {
|
||||
const normalizedAuthoritySourcePath = String(authoritySourcePath || '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/');
|
||||
const normalizedFallbackCanonicalId = String(fallbackCanonicalId || '').trim();
|
||||
const records = Array.isArray(authorityRecords) ? authorityRecords : [];
|
||||
|
||||
for (const record of records) {
|
||||
if (!record || typeof record !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordCanonicalId = String(record.canonicalId || '').trim();
|
||||
const recordAuthoritySourceType = String(record.authoritySourceType || '').trim();
|
||||
const recordAuthoritySourcePath = String(record.authoritySourcePath || '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/');
|
||||
const recordType = String(record.recordType || '').trim();
|
||||
|
||||
if (
|
||||
recordType === 'metadata-authority' &&
|
||||
recordAuthoritySourceType === 'sidecar' &&
|
||||
recordAuthoritySourcePath === normalizedAuthoritySourcePath &&
|
||||
recordCanonicalId.length > 0
|
||||
) {
|
||||
return recordCanonicalId;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedFallbackCanonicalId;
|
||||
}
|
||||
|
||||
isExemplarHelpCatalogRow({ moduleName, name, workflowFile, command, canonicalId }) {
|
||||
if (moduleName !== 'core') return false;
|
||||
|
||||
|
|
@ -1830,7 +1885,7 @@ class Installer {
|
|||
];
|
||||
}
|
||||
|
||||
isExemplarCommandLabelCandidate({ workflowFile, name, rawCommandValue, canonicalId, legacyName }) {
|
||||
isCommandLabelCandidate({ workflowFile, name, rawCommandValue, canonicalId, legacyName, workflowFileContractPath, nameCandidates = [] }) {
|
||||
const normalizedWorkflowFile = String(workflowFile || '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/')
|
||||
|
|
@ -1849,13 +1904,27 @@ class Installer {
|
|||
.toLowerCase()
|
||||
.replace(/^\/+/, '');
|
||||
|
||||
const isHelpWorkflow = normalizedWorkflowFile.endsWith('/core/tasks/help.md');
|
||||
const isExemplarIdentity =
|
||||
normalizedName === 'bmad-help' ||
|
||||
normalizedCommandValue === normalizedCanonicalId ||
|
||||
(normalizedLegacyName.length > 0 && normalizedCommandValue === normalizedLegacyName);
|
||||
const normalizedWorkflowFileContractPath = String(workflowFileContractPath || '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/')
|
||||
.toLowerCase();
|
||||
const workflowMarker = '/core/tasks/';
|
||||
const markerIndex = normalizedWorkflowFileContractPath.indexOf(workflowMarker);
|
||||
const workflowSuffix = markerIndex === -1 ? normalizedWorkflowFileContractPath : normalizedWorkflowFileContractPath.slice(markerIndex);
|
||||
const hasWorkflowMatch = workflowSuffix.length > 0 && normalizedWorkflowFile.endsWith(workflowSuffix);
|
||||
|
||||
return isHelpWorkflow && isExemplarIdentity;
|
||||
const normalizedNameCandidates = (Array.isArray(nameCandidates) ? nameCandidates : [])
|
||||
.map((candidate) =>
|
||||
String(candidate || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
)
|
||||
.filter((candidate) => candidate.length > 0);
|
||||
const matchesNameCandidate = normalizedNameCandidates.includes(normalizedName);
|
||||
const isCanonicalCommand = normalizedCanonicalId.length > 0 && normalizedCommandValue === normalizedCanonicalId;
|
||||
const isLegacyCommand = normalizedLegacyName.length > 0 && normalizedCommandValue === normalizedLegacyName;
|
||||
|
||||
return hasWorkflowMatch && (matchesNameCandidate || isCanonicalCommand || isLegacyCommand);
|
||||
}
|
||||
|
||||
async writeCsvArtifact(filePath, columns, rows) {
|
||||
|
|
@ -1886,6 +1955,31 @@ class Installer {
|
|||
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
||||
bmadFolderName: this.bmadFolderName || BMAD_FOLDER_NAME,
|
||||
});
|
||||
const shardDocCanonicalId = this.resolveCanonicalIdFromAuthorityRecords({
|
||||
authorityRecords: this.shardDocAuthorityRecords || [],
|
||||
authoritySourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
||||
fallbackCanonicalId: 'bmad-shard-doc',
|
||||
});
|
||||
const commandLabelContracts = [
|
||||
{
|
||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
||||
legacyName: sidecarAwareExemplar.legacyName,
|
||||
displayedCommandLabel: sidecarAwareExemplar.displayedCommandLabel,
|
||||
authoritySourceType: sidecarAwareExemplar.authoritySourceType,
|
||||
authoritySourcePath: sidecarAwareExemplar.authoritySourcePath,
|
||||
workflowFilePath: sidecarAwareExemplar.row['workflow-file'],
|
||||
nameCandidates: [sidecarAwareExemplar.row.name],
|
||||
},
|
||||
{
|
||||
canonicalId: shardDocCanonicalId,
|
||||
legacyName: 'shard-doc',
|
||||
displayedCommandLabel: renderDisplayedCommandLabel(shardDocCanonicalId),
|
||||
authoritySourceType: 'sidecar',
|
||||
authoritySourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
||||
workflowFilePath: EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH,
|
||||
nameCandidates: ['shard document', 'shard-doc'],
|
||||
},
|
||||
];
|
||||
let exemplarRowWritten = false;
|
||||
|
||||
// Load agent manifest for agent info lookup
|
||||
|
|
@ -2110,31 +2204,37 @@ class Installer {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.isExemplarCommandLabelCandidate({
|
||||
for (const contract of commandLabelContracts) {
|
||||
const isContractCandidate = this.isCommandLabelCandidate({
|
||||
workflowFile,
|
||||
name,
|
||||
rawCommandValue,
|
||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
||||
legacyName: sidecarAwareExemplar.legacyName,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
canonicalId: contract.canonicalId,
|
||||
legacyName: contract.legacyName,
|
||||
workflowFileContractPath: contract.workflowFilePath,
|
||||
nameCandidates: contract.nameCandidates,
|
||||
});
|
||||
if (isContractCandidate) {
|
||||
const displayedCommandLabel = renderDisplayedCommandLabel(rawCommandValue);
|
||||
commandLabelRowsFromMergedCatalog.push({
|
||||
surface: `${this.bmadFolderName || BMAD_FOLDER_NAME}/_config/bmad-help.csv`,
|
||||
canonicalId: contract.canonicalId,
|
||||
rawCommandValue,
|
||||
displayedCommandLabel,
|
||||
normalizedDisplayedLabel: normalizeDisplayedCommandLabel(displayedCommandLabel),
|
||||
authoritySourceType: contract.authoritySourceType,
|
||||
authoritySourcePath: contract.authoritySourcePath,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const displayedCommandLabel = renderDisplayedCommandLabel(rawCommandValue);
|
||||
commandLabelRowsFromMergedCatalog.push({
|
||||
surface: `${this.bmadFolderName || BMAD_FOLDER_NAME}/_config/bmad-help.csv`,
|
||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
||||
rawCommandValue,
|
||||
displayedCommandLabel,
|
||||
normalizedDisplayedLabel: normalizeDisplayedCommandLabel(displayedCommandLabel),
|
||||
authoritySourceType: sidecarAwareExemplar.authoritySourceType,
|
||||
authoritySourcePath: sidecarAwareExemplar.authoritySourcePath,
|
||||
});
|
||||
}
|
||||
|
||||
const exemplarRowCount = commandLabelRowsFromMergedCatalog.length;
|
||||
const commandLabelRowCountByCanonicalId = new Map(commandLabelContracts.map((contract) => [contract.canonicalId, 0]));
|
||||
for (const row of commandLabelRowsFromMergedCatalog) {
|
||||
commandLabelRowCountByCanonicalId.set(row.canonicalId, (commandLabelRowCountByCanonicalId.get(row.canonicalId) || 0) + 1);
|
||||
}
|
||||
const exemplarRowCount = commandLabelRowCountByCanonicalId.get(sidecarAwareExemplar.canonicalId) || 0;
|
||||
|
||||
this.helpCatalogPipelineRows = sidecarAwareExemplar.pipelineStageRows.map((row) => ({
|
||||
...row,
|
||||
|
|
@ -2144,15 +2244,24 @@ class Installer {
|
|||
}));
|
||||
this.helpCatalogCommandLabelReportRows = commandLabelRowsFromMergedCatalog.map((row) => ({
|
||||
...row,
|
||||
rowCountForCanonicalId: exemplarRowCount,
|
||||
status: exemplarRowCount === 1 ? 'PASS' : 'FAIL',
|
||||
rowCountForCanonicalId: commandLabelRowCountByCanonicalId.get(row.canonicalId) || 0,
|
||||
status: (commandLabelRowCountByCanonicalId.get(row.canonicalId) || 0) === 1 ? 'PASS' : 'FAIL',
|
||||
}));
|
||||
|
||||
const commandLabelContractResult = evaluateExemplarCommandLabelReportRows(this.helpCatalogCommandLabelReportRows, {
|
||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
||||
displayedCommandLabel: sidecarAwareExemplar.displayedCommandLabel,
|
||||
});
|
||||
if (!commandLabelContractResult.valid) {
|
||||
const commandLabelContractFailures = new Map();
|
||||
for (const contract of commandLabelContracts) {
|
||||
const commandLabelContractResult = evaluateExemplarCommandLabelReportRows(this.helpCatalogCommandLabelReportRows, {
|
||||
canonicalId: contract.canonicalId,
|
||||
displayedCommandLabel: contract.displayedCommandLabel,
|
||||
authoritySourceType: contract.authoritySourceType,
|
||||
authoritySourcePath: contract.authoritySourcePath,
|
||||
});
|
||||
if (!commandLabelContractResult.valid) {
|
||||
commandLabelContractFailures.set(contract.canonicalId, commandLabelContractResult.reason);
|
||||
}
|
||||
}
|
||||
|
||||
if (commandLabelContractFailures.size > 0) {
|
||||
this.helpCatalogPipelineRows = this.helpCatalogPipelineRows.map((row) => ({
|
||||
...row,
|
||||
stageStatus: 'FAIL',
|
||||
|
|
@ -2161,14 +2270,19 @@ class Installer {
|
|||
this.helpCatalogCommandLabelReportRows = this.helpCatalogCommandLabelReportRows.map((row) => ({
|
||||
...row,
|
||||
status: 'FAIL',
|
||||
failureReason: commandLabelContractResult.reason,
|
||||
failureReason: commandLabelContractFailures.get(row.canonicalId) || row.failureReason || '',
|
||||
}));
|
||||
|
||||
const commandLabelFailureSummary = [...commandLabelContractFailures.entries()]
|
||||
.sort(([leftCanonicalId], [rightCanonicalId]) => leftCanonicalId.localeCompare(rightCanonicalId))
|
||||
.map(([canonicalId, reason]) => `${canonicalId}:${reason}`)
|
||||
.join('|');
|
||||
|
||||
const commandLabelError = new Error(
|
||||
`${HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED}: ${commandLabelContractResult.reason}`,
|
||||
`${HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED}: ${commandLabelFailureSummary}`,
|
||||
);
|
||||
commandLabelError.code = HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED;
|
||||
commandLabelError.detail = commandLabelContractResult.reason;
|
||||
commandLabelError.detail = commandLabelFailureSummary;
|
||||
throw commandLabelError;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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 CANONICAL_ALIAS_TABLE_COLUMNS = Object.freeze([
|
||||
'canonicalId',
|
||||
'alias',
|
||||
|
|
@ -55,6 +56,35 @@ const LOCKED_CANONICAL_ALIAS_TABLE_EXEMPLAR_ROWS = Object.freeze([
|
|||
resolutionEligibility: 'slash-command-only',
|
||||
}),
|
||||
]);
|
||||
const LOCKED_CANONICAL_ALIAS_TABLE_SHARD_DOC_ROWS = Object.freeze([
|
||||
Object.freeze({
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
alias: 'bmad-shard-doc',
|
||||
aliasType: 'canonical-id',
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:canonical-id',
|
||||
normalizedAliasValue: 'bmad-shard-doc',
|
||||
rawIdentityHasLeadingSlash: false,
|
||||
resolutionEligibility: 'canonical-id-only',
|
||||
}),
|
||||
Object.freeze({
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
alias: 'shard-doc',
|
||||
aliasType: 'legacy-name',
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:legacy-name',
|
||||
normalizedAliasValue: 'shard-doc',
|
||||
rawIdentityHasLeadingSlash: false,
|
||||
resolutionEligibility: 'legacy-name-only',
|
||||
}),
|
||||
Object.freeze({
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
alias: '/bmad-shard-doc',
|
||||
aliasType: 'slash-command',
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:slash-command',
|
||||
normalizedAliasValue: 'bmad-shard-doc',
|
||||
rawIdentityHasLeadingSlash: true,
|
||||
resolutionEligibility: 'slash-command-only',
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Generates manifest files for installed workflows, agents, and tasks
|
||||
|
|
@ -68,6 +98,73 @@ class ManifestGenerator {
|
|||
this.modules = [];
|
||||
this.files = [];
|
||||
this.selectedIdes = [];
|
||||
this.includeConvertedShardDocAliasRows = null;
|
||||
}
|
||||
|
||||
normalizeTaskAuthorityRecords(records) {
|
||||
if (!Array.isArray(records)) return [];
|
||||
|
||||
const normalized = [];
|
||||
for (const record of records) {
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const canonicalId = String(record.canonicalId ?? '').trim();
|
||||
const authoritySourceType = String(record.authoritySourceType ?? '').trim();
|
||||
const authoritySourcePath = String(record.authoritySourcePath ?? '').trim();
|
||||
const sourcePath = String(record.sourcePath ?? '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/');
|
||||
const recordType = String(record.recordType ?? '').trim();
|
||||
|
||||
if (!canonicalId || !authoritySourceType || !authoritySourcePath || !sourcePath || !recordType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized.push({
|
||||
recordType,
|
||||
canonicalId,
|
||||
authoritySourceType,
|
||||
authoritySourcePath,
|
||||
sourcePath,
|
||||
});
|
||||
}
|
||||
|
||||
normalized.sort((left, right) => {
|
||||
const leftKey = `${left.canonicalId}|${left.recordType}|${left.authoritySourceType}|${left.authoritySourcePath}|${left.sourcePath}`;
|
||||
const rightKey = `${right.canonicalId}|${right.recordType}|${right.authoritySourceType}|${right.authoritySourcePath}|${right.sourcePath}`;
|
||||
return leftKey.localeCompare(rightKey);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
buildTaskAuthorityProjectionIndex(records) {
|
||||
const projectionIndex = new Map();
|
||||
for (const record of records) {
|
||||
if (!record || record.recordType !== 'metadata-authority' || record.authoritySourceType !== 'sidecar') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceMatch = String(record.sourcePath)
|
||||
.replaceAll('\\', '/')
|
||||
.match(/\/src\/([^/]+)\/tasks\/([^/.]+)\.(?:md|xml)$/i);
|
||||
if (!sourceMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleName = sourceMatch[1];
|
||||
const taskName = sourceMatch[2];
|
||||
projectionIndex.set(`${moduleName}:${taskName}`, {
|
||||
legacyName: taskName,
|
||||
canonicalId: record.canonicalId,
|
||||
authoritySourceType: record.authoritySourceType,
|
||||
authoritySourcePath: record.authoritySourcePath,
|
||||
});
|
||||
}
|
||||
|
||||
return projectionIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,6 +279,13 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
this.helpAuthorityRecords = await this.normalizeHelpAuthorityRecords(options.helpAuthorityRecords);
|
||||
const taskAuthorityInput = Object.prototype.hasOwnProperty.call(options, 'taskAuthorityRecords')
|
||||
? options.taskAuthorityRecords
|
||||
: options.helpAuthorityRecords;
|
||||
this.taskAuthorityRecords = this.normalizeTaskAuthorityRecords(taskAuthorityInput);
|
||||
this.includeConvertedShardDocAliasRows = Object.prototype.hasOwnProperty.call(options, 'includeConvertedShardDocAliasRows')
|
||||
? options.includeConvertedShardDocAliasRows === true
|
||||
: null;
|
||||
|
||||
// Filter out any undefined/null values from IDE list
|
||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||
|
|
@ -958,15 +1062,10 @@ class ManifestGenerator {
|
|||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
const compatibilitySurfacePath = `${this.bmadFolderName || '_bmad'}/_config/task-manifest.csv`;
|
||||
const sidecarAuthorityRecord = Array.isArray(this.helpAuthorityRecords)
|
||||
? this.helpAuthorityRecords.find(
|
||||
(record) => record?.canonicalId === 'bmad-help' && record?.authoritySourceType === 'sidecar' && record?.authoritySourcePath,
|
||||
)
|
||||
: null;
|
||||
const exemplarAuthoritySourceType = sidecarAuthorityRecord ? sidecarAuthorityRecord.authoritySourceType : 'sidecar';
|
||||
const exemplarAuthoritySourcePath = sidecarAuthorityRecord
|
||||
? sidecarAuthorityRecord.authoritySourcePath
|
||||
: 'bmad-fork/src/core/tasks/help.artifact.yaml';
|
||||
const taskAuthorityRecords = Array.isArray(this.taskAuthorityRecords)
|
||||
? this.taskAuthorityRecords
|
||||
: this.normalizeTaskAuthorityRecords(this.helpAuthorityRecords);
|
||||
const taskAuthorityProjectionIndex = this.buildTaskAuthorityProjectionIndex(taskAuthorityRecords);
|
||||
|
||||
// Read existing manifest to preserve entries
|
||||
const existingEntries = new Map();
|
||||
|
|
@ -1015,7 +1114,7 @@ class ManifestGenerator {
|
|||
for (const task of this.tasks) {
|
||||
const key = `${task.module}:${task.name}`;
|
||||
const previousRecord = allTasks.get(key);
|
||||
const isExemplarHelpTask = task.module === 'core' && task.name === 'help';
|
||||
const authorityProjection = taskAuthorityProjectionIndex.get(key);
|
||||
|
||||
allTasks.set(key, {
|
||||
name: task.name,
|
||||
|
|
@ -1024,10 +1123,10 @@ class ManifestGenerator {
|
|||
module: task.module,
|
||||
path: task.path,
|
||||
standalone: task.standalone,
|
||||
legacyName: isExemplarHelpTask ? 'help' : previousRecord?.legacyName || task.name,
|
||||
canonicalId: isExemplarHelpTask ? 'bmad-help' : previousRecord?.canonicalId || '',
|
||||
authoritySourceType: isExemplarHelpTask ? exemplarAuthoritySourceType : previousRecord?.authoritySourceType || '',
|
||||
authoritySourcePath: isExemplarHelpTask ? exemplarAuthoritySourcePath : previousRecord?.authoritySourcePath || '',
|
||||
legacyName: authorityProjection ? authorityProjection.legacyName : previousRecord?.legacyName || task.name,
|
||||
canonicalId: authorityProjection ? authorityProjection.canonicalId : previousRecord?.canonicalId || '',
|
||||
authoritySourceType: authorityProjection ? authorityProjection.authoritySourceType : previousRecord?.authoritySourceType || '',
|
||||
authoritySourcePath: authorityProjection ? authorityProjection.authoritySourcePath : previousRecord?.authoritySourcePath || '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1070,18 +1169,64 @@ class ManifestGenerator {
|
|||
};
|
||||
}
|
||||
|
||||
buildCanonicalAliasProjectionRows(authoritySourceType, authoritySourcePath) {
|
||||
return LOCKED_CANONICAL_ALIAS_TABLE_EXEMPLAR_ROWS.map((row) => ({
|
||||
canonicalId: row.canonicalId,
|
||||
alias: row.alias,
|
||||
aliasType: row.aliasType,
|
||||
authoritySourceType,
|
||||
authoritySourcePath,
|
||||
rowIdentity: row.rowIdentity,
|
||||
normalizedAliasValue: row.normalizedAliasValue,
|
||||
rawIdentityHasLeadingSlash: row.rawIdentityHasLeadingSlash,
|
||||
resolutionEligibility: row.resolutionEligibility,
|
||||
}));
|
||||
resolveShardDocAliasAuthorityRecord() {
|
||||
const sidecarAuthorityRecord = Array.isArray(this.taskAuthorityRecords)
|
||||
? this.taskAuthorityRecords.find(
|
||||
(record) => record?.canonicalId === 'bmad-shard-doc' && record?.authoritySourceType === 'sidecar' && record?.authoritySourcePath,
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
authoritySourceType: sidecarAuthorityRecord ? sidecarAuthorityRecord.authoritySourceType : 'sidecar',
|
||||
authoritySourcePath: sidecarAuthorityRecord
|
||||
? sidecarAuthorityRecord.authoritySourcePath
|
||||
: DEFAULT_EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
hasShardDocTaskAuthorityProjection() {
|
||||
if (!Array.isArray(this.taskAuthorityRecords)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.taskAuthorityRecords.some(
|
||||
(record) =>
|
||||
record?.recordType === 'metadata-authority' &&
|
||||
record?.canonicalId === 'bmad-shard-doc' &&
|
||||
record?.authoritySourceType === 'sidecar' &&
|
||||
String(record?.authoritySourcePath || '').trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
shouldProjectShardDocAliasRows() {
|
||||
if (this.includeConvertedShardDocAliasRows === true) {
|
||||
return true;
|
||||
}
|
||||
if (this.includeConvertedShardDocAliasRows === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasShardDocTaskAuthorityProjection();
|
||||
}
|
||||
|
||||
buildCanonicalAliasProjectionRows() {
|
||||
const buildRows = (lockedRows, authorityRecord) =>
|
||||
lockedRows.map((row) => ({
|
||||
canonicalId: row.canonicalId,
|
||||
alias: row.alias,
|
||||
aliasType: row.aliasType,
|
||||
authoritySourceType: authorityRecord.authoritySourceType,
|
||||
authoritySourcePath: authorityRecord.authoritySourcePath,
|
||||
rowIdentity: row.rowIdentity,
|
||||
normalizedAliasValue: row.normalizedAliasValue,
|
||||
rawIdentityHasLeadingSlash: row.rawIdentityHasLeadingSlash,
|
||||
resolutionEligibility: row.resolutionEligibility,
|
||||
}));
|
||||
|
||||
const rows = [...buildRows(LOCKED_CANONICAL_ALIAS_TABLE_EXEMPLAR_ROWS, this.resolveExemplarAliasAuthorityRecord())];
|
||||
if (this.shouldProjectShardDocAliasRows()) {
|
||||
rows.push(...buildRows(LOCKED_CANONICAL_ALIAS_TABLE_SHARD_DOC_ROWS, this.resolveShardDocAliasAuthorityRecord()));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1091,8 +1236,7 @@ class ManifestGenerator {
|
|||
async writeCanonicalAliasManifest(cfgDir) {
|
||||
const csvPath = path.join(cfgDir, 'canonical-aliases.csv');
|
||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
const { authoritySourceType, authoritySourcePath } = this.resolveExemplarAliasAuthorityRecord();
|
||||
const projectedRows = this.buildCanonicalAliasProjectionRows(authoritySourceType, authoritySourcePath);
|
||||
const projectedRows = this.buildCanonicalAliasProjectionRows();
|
||||
|
||||
let csvContent = `${CANONICAL_ALIAS_TABLE_COLUMNS.join(',')}\n`;
|
||||
for (const row of projectedRows) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,13 @@ const PROJECTION_COMPATIBILITY_ERROR_CODES = Object.freeze({
|
|||
HELP_CATALOG_HEADER_WAVE1_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_WAVE1_MISMATCH',
|
||||
HELP_CATALOG_REQUIRED_COLUMN_MISSING: 'ERR_HELP_CATALOG_COMPAT_REQUIRED_COLUMN_MISSING',
|
||||
HELP_CATALOG_EXEMPLAR_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_EXEMPLAR_ROW_CONTRACT_FAILED',
|
||||
HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_SHARD_DOC_ROW_CONTRACT_FAILED',
|
||||
GITHUB_COPILOT_WORKFLOW_FILE_MISSING: 'ERR_GITHUB_COPILOT_HELP_WORKFLOW_FILE_MISSING',
|
||||
COMMAND_DOC_PARSE_FAILED: 'ERR_COMMAND_DOC_CONSISTENCY_PARSE_FAILED',
|
||||
COMMAND_DOC_CANONICAL_COMMAND_MISSING: 'ERR_COMMAND_DOC_CONSISTENCY_CANONICAL_COMMAND_MISSING',
|
||||
COMMAND_DOC_CANONICAL_COMMAND_AMBIGUOUS: 'ERR_COMMAND_DOC_CONSISTENCY_CANONICAL_COMMAND_AMBIGUOUS',
|
||||
COMMAND_DOC_ALIAS_AMBIGUOUS: 'ERR_COMMAND_DOC_CONSISTENCY_ALIAS_AMBIGUOUS',
|
||||
COMMAND_DOC_GENERATED_SURFACE_MISMATCH: 'ERR_COMMAND_DOC_CONSISTENCY_GENERATED_SURFACE_MISMATCH',
|
||||
});
|
||||
|
||||
class ProjectionCompatibilityError extends Error {
|
||||
|
|
@ -177,6 +183,37 @@ function normalizeWorkflowPath(value) {
|
|||
return normalizeSourcePath(value).toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeDisplayedCommandLabel(value) {
|
||||
const normalized = normalizeValue(value).toLowerCase().replace(/^\/+/, '');
|
||||
return normalized.length > 0 ? `/${normalized}` : '';
|
||||
}
|
||||
|
||||
function parseDocumentedSlashCommands(markdownContent, options = {}) {
|
||||
const sourcePath = normalizeSourcePath(options.sourcePath || 'docs/reference/commands.md');
|
||||
const surface = options.surface || 'command-doc-consistency';
|
||||
const content = String(markdownContent ?? '');
|
||||
const commandPattern = /\|\s*`(\/[^`]+)`\s*\|/g;
|
||||
const commands = [];
|
||||
let match;
|
||||
while ((match = commandPattern.exec(content)) !== null) {
|
||||
commands.push(normalizeDisplayedCommandLabel(match[1]));
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_PARSE_FAILED,
|
||||
detail: 'Unable to find slash-command rows in command reference markdown',
|
||||
surface,
|
||||
fieldPath: 'docs.reference.commands',
|
||||
sourcePath,
|
||||
observedValue: '<no-slash-command-rows>',
|
||||
expectedValue: '| `/bmad-...` |',
|
||||
});
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
function validateTaskManifestLoaderEntries(rows, options = {}) {
|
||||
const surface = options.surface || 'task-manifest-loader';
|
||||
const sourcePath = normalizeSourcePath(options.sourcePath || '_bmad/_config/task-manifest.csv');
|
||||
|
|
@ -261,6 +298,23 @@ function validateHelpCatalogLoaderEntries(rows, options = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
const shardDocRows = parsedRows.filter(
|
||||
(row) =>
|
||||
normalizeCommandValue(row.command) === 'bmad-shard-doc' &&
|
||||
normalizeWorkflowPath(row['workflow-file']).endsWith('/core/tasks/shard-doc.xml'),
|
||||
);
|
||||
if (shardDocRows.length !== 1) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED,
|
||||
detail: 'Exactly one shard-doc compatibility row is required for help catalog consumers',
|
||||
surface,
|
||||
fieldPath: 'rows[*].command',
|
||||
sourcePath,
|
||||
observedValue: String(shardDocRows.length),
|
||||
expectedValue: '1',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -392,6 +446,84 @@ function validateHelpCatalogCompatibilitySurface(csvContent, options = {}) {
|
|||
return { headerColumns, rows };
|
||||
}
|
||||
|
||||
function validateCommandDocSurfaceConsistency(commandDocMarkdown, options = {}) {
|
||||
const surface = options.surface || 'command-doc-consistency';
|
||||
const sourcePath = normalizeSourcePath(options.sourcePath || 'docs/reference/commands.md');
|
||||
const canonicalId = normalizeValue(options.canonicalId || 'bmad-shard-doc');
|
||||
const expectedDisplayedCommandLabel = normalizeDisplayedCommandLabel(options.expectedDisplayedCommandLabel || '/bmad-shard-doc');
|
||||
const disallowedAliasLabels = Array.isArray(options.disallowedAliasLabels) ? options.disallowedAliasLabels : ['/shard-doc'];
|
||||
const commandLabelRows = Array.isArray(options.commandLabelRows) ? options.commandLabelRows : [];
|
||||
|
||||
const documentedCommands = parseDocumentedSlashCommands(commandDocMarkdown, {
|
||||
sourcePath,
|
||||
surface,
|
||||
});
|
||||
const documentedCanonicalMatches = documentedCommands.filter((commandLabel) => commandLabel === expectedDisplayedCommandLabel);
|
||||
if (documentedCanonicalMatches.length === 0) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_CANONICAL_COMMAND_MISSING,
|
||||
detail: 'Expected canonical command is missing from command reference markdown',
|
||||
surface,
|
||||
fieldPath: 'docs.reference.commands.canonical-command',
|
||||
sourcePath,
|
||||
observedValue: '<missing>',
|
||||
expectedValue: expectedDisplayedCommandLabel,
|
||||
});
|
||||
}
|
||||
if (documentedCanonicalMatches.length > 1) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_CANONICAL_COMMAND_AMBIGUOUS,
|
||||
detail: 'Canonical command appears multiple times in command reference markdown',
|
||||
surface,
|
||||
fieldPath: 'docs.reference.commands.canonical-command',
|
||||
sourcePath,
|
||||
observedValue: String(documentedCanonicalMatches.length),
|
||||
expectedValue: '1',
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedDisallowedAliases = disallowedAliasLabels.map((label) => normalizeDisplayedCommandLabel(label)).filter(Boolean);
|
||||
const presentDisallowedAlias = normalizedDisallowedAliases.find((label) => documentedCommands.includes(label));
|
||||
if (presentDisallowedAlias) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_ALIAS_AMBIGUOUS,
|
||||
detail: 'Disallowed alias command detected in command reference markdown',
|
||||
surface,
|
||||
fieldPath: 'docs.reference.commands.alias-command',
|
||||
sourcePath,
|
||||
observedValue: presentDisallowedAlias,
|
||||
expectedValue: expectedDisplayedCommandLabel,
|
||||
});
|
||||
}
|
||||
|
||||
const generatedCanonicalRows = commandLabelRows.filter((row) => normalizeValue(row.canonicalId) === canonicalId);
|
||||
const generatedMatchingRows = generatedCanonicalRows.filter(
|
||||
(row) => normalizeDisplayedCommandLabel(row.displayedCommandLabel) === expectedDisplayedCommandLabel,
|
||||
);
|
||||
if (generatedCanonicalRows.length === 0 || generatedMatchingRows.length !== 1) {
|
||||
throwCompatibilityError({
|
||||
code: PROJECTION_COMPATIBILITY_ERROR_CODES.COMMAND_DOC_GENERATED_SURFACE_MISMATCH,
|
||||
detail: 'Generated command-label surface does not match canonical command-doc contract',
|
||||
surface,
|
||||
fieldPath: 'generated.command-label-report',
|
||||
sourcePath: normalizeSourcePath(options.generatedSurfacePath || '_bmad/_config/bmad-help-command-label-report.csv'),
|
||||
observedValue:
|
||||
generatedCanonicalRows
|
||||
.map((row) => normalizeDisplayedCommandLabel(row.displayedCommandLabel))
|
||||
.filter(Boolean)
|
||||
.join('|') || '<missing>',
|
||||
expectedValue: expectedDisplayedCommandLabel,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
canonicalId,
|
||||
expectedDisplayedCommandLabel,
|
||||
documentedCommands,
|
||||
generatedCanonicalCommand: expectedDisplayedCommandLabel,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROJECTION_COMPATIBILITY_ERROR_CODES,
|
||||
ProjectionCompatibilityError,
|
||||
|
|
@ -404,4 +536,5 @@ module.exports = {
|
|||
validateHelpCatalogCompatibilitySurface,
|
||||
validateHelpCatalogLoaderEntries,
|
||||
validateGithubCopilotHelpLoaderEntries,
|
||||
validateCommandDocSurfaceConsistency,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,334 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { getProjectRoot, getSourcePath } = require('../../../lib/project-root');
|
||||
|
||||
const SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES = Object.freeze({
|
||||
SIDECAR_FILE_NOT_FOUND: 'ERR_SHARD_DOC_AUTHORITY_SIDECAR_FILE_NOT_FOUND',
|
||||
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',
|
||||
SOURCE_XML_FILE_NOT_FOUND: 'ERR_SHARD_DOC_AUTHORITY_SOURCE_XML_FILE_NOT_FOUND',
|
||||
COMPATIBILITY_FILE_NOT_FOUND: 'ERR_SHARD_DOC_AUTHORITY_COMPATIBILITY_FILE_NOT_FOUND',
|
||||
COMPATIBILITY_PARSE_FAILED: 'ERR_SHARD_DOC_AUTHORITY_COMPATIBILITY_PARSE_FAILED',
|
||||
COMPATIBILITY_ROW_MISSING: 'ERR_SHARD_DOC_AUTHORITY_COMPATIBILITY_ROW_MISSING',
|
||||
COMPATIBILITY_ROW_DUPLICATE: 'ERR_SHARD_DOC_AUTHORITY_COMPATIBILITY_ROW_DUPLICATE',
|
||||
COMMAND_MISMATCH: 'ERR_SHARD_DOC_AUTHORITY_COMMAND_MISMATCH',
|
||||
DISPLAY_NAME_MISMATCH: 'ERR_SHARD_DOC_AUTHORITY_DISPLAY_NAME_MISMATCH',
|
||||
DUPLICATE_CANONICAL_COMMAND: 'ERR_SHARD_DOC_AUTHORITY_DUPLICATE_CANONICAL_COMMAND',
|
||||
});
|
||||
|
||||
const SHARD_DOC_LOCKED_CANONICAL_ID = 'bmad-shard-doc';
|
||||
const SHARD_DOC_LOCKED_AUTHORITATIVE_PRESENCE_KEY = `capability:${SHARD_DOC_LOCKED_CANONICAL_ID}`;
|
||||
|
||||
class ShardDocAuthorityValidationError extends Error {
|
||||
constructor({ code, detail, fieldPath, sourcePath, observedValue, expectedValue }) {
|
||||
const message = `${code}: ${detail} (fieldPath=${fieldPath}, sourcePath=${sourcePath})`;
|
||||
super(message);
|
||||
this.name = 'ShardDocAuthorityValidationError';
|
||||
this.code = code;
|
||||
this.detail = detail;
|
||||
this.fieldPath = fieldPath;
|
||||
this.sourcePath = sourcePath;
|
||||
this.observedValue = observedValue;
|
||||
this.expectedValue = expectedValue;
|
||||
this.fullMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSourcePath(value) {
|
||||
if (!value) return '';
|
||||
return String(value).replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
function toProjectRelativePath(filePath) {
|
||||
const projectRoot = getProjectRoot();
|
||||
const relative = path.relative(projectRoot, filePath);
|
||||
|
||||
if (!relative || relative.startsWith('..')) {
|
||||
return normalizeSourcePath(path.resolve(filePath));
|
||||
}
|
||||
|
||||
return normalizeSourcePath(relative);
|
||||
}
|
||||
|
||||
function hasOwn(obj, key) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
|
||||
function isBlankString(value) {
|
||||
return typeof value !== 'string' || value.trim().length === 0;
|
||||
}
|
||||
|
||||
function csvMatchValue(value) {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
function createValidationError(code, detail, fieldPath, sourcePath, observedValue, expectedValue) {
|
||||
throw new ShardDocAuthorityValidationError({
|
||||
code,
|
||||
detail,
|
||||
fieldPath,
|
||||
sourcePath,
|
||||
observedValue,
|
||||
expectedValue,
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSidecarMetadata(sidecarData, sidecarSourcePath, sourceXmlSourcePath) {
|
||||
const requiredFields = ['canonicalId', 'displayName', 'description', 'sourcePath'];
|
||||
for (const requiredField of requiredFields) {
|
||||
if (!hasOwn(sidecarData, requiredField)) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_INVALID_METADATA,
|
||||
`Missing required sidecar metadata field "${requiredField}"`,
|
||||
requiredField,
|
||||
sidecarSourcePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const requiredField of requiredFields) {
|
||||
if (isBlankString(sidecarData[requiredField])) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_INVALID_METADATA,
|
||||
`Required sidecar metadata field "${requiredField}" must be a non-empty string`,
|
||||
requiredField,
|
||||
sidecarSourcePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCanonicalId = String(sidecarData.canonicalId).trim();
|
||||
if (normalizedCanonicalId !== SHARD_DOC_LOCKED_CANONICAL_ID) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH,
|
||||
'Converted shard-doc sidecar canonicalId must remain locked to bmad-shard-doc',
|
||||
'canonicalId',
|
||||
sidecarSourcePath,
|
||||
normalizedCanonicalId,
|
||||
SHARD_DOC_LOCKED_CANONICAL_ID,
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedDeclaredSourcePath = normalizeSourcePath(sidecarData.sourcePath);
|
||||
if (normalizedDeclaredSourcePath !== sourceXmlSourcePath) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_INVALID_METADATA,
|
||||
'Sidecar sourcePath must match shard-doc XML source path',
|
||||
'sourcePath',
|
||||
sidecarSourcePath,
|
||||
normalizedDeclaredSourcePath,
|
||||
sourceXmlSourcePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function parseCompatibilityRows(compatibilityCatalogPath, compatibilityCatalogSourcePath) {
|
||||
if (!(await fs.pathExists(compatibilityCatalogPath))) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_FILE_NOT_FOUND,
|
||||
'Expected module-help compatibility catalog file was not found',
|
||||
'<file>',
|
||||
compatibilityCatalogSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
let csvRaw;
|
||||
try {
|
||||
csvRaw = await fs.readFile(compatibilityCatalogPath, 'utf8');
|
||||
} catch (error) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_PARSE_FAILED,
|
||||
`Unable to read compatibility catalog file: ${error.message}`,
|
||||
'<document>',
|
||||
compatibilityCatalogSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return csv.parse(csvRaw, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
relax_column_count: true,
|
||||
trim: true,
|
||||
});
|
||||
} catch (error) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_PARSE_FAILED,
|
||||
`CSV parse failure: ${error.message}`,
|
||||
'<document>',
|
||||
compatibilityCatalogSourcePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCompatibilityPrecedence({ rows, displayName, workflowFilePath, compatibilityCatalogSourcePath }) {
|
||||
const workflowMatches = rows.filter((row) => csvMatchValue(row['workflow-file']) === workflowFilePath);
|
||||
|
||||
if (workflowMatches.length === 0) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_MISSING,
|
||||
'Converted shard-doc compatibility row is missing from module-help catalog',
|
||||
'workflow-file',
|
||||
compatibilityCatalogSourcePath,
|
||||
'<missing>',
|
||||
workflowFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (workflowMatches.length > 1) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_DUPLICATE,
|
||||
'Converted shard-doc compatibility row appears more than once in module-help catalog',
|
||||
'workflow-file',
|
||||
compatibilityCatalogSourcePath,
|
||||
workflowMatches.length,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalCommandMatches = rows.filter((row) => csvMatchValue(row.command) === SHARD_DOC_LOCKED_CANONICAL_ID);
|
||||
if (canonicalCommandMatches.length > 1) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.DUPLICATE_CANONICAL_COMMAND,
|
||||
'Converted shard-doc canonical command appears in more than one compatibility row',
|
||||
'command',
|
||||
compatibilityCatalogSourcePath,
|
||||
canonicalCommandMatches.length,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
const shardDocRow = workflowMatches[0];
|
||||
const observedCommand = csvMatchValue(shardDocRow.command);
|
||||
if (!observedCommand || observedCommand !== SHARD_DOC_LOCKED_CANONICAL_ID) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH,
|
||||
'Converted shard-doc compatibility command must match locked canonical command bmad-shard-doc',
|
||||
'command',
|
||||
compatibilityCatalogSourcePath,
|
||||
observedCommand || '<empty>',
|
||||
SHARD_DOC_LOCKED_CANONICAL_ID,
|
||||
);
|
||||
}
|
||||
|
||||
const observedDisplayName = csvMatchValue(shardDocRow.name);
|
||||
if (observedDisplayName && observedDisplayName !== displayName) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.DISPLAY_NAME_MISMATCH,
|
||||
'Converted shard-doc compatibility name must match sidecar displayName when provided',
|
||||
'name',
|
||||
compatibilityCatalogSourcePath,
|
||||
observedDisplayName,
|
||||
displayName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildShardDocAuthorityRecords({ canonicalId, sidecarSourcePath, sourceXmlSourcePath }) {
|
||||
return [
|
||||
{
|
||||
recordType: 'metadata-authority',
|
||||
canonicalId,
|
||||
authoritativePresenceKey: SHARD_DOC_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||
authoritySourceType: 'sidecar',
|
||||
authoritySourcePath: sidecarSourcePath,
|
||||
sourcePath: sourceXmlSourcePath,
|
||||
},
|
||||
{
|
||||
recordType: 'source-body-authority',
|
||||
canonicalId,
|
||||
authoritativePresenceKey: SHARD_DOC_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||
authoritySourceType: 'source-xml',
|
||||
authoritySourcePath: sourceXmlSourcePath,
|
||||
sourcePath: sourceXmlSourcePath,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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));
|
||||
const sourceXmlSourcePath = normalizeSourcePath(options.sourceXmlSourcePath || toProjectRelativePath(sourceXmlPath));
|
||||
const compatibilityCatalogSourcePath = normalizeSourcePath(
|
||||
options.compatibilityCatalogSourcePath || toProjectRelativePath(compatibilityCatalogPath),
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(sidecarPath))) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND,
|
||||
'Expected shard-doc sidecar metadata file was not found',
|
||||
'<file>',
|
||||
sidecarSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
let sidecarData;
|
||||
try {
|
||||
const sidecarRaw = await fs.readFile(sidecarPath, 'utf8');
|
||||
sidecarData = yaml.parse(sidecarRaw);
|
||||
} catch (error) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||
`YAML parse failure: ${error.message}`,
|
||||
'<document>',
|
||||
sidecarSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_INVALID_METADATA,
|
||||
'Sidecar root must be a YAML mapping object',
|
||||
'<document>',
|
||||
sidecarSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
ensureSidecarMetadata(sidecarData, sidecarSourcePath, sourceXmlSourcePath);
|
||||
|
||||
if (!(await fs.pathExists(sourceXmlPath))) {
|
||||
createValidationError(
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES.SOURCE_XML_FILE_NOT_FOUND,
|
||||
'Expected shard-doc XML source file was not found',
|
||||
'<file>',
|
||||
sourceXmlSourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
const compatibilityRows = await parseCompatibilityRows(compatibilityCatalogPath, compatibilityCatalogSourcePath);
|
||||
validateCompatibilityPrecedence({
|
||||
rows: compatibilityRows,
|
||||
displayName: sidecarData.displayName.trim(),
|
||||
workflowFilePath: compatibilityWorkflowFilePath,
|
||||
compatibilityCatalogSourcePath,
|
||||
});
|
||||
|
||||
const canonicalId = SHARD_DOC_LOCKED_CANONICAL_ID;
|
||||
const authoritativeRecords = buildShardDocAuthorityRecords({
|
||||
canonicalId,
|
||||
sidecarSourcePath,
|
||||
sourceXmlSourcePath,
|
||||
});
|
||||
|
||||
return {
|
||||
canonicalId,
|
||||
authoritativePresenceKey: SHARD_DOC_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||
authoritativeRecords,
|
||||
checkedSurfaces: [sourceXmlSourcePath, compatibilityCatalogSourcePath],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SHARD_DOC_AUTHORITY_VALIDATION_ERROR_CODES,
|
||||
SHARD_DOC_LOCKED_CANONICAL_ID,
|
||||
ShardDocAuthorityValidationError,
|
||||
buildShardDocAuthorityRecords,
|
||||
validateShardDocAuthoritySplitAndPrecedence,
|
||||
};
|
||||
|
|
@ -14,6 +14,8 @@ const HELP_SIDECAR_REQUIRED_FIELDS = Object.freeze([
|
|||
'dependencies',
|
||||
]);
|
||||
|
||||
const SHARD_DOC_SIDECAR_REQUIRED_FIELDS = Object.freeze([...HELP_SIDECAR_REQUIRED_FIELDS]);
|
||||
|
||||
const HELP_SIDECAR_ERROR_CODES = Object.freeze({
|
||||
FILE_NOT_FOUND: 'ERR_HELP_SIDECAR_FILE_NOT_FOUND',
|
||||
PARSE_FAILED: 'ERR_HELP_SIDECAR_PARSE_FAILED',
|
||||
|
|
@ -29,8 +31,24 @@ const HELP_SIDECAR_ERROR_CODES = Object.freeze({
|
|||
SOURCEPATH_BASENAME_MISMATCH: 'ERR_SIDECAR_SOURCEPATH_BASENAME_MISMATCH',
|
||||
});
|
||||
|
||||
const SHARD_DOC_SIDECAR_ERROR_CODES = Object.freeze({
|
||||
FILE_NOT_FOUND: 'ERR_SHARD_DOC_SIDECAR_FILE_NOT_FOUND',
|
||||
PARSE_FAILED: 'ERR_SHARD_DOC_SIDECAR_PARSE_FAILED',
|
||||
INVALID_ROOT_OBJECT: 'ERR_SHARD_DOC_SIDECAR_INVALID_ROOT_OBJECT',
|
||||
REQUIRED_FIELD_MISSING: 'ERR_SHARD_DOC_SIDECAR_REQUIRED_FIELD_MISSING',
|
||||
REQUIRED_FIELD_EMPTY: 'ERR_SHARD_DOC_SIDECAR_REQUIRED_FIELD_EMPTY',
|
||||
ARTIFACT_TYPE_INVALID: 'ERR_SHARD_DOC_SIDECAR_ARTIFACT_TYPE_INVALID',
|
||||
MODULE_INVALID: 'ERR_SHARD_DOC_SIDECAR_MODULE_INVALID',
|
||||
DEPENDENCIES_MISSING: 'ERR_SHARD_DOC_SIDECAR_DEPENDENCIES_MISSING',
|
||||
DEPENDENCIES_REQUIRES_INVALID: 'ERR_SHARD_DOC_SIDECAR_DEPENDENCIES_REQUIRES_INVALID',
|
||||
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',
|
||||
});
|
||||
|
||||
const HELP_EXEMPLAR_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.md';
|
||||
const HELP_EXEMPLAR_SUPPORTED_SCHEMA_MAJOR = 1;
|
||||
const SHARD_DOC_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
|
||||
const SIDECAR_SUPPORTED_SCHEMA_MAJOR = 1;
|
||||
|
||||
class SidecarContractError extends Error {
|
||||
constructor({ code, detail, fieldPath, sourcePath }) {
|
||||
|
|
@ -108,43 +126,42 @@ function createValidationError(code, fieldPath, sourcePath, detail) {
|
|||
});
|
||||
}
|
||||
|
||||
function validateHelpSidecarContractData(sidecarData, options = {}) {
|
||||
const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/help.artifact.yaml');
|
||||
function validateSidecarContractData(sidecarData, options) {
|
||||
const {
|
||||
sourcePath,
|
||||
requiredFields,
|
||||
requiredNonEmptyStringFields,
|
||||
errorCodes,
|
||||
expectedArtifactType,
|
||||
expectedModule,
|
||||
expectedCanonicalSourcePath,
|
||||
missingDependenciesDetail,
|
||||
dependenciesObjectDetail,
|
||||
dependenciesRequiresArrayDetail,
|
||||
dependenciesRequiresNotEmptyDetail,
|
||||
artifactTypeDetail,
|
||||
moduleDetail,
|
||||
requiresMustBeEmpty,
|
||||
} = options;
|
||||
|
||||
if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.INVALID_ROOT_OBJECT,
|
||||
'<document>',
|
||||
sourcePath,
|
||||
'Sidecar root must be a YAML mapping object.',
|
||||
);
|
||||
createValidationError(errorCodes.INVALID_ROOT_OBJECT, '<document>', sourcePath, 'Sidecar root must be a YAML mapping object.');
|
||||
}
|
||||
|
||||
for (const field of HELP_SIDECAR_REQUIRED_FIELDS) {
|
||||
for (const field of requiredFields) {
|
||||
if (!hasOwn(sidecarData, field)) {
|
||||
if (field === 'dependencies') {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING,
|
||||
field,
|
||||
sourcePath,
|
||||
'Exemplar sidecar requires an explicit dependencies block.',
|
||||
);
|
||||
createValidationError(errorCodes.DEPENDENCIES_MISSING, field, sourcePath, missingDependenciesDetail);
|
||||
}
|
||||
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING,
|
||||
field,
|
||||
sourcePath,
|
||||
`Missing required sidecar field "${field}".`,
|
||||
);
|
||||
createValidationError(errorCodes.REQUIRED_FIELD_MISSING, field, sourcePath, `Missing required sidecar field "${field}".`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredNonEmptyStringFields = ['canonicalId', 'sourcePath', 'displayName', 'description'];
|
||||
for (const field of requiredNonEmptyStringFields) {
|
||||
if (isBlankString(sidecarData[field])) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY,
|
||||
errorCodes.REQUIRED_FIELD_EMPTY,
|
||||
field,
|
||||
sourcePath,
|
||||
`Required sidecar field "${field}" must be a non-empty string.`,
|
||||
|
|
@ -153,58 +170,33 @@ function validateHelpSidecarContractData(sidecarData, options = {}) {
|
|||
}
|
||||
|
||||
const schemaMajorVersion = parseSchemaMajorVersion(sidecarData.schemaVersion);
|
||||
if (schemaMajorVersion !== HELP_EXEMPLAR_SUPPORTED_SCHEMA_MAJOR) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
|
||||
'schemaVersion',
|
||||
sourcePath,
|
||||
'sidecar schema major version is unsupported',
|
||||
);
|
||||
if (schemaMajorVersion !== SIDECAR_SUPPORTED_SCHEMA_MAJOR) {
|
||||
createValidationError(errorCodes.MAJOR_VERSION_UNSUPPORTED, 'schemaVersion', sourcePath, 'sidecar schema major version is unsupported');
|
||||
}
|
||||
|
||||
if (sidecarData.artifactType !== 'task') {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID,
|
||||
'artifactType',
|
||||
sourcePath,
|
||||
'Wave-1 exemplar requires artifactType to equal "task".',
|
||||
);
|
||||
if (sidecarData.artifactType !== expectedArtifactType) {
|
||||
createValidationError(errorCodes.ARTIFACT_TYPE_INVALID, 'artifactType', sourcePath, artifactTypeDetail);
|
||||
}
|
||||
|
||||
if (sidecarData.module !== 'core') {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.MODULE_INVALID,
|
||||
'module',
|
||||
sourcePath,
|
||||
'Wave-1 exemplar requires module to equal "core".',
|
||||
);
|
||||
if (sidecarData.module !== expectedModule) {
|
||||
createValidationError(errorCodes.MODULE_INVALID, 'module', sourcePath, moduleDetail);
|
||||
}
|
||||
|
||||
const dependencies = sidecarData.dependencies;
|
||||
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING,
|
||||
'dependencies',
|
||||
sourcePath,
|
||||
'Exemplar sidecar requires an explicit dependencies object.',
|
||||
);
|
||||
createValidationError(errorCodes.DEPENDENCIES_MISSING, 'dependencies', sourcePath, dependenciesObjectDetail);
|
||||
}
|
||||
|
||||
if (!hasOwn(dependencies, 'requires') || !Array.isArray(dependencies.requires)) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID,
|
||||
'dependencies.requires',
|
||||
sourcePath,
|
||||
'Exemplar dependencies.requires must be an array.',
|
||||
);
|
||||
createValidationError(errorCodes.DEPENDENCIES_REQUIRES_INVALID, 'dependencies.requires', sourcePath, dependenciesRequiresArrayDetail);
|
||||
}
|
||||
|
||||
if (dependencies.requires.length > 0) {
|
||||
if (requiresMustBeEmpty && dependencies.requires.length > 0) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY,
|
||||
errorCodes.DEPENDENCIES_REQUIRES_NOT_EMPTY,
|
||||
'dependencies.requires',
|
||||
sourcePath,
|
||||
'Wave-1 exemplar requires explicit zero dependencies: dependencies.requires must be [].',
|
||||
dependenciesRequiresNotEmptyDetail,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -212,12 +204,12 @@ function validateHelpSidecarContractData(sidecarData, options = {}) {
|
|||
const sidecarBasename = path.posix.basename(sourcePath);
|
||||
const expectedSidecarBasename = getExpectedSidecarBasenameFromSourcePath(normalizedDeclaredSourcePath);
|
||||
|
||||
const sourcePathMismatch = normalizedDeclaredSourcePath !== HELP_EXEMPLAR_CANONICAL_SOURCE_PATH;
|
||||
const sourcePathMismatch = normalizedDeclaredSourcePath !== expectedCanonicalSourcePath;
|
||||
const basenameMismatch = !expectedSidecarBasename || sidecarBasename !== expectedSidecarBasename;
|
||||
|
||||
if (sourcePathMismatch || basenameMismatch) {
|
||||
createValidationError(
|
||||
HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH,
|
||||
errorCodes.SOURCEPATH_BASENAME_MISMATCH,
|
||||
'sourcePath',
|
||||
sourcePath,
|
||||
'sidecar basename does not match sourcePath basename',
|
||||
|
|
@ -225,6 +217,46 @@ function validateHelpSidecarContractData(sidecarData, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateHelpSidecarContractData(sidecarData, options = {}) {
|
||||
const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/help.artifact.yaml');
|
||||
validateSidecarContractData(sidecarData, {
|
||||
sourcePath,
|
||||
requiredFields: HELP_SIDECAR_REQUIRED_FIELDS,
|
||||
requiredNonEmptyStringFields: ['canonicalId', 'sourcePath', 'displayName', 'description'],
|
||||
errorCodes: HELP_SIDECAR_ERROR_CODES,
|
||||
expectedArtifactType: 'task',
|
||||
expectedModule: 'core',
|
||||
expectedCanonicalSourcePath: HELP_EXEMPLAR_CANONICAL_SOURCE_PATH,
|
||||
missingDependenciesDetail: 'Exemplar sidecar requires an explicit dependencies block.',
|
||||
dependenciesObjectDetail: 'Exemplar sidecar requires an explicit dependencies object.',
|
||||
dependenciesRequiresArrayDetail: 'Exemplar dependencies.requires must be an array.',
|
||||
dependenciesRequiresNotEmptyDetail: 'Wave-1 exemplar requires explicit zero dependencies: dependencies.requires must be [].',
|
||||
artifactTypeDetail: 'Wave-1 exemplar requires artifactType to equal "task".',
|
||||
moduleDetail: 'Wave-1 exemplar requires module to equal "core".',
|
||||
requiresMustBeEmpty: true,
|
||||
});
|
||||
}
|
||||
|
||||
function validateShardDocSidecarContractData(sidecarData, options = {}) {
|
||||
const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/shard-doc.artifact.yaml');
|
||||
validateSidecarContractData(sidecarData, {
|
||||
sourcePath,
|
||||
requiredFields: SHARD_DOC_SIDECAR_REQUIRED_FIELDS,
|
||||
requiredNonEmptyStringFields: ['canonicalId', 'sourcePath', 'displayName', 'description'],
|
||||
errorCodes: SHARD_DOC_SIDECAR_ERROR_CODES,
|
||||
expectedArtifactType: 'task',
|
||||
expectedModule: 'core',
|
||||
expectedCanonicalSourcePath: SHARD_DOC_CANONICAL_SOURCE_PATH,
|
||||
missingDependenciesDetail: 'Shard-doc sidecar requires an explicit dependencies block.',
|
||||
dependenciesObjectDetail: 'Shard-doc sidecar requires an explicit dependencies object.',
|
||||
dependenciesRequiresArrayDetail: 'Shard-doc dependencies.requires must be an array.',
|
||||
dependenciesRequiresNotEmptyDetail: 'Wave-2 shard-doc contract requires explicit zero dependencies: dependencies.requires must be [].',
|
||||
artifactTypeDetail: 'Wave-2 shard-doc contract requires artifactType to equal "task".',
|
||||
moduleDetail: 'Wave-2 shard-doc contract requires module to equal "core".',
|
||||
requiresMustBeEmpty: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core', 'tasks', 'help.artifact.yaml'), options = {}) {
|
||||
const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath));
|
||||
|
||||
|
|
@ -253,10 +285,42 @@ async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core
|
|||
validateHelpSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
||||
}
|
||||
|
||||
async function validateShardDocSidecarContractFile(sidecarPath = getSourcePath('core', 'tasks', 'shard-doc.artifact.yaml'), options = {}) {
|
||||
const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath));
|
||||
|
||||
if (!(await fs.pathExists(sidecarPath))) {
|
||||
createValidationError(
|
||||
SHARD_DOC_SIDECAR_ERROR_CODES.FILE_NOT_FOUND,
|
||||
'<file>',
|
||||
normalizedSourcePath,
|
||||
'Expected shard-doc sidecar file was not found.',
|
||||
);
|
||||
}
|
||||
|
||||
let parsedSidecar;
|
||||
try {
|
||||
const sidecarRaw = await fs.readFile(sidecarPath, 'utf8');
|
||||
parsedSidecar = yaml.parse(sidecarRaw);
|
||||
} catch (error) {
|
||||
createValidationError(
|
||||
SHARD_DOC_SIDECAR_ERROR_CODES.PARSE_FAILED,
|
||||
'<document>',
|
||||
normalizedSourcePath,
|
||||
`YAML parse failure: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
validateShardDocSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HELP_SIDECAR_REQUIRED_FIELDS,
|
||||
SHARD_DOC_SIDECAR_REQUIRED_FIELDS,
|
||||
HELP_SIDECAR_ERROR_CODES,
|
||||
SHARD_DOC_SIDECAR_ERROR_CODES,
|
||||
SidecarContractError,
|
||||
validateHelpSidecarContractData,
|
||||
validateHelpSidecarContractFile,
|
||||
validateShardDocSidecarContractData,
|
||||
validateShardDocSidecarContractFile,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -854,13 +854,16 @@ class Wave1ValidationHarness {
|
|||
|
||||
const generator = new ManifestGenerator();
|
||||
generator.bmadFolderName = runtimeFolder;
|
||||
generator.helpAuthorityRecords = [
|
||||
generator.taskAuthorityRecords = [
|
||||
{
|
||||
recordType: 'metadata-authority',
|
||||
canonicalId: 'bmad-help',
|
||||
authoritySourceType: 'sidecar',
|
||||
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
||||
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
||||
},
|
||||
];
|
||||
generator.helpAuthorityRecords = [...generator.taskAuthorityRecords];
|
||||
generator.tasks = perturbed
|
||||
? []
|
||||
: [
|
||||
|
|
@ -931,6 +934,21 @@ class Wave1ValidationHarness {
|
|||
'output-location': '',
|
||||
outputs: '',
|
||||
},
|
||||
{
|
||||
module: 'core',
|
||||
phase: 'anytime',
|
||||
name: 'Shard Document',
|
||||
code: 'SD',
|
||||
sequence: '',
|
||||
'workflow-file': `${runtimeFolder}/core/tasks/shard-doc.xml`,
|
||||
command: 'bmad-shard-doc',
|
||||
required: 'false',
|
||||
agent: '',
|
||||
options: '',
|
||||
description: 'Split large markdown documents into smaller files by section with an index.',
|
||||
'output-location': '',
|
||||
outputs: '',
|
||||
},
|
||||
];
|
||||
await fs.writeFile(path.join(coreDir, 'module-help.csv'), serializeCsv(MODULE_HELP_COMPAT_COLUMNS, moduleHelpFixtureRows), 'utf8');
|
||||
await fs.writeFile(
|
||||
|
|
|
|||
|
|
@ -16,19 +16,62 @@ const CODEX_EXPORT_DERIVATION_ERROR_CODES = Object.freeze({
|
|||
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',
|
||||
DUPLICATE_EXPORT_SURFACE: 'ERR_CODEX_EXPORT_DUPLICATE_EXPORT_SURFACE',
|
||||
});
|
||||
|
||||
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_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_HELP_EXPORT_DERIVATION_SOURCE_TYPE = 'sidecar-canonical-id';
|
||||
const EXEMPLAR_SIDECAR_SOURCE_CANDIDATES = Object.freeze([
|
||||
const SHARD_DOC_EXPORT_ALIAS_ROWS = Object.freeze([
|
||||
Object.freeze({
|
||||
segments: ['bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'],
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:canonical-id',
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
normalizedAliasValue: 'bmad-shard-doc',
|
||||
rawIdentityHasLeadingSlash: false,
|
||||
}),
|
||||
Object.freeze({
|
||||
segments: ['src', 'core', 'tasks', 'help.artifact.yaml'],
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:legacy-name',
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
normalizedAliasValue: 'shard-doc',
|
||||
rawIdentityHasLeadingSlash: false,
|
||||
}),
|
||||
Object.freeze({
|
||||
rowIdentity: 'alias-row:bmad-shard-doc:slash-command',
|
||||
canonicalId: 'bmad-shard-doc',
|
||||
normalizedAliasValue: 'bmad-shard-doc',
|
||||
rawIdentityHasLeadingSlash: true,
|
||||
}),
|
||||
]);
|
||||
const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({
|
||||
help: Object.freeze({
|
||||
taskSourcePath: EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH,
|
||||
sourcePathSuffix: '/core/tasks/help.md',
|
||||
sidecarSourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sidecarSourceCandidates: Object.freeze([
|
||||
Object.freeze({
|
||||
segments: ['bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'],
|
||||
}),
|
||||
Object.freeze({
|
||||
segments: ['src', 'core', 'tasks', 'help.artifact.yaml'],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
'shard-doc': Object.freeze({
|
||||
taskSourcePath: EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH,
|
||||
sourcePathSuffix: '/core/tasks/shard-doc.xml',
|
||||
sidecarSourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sidecarSourceCandidates: Object.freeze([
|
||||
Object.freeze({
|
||||
segments: ['bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'],
|
||||
}),
|
||||
Object.freeze({
|
||||
segments: ['src', 'core', 'tasks', 'shard-doc.artifact.yaml'],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
class CodexExportDerivationError extends Error {
|
||||
constructor({ code, detail, fieldPath, sourcePath, observedValue, cause = null }) {
|
||||
|
|
@ -53,6 +96,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||
constructor() {
|
||||
super('codex', 'Codex', false);
|
||||
this.exportDerivationRecords = [];
|
||||
this.exportSurfaceIdentityOwners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,6 +113,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||
|
||||
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
||||
this.exportDerivationRecords = [];
|
||||
this.exportSurfaceIdentityOwners = new Map();
|
||||
|
||||
// Clean up old .codex/prompts locations (both global and project)
|
||||
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
|
||||
|
|
@ -246,14 +291,19 @@ class CodexSetup extends BaseIdeSetup {
|
|||
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
|
||||
* @returns {number} Number of skills written
|
||||
*/
|
||||
isExemplarHelpTaskArtifact(artifact = {}) {
|
||||
getConvertedTaskExportTarget(artifact = {}) {
|
||||
if (artifact.type !== 'task' || artifact.module !== 'core') {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedName = String(artifact.name || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const exportTarget = EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS[normalizedName];
|
||||
if (!exportTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedRelativePath = String(artifact.relativePath || '')
|
||||
.trim()
|
||||
.replaceAll('\\', '/')
|
||||
|
|
@ -263,11 +313,17 @@ class CodexSetup extends BaseIdeSetup {
|
|||
.replaceAll('\\', '/')
|
||||
.toLowerCase();
|
||||
|
||||
if (normalizedName !== 'help') {
|
||||
return false;
|
||||
const normalizedRelativePathWithRoot = normalizedRelativePath.startsWith('/') ? normalizedRelativePath : `/${normalizedRelativePath}`;
|
||||
if (!normalizedRelativePathWithRoot.endsWith(`/core/tasks/${normalizedName}.md`)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizedRelativePath.endsWith('/core/tasks/help.md') || normalizedSourcePath.endsWith('/core/tasks/help.md');
|
||||
const normalizedSourcePathWithRoot = normalizedSourcePath.startsWith('/') ? normalizedSourcePath : `/${normalizedSourcePath}`;
|
||||
if (normalizedSourcePath && !normalizedSourcePathWithRoot.endsWith(exportTarget.sourcePathSuffix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportTarget;
|
||||
}
|
||||
|
||||
throwExportDerivationError({ code, detail, fieldPath, sourcePath, observedValue, cause = null }) {
|
||||
|
|
@ -281,8 +337,8 @@ class CodexSetup extends BaseIdeSetup {
|
|||
});
|
||||
}
|
||||
|
||||
async loadExemplarHelpSidecar(projectDir) {
|
||||
for (const candidate of EXEMPLAR_SIDECAR_SOURCE_CANDIDATES) {
|
||||
async loadConvertedTaskSidecar(projectDir, exportTarget) {
|
||||
for (const candidate of exportTarget.sidecarSourceCandidates) {
|
||||
const sidecarPath = path.join(projectDir, ...candidate.segments);
|
||||
if (await fs.pathExists(sidecarPath)) {
|
||||
let sidecarData;
|
||||
|
|
@ -293,7 +349,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||
detail: `YAML parse failure: ${error.message}`,
|
||||
fieldPath: '<document>',
|
||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sourcePath: exportTarget.sidecarSourcePath,
|
||||
observedValue: '<parse-error>',
|
||||
cause: error,
|
||||
});
|
||||
|
|
@ -304,7 +360,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||
detail: 'sidecar root must be a YAML mapping object',
|
||||
fieldPath: '<document>',
|
||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sourcePath: exportTarget.sidecarSourcePath,
|
||||
observedValue: typeof sidecarData,
|
||||
});
|
||||
}
|
||||
|
|
@ -315,14 +371,14 @@ class CodexSetup extends BaseIdeSetup {
|
|||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_MISSING,
|
||||
detail: 'sidecar canonicalId is required for exemplar export derivation',
|
||||
fieldPath: 'canonicalId',
|
||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sourcePath: exportTarget.sidecarSourcePath,
|
||||
observedValue: canonicalId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
canonicalId,
|
||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sourcePath: exportTarget.sidecarSourcePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -331,15 +387,15 @@ class CodexSetup extends BaseIdeSetup {
|
|||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND,
|
||||
detail: 'expected exemplar sidecar metadata file was not found',
|
||||
fieldPath: '<file>',
|
||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||
sourcePath: exportTarget.sidecarSourcePath,
|
||||
observedValue: projectDir,
|
||||
});
|
||||
}
|
||||
|
||||
async resolveSkillIdentityFromArtifact(artifact, projectDir) {
|
||||
const inferredSkillName = toDashPath(artifact.relativePath).replace(/\.md$/, '');
|
||||
const isExemplarHelpTask = this.isExemplarHelpTaskArtifact(artifact);
|
||||
if (!isExemplarHelpTask) {
|
||||
const exportTarget = this.getConvertedTaskExportTarget(artifact);
|
||||
if (!exportTarget) {
|
||||
return {
|
||||
skillName: inferredSkillName,
|
||||
canonicalId: inferredSkillName,
|
||||
|
|
@ -348,14 +404,19 @@ class CodexSetup extends BaseIdeSetup {
|
|||
};
|
||||
}
|
||||
|
||||
const sidecarData = await this.loadExemplarHelpSidecar(projectDir);
|
||||
const sidecarData = await this.loadConvertedTaskSidecar(projectDir, exportTarget);
|
||||
|
||||
let canonicalResolution;
|
||||
try {
|
||||
canonicalResolution = await normalizeAndResolveExemplarAlias(sidecarData.canonicalId, {
|
||||
const aliasResolutionOptions = {
|
||||
fieldPath: 'canonicalId',
|
||||
sourcePath: sidecarData.sourcePath,
|
||||
});
|
||||
};
|
||||
if (exportTarget.taskSourcePath === EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH) {
|
||||
aliasResolutionOptions.aliasRows = SHARD_DOC_EXPORT_ALIAS_ROWS;
|
||||
aliasResolutionOptions.aliasTableSourcePath = '_bmad/_config/canonical-aliases.csv';
|
||||
}
|
||||
canonicalResolution = await normalizeAndResolveExemplarAlias(sidecarData.canonicalId, aliasResolutionOptions);
|
||||
} catch (error) {
|
||||
this.throwExportDerivationError({
|
||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED,
|
||||
|
|
@ -383,6 +444,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||
canonicalId: skillName,
|
||||
exportIdDerivationSourceType: EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE,
|
||||
exportIdDerivationSourcePath: sidecarData.sourcePath,
|
||||
exportIdDerivationTaskSourcePath: exportTarget.taskSourcePath,
|
||||
exportIdDerivationEvidence: `applied:${canonicalResolution.preAliasNormalizedValue}|leadingSlash:${canonicalResolution.rawIdentityHasLeadingSlash}->${canonicalResolution.postAliasCanonicalId}|rows:${canonicalResolution.aliasRowLocator}`,
|
||||
};
|
||||
}
|
||||
|
|
@ -402,6 +464,33 @@ class CodexSetup extends BaseIdeSetup {
|
|||
|
||||
// Create skill directory
|
||||
const skillDir = path.join(destDir, skillName);
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
const normalizedSkillPath = skillPath.replaceAll('\\', '/');
|
||||
const ownerRecord = {
|
||||
artifactType,
|
||||
sourcePath: String(artifact.sourcePath || artifact.relativePath || '<unknown>'),
|
||||
};
|
||||
const existingOwner = this.exportSurfaceIdentityOwners.get(normalizedSkillPath);
|
||||
if (existingOwner) {
|
||||
this.throwExportDerivationError({
|
||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.DUPLICATE_EXPORT_SURFACE,
|
||||
detail: `duplicate export surface path already claimed by ${existingOwner.artifactType}:${existingOwner.sourcePath}`,
|
||||
fieldPath: 'canonicalId',
|
||||
sourcePath: ownerRecord.sourcePath,
|
||||
observedValue: normalizedSkillPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (await fs.pathExists(skillPath)) {
|
||||
this.throwExportDerivationError({
|
||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.DUPLICATE_EXPORT_SURFACE,
|
||||
detail: 'duplicate export surface path already exists on disk',
|
||||
fieldPath: 'canonicalId',
|
||||
sourcePath: ownerRecord.sourcePath,
|
||||
observedValue: normalizedSkillPath,
|
||||
});
|
||||
}
|
||||
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
// Transform content: rewrite frontmatter for skills format
|
||||
|
|
@ -409,14 +498,14 @@ class CodexSetup extends BaseIdeSetup {
|
|||
|
||||
// Write SKILL.md with platform-native line endings
|
||||
const platformContent = skillContent.replaceAll('\n', os.EOL);
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(skillPath, platformContent, 'utf8');
|
||||
this.exportSurfaceIdentityOwners.set(normalizedSkillPath, ownerRecord);
|
||||
writtenCount++;
|
||||
|
||||
if (exportIdentity.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE) {
|
||||
this.exportDerivationRecords.push({
|
||||
exportPath: path.join('.agents', 'skills', skillName, 'SKILL.md').replaceAll('\\', '/'),
|
||||
sourcePath: EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH,
|
||||
sourcePath: exportIdentity.exportIdDerivationTaskSourcePath || EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH,
|
||||
canonicalId: exportIdentity.canonicalId,
|
||||
visibleId: skillName,
|
||||
visibleSurfaceClass: 'export-id',
|
||||
|
|
|
|||
Loading…
Reference in New Issue