clearotron 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.env.example +36 -37
  2. package/CONTRIBUTING.md +8 -4
  3. package/INSTALL.md +24 -4
  4. package/README.md +7 -6
  5. package/bin/example.mjs +6 -5
  6. package/bin/onboard.mjs +175 -9
  7. package/bin/start.mjs +66 -4
  8. package/build-info.json +2 -2
  9. package/demo/README.md +1 -1
  10. package/docs/GLOSSARY.md +85 -0
  11. package/docs/README.md +1 -0
  12. package/docs/architecture/01-product-overview.md +21 -9
  13. package/docs/architecture/05-config-governance.md +5 -0
  14. package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
  15. package/driver/CHANGELOG.md +34 -0
  16. package/driver/README.md +25 -6
  17. package/driver/connotation-search.mjs +1 -1
  18. package/driver/contract-audit.mjs +5 -1
  19. package/driver/contract-e3-baseline.json +11 -11
  20. package/driver/doubt-selection.mjs +1 -1
  21. package/driver/drainer-identity.mjs +1 -1
  22. package/driver/effort-model.mjs +2 -2
  23. package/driver/engine/probe.mjs +45 -5
  24. package/driver/gateway.mjs +2 -2
  25. package/driver/outbox-backoff.mjs +1 -1
  26. package/driver/package.json +1 -1
  27. package/driver/pipeline.mjs +206 -101
  28. package/driver/plain-register.mjs +16 -2
  29. package/driver/portal-config-view.mjs +30 -1
  30. package/driver/portal-local-auth.mjs +5 -1
  31. package/driver/portal-service.mjs +53 -2
  32. package/driver/predelivery-lint.mjs +54 -25
  33. package/driver/publish/render.mjs +109 -14
  34. package/driver/search-policy.mjs +1 -1
  35. package/driver/stage-context.mjs +13 -0
  36. package/driver/stages.mjs +51 -4
  37. package/driver/suite-census.json +97 -31
  38. package/driver/systemd/clearotron-worker.service +3 -3
  39. package/driver/tokens.mjs +1 -1
  40. package/driver/unit-inventory.mjs +34 -4
  41. package/mcp-server/CHANGELOG.md +2 -0
  42. package/mcp-server/package.json +1 -1
  43. package/package.json +4 -10
  44. package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
  45. package/portal-ui/dist/index.html +1 -1
  46. package/portal-ui/package.json +1 -1
  47. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  48. package/providers/oauth-mcp-bridge/package.json +1 -1
  49. package/scripts/added-reference-check.mjs +45 -89
  50. package/scripts/e2e.mjs +2 -2
  51. package/scripts/generated-files-are-current.mjs +125 -0
  52. package/scripts/mint-names-in-force.mjs +4 -2
  53. package/scripts/mint-public-residue.mjs +74 -0
  54. package/scripts/mint-reference-strip-backlog.mjs +12 -1
  55. package/scripts/mint-suite-census.mjs +5 -2
  56. package/scripts/render-check.mjs +13 -2
  57. package/scripts/report-frame-check.mjs +8 -1
  58. package/scripts/report-screenshot.mjs +6 -3
  59. package/scripts/revisit-render-check.mjs +6 -3
  60. package/scripts/score.mjs +1 -1
  61. package/scripts/strip-tracker-citations.mjs +9 -1
  62. package/scripts/test-full.mjs +226 -0
  63. package/shared/README.md +2 -1
  64. package/shared/browser-temp-root.mjs +142 -0
  65. package/shared/checkout-move.mjs +1 -1
  66. package/shared/client-door.mjs +4 -3
  67. package/shared/identifier-scan.mjs +1 -1
  68. package/shared/install-auth.mjs +35 -0
  69. package/shared/invocation.mjs +5 -1
  70. package/shared/listen.mjs +55 -3
  71. package/shared/names-in-force.mjs +4 -1
  72. package/shared/os-advice.mjs +91 -0
  73. package/shared/reap-on-exit.mjs +42 -0
  74. package/shared/reference-guard-classes.mjs +351 -0
  75. package/shared/suite-census.mjs +31 -3
  76. package/shared/withheld-paths-access.mjs +36 -18
@@ -0,0 +1,351 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // WHAT A PUBLIC TREE MUST NOT ACQUIRE, AS ONE TABLE.
5
+ //
6
+ // `scripts/added-reference-check.mjs` refuses these in a diff, so a pull request cannot add one. The
7
+ // floor beside `driver/test/fixtures/public-residue-backlog.json` counts what is already here, so the
8
+ // standing population can only fall. Two readers, one table: a class the diff guard refuses and the
9
+ // census does not count is a class whose number nobody can act on, and the reverse is a number nobody
10
+ // can hold.
11
+ //
12
+ // ── WHY THE TABLE CAN BE PUBLISHED AT ALL ────────────────────────────────────────────────────────
13
+ //
14
+ // A guard is a list of what it looks for, and this file ships in the tree it protects. So the test for
15
+ // every entry is whether SPELLING IT DISCLOSES ANYTHING. Generic account names, role words and the
16
+ // repository's own retired names disclose nobody. A person's name and the names given to working
17
+ // sessions do, and they are not here: those stay in the private table that
18
+ // `shared/identifier-sentinels.mjs` describes, reached through `CLEAROTRON_IDENTIFIER_BLOCKLIST`, and
19
+ // this file does not restate them under another heading.
20
+ //
21
+ // That split is a real limit and it is stated rather than papered over: public CI cannot refuse a name
22
+ // it is not allowed to know. What it can refuse is every class below, which is what let the last set
23
+ // through.
24
+ //
25
+ // QUOTING A BAD LINE IS STILL WRITING IT, and there is no exemption for backticks. Both comments in
26
+ // this file that once showed a banned token as an example were refused by the guard they describe, and
27
+ // the answer was to reword them rather than to excuse a span. An exemption for quoted text would be a
28
+ // hole in exactly the classes that need none: an account name inside backticks is as published as one
29
+ // outside them, and any author could quote their way past. Where a bad input genuinely has to be
30
+ // SHOWN, it goes in a test arm as a string literal — which this guard does not read, by the same rule
31
+ // that keeps it away from CSS and composite keys.
32
+ //
33
+ // ── WHAT IS DELIBERATELY NOT HERE ────────────────────────────────────────────────────────────────
34
+ //
35
+ // PRODUCT VOCABULARY. `lane`, `round`, `box`, `ruling`, `prelim`, `knockout`, `seat` and `jx` are this
36
+ // product's own nouns. A guard refusing them fires thousands of times, and a guard that fires on
37
+ // correct prose is one whose next reader deletes from the workflow.
38
+ //
39
+ // `deploy` IS A SKILL NAME AND IS NOT BANNED, for the same reason. It is an ordinary English word and a
40
+ // paragraph about deploying would be refused line by line.
41
+ //
42
+ // `clawdi` IS NOT BANNED, and this one was measured rather than reasoned. It reads like an internal
43
+ // name and it is the product's own default agent id: `CLEAROTRON_DEFAULT_AGENT=clawdi` is in
44
+ // INSTALL.md, in the configuration reference and in the operations runbook, and it is a path segment in
45
+ // every run directory. 49 lines carry it in that sense and none carries it as a reference to the other
46
+ // product, which is the class that would have been worth refusing. Banning it would refuse the install
47
+ // instructions a stranger reads first.
48
+
49
+ import { execFileSync } from "node:child_process";
50
+ import { CUT_RECORD_PRESENT, isWithheld } from "./withheld-paths-access.mjs";
51
+
52
+ // ── A COLOUR IS NOT A CITATION, AND ONE PROPERTY LIST SETTLES BOTH READINGS ──────────────────────
53
+ //
54
+ // A three-digit hex colour and a three-digit reference are the same characters, and the digits cannot
55
+ // tell you which one you are looking at. The SITE settles it: a value whose property is a colour is a
56
+ // colour.
57
+ //
58
+ // TWO RULES FROM ONE LIST, and they are not interchangeable — this is the split
59
+ // `shared/identifier-scan.mjs` had to learn after a line-scoped exemption quietly cleared every other
60
+ // name on the line beside the one that earned it.
61
+ //
62
+ // TOKEN-SCOPED (`COLOUR_PROPERTY`) — anchored at the end of the text BEFORE a token, so it exempts
63
+ // that token and nothing else. This is what the diff guard uses: a stylesheet line may hold a colour
64
+ // and a citation, and only the colour is excused.
65
+ //
66
+ // LINE-SCOPED (`COLOUR_SITE`) — true anywhere on the line. Correct only where the whole line is a
67
+ // declaration and there is nothing else on it to miss, which is how
68
+ // `driver/test/prompt-payload-names-no-tracker-issue.test.mjs` reads a stylesheet.
69
+ //
70
+ // Lifted here from that test so the two stop being separate spellings of one rule.
71
+ const COLOUR_PROPERTIES = "color|background|background-color|border|border-color|fill|stroke|outline|box-shadow|text-shadow";
72
+
73
+ /** Anchored: true when the text immediately before a token is a colour property awaiting its value. */
74
+ export const COLOUR_PROPERTY = new RegExp(`(?:^|[;{\\s(,])(?:${COLOUR_PROPERTIES})\\s*:\\s*$`, "i");
75
+
76
+ /** Unanchored: true when the line is a colour declaration at all. */
77
+ export const COLOUR_SITE = new RegExp(`(?:^|[;{\\s])(?:${COLOUR_PROPERTIES})\\s*:`, "i");
78
+
79
+ const HEX_COLOUR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
80
+
81
+ /** Strip hex colours, so what remains is only tokens that could be a reference. */
82
+ export const withoutColourValues = (line) => String(line).replace(HEX_COLOUR, (m, offset, whole) => {
83
+ if (/[a-fA-F]/.test(m.slice(1))) return ""; // letters ⇒ not a decimal number
84
+ return COLOUR_PROPERTY.test(whole.slice(0, offset)) ? "" : m; // all digits ⇒ the property decides
85
+ });
86
+
87
+ /** Strip the spans where a `#NNN` is an address rather than a reference. */
88
+ export const withoutLinkTargets = (line) => String(line)
89
+ .replace(/\]\([^)]*\)/g, "]()") // markdown link targets, anchors included
90
+ .replace(/https?:\/\/\S+/g, "") // bare URLs and their fragments
91
+ .replace(/<[^>]*>/g, ""); // angle-bracket autolinks
92
+
93
+ // A `#` COMMENT IS A COMMENT WHEREVER THE FILE FORMAT SAYS SO, not only in YAML. Extensionless is
94
+ // deliberate: a systemd unit or a dotfile often has no extension worth matching, so the KNOWN
95
+ // `#`-comment names are listed and everything else keeps the source rule.
96
+ const HASH_COMMENT = /(^|\/)(\.env[^/]*|[^/]*\.(ya?ml|sh|bash|service|timer|path|socket|conf|ini|toml|properties)|Dockerfile[^/]*|Makefile|\.gitignore|\.gitattributes)$/;
97
+
98
+ /** Is this line one the guard reads at all? Comments in source, everything in markdown. */
99
+ export const isProse = (path, line) => {
100
+ if (/\.mde?$/.test(path) || path.endsWith(".md")) return true;
101
+ const t = String(line).trim();
102
+ if (HASH_COMMENT.test(path)) return t.startsWith("#");
103
+ return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
104
+ };
105
+
106
+ // ── THE TWO TREES THIS GUARD DOES NOT READ ──────────────────────────────────────────────────────
107
+ //
108
+ // `demo/**` is recorded public-register data and the run artifacts replayed from it. The names in it
109
+ // are real trademark owners on a real register, which is what the demo is for; a guard reading it
110
+ // would refuse the product's own worked example.
111
+ //
112
+ // `driver/skills/**` is the engine's instruction text, and it has its own stricter guard next door in
113
+ // `driver/test/prompt-payload-names-no-tracker-issue.test.mjs` — which refuses a citation there in any
114
+ // form, prose line or not. Reading it here as well would put one tree under two rules that disagree
115
+ // about what a comment is.
116
+ const UNREAD_TREES = [/^demo\//, /^driver\/skills\//];
117
+
118
+ /** Does the guard read this path at all? */
119
+ export const isScannable = (path) => !UNREAD_TREES.some((re) => re.test(path));
120
+
121
+ // ── THE CLASSES ─────────────────────────────────────────────────────────────────────────────────
122
+ //
123
+ // Each is `{ id, pattern, why }`. The id is what the census counts under and what a floor entry names,
124
+ // so it is stable text: renaming one re-mints the fixture and the diff says so.
125
+ //
126
+ // `pattern` carries no `g` flag. A shared `g`-flagged regex carries `lastIndex` between `test` and
127
+ // `exec` calls, so the same instance answers differently on its second use — the kind of defect that
128
+ // shows up as one arm in ten failing. `EVERY_MATCH` below adds the flag for the one operation that
129
+ // needs it, and `String.prototype.matchAll` iterates a clone rather than the original (asserted in
130
+ // driver/test/the-public-residue-is-a-floor.test.mjs, because it is a property of the runtime and not
131
+ // of this file).
132
+
133
+ /** @type {{id: string, pattern: RegExp, why: string}[]} */
134
+ export const CLASSES = [
135
+ {
136
+ id: "bare-reference",
137
+ pattern: /#[0-9]{3,}/,
138
+ // THE REMEDY MOVED WITH THE CLASS BELOW. This used to read "write `tracker issue NNN`", which is
139
+ // now the next entry in this table — a guard telling authors to write the form it refuses two
140
+ // lines later. The remedy is not another spelling of the address; it is to drop the address.
141
+ why: "a bare #NNN reference — GitHub links it into whichever repository renders it, which is not "
142
+ + "the one the number belongs to, and it lives on in public history. Say why the code is as it "
143
+ + "is; the citation belongs in the commit message or the pull request body.",
144
+ },
145
+ {
146
+ // THE SPELLED FORM WAS THE REMEDY AND IS NOW THE CLASS. `tracker issue NNN` was what this guard
147
+ // told authors to write instead of `#NNN`, because it carries no `#` and GitHub cannot linkify it.
148
+ // That was right about the linkifying and wrong about the address: the tree carries the reason for
149
+ // a decision, never its address, and a reader outside this project cannot open the number either
150
+ // way. So the citation moves to the commit message and the pull request body, where it belongs,
151
+ // and the code says why rather than where.
152
+ id: "spelled-citation",
153
+ pattern: /\btracker\s+issues?\s+[0-9]+/i,
154
+ why: "a spelled tracker citation — the reason for a decision belongs in the tree, its address does "
155
+ + "not. A reader outside this project cannot open the number. Say why the code is as it is; put "
156
+ + "the citation in the commit message or the pull request body.",
157
+ },
158
+ {
159
+ id: "login",
160
+ pattern: /\b(?:testuser|azureuser|devuser)[0-9]*\b/i,
161
+ why: "an account name from the machines this is built on. Say what the account is for — 'the "
162
+ + "account the timer runs as' — not what it is called.",
163
+ },
164
+ {
165
+ // THE PLACEHOLDER IS THE POINT OF THE NARROW LIST. `/home/you/trademark/pool` is what INSTALL.md
166
+ // and docs/E2E.md tell a reader to write, and it is the first thing a stranger copies. A
167
+ // `/home/<anything>/` rule would refuse the install instructions, so the accounts are named.
168
+ id: "home-path",
169
+ pattern: /\/home\/(?:testuser|azureuser|devuser|clearotron)[0-9]*\b/i,
170
+ why: "a home directory on one of the machines this is built on. Use the placeholder the install "
171
+ + "documentation uses, or name the setting rather than the path.",
172
+ },
173
+ {
174
+ // ── DECLARED HERE, SPELLED ELSEWHERE, AND THE REASON IS THIS ENTRY'S OWN SENTENCE ──────────────
175
+ //
176
+ // This class had a pattern naming two private repositories as literals, and it shipped in the tree
177
+ // it protects — so the guard published exactly what it exists to refuse. Its own `why` says it: a
178
+ // public tree naming one publishes it, and the name outlives every link to it.
179
+ //
180
+ // THE SPLIT AT THE TOP OF THIS FILE ALREADY HAD THE ANSWER and this entry was on the wrong side of
181
+ // it. A generic account name discloses nobody, which is why the logins are spelled out. A
182
+ // repository name is not a generic word: it is a unique identifier of a private asset, and it
183
+ // belongs with the personal names in the private table rather than here.
184
+ //
185
+ // FOUND BY A REVIEWER, AND DEMONSTRATED RATHER THAN ARGUED. The private scan refused their review
186
+ // on its first pass, because reporting the defect meant quoting the pattern. A class whose literals
187
+ // cannot be discussed in a public review without tripping a guard does not belong in a public file
188
+ // — which is this file's own no-exemption-for-quoting rule, arriving from the other side.
189
+ //
190
+ // WHAT IS LOST, SAID PLAINLY RATHER THAN LEFT AS AN ABSENCE. Commit bodies are covered: the private
191
+ // merge scan refuses this class today. What this entry would have added is FILE coverage, and
192
+ // public CI cannot have it without the literals. So the class is declared with no pattern, skipped
193
+ // here, and populated only where the roster is. A guard that cannot spell what it looks for says so
194
+ // rather than looking clean.
195
+ id: "private-repo-name",
196
+ pattern: null,
197
+ why: "a private repository name. A public tree naming one publishes it, and the name outlives "
198
+ + "every link to it. Refused in commit bodies by the private merge scan; not spellable here.",
199
+ },
200
+ {
201
+ // THE FOUR ARE SPELLED OUT RATHER THAN `role-\w+`. `role-shaping` is a real phrase in
202
+ // driver/portal-service.mjs about what the report does with a party's role, and a prefix rule
203
+ // refuses it — the first false positive would land on product prose, which is how a guard loses
204
+ // its `&&`.
205
+ // AND ONE OF THEM IS A REGISTERED MARK, which matters in this product and nowhere else. The bare
206
+ // word in the pattern below — the one that is not prefixed `role-` — is banned because 20 files
207
+ // carry it as our own word for a role, and no current use of it as a mark is in this tree today. But the corpus this product searches is MARKS, and that is
208
+ // a well-known registered one — so the first false positive here will be a fixture, a worked example
209
+ // or a doctrine line that names it legitimately. That is the same defect this codebase has met three
210
+ // times in a different costume: a check that cannot tell a mark from its own vocabulary. When it
211
+ // arrives, the fix is a site rule that exempts the mark where a mark belongs — not a widened pattern,
212
+ // and not deleting the class, which would put 20 real leaks back.
213
+ id: "role-name",
214
+ pattern: /\brole-(?:dev|e2e|design|overwatch)\b|\boverwatch\b|\bclearance-runs\b/i,
215
+ why: "our own word for how this is built, not the reader's. Say what was done and how it was "
216
+ + "verified; a stranger cannot use the organisation chart and should not have to meet it.",
217
+ },
218
+ {
219
+ id: "agent-trailer",
220
+ // The comment leader is matched so the trailer is recognised at the start of a comment, and
221
+ // CAPTURED OUT of the report: a reader told that the offending token includes the comment leader
222
+ // has been handed the syntax around the finding as though it were part of it.
223
+ pattern: /^[\s*/#]*(Agent:\s*role-\w+)/,
224
+ why: "the agent trailer, which belongs on private surfaces only. On a public surface the "
225
+ + "attribution is the commit author and nothing else.",
226
+ },
227
+ {
228
+ // WORSE THAN THE CLASS ABOVE BECAUSE NOBODY READS IT AS PROSE. This one arrives in a template,
229
+ // survives review by looking like machinery, and a commit message cannot be amended once pushed.
230
+ // The same class is refused in commit bodies by the merge scan; nothing refused it inside a FILE
231
+ // until this, and a file is the surface that stays.
232
+ id: "machine-trailer",
233
+ pattern: /Co-Authored-By:\s*Claude|Claude-Session:|Generated with \[Claude|claude\.ai\/code\/session/,
234
+ why: "a machine-written attribution trailer or session link. On a public surface the attribution "
235
+ + "is the commit author and nothing else.",
236
+ },
237
+ ];
238
+
239
+ /**
240
+ * Every class this line offends, as `{ id, token, why }`. Empty when the line is not read, is not
241
+ * prose, or is clean.
242
+ *
243
+ * Colour values and link targets are stripped once, before any class sees the line, so a class cannot
244
+ * be written in a way that reintroduces either exclusion by accident.
245
+ */
246
+ // EVERY OCCURRENCE, NOT THE FIRST. This was one-per-class-per-line until the census arm planted two
247
+ // citations on one line and read back 1. For the diff guard the difference is cosmetic — the reader is
248
+ // shown the line either way — but the backlog is a COUNT, and a count that stops at the first hit lets
249
+ // a floored line quietly gain a second one. The permissive half of a gate is the dangerous half.
250
+ const EVERY_MATCH = new Map(CLASSES.filter((c) => c.pattern).map((c) => [c.id, new RegExp(c.pattern.source, c.pattern.flags + "g")]));
251
+
252
+ export function offendingClasses(path, line) {
253
+ if (!isScannable(path) || !isProse(path, line)) return [];
254
+ const text = withoutColourValues(withoutLinkTargets(line));
255
+ const out = [];
256
+ for (const c of CLASSES) {
257
+ // A CLASS WITH NO PATTERN IS ONE THIS TREE CANNOT SPELL, not one with nothing to find. It stays in
258
+ // the table so the census keeps its column and a reader meets the limit where the rule is, rather
259
+ // than inferring it from an absence.
260
+ const re = EVERY_MATCH.get(c.id);
261
+ if (!re) continue;
262
+ // A capture group, where a class has one, is the offending text with its surroundings dropped.
263
+ for (const m of text.matchAll(re)) out.push({ id: c.id, token: m[1] ?? m[0], why: c.why });
264
+ }
265
+ return out;
266
+ }
267
+
268
+ /**
269
+ * The standing population, per file, as `{ total, files: { path: number[] } }` — one count per class,
270
+ * in `CLASSES` order, for every file carrying at least one.
271
+ *
272
+ * A file with no hit is absent rather than zero-filled, so the fixture shrinks as the tree is repaired
273
+ * instead of recording a growing list of clean files.
274
+ *
275
+ * @param {string[]} files repo-relative paths
276
+ * @param {(path: string) => string} read
277
+ */
278
+ const COLUMN = new Map(CLASSES.map((c, i) => [c.id, i]));
279
+
280
+ // ── THE CENSUS IS A STATEMENT ABOUT THE PUBLISHED TREE, AND IT CAN BE RUN OVER A BIGGER ONE ──────
281
+ //
282
+ // The withheld corpus is laid back over this tree at its pre-cut paths to run the suite with it. Under
283
+ // that overlay `git ls-files` returns 110 files that are not in the published repository, and measured
284
+ // before this existed they carry 360 hits — 299 of them the one class this floor holds at ZERO. So the
285
+ // arms would have gone red on files the committed fixture cannot record: writing them into it would
286
+ // publish the withheld path list, which is the thing the cut exists to prevent.
287
+ //
288
+ // `isWithheld` is the instrument for that question and this is its THIRD reader — corrected 2026-09-09
289
+ // from "sixth", which counted three readers that do not exist. Its own file states
290
+ // the condition a new caller must meet — the fallback has to make the caller STRICTER, never looser —
291
+ // and this one does: with no cut record it answers false for everything, nothing is skipped, and the
292
+ // census counts the whole tree.
293
+ //
294
+ // AND IT IS INERT TODAY, WHICH IS SAID HERE RATHER THAN LEFT TO BE DISCOVERED. Measured: neither the
295
+ // published repository nor the one the overlay lays from carries `shared/withheld-paths.mjs`, so
296
+ // `CUT_RECORD_PRESENT` is false in both and this skip has never yet excluded a path. It stays because
297
+ // it is the right question and costs nothing the day a record exists. What actually separates the two
298
+ // populations today is the caller's business, and the floor test does it by asking which files are in
299
+ // HEAD: the overlay stages what it lays and never commits it, so a laid path is in the index and not
300
+ // in the published tree. An armed-but-blind guard is worth having only while it says which it is.
301
+ //
302
+ // THE COUNT IS RETURNED, NOT SWALLOWED. A skip nobody can see is how an exclusion becomes an outage:
303
+ // a predicate that started answering true for everything would report a clean, empty census, and that
304
+ // reads exactly like a repaired tree. The arms assert on `skipped` in both directions.
305
+ export function censusOf(files, read) {
306
+ const out = { total: 0, files: {}, skipped: 0, cutRecord: CUT_RECORD_PRESENT };
307
+ for (const path of files) {
308
+ if (!isScannable(path)) continue;
309
+ if (isWithheld(path)) { out.skipped++; continue; }
310
+ let text;
311
+ try { text = read(path); } catch { continue; }
312
+ if (text.includes("\0")) continue; // a binary blob is not prose
313
+ const counts = CLASSES.map(() => 0);
314
+ for (const line of text.split("\n")) {
315
+ for (const { id } of offendingClasses(path, line)) counts[COLUMN.get(id)]++;
316
+ }
317
+ const sum = counts.reduce((a, b) => a + b, 0);
318
+ if (sum) { out.files[path] = counts; out.total += sum; }
319
+ }
320
+ return out;
321
+ }
322
+
323
+ /**
324
+ * The files this checkout PUBLISHES, out of the ones it tracks.
325
+ *
326
+ * The suite is also run over a bigger tree: the withheld corpus is laid back over a clone at its
327
+ * pre-cut paths so our own arms can run against it. That overlay stages what it lays and never commits
328
+ * it, so a laid path is in the index and not in HEAD — which is the whole discriminator, and it is
329
+ * exact rather than a heuristic about paths.
330
+ *
331
+ * The backlog is a statement about the published repository, so both the floor and the mint read this
332
+ * and not the raw tracked list. Two spellings of "the population" is one population and one guess.
333
+ *
334
+ * @param {string[]} trackedList what `trackedFiles` returned — enumerated there so the loud
335
+ * no-checkout skip and the corpus marker stay in one place
336
+ * @param {string} root
337
+ * @returns {{files: string[], laid: number} | {error: string}}
338
+ */
339
+ export function publishedOf(trackedList, root) {
340
+ let head;
341
+ try {
342
+ head = new Set(execFileSync("git", ["-C", root, "ls-tree", "-r", "HEAD", "--name-only"],
343
+ { encoding: "utf8", maxBuffer: 1 << 28 }).split("\n").filter(Boolean));
344
+ } catch (e) {
345
+ // A tree with no HEAD cannot say what it published, and that is a could-not-look. It must not
346
+ // become "nothing is laid here", which is the permissive reading and the one that passes.
347
+ return { error: `could not read HEAD in ${root}: ${String(e.message).split("\n")[0]}` };
348
+ }
349
+ const files = trackedList.filter((f) => head.has(f));
350
+ return { files, laid: trackedList.length - files.length };
351
+ }
@@ -38,7 +38,7 @@ export const CENSUS_WORKSPACES = Object.freeze([
38
38
 
39
39
  // ── — THE FOURTH POPULATION, AND WHY IT IS NOT A FOURTH WORKSPACE ROW ───────────────────────
40
40
  //
41
- // MEASURED (eggie/Jerry, 2026-08-24): `npm run test:providers` on the control tree ran 59 files / 624
41
+ // MEASURED (in testing, 2026-08-24): `npm run test:providers` on the control tree ran 59 files / 624
42
42
  // tests; on the cut tree, 47 files / 426 tests. **198 tests disappeared and the command exited 0.** No
43
43
  // failure, no warning, no line saying a file was not picked up.
44
44
  //
@@ -62,6 +62,13 @@ export const CENSUS_ROOT_SCRIPTS = Object.freeze([
62
62
  Object.freeze({
63
63
  script: "test:providers",
64
64
  globs: Object.freeze(["providers/_shared/test/*.test.mjs", "providers/*/test/*.test.mjs"]),
65
+ // THE COMMAND NO LONGER SPELLS THOSE GLOBS; it reaches them through this file, which builds the
66
+ // list in code so the corpus has one definition and two entry points. The literals above stay
67
+ // exactly as they were — they are this module's EXPECTATION, and deleting them in favour of the
68
+ // resolved list would leave two derived sides with nothing to disagree about, which is the defect
69
+ // the block above this table exists to record. What changes is only how the measurement is taken:
70
+ // the resolved list is asked whether it still reaches each glob.
71
+ resolvedBy: "scripts/test-full.mjs",
65
72
  }),
66
73
  ]);
67
74
 
@@ -74,7 +81,7 @@ export const CENSUS_ROOT_SCRIPTS = Object.freeze([
74
81
  *
75
82
  * PURE: `readManifest` is injected, so the canary drives every branch over a planted manifest.
76
83
  */
77
- export function rootScriptDisagreements(readManifest, declared = CENSUS_ROOT_SCRIPTS) {
84
+ export function rootScriptDisagreements(readManifest, declared = CENSUS_ROOT_SCRIPTS, resolveFiles = null) {
78
85
  const root = readManifest("package.json");
79
86
  if (!root) return ["the root package.json could not be read, so what the root scripts collect is unknown"];
80
87
  const scripts = root.scripts ?? {};
@@ -86,8 +93,23 @@ export function rootScriptDisagreements(readManifest, declared = CENSUS_ROOT_SCR
86
93
  continue;
87
94
  }
88
95
  for (const g of globs) {
89
- if (!line.includes(g)) {
96
+ if (line.includes(g)) continue;
97
+ // THE COMMAND DOES NOT SPELL THIS GLOB. That is a disagreement unless the script says it
98
+ // resolves the corpus elsewhere AND the resolved list is available to be checked. A declared
99
+ // `resolvedBy` with no resolver in hand is a could-not-look, never a pass: it would let an
100
+ // entry opt out of the whole check by naming a file.
101
+ const via = declared.find((d) => d.script === script)?.resolvedBy;
102
+ if (!via) {
90
103
  out.push(`\`${script}\` is censused as collecting ${g} and its command is: ${line.trim()}`);
104
+ } else if (!line.includes(via)) {
105
+ out.push(`\`${script}\` is censused as resolving its corpus through ${via}, and its command `
106
+ + `does not name it: ${line.trim()}`);
107
+ } else if (typeof resolveFiles !== "function") {
108
+ out.push(`\`${script}\` resolves its corpus through ${via} and no resolver was supplied, so `
109
+ + `what it collects could not be read — which is not the same as it being right`);
110
+ } else if (!resolveFiles(script).some((f) => globMatches(g, f))) {
111
+ out.push(`\`${script}\` is censused as collecting ${g}; ${via} resolves a corpus that `
112
+ + `reaches no such file`);
91
113
  }
92
114
  }
93
115
  }
@@ -117,6 +139,12 @@ export function rootScriptDisagreements(readManifest, declared = CENSUS_ROOT_SCR
117
139
  // PURE AND INJECTABLE. `readManifest` is passed in, so the canary drives every branch over planted
118
140
  // manifests without touching the tree — the same rule the rest of this module follows.
119
141
 
142
+ /** Does one `*`-style glob match this path? `*` stops at a separator, as a shell's does. */
143
+ export function globMatches(glob, path) {
144
+ const re = new RegExp("^" + glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*") + "$");
145
+ return re.test(path);
146
+ }
147
+
120
148
  /** `npm run <name>` inside a script, so an indirection resolves within its own manifest. */
121
149
  const NPM_RUN = /^\s*npm\s+run\s+([A-Za-z0-9:_-]+)\s*$/;
122
150
 
@@ -8,33 +8,50 @@
8
8
  // private." `shared/withheld-paths.mjs` STAYS BEHIND. The reading where it ships with a rewritten
9
9
  // header is dead and must not be revived.
10
10
  //
11
- // Five files read that record and are wanted on the public tree: `scripts/citation-line-check.mjs`,
12
- // `scripts/mint-suite-census.mjs`, and the arms `no-caveat-repair`,
13
- // `signa-mock-lane-is-unreachable-from-a-run` and `the-providers-suite-is-censused`. A static import of
14
- // a module that is not there throws before anything runs for a test that means its cases VANISH FROM
15
- // THE COUNT rather than failing, which is the 354-to-279 shape already records. This
16
- // module is how they degrade on purpose instead.
11
+ // THREE files read that record, and all three are in this repository:
12
+ // `scripts/citation-line-check.mjs`, `scripts/mint-suite-census.mjs` and
13
+ // `shared/reference-guard-classes.mjs`. A static import of a module that is not there throws before
14
+ // anything runs, and for a test that means its cases VANISH FROM THE COUNT rather than failing. This
15
+ // module is how those three degrade on purpose instead.
16
+ //
17
+ // The list above was wrong in both directions and is corrected here (measured 2026-09-09): it said
18
+ // five, naming three tests that exist in this repository under no path, and it omitted
19
+ // `shared/reference-guard-classes.mjs`, which had already recorded itself as a reader in its own file.
17
20
  //
18
21
  // ── WHY DEGRADING IS SAFE HERE, WHICH IS THE WHOLE ARGUMENT ──────────────────────────────────────
19
22
  //
20
- // Every one of the five asks the record ONE question: is this file absent because it was deliberately
23
+ // Every one of the three asks the record ONE question: is this file absent because it was deliberately
21
24
  // withheld, or absent because something broke? On the public tree nothing was withheld FROM that tree,
22
25
  // so the honest answer is "nothing is withheld" — and each caller then becomes STRICTER, never weaker:
23
26
  //
24
27
  // citation-line-check every file crosses the cut, so every citation must resolve
25
28
  // mint-suite-census no exemption, so a removed test file is a LOSS
26
- // the three arms an absence is damage, never a stated consequence
29
+ // reference-guard-classes nothing is skipped, so the whole tree is counted
30
+ //
31
+ // MEASURED, not argued (2026-09-09): `censusOf` was run twice over one file list. With no record it
32
+ // counted four files and skipped none; with a record naming two of them it counted two and skipped
33
+ // two. Absent, the record cannot hide a defect.
27
34
  //
28
35
  // A fallback that can only tighten is one that cannot hide a defect. That is the property that makes
29
36
  // this safe to do without a flag, and it is the property to re-check before adding a sixth caller: if a
30
37
  // new caller would be LOOSER without the record, it does not belong here.
31
38
  //
32
- // ── AND IT ANNOUNCES, SO IT CANNOT FIRE UNNOTICED WHERE IT MUST NOT ──────────────────────────────
39
+ // ── NO TREE CARRIES THE RECORD TODAY, AND THE MODE LINE SAYS WHICH WAY THAT LEANS ───────────────
40
+ //
41
+ // This block used to say the record exists here and that losing it "would quietly relax five checks".
42
+ // Both halves were false, and the second contradicted the paragraph above it. Measured 2026-09-09:
43
+ // `CUT_RECORD_PRESENT` is false here and false in the larger tree the suite is also run against — the
44
+ // record exists in neither, so every reader has always run in fallback, and fallback is the strict side.
45
+ //
46
+ // The mode is still announced once to stderr, the way `announceBlocklistMode` does for the identifier
47
+ // roster. What the line may NOT do is call itself an alarm: this module can see whether the record is
48
+ // present, and cannot see whether it ought to be. So it states the mode and what that means for the
49
+ // three readers, and leaves the judgement to a reader who knows which tree they are looking at.
33
50
  //
34
- // On our tree the record exists and behaviour is unchanged. If it ever went missing here, the fallback
35
- // would quietly relax five checks the exact silent-pass this repository keeps paying for. So the mode
36
- // is announced once to stderr, the way `announceBlocklistMode` does for the identifier roster, and an
37
- // arm pins that the announcement happens.
51
+ // NOTHING PINS THIS ANNOUNCEMENT, and this block used to claim something did. Measured 2026-09-09: no
52
+ // test in either repository imports `announceWithheldMode` or asserts its line, so the wording above is
53
+ // held by nothing and a silent removal of it would pass. That gap is filed rather than fixed here,
54
+ // because closing it adds a test file and this change is comment text only.
38
55
 
39
56
  let record = null;
40
57
  try {
@@ -59,15 +76,16 @@ export const isWithheld = (relPath) => (record ? record.isWithheld(relPath) : fa
59
76
 
60
77
  let announced = false;
61
78
  /**
62
- * Say which mode this process is in, once, to stderr. A green run in fallback mode on a tree that
63
- * SHOULD carry the record is five checks quietly relaxed, and the only thing standing between that and
64
- * silence is this line.
79
+ * Say which mode this process is in, once, to stderr. Fallback is the strict side, so this line is a
80
+ * statement of fact rather than a warning: it reports what the three readers will do, and whether that
81
+ * is right depends on the tree, which this module cannot see.
65
82
  */
66
83
  export function announceWithheldMode() {
67
84
  const line = CUT_RECORD_PRESENT
68
85
  ? `[repo-guard] cut record present — ${WITHHELD.length} withheld entry/entries; absences they cover are stated consequences`
69
- : "[repo-guard] NO cut record in this tree — nothing counts as withheld, so every absence is damage "
70
- + "and every citation must resolve. This is correct on the published tree and an ALARM anywhere else.";
86
+ : "[repo-guard] no cut record in this tree — nothing counts as withheld, so every absence is damage "
87
+ + "and every citation must resolve. This is the STRICTER of the two modes: it cannot hide a defect, "
88
+ + "and it is the expected mode wherever nothing was withheld from the tree in hand.";
71
89
  if (!announced) { announced = true; console.error(line); }
72
90
  return line;
73
91
  }