|
50 | 50 | import { createHash } from 'node:crypto'; |
51 | 51 | import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
52 | 52 | import { tmpdir } from 'node:os'; |
53 | | -import { dirname, join } from 'node:path'; |
| 53 | +import { dirname, join, sep } from 'node:path'; |
54 | 54 | import { fileURLToPath } from 'node:url'; |
55 | 55 |
|
56 | 56 | import { isEntrypoint } from './invoked-as.mjs'; |
57 | 57 |
|
58 | 58 | const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); |
59 | 59 |
|
| 60 | +/** The three files `checkTree` opens, repo-relative. */ |
| 61 | +const READ_PATHS = { |
| 62 | + artefact: 'sdui.manifest.json', |
| 63 | + record: join('scripts', 'sdui-manifest.record.json'), |
| 64 | + pin: '.objectui-sha', |
| 65 | +}; |
| 66 | + |
| 67 | +/** |
| 68 | + * The population this gate reads, declared for `scripts/pm/dispatch-gates.mjs`. |
| 69 | + * |
| 70 | + * `checkTree` opens exactly three files. None of them was visible to the |
| 71 | + * derivation: it reads SOURCE TEXT, and a literal with no separator is refused |
| 72 | + * by `hintCovers` as too generic (`sdui.manifest.json`), while `.objectui-sha` |
| 73 | + * is not one of the dot-prefixed names the extractor admits and the record path |
| 74 | + * is assembled with `join()`. So both of this gate's CI invocations — the scan |
| 75 | + * and its `--self-test` — were scored `undetermined` for EVERY card, absent from |
| 76 | + * every dispatch brief and every `--commands` harvest, while CI ran them on each |
| 77 | + * pull request. That cost is sharpest here: the cards that implicate this gate |
| 78 | + * are exactly the ones that move the objectui pin or regenerate the manifest. |
| 79 | + * |
| 80 | + * `ROOT_WATCH_HINTS` is the mixed-roots spelling of the idiom, which is what |
| 81 | + * this population is: two repo-ROOT files and one under `scripts/`. The |
| 82 | + * repo-root pair carries the `/**` suffix because a bare single-segment literal |
| 83 | + * is refused; the collapse reduces each one back to the single file it names and |
| 84 | + * to nothing else. |
| 85 | + * |
| 86 | + * ⛔ Not a whole-tree marker, and not `scripts/**`: three files are three files. |
| 87 | + * |
| 88 | + * The self-test derives the coupling from `READ_PATHS` — the same object |
| 89 | + * `checkTree` reads — so a moved or added read reds here rather than leaving the |
| 90 | + * declaration describing the old population. |
| 91 | + */ |
| 92 | +const ROOT_WATCH_HINTS = [ |
| 93 | + 'sdui.manifest.json/**', |
| 94 | + 'scripts/sdui-manifest.record.json', |
| 95 | + '.objectui-sha/**', |
| 96 | +]; |
| 97 | + |
60 | 98 | /** Gate one tree. Returns a list of problems; empty = green. */ |
61 | 99 | export function checkTree(root) { |
62 | 100 | const problems = []; |
63 | | - const artefactPath = join(root, 'sdui.manifest.json'); |
64 | | - const recordPath = join(root, 'scripts', 'sdui-manifest.record.json'); |
65 | | - const pinPath = join(root, '.objectui-sha'); |
| 101 | + const artefactPath = join(root, READ_PATHS.artefact); |
| 102 | + const recordPath = join(root, READ_PATHS.record); |
| 103 | + const pinPath = join(root, READ_PATHS.pin); |
66 | 104 |
|
67 | 105 | if (!existsSync(artefactPath)) { |
68 | 106 | problems.push( |
@@ -247,6 +285,26 @@ function selfTest() { |
247 | 285 | for (const p of problems) console.error(` ${p}`); |
248 | 286 | } |
249 | 287 | } |
| 288 | + // ── The dispatch-gates population declaration ────────────────────────── |
| 289 | + // Filed outside the cases table, deliberately: each table ROW is a declared |
| 290 | + // battery here, so an assertion added as a row would owe a roster entry for a |
| 291 | + // case that gates nothing about `checkTree`'s verdicts. |
| 292 | + const declFail = (message) => { console.error(`✗ self-test: ${message}`); failures++; }; |
| 293 | + const readPosix = Object.values(READ_PATHS).map((f) => f.split(sep).join('/')); |
| 294 | + const declared = ROOT_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, '')); |
| 295 | + if (!readPosix.every((f) => declared.includes(f))) { |
| 296 | + declFail('a file checkTree reads is not declared for dispatch-gates — an unreadable literal is how ' |
| 297 | + + 'this gate came to declare nothing at all, and it scores `undetermined` for every card again.'); |
| 298 | + } |
| 299 | + if (!declared.every((h) => readPosix.includes(h))) { |
| 300 | + declFail('ROOT_WATCH_HINTS declares a path this gate does not read — a declaration that has drifted ' |
| 301 | + + 'from the reads replaces a silent gate with a lying one.'); |
| 302 | + } |
| 303 | + if (!ROOT_WATCH_HINTS.filter((h) => !h.replace(/\/\*+$/, '').includes('/')).every((h) => h.endsWith('/**'))) { |
| 304 | + declFail('a repo-ROOT file is declared without the subtree suffix — a bare single-segment literal is ' |
| 305 | + + 'refused by hintCovers as too generic, so it would contribute no hint at all.'); |
| 306 | + } |
| 307 | + |
250 | 308 | // ── The floor: every declared row RAN, and ran its case (#13489) ─────── |
251 | 309 | // |
252 | 310 | // Evaluated after every row has had its chance and BEFORE the verdict, so the |
|
0 commit comments