Compare commits
2 Commits
e15f402d47
...
ecd466f7a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecd466f7a3 | ||
|
|
b01b3de741 |
Submodule nodes/dashboardAPI updated: a9fc51d6f0...fb5a9ebff8
Submodule nodes/diffuser updated: 8c03fe774c...37ecfe5726
Submodule nodes/measurement updated: 1a16f9c4f1...b0e8bbb95d
Submodule nodes/monster updated: 76951f104d...aff866bd9b
Submodule nodes/reactor updated: cb49bb8b4d...6b8ae5cfc3
Submodule nodes/settler updated: d54cb66105...93ea000734
Submodule nodes/valve updated: 87214788d2...144460e6ba
Submodule nodes/valveGroupControl updated: 9552e4fba9...d81aedc9bc
52
tools/wiki-gen/README.md
Normal file
52
tools/wiki-gen/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# @evolv/wiki-gen
|
||||
|
||||
Generate the AUTOGEN sections of per-node wikis from the source of truth
|
||||
(`nodes/<n>/src/commands/index.js`).
|
||||
|
||||
## What it generates
|
||||
|
||||
Replaces content between these markers:
|
||||
|
||||
```markdown
|
||||
<!-- BEGIN AUTOGEN: topic-contract — populate via wiki-gen tool (TODO) -->
|
||||
... wiki-gen overwrites this block ...
|
||||
<!-- END AUTOGEN: topic-contract -->
|
||||
```
|
||||
|
||||
The 9 wikis uplifted in 2026-05 carry these markers in
|
||||
`wiki/Reference-Contracts.md` (and some in `wiki/Home.md`); `wiki-gen`
|
||||
keeps them in sync with the registry.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# regenerate every node
|
||||
node tools/wiki-gen/bin/wiki-gen.js
|
||||
|
||||
# one node
|
||||
node tools/wiki-gen/bin/wiki-gen.js nodes/rotatingMachine
|
||||
|
||||
# CI check: fail if any AUTOGEN block is out of date
|
||||
node tools/wiki-gen/bin/wiki-gen.js --check
|
||||
```
|
||||
|
||||
## What it writes
|
||||
|
||||
For each registry descriptor:
|
||||
|
||||
| Column | Source |
|
||||
|---|---|
|
||||
| Canonical topic | `descriptor.topic` |
|
||||
| Aliases | `descriptor.aliases` (deprecation candidates) |
|
||||
| Payload | `descriptor.payloadSchema` |
|
||||
| Unit | `descriptor.units.measure` + `descriptor.units.default` (or `—`) |
|
||||
| Effect | `descriptor.description` |
|
||||
|
||||
## Out of scope (for now)
|
||||
|
||||
- The `data-model` AUTOGEN block (sample of `getOutput()`) — requires
|
||||
instantiating the domain class, which depends on `generalFunctions`.
|
||||
The 9 wikis carry hand-written placeholders inside those markers;
|
||||
upgrading to runtime sampling is a follow-up.
|
||||
|
||||
Run after touching `src/commands/index.js` in any node, or as a CI gate.
|
||||
150
tools/wiki-gen/bin/wiki-gen.js
Normal file
150
tools/wiki-gen/bin/wiki-gen.js
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/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 };
|
||||
13
tools/wiki-gen/package.json
Normal file
13
tools/wiki-gen/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@evolv/wiki-gen",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Generate the AUTOGEN sections of nodes/<n>/wiki/Reference-Contracts.md from commands/index.js",
|
||||
"bin": {
|
||||
"evolv-wiki-gen": "bin/wiki-gen.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test test/*.test.js"
|
||||
},
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
Reference in New Issue
Block a user