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 = {}) {
|
function evaluateExemplarCommandLabelReportRows(rows, options = {}) {
|
||||||
const expectedCanonicalId = frontmatterMatchValue(options.canonicalId || EXEMPLAR_HELP_CATALOG_CANONICAL_ID);
|
const expectedCanonicalId = frontmatterMatchValue(options.canonicalId || EXEMPLAR_HELP_CATALOG_CANONICAL_ID);
|
||||||
const expectedDisplayedLabel = frontmatterMatchValue(options.displayedCommandLabel || `/${expectedCanonicalId}`);
|
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 normalizedExpectedDisplayedLabel = normalizeDisplayedCommandLabel(expectedDisplayedLabel);
|
||||||
|
|
||||||
const targetRows = (Array.isArray(rows) ? rows : []).filter(
|
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>')}` };
|
return { valid: false, reason: `invalid-row-count-for-canonical-id:${String(row.rowCountForCanonicalId ?? '<empty>')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frontmatterMatchValue(row.authoritySourceType) !== 'sidecar') {
|
if (frontmatterMatchValue(row.authoritySourceType) !== expectedAuthoritySourceType) {
|
||||||
return { valid: false, reason: `invalid-authority-source-type:${frontmatterMatchValue(row.authoritySourceType) || '<empty>'}` };
|
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 {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
reason: `invalid-authority-source-path:${frontmatterMatchValue(row.authoritySourcePath) || '<empty>'}`,
|
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 { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
const { DependencyResolver } = require('./dependency-resolver');
|
const { DependencyResolver } = require('./dependency-resolver');
|
||||||
const { ConfigCollector } = require('./config-collector');
|
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 { validateHelpAuthoritySplitAndPrecedence } = require('./help-authority-validator');
|
||||||
|
const { validateShardDocAuthoritySplitAndPrecedence } = require('./shard-doc-authority-validator');
|
||||||
const {
|
const {
|
||||||
HELP_CATALOG_GENERATION_ERROR_CODES,
|
HELP_CATALOG_GENERATION_ERROR_CODES,
|
||||||
buildSidecarAwareExemplarHelpRow,
|
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_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_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 {
|
class Installer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -44,7 +49,9 @@ class Installer {
|
||||||
this.configCollector = new ConfigCollector();
|
this.configCollector = new ConfigCollector();
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.validateHelpSidecarContractFile = validateHelpSidecarContractFile;
|
this.validateHelpSidecarContractFile = validateHelpSidecarContractFile;
|
||||||
|
this.validateShardDocSidecarContractFile = validateShardDocSidecarContractFile;
|
||||||
this.validateHelpAuthoritySplitAndPrecedence = validateHelpAuthoritySplitAndPrecedence;
|
this.validateHelpAuthoritySplitAndPrecedence = validateHelpAuthoritySplitAndPrecedence;
|
||||||
|
this.validateShardDocAuthoritySplitAndPrecedence = validateShardDocAuthoritySplitAndPrecedence;
|
||||||
this.ManifestGenerator = ManifestGenerator;
|
this.ManifestGenerator = ManifestGenerator;
|
||||||
this.installedFiles = new Set(); // Track all installed files
|
this.installedFiles = new Set(); // Track all installed files
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
|
|
@ -56,12 +63,27 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async runConfigurationGenerationTask({ message, bmadDir, moduleConfigs, config, allModules, addResult }) {
|
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.
|
// 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...');
|
message('Validating exemplar sidecar contract...');
|
||||||
await this.validateHelpSidecarContractFile();
|
await this.validateHelpSidecarContractFile();
|
||||||
|
|
||||||
|
addResult('Shard-doc sidecar contract', 'ok', 'validated');
|
||||||
addResult('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...');
|
message('Validating authority split and frontmatter precedence...');
|
||||||
const helpAuthorityValidation = await this.validateHelpAuthoritySplitAndPrecedence({
|
const helpAuthorityValidation = await this.validateHelpAuthoritySplitAndPrecedence({
|
||||||
bmadDir,
|
bmadDir,
|
||||||
|
|
@ -109,6 +131,7 @@ class Installer {
|
||||||
ides: config.ides || [],
|
ides: config.ides || [],
|
||||||
preservedModules: modulesForCsvPreserve,
|
preservedModules: modulesForCsvPreserve,
|
||||||
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
||||||
|
taskAuthorityRecords: [...(this.helpAuthorityRecords || []), ...(this.shardDocAuthorityRecords || [])],
|
||||||
});
|
});
|
||||||
|
|
||||||
addResult(
|
addResult(
|
||||||
|
|
@ -1780,6 +1803,38 @@ class Installer {
|
||||||
/**
|
/**
|
||||||
* Private: Create directory structure
|
* 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 }) {
|
isExemplarHelpCatalogRow({ moduleName, name, workflowFile, command, canonicalId }) {
|
||||||
if (moduleName !== 'core') return false;
|
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 || '')
|
const normalizedWorkflowFile = String(workflowFile || '')
|
||||||
.trim()
|
.trim()
|
||||||
.replaceAll('\\', '/')
|
.replaceAll('\\', '/')
|
||||||
|
|
@ -1849,13 +1904,27 @@ class Installer {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/^\/+/, '');
|
.replace(/^\/+/, '');
|
||||||
|
|
||||||
const isHelpWorkflow = normalizedWorkflowFile.endsWith('/core/tasks/help.md');
|
const normalizedWorkflowFileContractPath = String(workflowFileContractPath || '')
|
||||||
const isExemplarIdentity =
|
.trim()
|
||||||
normalizedName === 'bmad-help' ||
|
.replaceAll('\\', '/')
|
||||||
normalizedCommandValue === normalizedCanonicalId ||
|
.toLowerCase();
|
||||||
(normalizedLegacyName.length > 0 && normalizedCommandValue === normalizedLegacyName);
|
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) {
|
async writeCsvArtifact(filePath, columns, rows) {
|
||||||
|
|
@ -1886,6 +1955,31 @@ class Installer {
|
||||||
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
||||||
bmadFolderName: this.bmadFolderName || BMAD_FOLDER_NAME,
|
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;
|
let exemplarRowWritten = false;
|
||||||
|
|
||||||
// Load agent manifest for agent info lookup
|
// Load agent manifest for agent info lookup
|
||||||
|
|
@ -2110,31 +2204,37 @@ class Installer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
for (const contract of commandLabelContracts) {
|
||||||
!this.isExemplarCommandLabelCandidate({
|
const isContractCandidate = this.isCommandLabelCandidate({
|
||||||
workflowFile,
|
workflowFile,
|
||||||
name,
|
name,
|
||||||
rawCommandValue,
|
rawCommandValue,
|
||||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
canonicalId: contract.canonicalId,
|
||||||
legacyName: sidecarAwareExemplar.legacyName,
|
legacyName: contract.legacyName,
|
||||||
})
|
workflowFileContractPath: contract.workflowFilePath,
|
||||||
) {
|
nameCandidates: contract.nameCandidates,
|
||||||
continue;
|
});
|
||||||
|
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) => ({
|
this.helpCatalogPipelineRows = sidecarAwareExemplar.pipelineStageRows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
|
|
@ -2144,15 +2244,24 @@ class Installer {
|
||||||
}));
|
}));
|
||||||
this.helpCatalogCommandLabelReportRows = commandLabelRowsFromMergedCatalog.map((row) => ({
|
this.helpCatalogCommandLabelReportRows = commandLabelRowsFromMergedCatalog.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
rowCountForCanonicalId: exemplarRowCount,
|
rowCountForCanonicalId: commandLabelRowCountByCanonicalId.get(row.canonicalId) || 0,
|
||||||
status: exemplarRowCount === 1 ? 'PASS' : 'FAIL',
|
status: (commandLabelRowCountByCanonicalId.get(row.canonicalId) || 0) === 1 ? 'PASS' : 'FAIL',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const commandLabelContractResult = evaluateExemplarCommandLabelReportRows(this.helpCatalogCommandLabelReportRows, {
|
const commandLabelContractFailures = new Map();
|
||||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
for (const contract of commandLabelContracts) {
|
||||||
displayedCommandLabel: sidecarAwareExemplar.displayedCommandLabel,
|
const commandLabelContractResult = evaluateExemplarCommandLabelReportRows(this.helpCatalogCommandLabelReportRows, {
|
||||||
});
|
canonicalId: contract.canonicalId,
|
||||||
if (!commandLabelContractResult.valid) {
|
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) => ({
|
this.helpCatalogPipelineRows = this.helpCatalogPipelineRows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
stageStatus: 'FAIL',
|
stageStatus: 'FAIL',
|
||||||
|
|
@ -2161,14 +2270,19 @@ class Installer {
|
||||||
this.helpCatalogCommandLabelReportRows = this.helpCatalogCommandLabelReportRows.map((row) => ({
|
this.helpCatalogCommandLabelReportRows = this.helpCatalogCommandLabelReportRows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
status: 'FAIL',
|
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(
|
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.code = HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED;
|
||||||
commandLabelError.detail = commandLabelContractResult.reason;
|
commandLabelError.detail = commandLabelFailureSummary;
|
||||||
throw commandLabelError;
|
throw commandLabelError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const { validateTaskManifestCompatibilitySurface } = require('./projection-compa
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
const packageJson = require('../../../../../package.json');
|
const packageJson = require('../../../../../package.json');
|
||||||
const DEFAULT_EXEMPLAR_HELP_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml';
|
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([
|
const CANONICAL_ALIAS_TABLE_COLUMNS = Object.freeze([
|
||||||
'canonicalId',
|
'canonicalId',
|
||||||
'alias',
|
'alias',
|
||||||
|
|
@ -55,6 +56,35 @@ const LOCKED_CANONICAL_ALIAS_TABLE_EXEMPLAR_ROWS = Object.freeze([
|
||||||
resolutionEligibility: 'slash-command-only',
|
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
|
* Generates manifest files for installed workflows, agents, and tasks
|
||||||
|
|
@ -68,6 +98,73 @@ class ManifestGenerator {
|
||||||
this.modules = [];
|
this.modules = [];
|
||||||
this.files = [];
|
this.files = [];
|
||||||
this.selectedIdes = [];
|
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);
|
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
|
// Filter out any undefined/null values from IDE list
|
||||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||||
|
|
@ -958,15 +1062,10 @@ class ManifestGenerator {
|
||||||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
const compatibilitySurfacePath = `${this.bmadFolderName || '_bmad'}/_config/task-manifest.csv`;
|
const compatibilitySurfacePath = `${this.bmadFolderName || '_bmad'}/_config/task-manifest.csv`;
|
||||||
const sidecarAuthorityRecord = Array.isArray(this.helpAuthorityRecords)
|
const taskAuthorityRecords = Array.isArray(this.taskAuthorityRecords)
|
||||||
? this.helpAuthorityRecords.find(
|
? this.taskAuthorityRecords
|
||||||
(record) => record?.canonicalId === 'bmad-help' && record?.authoritySourceType === 'sidecar' && record?.authoritySourcePath,
|
: this.normalizeTaskAuthorityRecords(this.helpAuthorityRecords);
|
||||||
)
|
const taskAuthorityProjectionIndex = this.buildTaskAuthorityProjectionIndex(taskAuthorityRecords);
|
||||||
: null;
|
|
||||||
const exemplarAuthoritySourceType = sidecarAuthorityRecord ? sidecarAuthorityRecord.authoritySourceType : 'sidecar';
|
|
||||||
const exemplarAuthoritySourcePath = sidecarAuthorityRecord
|
|
||||||
? sidecarAuthorityRecord.authoritySourcePath
|
|
||||||
: 'bmad-fork/src/core/tasks/help.artifact.yaml';
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
|
|
@ -1015,7 +1114,7 @@ class ManifestGenerator {
|
||||||
for (const task of this.tasks) {
|
for (const task of this.tasks) {
|
||||||
const key = `${task.module}:${task.name}`;
|
const key = `${task.module}:${task.name}`;
|
||||||
const previousRecord = allTasks.get(key);
|
const previousRecord = allTasks.get(key);
|
||||||
const isExemplarHelpTask = task.module === 'core' && task.name === 'help';
|
const authorityProjection = taskAuthorityProjectionIndex.get(key);
|
||||||
|
|
||||||
allTasks.set(key, {
|
allTasks.set(key, {
|
||||||
name: task.name,
|
name: task.name,
|
||||||
|
|
@ -1024,10 +1123,10 @@ class ManifestGenerator {
|
||||||
module: task.module,
|
module: task.module,
|
||||||
path: task.path,
|
path: task.path,
|
||||||
standalone: task.standalone,
|
standalone: task.standalone,
|
||||||
legacyName: isExemplarHelpTask ? 'help' : previousRecord?.legacyName || task.name,
|
legacyName: authorityProjection ? authorityProjection.legacyName : previousRecord?.legacyName || task.name,
|
||||||
canonicalId: isExemplarHelpTask ? 'bmad-help' : previousRecord?.canonicalId || '',
|
canonicalId: authorityProjection ? authorityProjection.canonicalId : previousRecord?.canonicalId || '',
|
||||||
authoritySourceType: isExemplarHelpTask ? exemplarAuthoritySourceType : previousRecord?.authoritySourceType || '',
|
authoritySourceType: authorityProjection ? authorityProjection.authoritySourceType : previousRecord?.authoritySourceType || '',
|
||||||
authoritySourcePath: isExemplarHelpTask ? exemplarAuthoritySourcePath : previousRecord?.authoritySourcePath || '',
|
authoritySourcePath: authorityProjection ? authorityProjection.authoritySourcePath : previousRecord?.authoritySourcePath || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1070,18 +1169,64 @@ class ManifestGenerator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCanonicalAliasProjectionRows(authoritySourceType, authoritySourcePath) {
|
resolveShardDocAliasAuthorityRecord() {
|
||||||
return LOCKED_CANONICAL_ALIAS_TABLE_EXEMPLAR_ROWS.map((row) => ({
|
const sidecarAuthorityRecord = Array.isArray(this.taskAuthorityRecords)
|
||||||
canonicalId: row.canonicalId,
|
? this.taskAuthorityRecords.find(
|
||||||
alias: row.alias,
|
(record) => record?.canonicalId === 'bmad-shard-doc' && record?.authoritySourceType === 'sidecar' && record?.authoritySourcePath,
|
||||||
aliasType: row.aliasType,
|
)
|
||||||
authoritySourceType,
|
: null;
|
||||||
authoritySourcePath,
|
return {
|
||||||
rowIdentity: row.rowIdentity,
|
authoritySourceType: sidecarAuthorityRecord ? sidecarAuthorityRecord.authoritySourceType : 'sidecar',
|
||||||
normalizedAliasValue: row.normalizedAliasValue,
|
authoritySourcePath: sidecarAuthorityRecord
|
||||||
rawIdentityHasLeadingSlash: row.rawIdentityHasLeadingSlash,
|
? sidecarAuthorityRecord.authoritySourcePath
|
||||||
resolutionEligibility: row.resolutionEligibility,
|
: 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) {
|
async writeCanonicalAliasManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'canonical-aliases.csv');
|
const csvPath = path.join(cfgDir, 'canonical-aliases.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
const { authoritySourceType, authoritySourcePath } = this.resolveExemplarAliasAuthorityRecord();
|
const projectedRows = this.buildCanonicalAliasProjectionRows();
|
||||||
const projectedRows = this.buildCanonicalAliasProjectionRows(authoritySourceType, authoritySourcePath);
|
|
||||||
|
|
||||||
let csvContent = `${CANONICAL_ALIAS_TABLE_COLUMNS.join(',')}\n`;
|
let csvContent = `${CANONICAL_ALIAS_TABLE_COLUMNS.join(',')}\n`;
|
||||||
for (const row of projectedRows) {
|
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_HEADER_WAVE1_MISMATCH: 'ERR_HELP_CATALOG_COMPAT_HEADER_WAVE1_MISMATCH',
|
||||||
HELP_CATALOG_REQUIRED_COLUMN_MISSING: 'ERR_HELP_CATALOG_COMPAT_REQUIRED_COLUMN_MISSING',
|
HELP_CATALOG_REQUIRED_COLUMN_MISSING: 'ERR_HELP_CATALOG_COMPAT_REQUIRED_COLUMN_MISSING',
|
||||||
HELP_CATALOG_EXEMPLAR_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_EXEMPLAR_ROW_CONTRACT_FAILED',
|
HELP_CATALOG_EXEMPLAR_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_EXEMPLAR_ROW_CONTRACT_FAILED',
|
||||||
|
HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_SHARD_DOC_ROW_CONTRACT_FAILED',
|
||||||
GITHUB_COPILOT_WORKFLOW_FILE_MISSING: 'ERR_GITHUB_COPILOT_HELP_WORKFLOW_FILE_MISSING',
|
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 {
|
class ProjectionCompatibilityError extends Error {
|
||||||
|
|
@ -177,6 +183,37 @@ function normalizeWorkflowPath(value) {
|
||||||
return normalizeSourcePath(value).toLowerCase();
|
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 = {}) {
|
function validateTaskManifestLoaderEntries(rows, options = {}) {
|
||||||
const surface = options.surface || 'task-manifest-loader';
|
const surface = options.surface || 'task-manifest-loader';
|
||||||
const sourcePath = normalizeSourcePath(options.sourcePath || '_bmad/_config/task-manifest.csv');
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,6 +446,84 @@ function validateHelpCatalogCompatibilitySurface(csvContent, options = {}) {
|
||||||
return { headerColumns, rows };
|
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 = {
|
module.exports = {
|
||||||
PROJECTION_COMPATIBILITY_ERROR_CODES,
|
PROJECTION_COMPATIBILITY_ERROR_CODES,
|
||||||
ProjectionCompatibilityError,
|
ProjectionCompatibilityError,
|
||||||
|
|
@ -404,4 +536,5 @@ module.exports = {
|
||||||
validateHelpCatalogCompatibilitySurface,
|
validateHelpCatalogCompatibilitySurface,
|
||||||
validateHelpCatalogLoaderEntries,
|
validateHelpCatalogLoaderEntries,
|
||||||
validateGithubCopilotHelpLoaderEntries,
|
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',
|
'dependencies',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const SHARD_DOC_SIDECAR_REQUIRED_FIELDS = Object.freeze([...HELP_SIDECAR_REQUIRED_FIELDS]);
|
||||||
|
|
||||||
const HELP_SIDECAR_ERROR_CODES = Object.freeze({
|
const HELP_SIDECAR_ERROR_CODES = Object.freeze({
|
||||||
FILE_NOT_FOUND: 'ERR_HELP_SIDECAR_FILE_NOT_FOUND',
|
FILE_NOT_FOUND: 'ERR_HELP_SIDECAR_FILE_NOT_FOUND',
|
||||||
PARSE_FAILED: 'ERR_HELP_SIDECAR_PARSE_FAILED',
|
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',
|
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_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 {
|
class SidecarContractError extends Error {
|
||||||
constructor({ code, detail, fieldPath, sourcePath }) {
|
constructor({ code, detail, fieldPath, sourcePath }) {
|
||||||
|
|
@ -108,43 +126,42 @@ function createValidationError(code, fieldPath, sourcePath, detail) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHelpSidecarContractData(sidecarData, options = {}) {
|
function validateSidecarContractData(sidecarData, options) {
|
||||||
const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/help.artifact.yaml');
|
const {
|
||||||
|
sourcePath,
|
||||||
|
requiredFields,
|
||||||
|
requiredNonEmptyStringFields,
|
||||||
|
errorCodes,
|
||||||
|
expectedArtifactType,
|
||||||
|
expectedModule,
|
||||||
|
expectedCanonicalSourcePath,
|
||||||
|
missingDependenciesDetail,
|
||||||
|
dependenciesObjectDetail,
|
||||||
|
dependenciesRequiresArrayDetail,
|
||||||
|
dependenciesRequiresNotEmptyDetail,
|
||||||
|
artifactTypeDetail,
|
||||||
|
moduleDetail,
|
||||||
|
requiresMustBeEmpty,
|
||||||
|
} = options;
|
||||||
|
|
||||||
if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) {
|
if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.INVALID_ROOT_OBJECT, '<document>', sourcePath, 'Sidecar root must be a YAML mapping object.');
|
||||||
HELP_SIDECAR_ERROR_CODES.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 (!hasOwn(sidecarData, field)) {
|
||||||
if (field === 'dependencies') {
|
if (field === 'dependencies') {
|
||||||
createValidationError(
|
createValidationError(errorCodes.DEPENDENCIES_MISSING, field, sourcePath, missingDependenciesDetail);
|
||||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING,
|
|
||||||
field,
|
|
||||||
sourcePath,
|
|
||||||
'Exemplar sidecar requires an explicit dependencies block.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createValidationError(
|
createValidationError(errorCodes.REQUIRED_FIELD_MISSING, field, sourcePath, `Missing required sidecar field "${field}".`);
|
||||||
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING,
|
|
||||||
field,
|
|
||||||
sourcePath,
|
|
||||||
`Missing required sidecar field "${field}".`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredNonEmptyStringFields = ['canonicalId', 'sourcePath', 'displayName', 'description'];
|
|
||||||
for (const field of requiredNonEmptyStringFields) {
|
for (const field of requiredNonEmptyStringFields) {
|
||||||
if (isBlankString(sidecarData[field])) {
|
if (isBlankString(sidecarData[field])) {
|
||||||
createValidationError(
|
createValidationError(
|
||||||
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY,
|
errorCodes.REQUIRED_FIELD_EMPTY,
|
||||||
field,
|
field,
|
||||||
sourcePath,
|
sourcePath,
|
||||||
`Required sidecar field "${field}" must be a non-empty string.`,
|
`Required sidecar field "${field}" must be a non-empty string.`,
|
||||||
|
|
@ -153,58 +170,33 @@ function validateHelpSidecarContractData(sidecarData, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaMajorVersion = parseSchemaMajorVersion(sidecarData.schemaVersion);
|
const schemaMajorVersion = parseSchemaMajorVersion(sidecarData.schemaVersion);
|
||||||
if (schemaMajorVersion !== HELP_EXEMPLAR_SUPPORTED_SCHEMA_MAJOR) {
|
if (schemaMajorVersion !== SIDECAR_SUPPORTED_SCHEMA_MAJOR) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.MAJOR_VERSION_UNSUPPORTED, 'schemaVersion', sourcePath, 'sidecar schema major version is unsupported');
|
||||||
HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
|
|
||||||
'schemaVersion',
|
|
||||||
sourcePath,
|
|
||||||
'sidecar schema major version is unsupported',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sidecarData.artifactType !== 'task') {
|
if (sidecarData.artifactType !== expectedArtifactType) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.ARTIFACT_TYPE_INVALID, 'artifactType', sourcePath, artifactTypeDetail);
|
||||||
HELP_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID,
|
|
||||||
'artifactType',
|
|
||||||
sourcePath,
|
|
||||||
'Wave-1 exemplar requires artifactType to equal "task".',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sidecarData.module !== 'core') {
|
if (sidecarData.module !== expectedModule) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.MODULE_INVALID, 'module', sourcePath, moduleDetail);
|
||||||
HELP_SIDECAR_ERROR_CODES.MODULE_INVALID,
|
|
||||||
'module',
|
|
||||||
sourcePath,
|
|
||||||
'Wave-1 exemplar requires module to equal "core".',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependencies = sidecarData.dependencies;
|
const dependencies = sidecarData.dependencies;
|
||||||
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.DEPENDENCIES_MISSING, 'dependencies', sourcePath, dependenciesObjectDetail);
|
||||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING,
|
|
||||||
'dependencies',
|
|
||||||
sourcePath,
|
|
||||||
'Exemplar sidecar requires an explicit dependencies object.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOwn(dependencies, 'requires') || !Array.isArray(dependencies.requires)) {
|
if (!hasOwn(dependencies, 'requires') || !Array.isArray(dependencies.requires)) {
|
||||||
createValidationError(
|
createValidationError(errorCodes.DEPENDENCIES_REQUIRES_INVALID, 'dependencies.requires', sourcePath, dependenciesRequiresArrayDetail);
|
||||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID,
|
|
||||||
'dependencies.requires',
|
|
||||||
sourcePath,
|
|
||||||
'Exemplar dependencies.requires must be an array.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dependencies.requires.length > 0) {
|
if (requiresMustBeEmpty && dependencies.requires.length > 0) {
|
||||||
createValidationError(
|
createValidationError(
|
||||||
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY,
|
errorCodes.DEPENDENCIES_REQUIRES_NOT_EMPTY,
|
||||||
'dependencies.requires',
|
'dependencies.requires',
|
||||||
sourcePath,
|
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 sidecarBasename = path.posix.basename(sourcePath);
|
||||||
const expectedSidecarBasename = getExpectedSidecarBasenameFromSourcePath(normalizedDeclaredSourcePath);
|
const expectedSidecarBasename = getExpectedSidecarBasenameFromSourcePath(normalizedDeclaredSourcePath);
|
||||||
|
|
||||||
const sourcePathMismatch = normalizedDeclaredSourcePath !== HELP_EXEMPLAR_CANONICAL_SOURCE_PATH;
|
const sourcePathMismatch = normalizedDeclaredSourcePath !== expectedCanonicalSourcePath;
|
||||||
const basenameMismatch = !expectedSidecarBasename || sidecarBasename !== expectedSidecarBasename;
|
const basenameMismatch = !expectedSidecarBasename || sidecarBasename !== expectedSidecarBasename;
|
||||||
|
|
||||||
if (sourcePathMismatch || basenameMismatch) {
|
if (sourcePathMismatch || basenameMismatch) {
|
||||||
createValidationError(
|
createValidationError(
|
||||||
HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH,
|
errorCodes.SOURCEPATH_BASENAME_MISMATCH,
|
||||||
'sourcePath',
|
'sourcePath',
|
||||||
sourcePath,
|
sourcePath,
|
||||||
'sidecar basename does not match sourcePath basename',
|
'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 = {}) {
|
async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core', 'tasks', 'help.artifact.yaml'), options = {}) {
|
||||||
const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath));
|
const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath));
|
||||||
|
|
||||||
|
|
@ -253,10 +285,42 @@ async function validateHelpSidecarContractFile(sidecarPath = getSourcePath('core
|
||||||
validateHelpSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
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 = {
|
module.exports = {
|
||||||
HELP_SIDECAR_REQUIRED_FIELDS,
|
HELP_SIDECAR_REQUIRED_FIELDS,
|
||||||
|
SHARD_DOC_SIDECAR_REQUIRED_FIELDS,
|
||||||
HELP_SIDECAR_ERROR_CODES,
|
HELP_SIDECAR_ERROR_CODES,
|
||||||
|
SHARD_DOC_SIDECAR_ERROR_CODES,
|
||||||
SidecarContractError,
|
SidecarContractError,
|
||||||
validateHelpSidecarContractData,
|
validateHelpSidecarContractData,
|
||||||
validateHelpSidecarContractFile,
|
validateHelpSidecarContractFile,
|
||||||
|
validateShardDocSidecarContractData,
|
||||||
|
validateShardDocSidecarContractFile,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -854,13 +854,16 @@ class Wave1ValidationHarness {
|
||||||
|
|
||||||
const generator = new ManifestGenerator();
|
const generator = new ManifestGenerator();
|
||||||
generator.bmadFolderName = runtimeFolder;
|
generator.bmadFolderName = runtimeFolder;
|
||||||
generator.helpAuthorityRecords = [
|
generator.taskAuthorityRecords = [
|
||||||
{
|
{
|
||||||
|
recordType: 'metadata-authority',
|
||||||
canonicalId: 'bmad-help',
|
canonicalId: 'bmad-help',
|
||||||
authoritySourceType: 'sidecar',
|
authoritySourceType: 'sidecar',
|
||||||
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
authoritySourcePath: SIDEcar_AUTHORITY_SOURCE_PATH,
|
||||||
|
sourcePath: SOURCE_MARKDOWN_SOURCE_PATH,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
generator.helpAuthorityRecords = [...generator.taskAuthorityRecords];
|
||||||
generator.tasks = perturbed
|
generator.tasks = perturbed
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
|
|
@ -931,6 +934,21 @@ class Wave1ValidationHarness {
|
||||||
'output-location': '',
|
'output-location': '',
|
||||||
outputs: '',
|
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(path.join(coreDir, 'module-help.csv'), serializeCsv(MODULE_HELP_COMPAT_COLUMNS, moduleHelpFixtureRows), 'utf8');
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,62 @@ const CODEX_EXPORT_DERIVATION_ERROR_CODES = Object.freeze({
|
||||||
SIDECAR_PARSE_FAILED: 'ERR_CODEX_EXPORT_SIDECAR_PARSE_FAILED',
|
SIDECAR_PARSE_FAILED: 'ERR_CODEX_EXPORT_SIDECAR_PARSE_FAILED',
|
||||||
CANONICAL_ID_MISSING: 'ERR_CODEX_EXPORT_CANONICAL_ID_MISSING',
|
CANONICAL_ID_MISSING: 'ERR_CODEX_EXPORT_CANONICAL_ID_MISSING',
|
||||||
CANONICAL_ID_DERIVATION_FAILED: 'ERR_CODEX_EXPORT_CANONICAL_ID_DERIVATION_FAILED',
|
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_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_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_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({
|
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({
|
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 {
|
class CodexExportDerivationError extends Error {
|
||||||
constructor({ code, detail, fieldPath, sourcePath, observedValue, cause = null }) {
|
constructor({ code, detail, fieldPath, sourcePath, observedValue, cause = null }) {
|
||||||
|
|
@ -53,6 +96,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('codex', 'Codex', false);
|
super('codex', 'Codex', false);
|
||||||
this.exportDerivationRecords = [];
|
this.exportDerivationRecords = [];
|
||||||
|
this.exportSurfaceIdentityOwners = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,6 +113,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
||||||
this.exportDerivationRecords = [];
|
this.exportDerivationRecords = [];
|
||||||
|
this.exportSurfaceIdentityOwners = new Map();
|
||||||
|
|
||||||
// Clean up old .codex/prompts locations (both global and project)
|
// Clean up old .codex/prompts locations (both global and project)
|
||||||
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
|
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')
|
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
|
||||||
* @returns {number} Number of skills written
|
* @returns {number} Number of skills written
|
||||||
*/
|
*/
|
||||||
isExemplarHelpTaskArtifact(artifact = {}) {
|
getConvertedTaskExportTarget(artifact = {}) {
|
||||||
if (artifact.type !== 'task' || artifact.module !== 'core') {
|
if (artifact.type !== 'task' || artifact.module !== 'core') {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedName = String(artifact.name || '')
|
const normalizedName = String(artifact.name || '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
const exportTarget = EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS[normalizedName];
|
||||||
|
if (!exportTarget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRelativePath = String(artifact.relativePath || '')
|
const normalizedRelativePath = String(artifact.relativePath || '')
|
||||||
.trim()
|
.trim()
|
||||||
.replaceAll('\\', '/')
|
.replaceAll('\\', '/')
|
||||||
|
|
@ -263,11 +313,17 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
.replaceAll('\\', '/')
|
.replaceAll('\\', '/')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
if (normalizedName !== 'help') {
|
const normalizedRelativePathWithRoot = normalizedRelativePath.startsWith('/') ? normalizedRelativePath : `/${normalizedRelativePath}`;
|
||||||
return false;
|
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 }) {
|
throwExportDerivationError({ code, detail, fieldPath, sourcePath, observedValue, cause = null }) {
|
||||||
|
|
@ -281,8 +337,8 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadExemplarHelpSidecar(projectDir) {
|
async loadConvertedTaskSidecar(projectDir, exportTarget) {
|
||||||
for (const candidate of EXEMPLAR_SIDECAR_SOURCE_CANDIDATES) {
|
for (const candidate of exportTarget.sidecarSourceCandidates) {
|
||||||
const sidecarPath = path.join(projectDir, ...candidate.segments);
|
const sidecarPath = path.join(projectDir, ...candidate.segments);
|
||||||
if (await fs.pathExists(sidecarPath)) {
|
if (await fs.pathExists(sidecarPath)) {
|
||||||
let sidecarData;
|
let sidecarData;
|
||||||
|
|
@ -293,7 +349,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||||
detail: `YAML parse failure: ${error.message}`,
|
detail: `YAML parse failure: ${error.message}`,
|
||||||
fieldPath: '<document>',
|
fieldPath: '<document>',
|
||||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
sourcePath: exportTarget.sidecarSourcePath,
|
||||||
observedValue: '<parse-error>',
|
observedValue: '<parse-error>',
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
|
|
@ -304,7 +360,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||||
detail: 'sidecar root must be a YAML mapping object',
|
detail: 'sidecar root must be a YAML mapping object',
|
||||||
fieldPath: '<document>',
|
fieldPath: '<document>',
|
||||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
sourcePath: exportTarget.sidecarSourcePath,
|
||||||
observedValue: typeof sidecarData,
|
observedValue: typeof sidecarData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -315,14 +371,14 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_MISSING,
|
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_MISSING,
|
||||||
detail: 'sidecar canonicalId is required for exemplar export derivation',
|
detail: 'sidecar canonicalId is required for exemplar export derivation',
|
||||||
fieldPath: 'canonicalId',
|
fieldPath: 'canonicalId',
|
||||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
sourcePath: exportTarget.sidecarSourcePath,
|
||||||
observedValue: canonicalId,
|
observedValue: canonicalId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canonicalId,
|
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,
|
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND,
|
||||||
detail: 'expected exemplar sidecar metadata file was not found',
|
detail: 'expected exemplar sidecar metadata file was not found',
|
||||||
fieldPath: '<file>',
|
fieldPath: '<file>',
|
||||||
sourcePath: EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH,
|
sourcePath: exportTarget.sidecarSourcePath,
|
||||||
observedValue: projectDir,
|
observedValue: projectDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveSkillIdentityFromArtifact(artifact, projectDir) {
|
async resolveSkillIdentityFromArtifact(artifact, projectDir) {
|
||||||
const inferredSkillName = toDashPath(artifact.relativePath).replace(/\.md$/, '');
|
const inferredSkillName = toDashPath(artifact.relativePath).replace(/\.md$/, '');
|
||||||
const isExemplarHelpTask = this.isExemplarHelpTaskArtifact(artifact);
|
const exportTarget = this.getConvertedTaskExportTarget(artifact);
|
||||||
if (!isExemplarHelpTask) {
|
if (!exportTarget) {
|
||||||
return {
|
return {
|
||||||
skillName: inferredSkillName,
|
skillName: inferredSkillName,
|
||||||
canonicalId: 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;
|
let canonicalResolution;
|
||||||
try {
|
try {
|
||||||
canonicalResolution = await normalizeAndResolveExemplarAlias(sidecarData.canonicalId, {
|
const aliasResolutionOptions = {
|
||||||
fieldPath: 'canonicalId',
|
fieldPath: 'canonicalId',
|
||||||
sourcePath: sidecarData.sourcePath,
|
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) {
|
} catch (error) {
|
||||||
this.throwExportDerivationError({
|
this.throwExportDerivationError({
|
||||||
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED,
|
code: CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED,
|
||||||
|
|
@ -383,6 +444,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
canonicalId: skillName,
|
canonicalId: skillName,
|
||||||
exportIdDerivationSourceType: EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE,
|
exportIdDerivationSourceType: EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE,
|
||||||
exportIdDerivationSourcePath: sidecarData.sourcePath,
|
exportIdDerivationSourcePath: sidecarData.sourcePath,
|
||||||
|
exportIdDerivationTaskSourcePath: exportTarget.taskSourcePath,
|
||||||
exportIdDerivationEvidence: `applied:${canonicalResolution.preAliasNormalizedValue}|leadingSlash:${canonicalResolution.rawIdentityHasLeadingSlash}->${canonicalResolution.postAliasCanonicalId}|rows:${canonicalResolution.aliasRowLocator}`,
|
exportIdDerivationEvidence: `applied:${canonicalResolution.preAliasNormalizedValue}|leadingSlash:${canonicalResolution.rawIdentityHasLeadingSlash}->${canonicalResolution.postAliasCanonicalId}|rows:${canonicalResolution.aliasRowLocator}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -402,6 +464,33 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
// Create skill directory
|
// Create skill directory
|
||||||
const skillDir = path.join(destDir, skillName);
|
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);
|
await fs.ensureDir(skillDir);
|
||||||
|
|
||||||
// Transform content: rewrite frontmatter for skills format
|
// Transform content: rewrite frontmatter for skills format
|
||||||
|
|
@ -409,14 +498,14 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
// Write SKILL.md with platform-native line endings
|
// Write SKILL.md with platform-native line endings
|
||||||
const platformContent = skillContent.replaceAll('\n', os.EOL);
|
const platformContent = skillContent.replaceAll('\n', os.EOL);
|
||||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
||||||
await fs.writeFile(skillPath, platformContent, 'utf8');
|
await fs.writeFile(skillPath, platformContent, 'utf8');
|
||||||
|
this.exportSurfaceIdentityOwners.set(normalizedSkillPath, ownerRecord);
|
||||||
writtenCount++;
|
writtenCount++;
|
||||||
|
|
||||||
if (exportIdentity.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE) {
|
if (exportIdentity.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE) {
|
||||||
this.exportDerivationRecords.push({
|
this.exportDerivationRecords.push({
|
||||||
exportPath: path.join('.agents', 'skills', skillName, 'SKILL.md').replaceAll('\\', '/'),
|
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,
|
canonicalId: exportIdentity.canonicalId,
|
||||||
visibleId: skillName,
|
visibleId: skillName,
|
||||||
visibleSurfaceClass: 'export-id',
|
visibleSurfaceClass: 'export-id',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue