Generates the markdown table inside <!-- BEGIN AUTOGEN: topic-contract --> blocks in nodes/<n>/wiki/Reference-Contracts.md from the canonical registry at src/commands/index.js. Replaces the agent-written placeholders the wiki uplift left behind. - Accepts both labelled and unlabelled END markers; rewrites to canonical '<!-- END AUTOGEN: topic-contract -->' on regeneration so future runs are consistent. - --check mode for CI (exit 1 if any block is out of date). - Out of scope for now: data-model AUTOGEN block (requires instantiating the domain; the 9 agent-written placeholders for that block stay until a follow-up tool lands). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
5.2 KiB
JavaScript
151 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const TOPIC_BEGIN = /<!--\s*BEGIN AUTOGEN:\s*topic-contract.*?-->/;
|
|
const ANY_END = /<!--\s*END AUTOGEN(?::\s*[a-z0-9-]+)?\s*-->/g;
|
|
const CANONICAL_BEGIN = '<!-- BEGIN AUTOGEN: topic-contract -->';
|
|
const CANONICAL_END = '<!-- END AUTOGEN: topic-contract -->';
|
|
|
|
function loadRegistry(nodeDir) {
|
|
const registryPath = path.join(nodeDir, 'src/commands/index.js');
|
|
if (!fs.existsSync(registryPath)) return null;
|
|
delete require.cache[require.resolve(registryPath)];
|
|
const descriptors = require(registryPath);
|
|
if (!Array.isArray(descriptors)) {
|
|
throw new Error('commands/index.js must export an array of descriptors');
|
|
}
|
|
return descriptors;
|
|
}
|
|
|
|
function renderTopicTable(descriptors) {
|
|
const lines = [];
|
|
lines.push('| Canonical topic | Aliases | Payload | Unit | Effect |');
|
|
lines.push('|---|---|---|---|---|');
|
|
for (const d of descriptors) {
|
|
if (!d || typeof d.topic !== 'string') continue;
|
|
const canonical = '`' + d.topic + '`';
|
|
const aliases = (Array.isArray(d.aliases) && d.aliases.length)
|
|
? d.aliases.map((a) => '`' + a + '`').join(', ')
|
|
: '—';
|
|
const payload = renderPayload(d.payloadSchema);
|
|
const unit = renderUnit(d.units);
|
|
const effect = (d.description || '').replace(/\|/g, '\\|').trim() || '—';
|
|
lines.push(`| ${canonical} | ${aliases} | ${payload} | ${unit} | ${effect} |`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function renderPayload(schema) {
|
|
if (!schema) return '—';
|
|
if (typeof schema === 'string') return '`' + schema + '`';
|
|
if (typeof schema !== 'object') return '—';
|
|
if (schema.type === 'any' || schema.type === undefined) return 'any';
|
|
if (typeof schema.type === 'string') return '`' + schema.type + '`';
|
|
return '—';
|
|
}
|
|
|
|
function renderUnit(units) {
|
|
if (!units || typeof units !== 'object') return '—';
|
|
const m = units.measure;
|
|
const d = units.default;
|
|
if (!m && !d) return '—';
|
|
if (m && d) return '`' + m + '` (default `' + d + '`)';
|
|
return '`' + (m || d) + '`';
|
|
}
|
|
|
|
function regenerateFile(filePath, replacement) {
|
|
if (!fs.existsSync(filePath)) return { changed: false, reason: 'file not found' };
|
|
const src = fs.readFileSync(filePath, 'utf8');
|
|
const beginMatch = src.match(TOPIC_BEGIN);
|
|
if (!beginMatch) return { changed: false, reason: 'no AUTOGEN markers' };
|
|
const afterBegin = beginMatch.index + beginMatch[0].length;
|
|
ANY_END.lastIndex = afterBegin;
|
|
const endMatch = ANY_END.exec(src);
|
|
if (!endMatch) return { changed: false, reason: 'BEGIN marker has no matching END' };
|
|
const endStart = endMatch.index;
|
|
const endStop = endStart + endMatch[0].length;
|
|
const block = `${CANONICAL_BEGIN}\n\n${replacement}\n\n${CANONICAL_END}`;
|
|
const next = src.slice(0, beginMatch.index) + block + src.slice(endStop);
|
|
if (next === src) return { changed: false, reason: 'already up to date' };
|
|
fs.writeFileSync(filePath, next, 'utf8');
|
|
return { changed: true };
|
|
}
|
|
|
|
function regenerateNode(nodeDir, opts) {
|
|
const nodeName = path.basename(nodeDir);
|
|
const descriptors = loadRegistry(nodeDir);
|
|
if (!descriptors) return { node: nodeName, skipped: true, reason: 'no commands/index.js' };
|
|
const table = renderTopicTable(descriptors);
|
|
const targets = [
|
|
path.join(nodeDir, 'wiki/Reference-Contracts.md'),
|
|
path.join(nodeDir, 'wiki/Home.md'),
|
|
];
|
|
const results = [];
|
|
for (const target of targets) {
|
|
const out = regenerateFile(target, table);
|
|
results.push({ file: path.relative(nodeDir, target), ...out });
|
|
}
|
|
return { node: nodeName, results };
|
|
}
|
|
|
|
function findNodes(repoRoot) {
|
|
const nodesDir = path.join(repoRoot, 'nodes');
|
|
return fs.readdirSync(nodesDir)
|
|
.filter((n) => fs.existsSync(path.join(nodesDir, n, 'src/commands/index.js')))
|
|
.map((n) => path.join(nodesDir, n));
|
|
}
|
|
|
|
function report(result, json) {
|
|
if (json) {
|
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
return;
|
|
}
|
|
if (result.skipped) {
|
|
process.stdout.write(`SKIP ${result.node} (${result.reason})\n`);
|
|
return;
|
|
}
|
|
for (const r of result.results) {
|
|
const tag = r.changed ? 'UPDATE' : (r.reason === 'no AUTOGEN markers' ? 'NO-MARK' : 'OK');
|
|
process.stdout.write(`${tag.padEnd(7)} ${result.node}/${r.file}${r.reason ? ' — ' + r.reason : ''}\n`);
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const json = args.includes('--json');
|
|
const check = args.includes('--check');
|
|
const positional = args.filter((a) => !a.startsWith('--'));
|
|
const repoRoot = path.resolve(__dirname, '../../..');
|
|
const targets = positional.length === 0
|
|
? findNodes(repoRoot)
|
|
: positional.map((p) => path.resolve(p));
|
|
let drift = false;
|
|
for (const nodeDir of targets) {
|
|
let res;
|
|
try { res = regenerateNode(nodeDir, { check }); }
|
|
catch (err) {
|
|
process.stderr.write(`[${path.basename(nodeDir)}] ERROR: ${err.message}\n`);
|
|
drift = true;
|
|
continue;
|
|
}
|
|
if (!res.skipped && check) {
|
|
for (const r of res.results) {
|
|
if (r.changed) drift = true;
|
|
}
|
|
}
|
|
report(res, json);
|
|
}
|
|
if (check && drift) {
|
|
process.stderr.write('\nAUTOGEN blocks are out of date. Run without --check to regenerate.\n');
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
|
|
module.exports = { renderTopicTable, regenerateFile, loadRegistry };
|