feat(installer): warn on non-canonical module-help.csv headers

mergeModuleHelpCatalogs now compares each per-module file's header
against the canonical schema and emits a one-shot prompts.log.warn per
module on drift, naming both the expected and actual header. Data
continues to load positionally so external modules built against the
old after/before schema still install cleanly — the warning is the
maintainer signal to rename their columns.

Centralize the canonical header in modules/module-help-schema.js so the
merger and the synthesizer (PluginResolver._buildSynthesizedHelpCsv)
read the same source of truth; future column renames are one edit.

Verified by installing all four bmad-org external modules
(bmb, cis, gds, tea) — every one ships the legacy after/before header
today and now fires an advisory warning while still merging cleanly
into _bmad/_config/bmad-help.csv with the canonical column names.
This commit is contained in:
Alex Verkhovsky 2026-05-01 12:26:28 -07:00
parent 1e339afe51
commit 95bed595a8
3 changed files with 28 additions and 6 deletions

View File

@ -12,6 +12,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
const { InstallPaths } = require('./install-paths'); const { InstallPaths } = require('./install-paths');
const { ExternalModuleManager } = require('../modules/external-manager'); const { ExternalModuleManager } = require('../modules/external-manager');
const { resolveModuleVersion } = require('../modules/version-resolver'); const { resolveModuleVersion } = require('../modules/version-resolver');
const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
const { ExistingInstall } = require('./existing-install'); const { ExistingInstall } = require('./existing-install');
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings'); const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
@ -942,8 +943,7 @@ class Installer {
*/ */
async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) { async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
const allRows = []; const allRows = [];
const headerRow = const headerRow = MODULE_HELP_CSV_HEADER;
'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs';
const COLUMN_COUNT = 13; const COLUMN_COUNT = 13;
const PHASE_INDEX = 7; const PHASE_INDEX = 7;
@ -976,9 +976,19 @@ class Installer {
const content = await fs.readFile(helpFilePath, 'utf8'); const content = await fs.readFile(helpFilePath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#')); const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
let headerWarned = false;
for (const line of lines) { for (const line of lines) {
// Skip header row // Header row: warn on drift from canonical schema, then skip.
// Data rows are loaded positionally regardless, so the warning
// is advisory — the maintainer should rename their columns.
if (line.startsWith('module,')) { if (line.startsWith('module,')) {
if (!headerWarned && line.trim() !== headerRow) {
await prompts.log.warn(
` ${moduleName}/module-help.csv header does not match canonical schema. ` +
`Expected: ${headerRow} | Found: ${line.trim()} | Data loaded positionally.`,
);
headerWarned = true;
}
continue; continue;
} }

View File

@ -0,0 +1,13 @@
/**
* Canonical schema for per-module `module-help.csv` files.
*
* Both the merger (`Installer.mergeModuleHelpCatalogs`) and the synthesizer
* (`PluginResolver._buildSynthesizedHelpCsv`) emit this exact header. The
* merger compares each per-module file's header against this string and
* warns on drift, so any rename here must be matched in external module
* authors' CSVs (or accepted as a positional fall-through with a warning).
*/
const MODULE_HELP_CSV_HEADER =
'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs';
module.exports = { MODULE_HELP_CSV_HEADER };

View File

@ -1,6 +1,7 @@
const fs = require('../fs-native'); const fs = require('../fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const { MODULE_HELP_CSV_HEADER } = require('./module-help-schema');
/** /**
* Resolves how to install a plugin from marketplace.json by analyzing * Resolves how to install a plugin from marketplace.json by analyzing
@ -338,9 +339,7 @@ class PluginResolver {
* @returns {string} CSV content * @returns {string} CSV content
*/ */
_buildSynthesizedHelpCsv(moduleName, skillInfos) { _buildSynthesizedHelpCsv(moduleName, skillInfos) {
const header = const rows = [MODULE_HELP_CSV_HEADER];
'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs';
const rows = [header];
for (const info of skillInfos) { for (const info of skillInfos) {
const displayName = this._formatDisplayName(info.name || info.dirName); const displayName = this._formatDisplayName(info.name || info.dirName);