feedback5 WW: split DESIGN.md into docs/design/ (the current rules) and docs/history/ (the per-release revisions), section numbers kept — a pure move: every ## N. block (heading, text, the --- after it) moves verbatim, each part file only gains a title line and a pointer to the index; docs/DESIGN.md keeps its header and becomes the index (a § → file table). design/overview.md §0 §1 §2 §11 §12, geometry-time.md §3 §4, engine.md §5 §7, match.md §6 §16, network.md §8 §14, client.md §9 §10 §13 §15; history/v2.2-playtest3.md §17, v2.3-playtest4.md §18, v2.4-playtest5.md §19, v2.5-playtest6.md §20, 0.1.1.md §21, 0.1.2.md §22, 0.1.3.md §23, 0.1.4.md §24, 0.2.0.md §25. Round trip: the index header plus the blocks in § order reproduce the pre-split DESIGN.md byte for byte (813 785 bytes, sha256 fe81884c…). Tests: test/helpers/designDocs.js designText() (the split script's splitBlocks / designText) reassembles the text in § order; docs-consistency and feedback1-gaps read it instead of docs/DESIGN.md (every pin unchanged); docs-paths guards the split (each § exactly once in docs/design/ or docs/history/, the index row of every file names its sections, every "DESIGN §N(.M)" cited in the repository is a heading — 779 in 327 files today); package.test keeps docs/design/ and docs/history/ out of the zips; golden unchanged

This commit is contained in:
sganggs committed 2026-10-06 19:08:44 +08:00
1 parent 0f9009954d
commit da1f5830ca
21 files changed
+3517 -3325

No files matched your search

+58 -1
View File
@@ -2,11 +2,16 @@
// (inline code, the diagrams and commands in fenced blocks) and every relative Markdown link. A path with a
// placeholder (`<codename>`, `{a,b}`) or a glob (`*`) is checked up to its last fixed directory; a path ending in `/`
// must be a directory. Outputs that a fresh checkout does not have (downloaded art, vendored libraries) are exempt.
// The design document (0.2.0 split): docs/DESIGN.md is the index, every `## N.` section sits exactly once in
// docs/design/ (the current rules) or docs/history/ (the per-release revisions), the index names each file with its
// sections, and every "DESIGN §N" / "DESIGN §N.M" cited anywhere in the repository is a heading of it.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, normalize } from 'node:path';
import { fileURLToPath } from 'node:url';
import { designText, DESIGN_DIRS } from './helpers/designDocs.js';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
/** Each document with the least number of paths it names (a guard against an extraction that finds nothing). */
@@ -84,3 +89,55 @@ test('the path extraction: placeholders, globs, directories and the exemptions',
assert.equal(missing('server/no-such-file.js'), 'server/no-such-file.js: not found');
assert.equal(missing('server/index.js/'), 'server/index.js/: not a directory');
});
/** The `## N.` section headings of a Markdown text. */
const sectionsOf = (md) => [...md.matchAll(/^## (\d+)\./gm)].map((m) => Number(m[1]));
test('DESIGN: every section is a `## N.` heading exactly once in docs/design/ or docs/history/, and the index names its file', () => {
const index = readFileSync(join(ROOT, 'docs/DESIGN.md'), 'utf8');
const where = new Map(); // § → the file that holds it
for (const dir of DESIGN_DIRS) {
for (const f of readdirSync(join(ROOT, dir)).filter((x) => x.endsWith('.md')).sort()) {
const file = `${dir}/${f}`;
const md = readFileSync(join(ROOT, file), 'utf8');
assert.match(md, /^# DESIGN §\d+(?:, §\d+)* — .+\n\nPart of \[DESIGN\.md\]\(\.\.\/DESIGN\.md\)/, `${file}: its title and the pointer to the index`);
for (const n of sectionsOf(md)) {
assert.ok(!where.has(n), `§${n} is in ${where.get(n)} and in ${file}`);
where.set(n, file);
}
}
}
assert.deepEqual(sectionsOf(index), [], 'the index holds no section of its own');
const max = Math.max(...where.keys());
assert.ok(max >= 25, `sections up to §${max}`);
for (let n = 0; n <= max; n++) assert.ok(where.has(n), `§${n} has a file`);
// the index's table: one row per file, naming exactly the sections that file holds, with a link that resolves
const listed = new Map();
for (const [, secs, text, href] of index.matchAll(/^\| ((?:§\d+(?:, )?)+) \| \[([^\]]+)\]\(([^)]+)\) \|/gm)) {
const file = `docs/${href}`;
assert.equal(text, href, `${file}: the link text is its path`);
assert.ok(existsSync(join(ROOT, file)), `${file} exists`);
const ns = secs.split(', ').map((x) => Number(x.slice(1)));
const held = [...where].filter(([, f]) => f === file).map(([n]) => n).sort((a, b) => a - b);
assert.deepEqual(ns, held, `${file}: the index row names the sections it holds`);
for (const n of ns) listed.set(n, file);
}
assert.equal(listed.size, where.size, 'the index names every section');
});
test('DESIGN: every "DESIGN §N" cited in the repository (code comments, tests, docs, data) is a heading of the design document', () => {
const heads = new Set([...designText(ROOT).matchAll(/^#{2,4} (\d+(?:\.\d+)*)[. ]/gm)].map((m) => m[1]));
const files = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { cwd: ROOT, encoding: 'utf8' })
.split('\n').filter((f) => /\.(?:m?js|cjs|md|json|html|css|ya?ml|sh|bat|ps1|py|txt)$/.test(f) && !f.startsWith('test/golden/'));
let cited = 0;
const bad = [];
for (const f of files) {
if (!existsSync(join(ROOT, f))) continue; // deleted in the working tree, not yet in the index
for (const m of readFileSync(join(ROOT, f), 'utf8').matchAll(/DESIGN(?:\.md)?[ ,]*§ ?(\d+(?:\.\d+)*)/g)) {
cited++;
if (!heads.has(m[1])) bad.push(`${f}: §${m[1]}`);
}
}
assert.ok(cited >= 700, `only ${cited} citations found — is the scan broken?`);
assert.deepEqual(bad, [], 'citations of a section or subsection with no heading');
});