feat(installer): add index-docs native skill authority projection
This commit is contained in:
parent
c7680ab1a8
commit
99537b20ab
|
|
@ -0,0 +1,9 @@
|
||||||
|
schemaVersion: 1
|
||||||
|
canonicalId: bmad-index-docs
|
||||||
|
artifactType: task
|
||||||
|
module: core
|
||||||
|
sourcePath: bmad-fork/src/core/tasks/index-docs.xml
|
||||||
|
displayName: Index Docs
|
||||||
|
description: "Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything."
|
||||||
|
dependencies:
|
||||||
|
requires: []
|
||||||
9
test/fixtures/index-docs/sidecar-negative/basename-path-mismatch/index-docs.artifact.yaml
vendored
Normal file
9
test/fixtures/index-docs/sidecar-negative/basename-path-mismatch/index-docs.artifact.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
schemaVersion: 1
|
||||||
|
canonicalId: bmad-index-docs
|
||||||
|
artifactType: task
|
||||||
|
module: core
|
||||||
|
sourcePath: bmad-fork/src/core/tasks/not-index-docs.xml
|
||||||
|
displayName: Index Docs
|
||||||
|
description: "Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything."
|
||||||
|
dependencies:
|
||||||
|
requires: []
|
||||||
9
test/fixtures/index-docs/sidecar-negative/unknown-major-version/index-docs.artifact.yaml
vendored
Normal file
9
test/fixtures/index-docs/sidecar-negative/unknown-major-version/index-docs.artifact.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
schemaVersion: 2
|
||||||
|
canonicalId: bmad-index-docs
|
||||||
|
artifactType: task
|
||||||
|
module: core
|
||||||
|
sourcePath: bmad-fork/src/core/tasks/index-docs.xml
|
||||||
|
displayName: Index Docs
|
||||||
|
description: "Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything."
|
||||||
|
dependencies:
|
||||||
|
requires: []
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -949,6 +949,22 @@ class HelpValidationHarness {
|
||||||
'output-location': '',
|
'output-location': '',
|
||||||
outputs: '',
|
outputs: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
module: 'core',
|
||||||
|
phase: 'anytime',
|
||||||
|
name: 'Index Docs',
|
||||||
|
code: 'ID',
|
||||||
|
sequence: '',
|
||||||
|
'workflow-file': `${runtimeFolder}/core/tasks/index-docs.xml`,
|
||||||
|
command: 'bmad-index-docs',
|
||||||
|
required: 'false',
|
||||||
|
agent: '',
|
||||||
|
options: '',
|
||||||
|
description:
|
||||||
|
'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.',
|
||||||
|
'output-location': '',
|
||||||
|
outputs: '',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
await 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(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
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 INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES = Object.freeze({
|
||||||
|
SIDECAR_FILE_NOT_FOUND: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_FILE_NOT_FOUND',
|
||||||
|
SIDECAR_PARSE_FAILED: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_PARSE_FAILED',
|
||||||
|
SIDECAR_INVALID_METADATA: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_INVALID_METADATA',
|
||||||
|
SIDECAR_CANONICAL_ID_MISMATCH: 'ERR_INDEX_DOCS_AUTHORITY_SIDECAR_CANONICAL_ID_MISMATCH',
|
||||||
|
SOURCE_XML_FILE_NOT_FOUND: 'ERR_INDEX_DOCS_AUTHORITY_SOURCE_XML_FILE_NOT_FOUND',
|
||||||
|
COMPATIBILITY_FILE_NOT_FOUND: 'ERR_INDEX_DOCS_AUTHORITY_COMPATIBILITY_FILE_NOT_FOUND',
|
||||||
|
COMPATIBILITY_PARSE_FAILED: 'ERR_INDEX_DOCS_AUTHORITY_COMPATIBILITY_PARSE_FAILED',
|
||||||
|
COMPATIBILITY_ROW_MISSING: 'ERR_INDEX_DOCS_AUTHORITY_COMPATIBILITY_ROW_MISSING',
|
||||||
|
COMPATIBILITY_ROW_DUPLICATE: 'ERR_INDEX_DOCS_AUTHORITY_COMPATIBILITY_ROW_DUPLICATE',
|
||||||
|
COMMAND_MISMATCH: 'ERR_INDEX_DOCS_AUTHORITY_COMMAND_MISMATCH',
|
||||||
|
DISPLAY_NAME_MISMATCH: 'ERR_INDEX_DOCS_AUTHORITY_DISPLAY_NAME_MISMATCH',
|
||||||
|
DUPLICATE_CANONICAL_COMMAND: 'ERR_INDEX_DOCS_AUTHORITY_DUPLICATE_CANONICAL_COMMAND',
|
||||||
|
});
|
||||||
|
|
||||||
|
const INDEX_DOCS_LOCKED_CANONICAL_ID = 'bmad-index-docs';
|
||||||
|
const INDEX_DOCS_LOCKED_AUTHORITATIVE_PRESENCE_KEY = `capability:${INDEX_DOCS_LOCKED_CANONICAL_ID}`;
|
||||||
|
|
||||||
|
class IndexDocsAuthorityValidationError extends Error {
|
||||||
|
constructor({ code, detail, fieldPath, sourcePath, observedValue, expectedValue }) {
|
||||||
|
const message = `${code}: ${detail} (fieldPath=${fieldPath}, sourcePath=${sourcePath})`;
|
||||||
|
super(message);
|
||||||
|
this.name = 'IndexDocsAuthorityValidationError';
|
||||||
|
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 IndexDocsAuthorityValidationError({
|
||||||
|
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(
|
||||||
|
INDEX_DOCS_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(
|
||||||
|
INDEX_DOCS_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 !== INDEX_DOCS_LOCKED_CANONICAL_ID) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_CANONICAL_ID_MISMATCH,
|
||||||
|
'Converted index-docs sidecar canonicalId must remain locked to bmad-index-docs',
|
||||||
|
'canonicalId',
|
||||||
|
sidecarSourcePath,
|
||||||
|
normalizedCanonicalId,
|
||||||
|
INDEX_DOCS_LOCKED_CANONICAL_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedDeclaredSourcePath = normalizeSourcePath(sidecarData.sourcePath);
|
||||||
|
if (normalizedDeclaredSourcePath !== sourceXmlSourcePath) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_INVALID_METADATA,
|
||||||
|
'Sidecar sourcePath must match index-docs XML source path',
|
||||||
|
'sourcePath',
|
||||||
|
sidecarSourcePath,
|
||||||
|
normalizedDeclaredSourcePath,
|
||||||
|
sourceXmlSourcePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseCompatibilityRows(compatibilityCatalogPath, compatibilityCatalogSourcePath) {
|
||||||
|
if (!(await fs.pathExists(compatibilityCatalogPath))) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_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(
|
||||||
|
INDEX_DOCS_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(
|
||||||
|
INDEX_DOCS_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(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_MISSING,
|
||||||
|
'Converted index-docs compatibility row is missing from module-help catalog',
|
||||||
|
'workflow-file',
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
'<missing>',
|
||||||
|
workflowFilePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowMatches.length > 1) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.COMPATIBILITY_ROW_DUPLICATE,
|
||||||
|
'Converted index-docs compatibility row appears more than once in module-help catalog',
|
||||||
|
'workflow-file',
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
workflowMatches.length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalCommandMatches = rows.filter((row) => csvMatchValue(row.command) === INDEX_DOCS_LOCKED_CANONICAL_ID);
|
||||||
|
if (canonicalCommandMatches.length > 1) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.DUPLICATE_CANONICAL_COMMAND,
|
||||||
|
'Converted index-docs canonical command appears in more than one compatibility row',
|
||||||
|
'command',
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
canonicalCommandMatches.length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexDocsRow = workflowMatches[0];
|
||||||
|
const observedCommand = csvMatchValue(indexDocsRow.command);
|
||||||
|
if (!observedCommand || observedCommand !== INDEX_DOCS_LOCKED_CANONICAL_ID) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.COMMAND_MISMATCH,
|
||||||
|
'Converted index-docs compatibility command must match locked canonical command bmad-index-docs',
|
||||||
|
'command',
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
observedCommand || '<empty>',
|
||||||
|
INDEX_DOCS_LOCKED_CANONICAL_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observedDisplayName = csvMatchValue(indexDocsRow.name);
|
||||||
|
if (observedDisplayName && observedDisplayName !== displayName) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.DISPLAY_NAME_MISMATCH,
|
||||||
|
'Converted index-docs compatibility name must match sidecar displayName when provided',
|
||||||
|
'name',
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
observedDisplayName,
|
||||||
|
displayName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndexDocsAuthorityRecords({ canonicalId, sidecarSourcePath, sourceXmlSourcePath }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
recordType: 'metadata-authority',
|
||||||
|
canonicalId,
|
||||||
|
authoritativePresenceKey: INDEX_DOCS_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||||
|
authoritySourceType: 'sidecar',
|
||||||
|
authoritySourcePath: sidecarSourcePath,
|
||||||
|
sourcePath: sourceXmlSourcePath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordType: 'source-body-authority',
|
||||||
|
canonicalId,
|
||||||
|
authoritativePresenceKey: INDEX_DOCS_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||||
|
authoritySourceType: 'source-xml',
|
||||||
|
authoritySourcePath: sourceXmlSourcePath,
|
||||||
|
sourcePath: sourceXmlSourcePath,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateIndexDocsAuthoritySplitAndPrecedence(options = {}) {
|
||||||
|
const sidecarPath = options.sidecarPath || getSourcePath('core', 'tasks', 'index-docs.artifact.yaml');
|
||||||
|
const sourceXmlPath = options.sourceXmlPath || getSourcePath('core', 'tasks', 'index-docs.xml');
|
||||||
|
const compatibilityCatalogPath = options.compatibilityCatalogPath || getSourcePath('core', 'module-help.csv');
|
||||||
|
const compatibilityWorkflowFilePath = options.compatibilityWorkflowFilePath || '_bmad/core/tasks/index-docs.xml';
|
||||||
|
|
||||||
|
const sidecarSourcePath = normalizeSourcePath(options.sidecarSourcePath || toProjectRelativePath(sidecarPath));
|
||||||
|
const sourceXmlSourcePath = normalizeSourcePath(options.sourceXmlSourcePath || toProjectRelativePath(sourceXmlPath));
|
||||||
|
const compatibilityCatalogSourcePath = normalizeSourcePath(
|
||||||
|
options.compatibilityCatalogSourcePath || toProjectRelativePath(compatibilityCatalogPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(sidecarPath))) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND,
|
||||||
|
'Expected index-docs sidecar metadata file was not found',
|
||||||
|
'<file>',
|
||||||
|
sidecarSourcePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sidecarData;
|
||||||
|
try {
|
||||||
|
sidecarData = yaml.parse(await fs.readFile(sidecarPath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SIDECAR_PARSE_FAILED,
|
||||||
|
`YAML parse failure: ${error.message}`,
|
||||||
|
'<document>',
|
||||||
|
sidecarSourcePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sidecarData || typeof sidecarData !== 'object' || Array.isArray(sidecarData)) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_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(
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES.SOURCE_XML_FILE_NOT_FOUND,
|
||||||
|
'Expected index-docs XML source file was not found',
|
||||||
|
'<file>',
|
||||||
|
sourceXmlSourcePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibilityRows = await parseCompatibilityRows(compatibilityCatalogPath, compatibilityCatalogSourcePath);
|
||||||
|
validateCompatibilityPrecedence({
|
||||||
|
rows: compatibilityRows,
|
||||||
|
displayName: String(sidecarData.displayName || '').trim(),
|
||||||
|
workflowFilePath: compatibilityWorkflowFilePath,
|
||||||
|
compatibilityCatalogSourcePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canonicalId = INDEX_DOCS_LOCKED_CANONICAL_ID;
|
||||||
|
const authoritativeRecords = buildIndexDocsAuthorityRecords({
|
||||||
|
canonicalId,
|
||||||
|
sidecarSourcePath,
|
||||||
|
sourceXmlSourcePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
canonicalId,
|
||||||
|
authoritativePresenceKey: INDEX_DOCS_LOCKED_AUTHORITATIVE_PRESENCE_KEY,
|
||||||
|
authoritativeRecords,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
INDEX_DOCS_AUTHORITY_VALIDATION_ERROR_CODES,
|
||||||
|
IndexDocsAuthorityValidationError,
|
||||||
|
validateIndexDocsAuthoritySplitAndPrecedence,
|
||||||
|
};
|
||||||
|
|
@ -9,9 +9,14 @@ 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, validateShardDocSidecarContractFile } = require('./sidecar-contract-validator');
|
const {
|
||||||
|
validateHelpSidecarContractFile,
|
||||||
|
validateShardDocSidecarContractFile,
|
||||||
|
validateIndexDocsSidecarContractFile,
|
||||||
|
} = require('./sidecar-contract-validator');
|
||||||
const { validateHelpAuthoritySplitAndPrecedence } = require('./help-authority-validator');
|
const { validateHelpAuthoritySplitAndPrecedence } = require('./help-authority-validator');
|
||||||
const { validateShardDocAuthoritySplitAndPrecedence } = require('./shard-doc-authority-validator');
|
const { validateShardDocAuthoritySplitAndPrecedence } = require('./shard-doc-authority-validator');
|
||||||
|
const { validateIndexDocsAuthoritySplitAndPrecedence } = require('./index-docs-authority-validator');
|
||||||
const {
|
const {
|
||||||
HELP_CATALOG_GENERATION_ERROR_CODES,
|
HELP_CATALOG_GENERATION_ERROR_CODES,
|
||||||
buildSidecarAwareExemplarHelpRow,
|
buildSidecarAwareExemplarHelpRow,
|
||||||
|
|
@ -36,6 +41,10 @@ const EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-d
|
||||||
const EXEMPLAR_SHARD_DOC_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
|
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_COMPATIBILITY_CATALOG_SOURCE_PATH = 'bmad-fork/src/core/module-help.csv';
|
||||||
const EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH = '_bmad/core/tasks/shard-doc.xml';
|
const EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH = '_bmad/core/tasks/shard-doc.xml';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_SOURCE_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_COMPATIBILITY_CATALOG_SOURCE_PATH = 'bmad-fork/src/core/module-help.csv';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_WORKFLOW_FILE_PATH = '_bmad/core/tasks/index-docs.xml';
|
||||||
|
|
||||||
class Installer {
|
class Installer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -51,14 +60,19 @@ class Installer {
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.validateHelpSidecarContractFile = validateHelpSidecarContractFile;
|
this.validateHelpSidecarContractFile = validateHelpSidecarContractFile;
|
||||||
this.validateShardDocSidecarContractFile = validateShardDocSidecarContractFile;
|
this.validateShardDocSidecarContractFile = validateShardDocSidecarContractFile;
|
||||||
|
this.validateIndexDocsSidecarContractFile = validateIndexDocsSidecarContractFile;
|
||||||
this.validateHelpAuthoritySplitAndPrecedence = validateHelpAuthoritySplitAndPrecedence;
|
this.validateHelpAuthoritySplitAndPrecedence = validateHelpAuthoritySplitAndPrecedence;
|
||||||
this.validateShardDocAuthoritySplitAndPrecedence = validateShardDocAuthoritySplitAndPrecedence;
|
this.validateShardDocAuthoritySplitAndPrecedence = validateShardDocAuthoritySplitAndPrecedence;
|
||||||
|
this.validateIndexDocsAuthoritySplitAndPrecedence = validateIndexDocsAuthoritySplitAndPrecedence;
|
||||||
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;
|
||||||
this.helpCatalogPipelineRows = [];
|
this.helpCatalogPipelineRows = [];
|
||||||
this.helpCatalogCommandLabelReportRows = [];
|
this.helpCatalogCommandLabelReportRows = [];
|
||||||
this.codexExportDerivationRecords = [];
|
this.codexExportDerivationRecords = [];
|
||||||
|
this.helpAuthorityRecords = [];
|
||||||
|
this.shardDocAuthorityRecords = [];
|
||||||
|
this.indexDocsAuthorityRecords = [];
|
||||||
this.latestHelpValidationRun = null;
|
this.latestHelpValidationRun = null;
|
||||||
this.latestShardDocValidationRun = null;
|
this.latestShardDocValidationRun = null;
|
||||||
this.helpValidationHarness = new HelpValidationHarness();
|
this.helpValidationHarness = new HelpValidationHarness();
|
||||||
|
|
@ -71,10 +85,14 @@ class Installer {
|
||||||
message('Validating shard-doc sidecar contract...');
|
message('Validating shard-doc sidecar contract...');
|
||||||
await this.validateShardDocSidecarContractFile();
|
await this.validateShardDocSidecarContractFile();
|
||||||
|
|
||||||
|
message('Validating index-docs sidecar contract...');
|
||||||
|
await this.validateIndexDocsSidecarContractFile();
|
||||||
|
|
||||||
message('Validating exemplar sidecar contract...');
|
message('Validating exemplar sidecar contract...');
|
||||||
await this.validateHelpSidecarContractFile();
|
await this.validateHelpSidecarContractFile();
|
||||||
|
|
||||||
addResult('Shard-doc sidecar contract', 'ok', 'validated');
|
addResult('Shard-doc sidecar contract', 'ok', 'validated');
|
||||||
|
addResult('Index-docs sidecar contract', 'ok', 'validated');
|
||||||
addResult('Sidecar contract', 'ok', 'validated');
|
addResult('Sidecar contract', 'ok', 'validated');
|
||||||
|
|
||||||
message('Validating shard-doc authority split and XML precedence...');
|
message('Validating shard-doc authority split and XML precedence...');
|
||||||
|
|
@ -87,6 +105,16 @@ class Installer {
|
||||||
this.shardDocAuthorityRecords = shardDocAuthorityValidation.authoritativeRecords;
|
this.shardDocAuthorityRecords = shardDocAuthorityValidation.authoritativeRecords;
|
||||||
addResult('Shard-doc authority split', 'ok', shardDocAuthorityValidation.authoritativePresenceKey);
|
addResult('Shard-doc authority split', 'ok', shardDocAuthorityValidation.authoritativePresenceKey);
|
||||||
|
|
||||||
|
message('Validating index-docs authority split and XML precedence...');
|
||||||
|
const indexDocsAuthorityValidation = await this.validateIndexDocsAuthoritySplitAndPrecedence({
|
||||||
|
sidecarSourcePath: EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH,
|
||||||
|
sourceXmlSourcePath: EXEMPLAR_INDEX_DOCS_SOURCE_XML_SOURCE_PATH,
|
||||||
|
compatibilityCatalogSourcePath: EXEMPLAR_INDEX_DOCS_COMPATIBILITY_CATALOG_SOURCE_PATH,
|
||||||
|
compatibilityWorkflowFilePath: EXEMPLAR_INDEX_DOCS_WORKFLOW_FILE_PATH,
|
||||||
|
});
|
||||||
|
this.indexDocsAuthorityRecords = indexDocsAuthorityValidation.authoritativeRecords;
|
||||||
|
addResult('Index-docs authority split', 'ok', indexDocsAuthorityValidation.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,
|
||||||
|
|
@ -134,7 +162,11 @@ class Installer {
|
||||||
ides: config.ides || [],
|
ides: config.ides || [],
|
||||||
preservedModules: modulesForCsvPreserve,
|
preservedModules: modulesForCsvPreserve,
|
||||||
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
helpAuthorityRecords: this.helpAuthorityRecords || [],
|
||||||
taskAuthorityRecords: [...(this.helpAuthorityRecords || []), ...(this.shardDocAuthorityRecords || [])],
|
taskAuthorityRecords: [
|
||||||
|
...(this.helpAuthorityRecords || []),
|
||||||
|
...(this.shardDocAuthorityRecords || []),
|
||||||
|
...(this.indexDocsAuthorityRecords || []),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
addResult(
|
addResult(
|
||||||
|
|
@ -1983,6 +2015,11 @@ class Installer {
|
||||||
authoritySourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
authoritySourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH,
|
||||||
fallbackCanonicalId: 'bmad-shard-doc',
|
fallbackCanonicalId: 'bmad-shard-doc',
|
||||||
});
|
});
|
||||||
|
const indexDocsCanonicalId = this.resolveCanonicalIdFromAuthorityRecords({
|
||||||
|
authorityRecords: this.indexDocsAuthorityRecords || [],
|
||||||
|
authoritySourcePath: EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH,
|
||||||
|
fallbackCanonicalId: 'bmad-index-docs',
|
||||||
|
});
|
||||||
const commandLabelContracts = [
|
const commandLabelContracts = [
|
||||||
{
|
{
|
||||||
canonicalId: sidecarAwareExemplar.canonicalId,
|
canonicalId: sidecarAwareExemplar.canonicalId,
|
||||||
|
|
@ -2002,6 +2039,15 @@ class Installer {
|
||||||
workflowFilePath: EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH,
|
workflowFilePath: EXEMPLAR_SHARD_DOC_WORKFLOW_FILE_PATH,
|
||||||
nameCandidates: ['shard document', 'shard-doc'],
|
nameCandidates: ['shard document', 'shard-doc'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
canonicalId: indexDocsCanonicalId,
|
||||||
|
legacyName: 'index-docs',
|
||||||
|
displayedCommandLabel: renderDisplayedCommandLabel(indexDocsCanonicalId),
|
||||||
|
authoritySourceType: 'sidecar',
|
||||||
|
authoritySourcePath: EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH,
|
||||||
|
workflowFilePath: EXEMPLAR_INDEX_DOCS_WORKFLOW_FILE_PATH,
|
||||||
|
nameCandidates: ['index docs', 'index-docs'],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let exemplarRowWritten = false;
|
let exemplarRowWritten = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const { validateTaskManifestCompatibilitySurface } = require('./projection-compa
|
||||||
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 DEFAULT_EXEMPLAR_SHARD_DOC_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml';
|
||||||
|
const DEFAULT_EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml';
|
||||||
const CANONICAL_ALIAS_TABLE_COLUMNS = Object.freeze([
|
const CANONICAL_ALIAS_TABLE_COLUMNS = Object.freeze([
|
||||||
'canonicalId',
|
'canonicalId',
|
||||||
'alias',
|
'alias',
|
||||||
|
|
@ -85,6 +86,35 @@ const LOCKED_CANONICAL_ALIAS_TABLE_SHARD_DOC_ROWS = Object.freeze([
|
||||||
resolutionEligibility: 'slash-command-only',
|
resolutionEligibility: 'slash-command-only',
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
const LOCKED_CANONICAL_ALIAS_TABLE_INDEX_DOCS_ROWS = Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
alias: 'bmad-index-docs',
|
||||||
|
aliasType: 'canonical-id',
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:canonical-id',
|
||||||
|
normalizedAliasValue: 'bmad-index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: false,
|
||||||
|
resolutionEligibility: 'canonical-id-only',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
alias: 'index-docs',
|
||||||
|
aliasType: 'legacy-name',
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:legacy-name',
|
||||||
|
normalizedAliasValue: 'index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: false,
|
||||||
|
resolutionEligibility: 'legacy-name-only',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
alias: '/bmad-index-docs',
|
||||||
|
aliasType: 'slash-command',
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:slash-command',
|
||||||
|
normalizedAliasValue: 'bmad-index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: true,
|
||||||
|
resolutionEligibility: 'slash-command-only',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates manifest files for installed workflows, agents, and tasks
|
* Generates manifest files for installed workflows, agents, and tasks
|
||||||
|
|
@ -99,6 +129,7 @@ class ManifestGenerator {
|
||||||
this.files = [];
|
this.files = [];
|
||||||
this.selectedIdes = [];
|
this.selectedIdes = [];
|
||||||
this.includeConvertedShardDocAliasRows = null;
|
this.includeConvertedShardDocAliasRows = null;
|
||||||
|
this.includeConvertedIndexDocsAliasRows = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeTaskAuthorityRecords(records) {
|
normalizeTaskAuthorityRecords(records) {
|
||||||
|
|
@ -286,6 +317,9 @@ class ManifestGenerator {
|
||||||
this.includeConvertedShardDocAliasRows = Object.prototype.hasOwnProperty.call(options, 'includeConvertedShardDocAliasRows')
|
this.includeConvertedShardDocAliasRows = Object.prototype.hasOwnProperty.call(options, 'includeConvertedShardDocAliasRows')
|
||||||
? options.includeConvertedShardDocAliasRows === true
|
? options.includeConvertedShardDocAliasRows === true
|
||||||
: null;
|
: null;
|
||||||
|
this.includeConvertedIndexDocsAliasRows = Object.prototype.hasOwnProperty.call(options, 'includeConvertedIndexDocsAliasRows')
|
||||||
|
? options.includeConvertedIndexDocsAliasRows === 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');
|
||||||
|
|
@ -1183,6 +1217,20 @@ class ManifestGenerator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveIndexDocsAliasAuthorityRecord() {
|
||||||
|
const sidecarAuthorityRecord = Array.isArray(this.taskAuthorityRecords)
|
||||||
|
? this.taskAuthorityRecords.find(
|
||||||
|
(record) => record?.canonicalId === 'bmad-index-docs' && record?.authoritySourceType === 'sidecar' && record?.authoritySourcePath,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
authoritySourceType: sidecarAuthorityRecord ? sidecarAuthorityRecord.authoritySourceType : 'sidecar',
|
||||||
|
authoritySourcePath: sidecarAuthorityRecord
|
||||||
|
? sidecarAuthorityRecord.authoritySourcePath
|
||||||
|
: DEFAULT_EXEMPLAR_INDEX_DOCS_SIDECAR_SOURCE_PATH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
hasShardDocTaskAuthorityProjection() {
|
hasShardDocTaskAuthorityProjection() {
|
||||||
if (!Array.isArray(this.taskAuthorityRecords)) {
|
if (!Array.isArray(this.taskAuthorityRecords)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1208,6 +1256,31 @@ class ManifestGenerator {
|
||||||
return this.hasShardDocTaskAuthorityProjection();
|
return this.hasShardDocTaskAuthorityProjection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasIndexDocsTaskAuthorityProjection() {
|
||||||
|
if (!Array.isArray(this.taskAuthorityRecords)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.taskAuthorityRecords.some(
|
||||||
|
(record) =>
|
||||||
|
record?.recordType === 'metadata-authority' &&
|
||||||
|
record?.canonicalId === 'bmad-index-docs' &&
|
||||||
|
record?.authoritySourceType === 'sidecar' &&
|
||||||
|
String(record?.authoritySourcePath || '').trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldProjectIndexDocsAliasRows() {
|
||||||
|
if (this.includeConvertedIndexDocsAliasRows === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.includeConvertedIndexDocsAliasRows === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasIndexDocsTaskAuthorityProjection();
|
||||||
|
}
|
||||||
|
|
||||||
buildCanonicalAliasProjectionRows() {
|
buildCanonicalAliasProjectionRows() {
|
||||||
const buildRows = (lockedRows, authorityRecord) =>
|
const buildRows = (lockedRows, authorityRecord) =>
|
||||||
lockedRows.map((row) => ({
|
lockedRows.map((row) => ({
|
||||||
|
|
@ -1226,6 +1299,9 @@ class ManifestGenerator {
|
||||||
if (this.shouldProjectShardDocAliasRows()) {
|
if (this.shouldProjectShardDocAliasRows()) {
|
||||||
rows.push(...buildRows(LOCKED_CANONICAL_ALIAS_TABLE_SHARD_DOC_ROWS, this.resolveShardDocAliasAuthorityRecord()));
|
rows.push(...buildRows(LOCKED_CANONICAL_ALIAS_TABLE_SHARD_DOC_ROWS, this.resolveShardDocAliasAuthorityRecord()));
|
||||||
}
|
}
|
||||||
|
if (this.shouldProjectIndexDocsAliasRows()) {
|
||||||
|
rows.push(...buildRows(LOCKED_CANONICAL_ALIAS_TABLE_INDEX_DOCS_ROWS, this.resolveIndexDocsAliasAuthorityRecord()));
|
||||||
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const PROJECTION_COMPATIBILITY_ERROR_CODES = Object.freeze({
|
||||||
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',
|
HELP_CATALOG_SHARD_DOC_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_SHARD_DOC_ROW_CONTRACT_FAILED',
|
||||||
|
HELP_CATALOG_INDEX_DOCS_ROW_CONTRACT_FAILED: 'ERR_HELP_CATALOG_COMPAT_INDEX_DOCS_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_PARSE_FAILED: 'ERR_COMMAND_DOC_CONSISTENCY_PARSE_FAILED',
|
||||||
COMMAND_DOC_CANONICAL_COMMAND_MISSING: 'ERR_COMMAND_DOC_CONSISTENCY_CANONICAL_COMMAND_MISSING',
|
COMMAND_DOC_CANONICAL_COMMAND_MISSING: 'ERR_COMMAND_DOC_CONSISTENCY_CANONICAL_COMMAND_MISSING',
|
||||||
|
|
@ -315,6 +316,23 @@ function validateHelpCatalogLoaderEntries(rows, options = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexDocsRows = parsedRows.filter(
|
||||||
|
(row) =>
|
||||||
|
normalizeCommandValue(row.command) === 'bmad-index-docs' &&
|
||||||
|
normalizeWorkflowPath(row['workflow-file']).endsWith('/core/tasks/index-docs.xml'),
|
||||||
|
);
|
||||||
|
if (indexDocsRows.length !== 1) {
|
||||||
|
throwCompatibilityError({
|
||||||
|
code: PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_INDEX_DOCS_ROW_CONTRACT_FAILED,
|
||||||
|
detail: 'Exactly one index-docs compatibility row is required for help catalog consumers',
|
||||||
|
surface,
|
||||||
|
fieldPath: 'rows[*].command',
|
||||||
|
sourcePath,
|
||||||
|
observedValue: String(indexDocsRows.length),
|
||||||
|
expectedValue: '1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -547,6 +547,22 @@ class ShardDocValidationHarness {
|
||||||
'output-location': '',
|
'output-location': '',
|
||||||
outputs: '',
|
outputs: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
module: 'core',
|
||||||
|
phase: 'anytime',
|
||||||
|
name: 'Index Docs',
|
||||||
|
code: 'ID',
|
||||||
|
sequence: '',
|
||||||
|
'workflow-file': `${runtimeFolder}/core/tasks/index-docs.xml`,
|
||||||
|
command: 'bmad-index-docs',
|
||||||
|
required: 'false',
|
||||||
|
agent: '',
|
||||||
|
options: '',
|
||||||
|
description:
|
||||||
|
'Create lightweight index for quick LLM scanning. Use when LLM needs to understand available docs without loading everything.',
|
||||||
|
'output-location': '',
|
||||||
|
outputs: '',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const HELP_SIDECAR_REQUIRED_FIELDS = Object.freeze([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SHARD_DOC_SIDECAR_REQUIRED_FIELDS = Object.freeze([...HELP_SIDECAR_REQUIRED_FIELDS]);
|
const SHARD_DOC_SIDECAR_REQUIRED_FIELDS = Object.freeze([...HELP_SIDECAR_REQUIRED_FIELDS]);
|
||||||
|
const INDEX_DOCS_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',
|
||||||
|
|
@ -46,8 +47,24 @@ const SHARD_DOC_SIDECAR_ERROR_CODES = Object.freeze({
|
||||||
SOURCEPATH_BASENAME_MISMATCH: 'ERR_SHARD_DOC_SIDECAR_SOURCEPATH_BASENAME_MISMATCH',
|
SOURCEPATH_BASENAME_MISMATCH: 'ERR_SHARD_DOC_SIDECAR_SOURCEPATH_BASENAME_MISMATCH',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const INDEX_DOCS_SIDECAR_ERROR_CODES = Object.freeze({
|
||||||
|
FILE_NOT_FOUND: 'ERR_INDEX_DOCS_SIDECAR_FILE_NOT_FOUND',
|
||||||
|
PARSE_FAILED: 'ERR_INDEX_DOCS_SIDECAR_PARSE_FAILED',
|
||||||
|
INVALID_ROOT_OBJECT: 'ERR_INDEX_DOCS_SIDECAR_INVALID_ROOT_OBJECT',
|
||||||
|
REQUIRED_FIELD_MISSING: 'ERR_INDEX_DOCS_SIDECAR_REQUIRED_FIELD_MISSING',
|
||||||
|
REQUIRED_FIELD_EMPTY: 'ERR_INDEX_DOCS_SIDECAR_REQUIRED_FIELD_EMPTY',
|
||||||
|
ARTIFACT_TYPE_INVALID: 'ERR_INDEX_DOCS_SIDECAR_ARTIFACT_TYPE_INVALID',
|
||||||
|
MODULE_INVALID: 'ERR_INDEX_DOCS_SIDECAR_MODULE_INVALID',
|
||||||
|
DEPENDENCIES_MISSING: 'ERR_INDEX_DOCS_SIDECAR_DEPENDENCIES_MISSING',
|
||||||
|
DEPENDENCIES_REQUIRES_INVALID: 'ERR_INDEX_DOCS_SIDECAR_DEPENDENCIES_REQUIRES_INVALID',
|
||||||
|
DEPENDENCIES_REQUIRES_NOT_EMPTY: 'ERR_INDEX_DOCS_SIDECAR_DEPENDENCIES_REQUIRES_NOT_EMPTY',
|
||||||
|
MAJOR_VERSION_UNSUPPORTED: 'ERR_INDEX_DOCS_SIDECAR_MAJOR_VERSION_UNSUPPORTED',
|
||||||
|
SOURCEPATH_BASENAME_MISMATCH: 'ERR_INDEX_DOCS_SIDECAR_SOURCEPATH_BASENAME_MISMATCH',
|
||||||
|
});
|
||||||
|
|
||||||
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 SHARD_DOC_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
|
const SHARD_DOC_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
|
||||||
|
const INDEX_DOCS_CANONICAL_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml';
|
||||||
const SIDECAR_SUPPORTED_SCHEMA_MAJOR = 1;
|
const SIDECAR_SUPPORTED_SCHEMA_MAJOR = 1;
|
||||||
|
|
||||||
class SidecarContractError extends Error {
|
class SidecarContractError extends Error {
|
||||||
|
|
@ -257,6 +274,26 @@ function validateShardDocSidecarContractData(sidecarData, options = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateIndexDocsSidecarContractData(sidecarData, options = {}) {
|
||||||
|
const sourcePath = normalizeSourcePath(options.errorSourcePath || 'src/core/tasks/index-docs.artifact.yaml');
|
||||||
|
validateSidecarContractData(sidecarData, {
|
||||||
|
sourcePath,
|
||||||
|
requiredFields: INDEX_DOCS_SIDECAR_REQUIRED_FIELDS,
|
||||||
|
requiredNonEmptyStringFields: ['canonicalId', 'sourcePath', 'displayName', 'description'],
|
||||||
|
errorCodes: INDEX_DOCS_SIDECAR_ERROR_CODES,
|
||||||
|
expectedArtifactType: 'task',
|
||||||
|
expectedModule: 'core',
|
||||||
|
expectedCanonicalSourcePath: INDEX_DOCS_CANONICAL_SOURCE_PATH,
|
||||||
|
missingDependenciesDetail: 'Index-docs sidecar requires an explicit dependencies block.',
|
||||||
|
dependenciesObjectDetail: 'Index-docs sidecar requires an explicit dependencies object.',
|
||||||
|
dependenciesRequiresArrayDetail: 'Index-docs dependencies.requires must be an array.',
|
||||||
|
dependenciesRequiresNotEmptyDetail: 'Index-docs contract requires explicit zero dependencies: dependencies.requires must be [].',
|
||||||
|
artifactTypeDetail: 'Index-docs contract requires artifactType to equal "task".',
|
||||||
|
moduleDetail: 'Index-docs 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));
|
||||||
|
|
||||||
|
|
@ -313,14 +350,49 @@ async function validateShardDocSidecarContractFile(sidecarPath = getSourcePath('
|
||||||
validateShardDocSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
validateShardDocSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateIndexDocsSidecarContractFile(
|
||||||
|
sidecarPath = getSourcePath('core', 'tasks', 'index-docs.artifact.yaml'),
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const normalizedSourcePath = normalizeSourcePath(options.errorSourcePath || toProjectRelativePath(sidecarPath));
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(sidecarPath))) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_SIDECAR_ERROR_CODES.FILE_NOT_FOUND,
|
||||||
|
'<file>',
|
||||||
|
normalizedSourcePath,
|
||||||
|
'Expected index-docs sidecar file was not found.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedSidecar;
|
||||||
|
try {
|
||||||
|
const sidecarRaw = await fs.readFile(sidecarPath, 'utf8');
|
||||||
|
parsedSidecar = yaml.parse(sidecarRaw);
|
||||||
|
} catch (error) {
|
||||||
|
createValidationError(
|
||||||
|
INDEX_DOCS_SIDECAR_ERROR_CODES.PARSE_FAILED,
|
||||||
|
'<document>',
|
||||||
|
normalizedSourcePath,
|
||||||
|
`YAML parse failure: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateIndexDocsSidecarContractData(parsedSidecar, { errorSourcePath: normalizedSourcePath });
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
HELP_SIDECAR_REQUIRED_FIELDS,
|
HELP_SIDECAR_REQUIRED_FIELDS,
|
||||||
SHARD_DOC_SIDECAR_REQUIRED_FIELDS,
|
SHARD_DOC_SIDECAR_REQUIRED_FIELDS,
|
||||||
|
INDEX_DOCS_SIDECAR_REQUIRED_FIELDS,
|
||||||
HELP_SIDECAR_ERROR_CODES,
|
HELP_SIDECAR_ERROR_CODES,
|
||||||
SHARD_DOC_SIDECAR_ERROR_CODES,
|
SHARD_DOC_SIDECAR_ERROR_CODES,
|
||||||
|
INDEX_DOCS_SIDECAR_ERROR_CODES,
|
||||||
SidecarContractError,
|
SidecarContractError,
|
||||||
validateHelpSidecarContractData,
|
validateHelpSidecarContractData,
|
||||||
validateHelpSidecarContractFile,
|
validateHelpSidecarContractFile,
|
||||||
validateShardDocSidecarContractData,
|
validateShardDocSidecarContractData,
|
||||||
validateShardDocSidecarContractFile,
|
validateShardDocSidecarContractFile,
|
||||||
|
validateIndexDocsSidecarContractData,
|
||||||
|
validateIndexDocsSidecarContractFile,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ const CODEX_EXPORT_DERIVATION_ERROR_CODES = Object.freeze({
|
||||||
|
|
||||||
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_SHARD_DOC_TASK_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.xml';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_TASK_XML_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.xml';
|
||||||
const EXEMPLAR_HELP_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/help.artifact.yaml';
|
const EXEMPLAR_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_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/shard-doc.artifact.yaml';
|
||||||
|
const EXEMPLAR_INDEX_DOCS_SIDECAR_CONTRACT_SOURCE_PATH = 'bmad-fork/src/core/tasks/index-docs.artifact.yaml';
|
||||||
const EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE = 'sidecar-canonical-id';
|
const EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE = 'sidecar-canonical-id';
|
||||||
const SHARD_DOC_EXPORT_ALIAS_ROWS = Object.freeze([
|
const SHARD_DOC_EXPORT_ALIAS_ROWS = Object.freeze([
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
|
|
@ -44,6 +46,26 @@ const SHARD_DOC_EXPORT_ALIAS_ROWS = Object.freeze([
|
||||||
rawIdentityHasLeadingSlash: true,
|
rawIdentityHasLeadingSlash: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
const INDEX_DOCS_EXPORT_ALIAS_ROWS = Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:canonical-id',
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
normalizedAliasValue: 'bmad-index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: false,
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:legacy-name',
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
normalizedAliasValue: 'index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: false,
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
rowIdentity: 'alias-row:bmad-index-docs:slash-command',
|
||||||
|
canonicalId: 'bmad-index-docs',
|
||||||
|
normalizedAliasValue: 'bmad-index-docs',
|
||||||
|
rawIdentityHasLeadingSlash: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({
|
const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({
|
||||||
help: Object.freeze({
|
help: Object.freeze({
|
||||||
taskSourcePath: EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH,
|
taskSourcePath: EXEMPLAR_HELP_TASK_MARKDOWN_SOURCE_PATH,
|
||||||
|
|
@ -62,6 +84,7 @@ const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({
|
||||||
taskSourcePath: EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH,
|
taskSourcePath: EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH,
|
||||||
sourcePathSuffix: '/core/tasks/shard-doc.xml',
|
sourcePathSuffix: '/core/tasks/shard-doc.xml',
|
||||||
sidecarSourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH,
|
sidecarSourcePath: EXEMPLAR_SHARD_DOC_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||||
|
aliasRows: SHARD_DOC_EXPORT_ALIAS_ROWS,
|
||||||
sidecarSourceCandidates: Object.freeze([
|
sidecarSourceCandidates: Object.freeze([
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
segments: ['bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'],
|
segments: ['bmad-fork', 'src', 'core', 'tasks', 'shard-doc.artifact.yaml'],
|
||||||
|
|
@ -71,6 +94,20 @@ const EXEMPLAR_CONVERTED_TASK_EXPORT_TARGETS = Object.freeze({
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
'index-docs': Object.freeze({
|
||||||
|
taskSourcePath: EXEMPLAR_INDEX_DOCS_TASK_XML_SOURCE_PATH,
|
||||||
|
sourcePathSuffix: '/core/tasks/index-docs.xml',
|
||||||
|
sidecarSourcePath: EXEMPLAR_INDEX_DOCS_SIDECAR_CONTRACT_SOURCE_PATH,
|
||||||
|
aliasRows: INDEX_DOCS_EXPORT_ALIAS_ROWS,
|
||||||
|
sidecarSourceCandidates: Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
segments: ['bmad-fork', 'src', 'core', 'tasks', 'index-docs.artifact.yaml'],
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
segments: ['src', 'core', 'tasks', 'index-docs.artifact.yaml'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
class CodexExportDerivationError extends Error {
|
class CodexExportDerivationError extends Error {
|
||||||
|
|
@ -412,8 +449,8 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
fieldPath: 'canonicalId',
|
fieldPath: 'canonicalId',
|
||||||
sourcePath: sidecarData.sourcePath,
|
sourcePath: sidecarData.sourcePath,
|
||||||
};
|
};
|
||||||
if (exportTarget.taskSourcePath === EXEMPLAR_SHARD_DOC_TASK_XML_SOURCE_PATH) {
|
if (Array.isArray(exportTarget.aliasRows)) {
|
||||||
aliasResolutionOptions.aliasRows = SHARD_DOC_EXPORT_ALIAS_ROWS;
|
aliasResolutionOptions.aliasRows = exportTarget.aliasRows;
|
||||||
aliasResolutionOptions.aliasTableSourcePath = '_bmad/_config/canonical-aliases.csv';
|
aliasResolutionOptions.aliasTableSourcePath = '_bmad/_config/canonical-aliases.csv';
|
||||||
}
|
}
|
||||||
canonicalResolution = await normalizeAndResolveExemplarAlias(sidecarData.canonicalId, aliasResolutionOptions);
|
canonicalResolution = await normalizeAndResolveExemplarAlias(sidecarData.canonicalId, aliasResolutionOptions);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue