From b01b3de74126501c96cca6965d412329aaf99b44 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Tue, 19 May 2026 10:11:45 +0200 Subject: [PATCH] =?UTF-8?q?tools:=20add=20wiki-gen=20=E2=80=94=20regenerat?= =?UTF-8?q?es=20topic-contract=20AUTOGEN=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generates the markdown table inside blocks in nodes//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 '' 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) --- tools/wiki-gen/README.md | 52 ++++++++++++ tools/wiki-gen/bin/wiki-gen.js | 150 +++++++++++++++++++++++++++++++++ tools/wiki-gen/package.json | 13 +++ 3 files changed, 215 insertions(+) create mode 100644 tools/wiki-gen/README.md create mode 100644 tools/wiki-gen/bin/wiki-gen.js create mode 100644 tools/wiki-gen/package.json diff --git a/tools/wiki-gen/README.md b/tools/wiki-gen/README.md new file mode 100644 index 0000000..da0354f --- /dev/null +++ b/tools/wiki-gen/README.md @@ -0,0 +1,52 @@ +# @evolv/wiki-gen + +Generate the AUTOGEN sections of per-node wikis from the source of truth +(`nodes//src/commands/index.js`). + +## What it generates + +Replaces content between these markers: + +```markdown + +... wiki-gen overwrites this block ... + +``` + +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. diff --git a/tools/wiki-gen/bin/wiki-gen.js b/tools/wiki-gen/bin/wiki-gen.js new file mode 100644 index 0000000..8ea51bb --- /dev/null +++ b/tools/wiki-gen/bin/wiki-gen.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const TOPIC_BEGIN = //; +const ANY_END = //g; +const CANONICAL_BEGIN = ''; +const CANONICAL_END = ''; + +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 }; diff --git a/tools/wiki-gen/package.json b/tools/wiki-gen/package.json new file mode 100644 index 0000000..c48f5aa --- /dev/null +++ b/tools/wiki-gen/package.json @@ -0,0 +1,13 @@ +{ + "name": "@evolv/wiki-gen", + "version": "0.1.0", + "private": true, + "description": "Generate the AUTOGEN sections of nodes//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" +}