A cold agent was given this repo and the documents it links to, and nothing else — no core source, no module-uo — and asked to build a module for a second game. It did, in one pass. The record is docs/modules/kit-acceptance.md; this is the repair list, plus the two things it recommended that were not defects. The one it could not find, because it had no core to render against: a module page built exactly as this kit teaches renders OUTSIDE the site. PublicLayout is the chrome, not the body. Core grew an opt-in `shell` prop for it (MODULE_API_VERSION 1.5.0, website#148); the template passes shell="narrow" and chapter 2 explains why you name a width and never a class. Fixed: - **F1, and the worst of them, because it lands in the first twenty minutes.** `npm run check:swagger` failed on a PRISTINE template on Windows: the check compared the committed fragment byte-for-byte and a default Windows clone is CRLF while the generator writes LF. The message blamed "the routes or their annotations". Now `template/.gitattributes` pins `eol=lf` and the comparison normalises line endings anyway — a check may only fail for the reason it names, and this one names a diagnosis. - **F3** — `.gitea/workflows/release.yml` carries `gitea.example.com` and `your-org/your-module` under a literal `# CHANGE THESE`, was not in the rename checklist, and `checkRenameSites.js` could not match it, so CI was silent by construction. Row added, pattern widened. (The agent reported both workflow flavours; only the Gitea one is affected — GitHub supplies its own variables. Corrected in the record.) The near-miss is kept in the check's comments and its suite: the obvious widening is `example\.com`, which fires on a fixture URL in checkImports.test.js. Every alternative has to be a string that cannot occur by accident, which is the same rule that made the id `examplegame`. - **F4** — the release bundle's include list was hardcoded, so adding `server/utils/` would have silently dropped it from every release while the bundle check stayed green. Inverted to an exclusion list, in both flavours, and run by hand because a release workflow never executes in CI. - **F5** — the annotation-quoting warning was wrong in both directions, and the correction is measured rather than reasoned. A backtick is harmless (the template's own description has two spans and they survive). A `"` is not, and it does not throw: `'A "quoted" status'` is silently TRUNCATED to `A "` while swagger-autogen prints Success and the error capture sees nothing. The only signal is check:swagger blaming your routes. - **F6** — `template/.gitignore`, so a copied template that is `git init`ed inherits ignore rules instead of nothing. - **F7** — the UI kit is eight exports across five rows, not seven. The contract said seven and this kit had faithfully carried the miscount out of it. Adopted, not defects: - Chapter 1 now says to run every check on the untouched copy first. That is what found F1; without a baseline the first failure is ambiguous forever. - The template ships the §2.7 self-check the agent wrote for itself. The rule has no CI in general — an outbound socket is not statically detectable — but a module can make a decidable claim about its own tree. Ported from its code with a header explaining how to NARROW it when a sidecar client arrives, since talking to your sidecar is the expected shape and is not what §2.7 forbids. The pin moves to website edge 4ad8b2b, the 1.5.0 bump, and template/module.json declares ^1.5.0 — so checkCoreApi's equality assertion still holds and the template uses a member that exists only at that ref and later. 32 server + 18 client template tests, 21 kit-script tests, all four checks green. Co-Authored-By: Claude <noreply@anthropic.com>
161 lines
6.6 KiB
JavaScript
161 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// The rename checklist in `template/README.md`, checked against the tree.
|
|
//
|
|
// A reader's first action is to copy `template/` and make it theirs, and the only
|
|
// thing telling them where the placeholder name is buried is that table. A
|
|
// checklist nobody verifies is wrong by the second edit to the template — someone
|
|
// adds a file, mentions the placeholder id in it, and every reader after that
|
|
// ships a module with a stray `examplegame` in its OpenAPI tags.
|
|
//
|
|
// So this asserts the table and the tree agree, in BOTH directions:
|
|
//
|
|
// • every file that still mentions the placeholder is listed, and
|
|
// • every listed file exists and still mentions it.
|
|
//
|
|
// The second half is the one that is easy to leave out and is the more valuable:
|
|
// an entry that has stopped matching is an entry that will be read as instructions
|
|
// to edit something that is not there. Same rule the identifier check in core's CI
|
|
// follows about its own exemptions — an exemption that no longer matches fails the
|
|
// build rather than being quietly tolerated.
|
|
//
|
|
// **Why the placeholder is `examplegame` and not `example`.** This is a whole-file
|
|
// text search, and `example` appears in ordinary English ("for example") all over
|
|
// prose that is not a rename site at all. A placeholder that cannot occur by
|
|
// accident is what makes a check like this answerable rather than a source of
|
|
// false alarms someone eventually learns to ignore.
|
|
//
|
|
// Usage: node scripts/checkRenameSites.js (from the repo root)
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const ROOT = path.resolve(__dirname, '..')
|
|
const TEMPLATE = path.join(ROOT, 'template')
|
|
const CHECKLIST = path.join(TEMPLATE, 'README.md')
|
|
|
|
// Anything a rename has to touch: the id (`examplegame`), the display name
|
|
// ("Example Game"), the placeholder world ("Example World"), and the two
|
|
// publishing placeholders in the Gitea release workflow (`gitea.example.com`,
|
|
// `your-org/your-module`). One pattern rather than four, because they are one
|
|
// decision — everything a reader must change before this template is theirs.
|
|
//
|
|
// **Every alternative has to be a string that cannot occur by accident**, which
|
|
// is the same rule that made the id `examplegame` rather than `example` (see the
|
|
// header). The publishing pair was added after the acceptance run found the
|
|
// release workflow carrying `# CHANGE THESE` placeholders that the checklist did
|
|
// not list and this pattern could not see — CI silent by construction
|
|
// (docs/modules/kit-acceptance.md, F3).
|
|
//
|
|
// The near-miss is worth keeping: the obvious widening is `example\.com`, and it
|
|
// is WRONG. `server/test/checkImports.test.js` uses `https://example.com/x` as a
|
|
// fixture — a URL in a string, testing that a URL in a string is not an import —
|
|
// and it is not a rename site. The host is matched in full instead.
|
|
const PLACEHOLDER = /example[ -]?(game|world)|gitea\.example\.com|your-(org|module)/i
|
|
|
|
// Directories with nothing of ours in them. `dist` and `node_modules` are build
|
|
// output — a chunk full of the placeholder is not a rename site, it is the
|
|
// consequence of one.
|
|
const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist'])
|
|
|
|
// The checklist is the one file exempt from the scan: it is a table OF the
|
|
// placeholder and would trivially list itself.
|
|
const SELF = 'README.md'
|
|
|
|
/** Every file under `template/`, template-relative, sorted. */
|
|
function templateFiles(dir = TEMPLATE, out = []) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.isDirectory()) {
|
|
if (SKIP_DIRS.has(entry.name)) continue
|
|
templateFiles(path.join(dir, entry.name), out)
|
|
} else if (entry.isFile()) {
|
|
out.push(path.relative(TEMPLATE, path.join(dir, entry.name)).split(path.sep).join('/'))
|
|
}
|
|
}
|
|
return out.sort()
|
|
}
|
|
|
|
/**
|
|
* The paths the checklist names, read from between its two markers.
|
|
*
|
|
* Delimited by explicit HTML comments rather than by looking for a heading or for
|
|
* every backticked path in the document: the README quotes plenty of paths in
|
|
* prose and in its tree diagram, and none of those are checklist entries. An
|
|
* explicit marker also means the table can be reformatted freely.
|
|
*/
|
|
function checklistPaths(markdown) {
|
|
const start = markdown.indexOf('<!-- rename-sites -->')
|
|
const end = markdown.indexOf('<!-- /rename-sites -->')
|
|
if (start === -1 || end === -1 || end < start) {
|
|
throw new Error(
|
|
'template/README.md has no <!-- rename-sites --> … <!-- /rename-sites --> block. ' +
|
|
'That block is the checklist this check exists to verify.',
|
|
)
|
|
}
|
|
const table = markdown.slice(start, end)
|
|
const paths = []
|
|
for (const line of table.split('\n')) {
|
|
// A table row whose first cell is a backticked path.
|
|
const match = /^\|\s*`([^`]+)`\s*\|/.exec(line.trim())
|
|
if (match) paths.push(match[1])
|
|
}
|
|
return paths
|
|
}
|
|
|
|
/** Everything wrong, as sentences. Empty means the checklist is current. */
|
|
function problems({ files, listed, contains }) {
|
|
const out = []
|
|
const listedSet = new Set(listed)
|
|
|
|
const duplicates = listed.filter((p, i) => listed.indexOf(p) !== i)
|
|
for (const p of new Set(duplicates)) out.push(`${p} is listed in the checklist twice.`)
|
|
|
|
for (const file of files) {
|
|
if (file === SELF) continue
|
|
if (!contains(file)) continue
|
|
if (!listedSet.has(file)) {
|
|
out.push(
|
|
`${file} still mentions the placeholder and is NOT in the rename checklist. ` +
|
|
'Add a row for it, or take the placeholder out of the file.',
|
|
)
|
|
}
|
|
}
|
|
|
|
const present = new Set(files)
|
|
for (const file of listed) {
|
|
if (!present.has(file)) {
|
|
out.push(`the checklist lists ${file}, which does not exist. Remove the row or restore the file.`)
|
|
} else if (!contains(file)) {
|
|
out.push(
|
|
`the checklist lists ${file}, which no longer mentions the placeholder. ` +
|
|
'A row that has stopped matching tells a reader to edit something that is not there.',
|
|
)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
module.exports = { PLACEHOLDER, checklistPaths, problems, templateFiles, TEMPLATE }
|
|
|
|
if (require.main !== module) return
|
|
|
|
if (!fs.existsSync(TEMPLATE)) {
|
|
console.log('checkRenameSites: no template/ yet — nothing to check')
|
|
process.exit(0)
|
|
}
|
|
|
|
const files = templateFiles()
|
|
const listed = checklistPaths(fs.readFileSync(CHECKLIST, 'utf8'))
|
|
const contains = (file) => PLACEHOLDER.test(fs.readFileSync(path.join(TEMPLATE, file), 'utf8'))
|
|
|
|
const found = problems({ files, listed, contains })
|
|
|
|
if (found.length) {
|
|
console.error(`\n${found.length} problem(s) with the rename checklist in template/README.md:\n`)
|
|
for (const p of found) console.error(` - ${p}`)
|
|
console.error('')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`OK — the rename checklist matches the template (${listed.length} files).`)
|