clearotron 0.3.2-beta.11 → 0.3.2-beta.13

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 (122) hide show
  1. package/.env.example +1 -1
  2. package/CONTRIBUTING.md +6 -5
  3. package/INSTALL.md +3 -4
  4. package/README.md +2 -1
  5. package/bin/clearotron.mjs +14 -0
  6. package/bin/connect.mjs +68 -3
  7. package/bin/example.mjs +12 -1
  8. package/bin/key.mjs +6 -1
  9. package/bin/onboard.mjs +23 -3
  10. package/bin/passphrase.mjs +4 -2
  11. package/bin/start.mjs +18 -6
  12. package/build-info.json +2 -2
  13. package/docs/CLIENT-MCP.md +6 -6
  14. package/docs/DELIVERY.md +3 -3
  15. package/docs/ONBOARDING.md +1 -1
  16. package/docs/PORTAL.md +3 -3
  17. package/docs/RELEASES.md +1 -1
  18. package/docs/SECURITY.md +1 -1
  19. package/docs/architecture/04-configuration-reference.md +4 -4
  20. package/docs/architecture/05-config-governance.md +10 -10
  21. package/docs/architecture/06-operations-runbook.md +3 -3
  22. package/docs/architecture/07-quality-and-audit.md +1 -1
  23. package/docs/architecture/08-development-guide.md +2 -2
  24. package/docs/architecture/09-security-and-data.md +1 -1
  25. package/docs/decisions/0002-no-dark-functionality.md +1 -1
  26. package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
  27. package/driver/CHANGELOG.md +29 -0
  28. package/driver/ask-ledger.mjs +1 -1
  29. package/driver/band-shape.mjs +1 -1
  30. package/driver/bundled-demos.mjs +2 -2
  31. package/driver/card-budget.mjs +2 -2
  32. package/driver/case-law-ledger.mjs +2 -2
  33. package/driver/connotation-search.mjs +5 -5
  34. package/driver/contract-e3-backlog.mjs +13 -13
  35. package/driver/contract-vocabulary.mjs +5 -5
  36. package/driver/coverage-form.mjs +2 -2
  37. package/driver/demo-container.mjs +26 -2
  38. package/driver/disposition-tool.mjs +2 -2
  39. package/driver/engine/CONTRACT.md +3 -3
  40. package/driver/engine/mcp/gather-config.mjs +1 -1
  41. package/driver/engine/mcp/recording-server.mjs +1 -1
  42. package/driver/engine/mcp/supplemental.mjs +22 -5
  43. package/driver/engine/openai-agent.mjs +2 -2
  44. package/driver/findings-model.mjs +21 -6
  45. package/driver/gateway.mjs +1 -1
  46. package/driver/knockout-next-step.mjs +72 -0
  47. package/driver/named-band.mjs +1 -1
  48. package/driver/package.json +1 -1
  49. package/driver/pipeline-knockout.mjs +27 -1
  50. package/driver/pipeline.mjs +4 -4
  51. package/driver/placement-union.mjs +1 -1
  52. package/driver/portal-mcp-client.mjs +1 -1
  53. package/driver/portal-report.mjs +25 -3
  54. package/driver/portal-service.mjs +25 -13
  55. package/driver/predelivery-lint.mjs +9 -4
  56. package/driver/progress.mjs +37 -1
  57. package/driver/publish/render-knockout.mjs +14 -9
  58. package/driver/publish/render.mjs +6 -6
  59. package/driver/record-discard.mjs +1 -1
  60. package/driver/register-availability.mjs +1 -1
  61. package/driver/register-count.mjs +1 -1
  62. package/driver/register-digest-record.mjs +1 -1
  63. package/driver/register-plan.mjs +81 -11
  64. package/driver/report-card-record.mjs +2 -2
  65. package/driver/result-noun-fields.mjs +3 -1
  66. package/driver/roster-verdict.mjs +2 -2
  67. package/driver/search-policy.mjs +1 -1
  68. package/driver/skeptic-record.mjs +1 -1
  69. package/driver/stages-knockout.mjs +1 -1
  70. package/driver/stages.mjs +1 -1
  71. package/driver/suite-census.json +72 -24
  72. package/driver/systemd/README.md +1 -1
  73. package/driver/unit-inventory.mjs +2 -2
  74. package/driver/verify-knockout.mjs +0 -27
  75. package/driver/verify.mjs +1 -1
  76. package/mcp-server/CHANGELOG.md +8 -0
  77. package/mcp-server/CONNECT.md +8 -8
  78. package/mcp-server/lib/runs.mjs +1 -1
  79. package/mcp-server/package.json +1 -1
  80. package/mcp-server/serve.mjs +27 -0
  81. package/package.json +1 -1
  82. package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
  83. package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
  84. package/portal-ui/dist/index.html +2 -2
  85. package/portal-ui/package.json +1 -1
  86. package/providers/free-tier/src/capabilities.js +2 -2
  87. package/providers/jx/src/core.js +3 -3
  88. package/providers/jx/src/turn-envelope.mjs +1 -1
  89. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  90. package/providers/oauth-mcp-bridge/package.json +1 -1
  91. package/providers/perplexity/README.md +1 -1
  92. package/providers/signa/src/capabilities.js +2 -2
  93. package/providers/signa/src/core.js +2 -2
  94. package/providers/uspto-local/src/core.js +1 -1
  95. package/providers/uspto-local/src/sync.js +1 -1
  96. package/scripts/README.md +2 -6
  97. package/scripts/ask-ai-render-check.mjs +26 -1
  98. package/scripts/citation-anchor-report.mjs +1 -1
  99. package/scripts/citation-line-check.mjs +3 -3
  100. package/scripts/dead-names.mjs +17 -16
  101. package/scripts/e2e.mjs +1 -1
  102. package/scripts/env-audit.mjs +7 -1
  103. package/scripts/env-classify.mjs +1 -1
  104. package/scripts/pack-publishable.mjs +1 -1
  105. package/scripts/release-artifact-seal.mjs +2 -2
  106. package/scripts/repo-writes.mjs +42 -0
  107. package/scripts/report-sections-render-check.mjs +245 -0
  108. package/scripts/report-theme-render-check.mjs +31 -3
  109. package/scripts/settings-render-check.mjs +15 -8
  110. package/scripts/strip-tracker-citations.mjs +4 -4
  111. package/scripts/test-run.mjs +108 -6
  112. package/shared/browser-temp-root.mjs +10 -3
  113. package/shared/client-door.mjs +38 -6
  114. package/shared/connect-clients.mjs +2 -0
  115. package/shared/identifier-scan.mjs +2 -2
  116. package/shared/invocation.mjs +1 -1
  117. package/shared/parent-watch.mjs +33 -0
  118. package/shared/reference-guard-classes.mjs +5 -3
  119. package/shared/running-start.mjs +14 -3
  120. package/shared/scope.mjs +22 -3
  121. package/shared/stdio-connect.mjs +39 -18
  122. package/shared/writing-standard-classes.mjs +2 -3
@@ -97,7 +97,7 @@ product doc.
97
97
  | **Allowlist** (`{version, grants:[{email, customer}]}`) | T2 | git + PR on the `CLIENT_ACCESS_MAP` file | see §2 row 4 | LIVE, file-only; surfaced read-only at `admin.access` |
98
98
  | **Ops tokens** (scope ops/user, verbs, companies, TTL) | T4 | `mint-token.mjs` CLI; jti denylist file | operator-held tokens | LIVE, CLI |
99
99
 
100
- ## 4b. The install surface names ()
100
+ ## 4b. The install surface names
101
101
 
102
102
  The variables a **user or installer** ever types carry the product’s own prefix. They are listed
103
103
  by name in §5 below and in the upgrade table in INSTALL.md.
@@ -123,14 +123,14 @@ has taken it.
123
123
 
124
124
  **`CLEAROTRON_JX_LANES` was held back from the August 2026 rename** — it was RETIRED 2026-07-27
125
125
  (`pipeline.mjs` `RETIRED_ENV`, `jx-units.mjs`) and nothing reads it, so renaming a dead name looked like
126
- handing an operator a name that warns about nothing. The owner's 2026-09-04 ruling reversed that: the
126
+ handing an operator a name that warns about nothing. A decision of 2026-09-04 reversed that: the
127
127
  whole namespace carries one prefix, dead names included, because a tree spelled two ways costs more
128
128
  than a retired row spelled consistently. The per-lane `CLEAROTRON_NATIVE_LANGUAGE_<code>` switch in the same row **is** live and is
129
- fail-OPEN: unset means ON (§5.5,).
129
+ fail-OPEN: unset means ON (§5.5).
130
130
 
131
131
  Variables outside the install surface were left alone by the August rename — `CLEAROTRON_ENGINE_MAX_BUFFER`
132
- and its siblings were never in that window. **That separate decision was taken on 2026-09-04**: the owner
133
- ruled the rename global and pre-cut, so the internals carry the house prefix too and the public tree never
132
+ and its siblings were never in that window. **That separate decision was taken on 2026-09-04**: the rename
133
+ became global and pre-cut, so the internals carry the house prefix too and the public tree never
134
134
  shows the old namespace. No compatibility layer, no alias reading, no migration — greenfield, and our own
135
135
  boxes rebuild.
136
136
 
@@ -207,10 +207,10 @@ move once the same window has been read across more runs.
207
207
  `CLEAROTRON_BAND_TRUTH_GATE` (**never disable in prod — restores the fabrication**),
208
208
  `CLEAROTRON_FRAME_REOPEN` (+`CLEAROTRON_FRAME_REOPEN_MAX`=1, `CLEAROTRON_REOPEN_MAX_FETCH`=150),
209
209
  `CLEAROTRON_REGISTER_GAP_CLAMP`, `CLEAROTRON_RECALL_PROBES`, `CLEAROTRON_RECALL_TRIPWIRE`, `CLEAROTRON_WARM_RETRY`,
210
- `CLEAROTRON_MODEL_WIRE_CHECK` ( — fails a turn whose
210
+ `CLEAROTRON_MODEL_WIRE_CHECK` (fails a turn whose
211
211
  provider reports a different model FAMILY than the driver asked for; disarming it silences the refusal
212
212
  and never the record: `modelActual`/`modelMismatch` keep landing on every dispatch row), `CLEAROTRON_FORM_REPAIR`
213
- ( — repairs a form-class stage failure inside the dispatch, up to twice; disarmed, the defect
213
+ (repairs a form-class stage failure inside the dispatch, up to twice; disarmed, the defect
214
214
  falls through to the retry ladder exactly as it did before, visibly, and is never swallowed as
215
215
  "validated fine"). Policy knob: `CLEAROTRON_UNREACHABLE_SENIOR`
216
216
  (open-item|clamp). Enumerate: `CLEAROTRON_ENUMERATE_CEILING` (600/OR-stack),
@@ -359,7 +359,7 @@ systemd, writes no heartbeat, and must keep saying "waiting to start" rather tha
359
359
  repo** — `git grep process.env.CLIENT_ACCESS` here returns nothing, so they are governed here and
360
360
  never appear in the audit).
361
361
 
362
- **Which identity source the portal runs () — T4, and it is chosen by name, never inferred.**
362
+ **Which identity source the portal runs — T4, and it is chosen by name, never inferred.**
363
363
  `PORTAL_AUTH_MODE` selects the door: unset or `auth-proxy` (the default for a hosted deployment) means
364
364
  any login system in front that authenticates in the browser and forwards a verifiable JWT per request.
365
365
  **Any OIDC or JWT proxy is a choice per deployment** — for example Cloudflare Access, which is not a
@@ -367,7 +367,7 @@ special case in the code; `local` means one address and one passphrase on loopba
367
367
  exactly the same thing** — normalised where the mode is read rather than by an alias row, because
368
368
  `shared/env-aliases.mjs` maps variable NAMES and there is no value-alias mechanism.
369
369
 
370
- **Bringing your own login provider ( item 1, completed by) — T4.**`PORTAL_OIDC_ISSUER`,
370
+ **Bringing your own login provider — T4.**`PORTAL_OIDC_ISSUER`,
371
371
  `PORTAL_JWKS_URL`, `PORTAL_EMAIL_CLAIM` and `PORTAL_AUTH_HEADER` are the portal-side spelling of the four
372
372
  values the staff MCP face already reads as `TRADEMARK_MCP_OIDC_ISSUER`, `TRADEMARK_MCP_JWKS_URL`,
373
373
  `TRADEMARK_MCP_EMAIL_CLAIM` and `TRADEMARK_MCP_AUTH_HEADER`. `makeAccessVerifier` has always accepted them; the portal
@@ -432,7 +432,7 @@ synthetic identity would answer before the mandatory key ever ran. The mode is l
432
432
  travels in a header or the query string), requires `TRADEMARK_MCP_ALLOWED_HOSTS` exactly as the
433
433
  authenticated door does, and mirrors its mandatory `CLEAROTRON_ACCESS_FILE`.
434
434
 
435
- **The local install sets all of the above itself.**`npx clearotron start` (`bin/start.mjs`) is a supervisor:
435
+ **The local install sets all of the above itself.**`clearotron start` (`bin/start.mjs`) is a supervisor:
436
436
  it resolves one set of ports, derives `PORTAL_MCP_URL` and `TRADEMARK_MCP_ALLOWED_HOSTS` from them, mints
437
437
  the ops key in memory, and hands each child an explicit environment carrying `CLEAROTRON_NO_ENV_FILE=1`. So
438
438
  exactly one process in that tree reads `<repo>/.env` — the supervisor — and nothing a laptop runs needs
@@ -73,7 +73,7 @@ properties below, each of which was learned the expensive way:
73
73
  mid-run feeds one expensive run two different skill versions. **Call
74
74
  check that nothing is in flight and abort if anything is** — read every queue this
75
75
  deployment would drain plus the run-slot locks, and refuse on any of them. `--override
76
- "<reason>"` is the owner-ordered exception; the reason is printed into the deploy output, and a
76
+ "<reason>"` is the operator's deliberate exception; the reason is printed into the deploy output, and a
77
77
  blank one exits 2 rather than passing. This step used to be a line of prose asking a human to
78
78
  check, which is not a guard on the night it matters. Then **stop the four trigger units** for
79
79
  the deploy window and install an **EXIT trap that restarts them on every exit path**, success or
@@ -184,7 +184,7 @@ rather than stage compute. That gateway is no longer part of the product, so the
184
184
  to probe and the file went with them. Nothing invoked it: not `package.json`, not `bin/`, not
185
185
  `scripts/`, not a unit, not CI. It was run by hand, and it spent real money when it was.
186
186
 
187
- What replaces each half: the free path check is `npx clearotron doctor` plus the runner's own preflights
187
+ What replaces each half: the free path check is `clearotron doctor` plus the runner's own preflights
188
188
  (`preflightEngineBinary`, `preflightCredentials`, `preflightDeploymentUrls`), each of which refuses by
189
189
  name before a run dir exists. The billable half has no replacement and needs none — stage compute is
190
190
  exercised by real runs and the A/B harness, which is what its own note already said.
@@ -289,7 +289,7 @@ Copy it, point `CLEAROTRON_ACCESS_FILE` at your copy, sign in as one of its addr
289
289
  exactly that organisation's companies. Then delete it and write your own — it names nobody real, which also
290
290
  means it grants nothing you have.
291
291
 
292
- `npx clearotron start` (§6) writes an empty roster (`{"tenants": {}}`) into its state directory: your own staff
292
+ `clearotron start` (§6) writes an empty roster (`{"tenants": {}}`) into its state directory: your own staff
293
293
  address is admitted, nobody else is enrolled, and enforcement is already on.
294
294
 
295
295
  ### Ops tokens — a credential for the verbs that spend
@@ -269,7 +269,7 @@ write nothing and 7 that do:
269
269
  `what_if_run` → one sandboxed stage; `what_if_result` → collect a queued one) — **executing** is
270
270
  local stdio only, never remote, for any principal. The confirmation token is a deliberate-action
271
271
  handshake, not a crypto boundary; the security boundary is ops scope + local-only execution.
272
- Since the owner's 2026-08-27 ruling an `account` principal reaches all three, and `what_if_run` on that
272
+ Since 2026-08-27 an `account` principal reaches all three, and `what_if_run` on that
273
273
  path ENQUEUES into `<runDir>/_experiments/_queue/` rather than shelling — `driver/whatif-worker.mjs`,
274
274
  drained by the runner, is what spawns the sandbox. Because the token is unsigned, a call on that path must
275
275
  also name its `runId` so the grant check fires, and the enqueue refuses a token naming another run.
@@ -88,7 +88,7 @@ These are the things a well-meaning refactor breaks. Each is enforced somewhere;
88
88
 
89
89
  Mechanically a config change ([04](04-configuration-reference.md#model-tiers-and-resolution)); two
90
90
  traps and one law. Traps: an alias not registered in the engine's model map now **refuses the
91
- dispatch by name** ( — it used to run sonnet silently and log the alias you asked for, which is
91
+ dispatch by name** (it used to run sonnet silently and log the alias you asked for, which is
92
92
  the `fable` lesson turned into an error), and `CLEAROTRON_SYNTHESIS_MODEL` is read at module load (fine
93
93
  for the oneshot service, stale in long-lived processes). The law: any grade-moving
94
94
  change — family, effort, tier remap — ships only through the paid A/B against the reference
@@ -221,7 +221,7 @@ Four realities to respect:
221
221
 
222
222
  - **Exclusion is by filename convention only.** Anything named `*.test.mjs` runs in CI; billable or
223
223
  manual harnesses must not match the glob (historical one-off proofs with hard-coded dev paths are
224
- not kept; the last billable hand-run harness, `selftest.mjs`, was deleted at).
224
+ not kept; the last billable hand-run harness, `selftest.mjs`, was deleted).
225
225
  - **The `||=` env guards leak**: a shell exporting a real register credential or
226
226
  `CLEAROTRON_PLAN_DISPATCH=on` is *not* overridden by the harness — run the suite in a clean env.
227
227
  (CI is safe.)
@@ -83,7 +83,7 @@ are no root units; everything is `systemd --user`.
83
83
  are stripped), and **`account`** (a signed-in person on the client face — the runs of the
84
84
  companies they are granted, reached either by the CF sign-in with no token, or by a per-person API
85
85
  key, `scope: account`, whose companies are re-read from the grants file on every request rather than
86
- baked into it. Wider *reach* than a report link and, since the owner's 2026-08-27 ruling, more
86
+ baked into it. Wider *reach* than a report link and, since a decision of 2026-08-27, more
87
87
  *depth* too: the audit chain — the audit trail, the reasoning narrative, the record artifacts, a
88
88
  register axis, and the `get_run` / `trace` / `decision_timeline` decision walk. Model identity
89
89
  and billed counts stay sealed (`get_telemetry`, `get_provider_usage`), as does the reviewers'
@@ -60,7 +60,7 @@ Every `CLEAROTRON_*` name read by shipping code declares an effect class, and an
60
60
  whatever the declaration test enforces. Do not re-derive it by hand; that is how 172, 159 and 352 all
61
61
  came to describe the same repository.
62
62
 
63
- ## Addendum — 2026-08-20, on implementation ( item 8)
63
+ ## Addendum — 2026-08-20, on implementation
64
64
 
65
65
  All four switches are deleted, as ruled. One consequence above is narrowed by what the code turned out
66
66
  to be, and it is recorded here rather than in a pull request body, because this document is what the
@@ -1,6 +1,6 @@
1
1
  # 0006 — What the public repository carries
2
2
 
3
- **Accepted 2026-08-17, by the owner.**
3
+ **Accepted 2026-08-17.**
4
4
 
5
5
  ## Context
6
6
 
@@ -74,7 +74,7 @@ announces which mode it is in, once, so a reader of any run can tell which colum
74
74
  "change the code" is the clause it answers to and this repository's working practice is that agents change
75
75
  the code. `CLAUDE.md` is not carried: it was only ever a one-line pointer at `AGENTS.md`, a second copy of
76
76
  one subject is a future contradiction ([ADR-0004](0004-documentation-structure.md)), and the de-identified
77
- public cut names that file specifically. Ruled by the owner 2026-08-19, on the question raised against the
77
+ public cut names that file specifically. Decided 2026-08-19, on the question raised against the
78
78
  recovery — where the two files had been held back because the revert that stripped them recorded no
79
79
  decision either way, and `shared/withheld-paths.mjs` did not cover them.
80
80
 
@@ -89,7 +89,7 @@ vendor-branded duplicate of it does not, however small.
89
89
  citations across code, comments, env examples and docs; all but six were rewritten, and the six declared
90
90
  are this record's own, where naming what is dropped is the content.
91
91
  - **A binding to a withheld document is retired.** `driver/doc-constants.mjs` pinned four prose figures in
92
- `docs/KNOCKOUT.md` and `docs/REGISTER-HIT-COUNTS.md`; those rows are gone, and the test pinning 's
92
+ `docs/KNOCKOUT.md` and `docs/REGISTER-HIT-COUNTS.md`; those rows are gone, and the test pinning the
93
93
  one-file-states-it-twice case now anchors to the shape rather than to a named document.
94
94
  - **Withholding is reversible**, which is why this is a list rather than a `git rm`. This repository keeps
95
95
  every word.
@@ -1,5 +1,34 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.2-beta.13
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: A conditional verdict whose conditions are all kept in the run record no longer shows an empty "conditional on" line. The rating word stands alone.
8
+ - Fixed: A running clearance's card and row show the stage it is in now, including a step back during a correction pass.
9
+ - Fixed: The portal's health check no longer includes an internal error message when the instructions store cannot be read. The message goes to the service log instead.
10
+ - Fixed: `doctor` no longer describes another program on the client door's port as this install's door. It says only that a process holds the port.
11
+ - Fixed: The connect lines name the exact Node your install runs on, and an older Node gets one plain line instead of a crash.
12
+ - Fixed: The demo no longer leaves copies of its samples in your temp directory, including when it is stopped mid-way.
13
+ - Fixed: Running the test suite no longer leaves demo copies in the machine's temporary directory.
14
+ - Fixed: On WSL, the Windows connect line names your Linux distribution, or says plainly to fill it in, instead of leaving it out.
15
+
16
+ ## 0.3.2-beta.12
17
+
18
+ ### Patch Changes
19
+
20
+ - Fixed: A demo started in the background with `npx` now stops completely when that `npx` is stopped, freeing its three ports and removing its folder.
21
+ - Fixed: The demo lists the door an assistant uses first, and a key refused at the wrong door is told the right door's address.
22
+ - Fixed: A knockout no longer asks the model what to do with each name, so each name's read keeps to the findings and the rating.
23
+ - Fixed: A knockout's per-name read no longer carries a next-step section the model wrote. It is removed before delivery instead of the batch being sent back to rewrite it, so knockouts finish sooner.
24
+ - Fixed: A report produced before this version no longer shows a second section menu inside the portal, unreadable in the dark theme.
25
+ - New: `clearotron connect --base <folder>` connects an assistant to a running demo, minting its key and naming its door in one step.
26
+ - Fixed: `doctor` now says the client door's access log has entries, instead of claiming it is being written.
27
+ - Fixed: The sign-in page names the install's one user, and the passphrase reset, key issue and doctor each say what they are for.
28
+ - Fixed: Once the client's own registered element is set aside, searches added later in the run no longer search it again.
29
+ - Fixed: Running the test suite on a clone whose portal build is out of date no longer rebuilds it and fails the first run.
30
+ - Fixed: Running the test suite no longer writes an access-log line under your home folder, and a stopped demo leaves no empty folders behind.
31
+
3
32
  ## 0.3.2-beta.11
4
33
 
5
34
  ### Patch Changes
@@ -170,7 +170,7 @@ function endingForQids(qids, join, ts) {
170
170
  return null;
171
171
  }
172
172
 
173
- /** The {ending, handoff} pair for one cross-check directive. Only `recall` is subject to 's
173
+ /** The {ending, handoff} pair for one cross-check directive. Only `recall` is subject to the
174
174
  * discharge rule; every other net keeps the ending it always had. PURE. */
175
175
  function recallEnding(name, qid, join, ts) {
176
176
  const ending = endingForQids([qid], join, ts);
@@ -769,7 +769,7 @@ export function buildBandShape(band, { targets = [], inScopeClasses = [], crowdC
769
769
  }
770
770
 
771
771
  const fmtN = (n) => Number(n ?? 0).toLocaleString("en-US");
772
- const cell = (v) => String(v ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim() || "—";
772
+ const cell = (v) => String(v ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim() || "—";
773
773
 
774
774
  // Bounded aggregate table: top rows by count, one honest remainder line — never for the floors.
775
775
  function topTable(md, title, cols, entries, cap = 30) {
@@ -3,8 +3,8 @@
3
3
  // bundled-demos.mjs — WHICH client bundles this repo ships, read from the directory that ships them.
4
4
  //
5
5
  // This was a hand-maintained triple. The directory grew a fourth bundle, the triple did not, and the
6
- // set-equality it feeds could no longer match anything — so the #83 detector, whose whole job is to
7
- // notice a door that has silently fallen back to the bundled roster, returned PASS on the #83
6
+ // set-equality it feeds could no longer match anything — so the roster-fallback detector, whose whole job is to
7
+ // notice a door that has silently fallen back to the bundled roster, returned PASS on exactly that
8
8
  // condition. A guard that cannot match reports that it found nothing wrong.
9
9
  //
10
10
  // The same shape had already produced two FALSE REFUSALS on this check, both from an exact match
@@ -74,13 +74,13 @@ function insertBulletAtSectionHead(text, headingRe, headingLine, bullet, { paren
74
74
  const m = text.match(headingRe);
75
75
  if (m) {
76
76
  const at = m.index + m[0].length;
77
- return `${text.slice(0, at)}\n${bullet}\n${text.slice(at).replace(/^\n/, "\n")}`;
77
+ return `${text.slice(0, at)}\n${bullet}\n${text.slice(at)}`;
78
78
  }
79
79
  if (parentHeadingRe) {
80
80
  const p = text.match(parentHeadingRe);
81
81
  if (p) {
82
82
  const at = p.index + p[0].length;
83
- return `${text.slice(0, at)}\n${headingLine}\n\n${bullet}\n${text.slice(at).replace(/^\n/, "\n")}`;
83
+ return `${text.slice(0, at)}\n${headingLine}\n\n${bullet}\n${text.slice(at)}`;
84
84
  }
85
85
  return `${text.replace(/\s*$/, "")}\n\n${parentHeadingLine}\n\n${headingLine}\n\n${bullet}\n`;
86
86
  }
@@ -114,8 +114,8 @@ export function parseCaseLawLedger(raw) {
114
114
  /**
115
115
  * The census a reader (and a scenario) asks the ledger for. PURE, and it never judges.
116
116
  *
117
- * `readByTerritory` is the one that answers the depth-dive condition: a dive names ONE territory
118
- * (/), so "did this dive read anything in its own territory" is a lookup, not an inference.
117
+ * `readByTerritory` is the one that answers the depth-dive condition: a dive names ONE territory,
118
+ * so "did this dive read anything in its own territory" is a lookup, not an inference.
119
119
  */
120
120
  export function caseLawRetrievalCensus(ledger) {
121
121
  const queries = ledger?.queries ?? [];
@@ -1363,7 +1363,7 @@ export function parseDispositionForm(raw) {
1363
1363
  // EVERY OBLIGATION SHIPS. No head(), no slice(), no "…and N more". The register-digest sibling
1364
1364
  // (gateway.mjs deferredSlicesRequiredRows) printed the first six qids per axis and elided the rest, and its
1365
1365
  // own doc block records what that cost: "Under advice that is an economy; under a requirement it makes
1366
- // compliance impossible." Same rule here, and 's: a value a model must copy is complete, or it is
1366
+ // compliance impossible." Same rule here: a value a model must copy is complete, or it is
1367
1367
  // visibly marked as cut. Nothing below is cut, so nothing below is marked.
1368
1368
  /**
1369
1369
  * The obligation set as the seat's own to-do list, for the tool result. Returns "" when nothing is owed —
@@ -1375,7 +1375,7 @@ export function renderConnotationObligations(ob, { ledgerPath = "your half ledge
1375
1375
  // ── — THE PAGE IS NUMBERED OFF THE SAME FUNCTION THAT RESOLVES THE NUMBER ─────────────────
1376
1376
  //
1377
1377
  // `obligationRows` is the driver's row list, and a `row_index` is a position into it. Numbering this
1378
- // page with its own counter would be two derivations of one ordering, which is the mistake 's
1378
+ // page with its own counter would be two derivations of one ordering, which is the mistake the
1379
1379
  // segment display already names in its own comment: "the display and the check cannot disagree, which
1380
1380
  // they would the moment either recomputed its own split". So the numbers come from the list itself.
1381
1381
  //
@@ -1610,7 +1610,7 @@ export function renderDispositionTable(rows, ob) {
1610
1610
  for (const e of ob?.queries ?? []) for (const r of e.results) if (r.id) byId.set(normId(r.id), r);
1611
1611
  // — the rows are the FORM's, and a row the seat never ruled on has nothing to render: it is
1612
1612
  // outstanding work, which the gate reports as a count, not a table row reading "—".
1613
- const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
1613
+ const cell = (s) => String(s ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
1614
1614
  const usable = (rows ?? []).filter((d) => d?.receipt_id && d?.ruling);
1615
1615
  // ── — A PARKED ROW IS NOT AN OUTSTANDING ROW, AND THE FILTER ABOVE STOPPED BEING SAFE ─────────
1616
1616
  //
@@ -1683,7 +1683,7 @@ const NO_POLICED_SECTION = "(no PR / reputational / connotation section)";
1683
1683
  // id that is not among THAT row's own candidates — the driver wrote those ids, so this is a damaged
1684
1684
  // form, not outstanding work, and its remedy is different.
1685
1685
  // undisposed_cite_too_short deleted — cannot occur; nothing is retyped.
1686
- // recurrent_uncited_form deleted — the form has no shape for the seat to get wrong. 's
1686
+ // recurrent_uncited_form deleted — the form has no shape for the seat to get wrong. The old
1687
1687
  // cited-but-unbound state was a property of PROSE (right page, wrong query string on the line); a
1688
1688
  // form row IS its binding, so the state has no representative.
1689
1689
  // no_source_cite deleted — advisory, prose-only, never reached a token.
@@ -1786,7 +1786,7 @@ export const CONNOTATION_FORM_REASONS = Object.freeze(
1786
1786
  *
1787
1787
  * `quote_unbound` and `form_damaged` are deliberately absent: those rows ARE ruled.
1788
1788
  *
1789
- * `parked` (/) is absent for the opposite reason and it is the load-bearing one: a parked row
1789
+ * `parked` is absent for the opposite reason and it is the load-bearing one: a parked row
1790
1790
  * is NOT ruled, and it is not outstanding either. Outstanding means "the seat still owes this", and the
1791
1791
  * whole point of the park is that the seat has been told to stop — leaving it here would keep the stage
1792
1792
  * failing forever on a row nobody may re-send, which is the deterministic death the park introduced.
@@ -456,7 +456,7 @@ export const E3_BACKLOG = [
456
456
  where: "driver/stages.mjs:3182",
457
457
  surface: "stage-message",
458
458
  evidence: "MACHINE FINDINGS (MANDATORY): … a JSON OBJECT {\"schema_version\":<FINDINGS_SCHEMA_VERSION>,\"rated_under_framework\":\"…\",\"findings\":[...],\"coverage\":[...],\"context_notes\":[...],\"actions\":[...],\"ask_answers\":[...]} … Each finding object has EXACTLY these keys: {\"ordinal\",\"mark\",\"owner\",\"band\",\"net\",\"bor",
459
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson via validators.narrative",
459
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson via validators.narrative",
460
460
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
461
461
  },
462
462
  // RETIRED 2026-08-16 — synthesis's dictated disposition list, and I am the one who left it standing.
@@ -479,7 +479,7 @@ export const E3_BACKLOG = [
479
479
  where: "driver/stages.mjs:3219",
480
480
  surface: "stage-message",
481
481
  evidence: "- manageable …: {\"category\":\"<EXACTLY one of large-competitor / commercial-partner / troll / well-known-enforcer>\",\"reason\":\"<one-two lines…>\"}",
482
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson",
482
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
483
483
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
484
484
  },
485
485
  {
@@ -488,7 +488,7 @@ export const E3_BACKLOG = [
488
488
  where: "driver/stages.mjs:3235",
489
489
  surface: "stage-message",
490
490
  evidence: "- meters: {\"mark_similarity\":{...},\"goods_proximity\":{...},\"use\":{...},\"enforcer\":{...}} — all four present, each {\"token\",\"basis\",\"source\"}. … mark_similarity = high | medium | low. goods_proximity = high | medium | low. enforcer = high | medium | low | unknown. use = confirmed | not-confirmed | un",
491
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; driver/verify.mjs:1171 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
491
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; driver/verify.mjs:1171 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
492
492
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
493
493
  },
494
494
  {
@@ -497,7 +497,7 @@ export const E3_BACKLOG = [
497
497
  where: "driver/stages.mjs:3236",
498
498
  surface: "stage-message",
499
499
  evidence: "- quadrant: {\"x\",\"y\"} numbers in [0,1]. x = goods/services proximity (0 = distant, 1 = identical). y = mark similarity (0 = distinct, 1 = identical).",
500
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson",
500
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
501
501
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
502
502
  },
503
503
  {
@@ -506,7 +506,7 @@ export const E3_BACKLOG = [
506
506
  where: "driver/stages.mjs:3237",
507
507
  surface: "stage-message",
508
508
  evidence: "- source: {\"source_type\",\"resolved_link\"}. source_type EXACTLY one of: register-vendor / register-euipo / common-law-marketplace / common-law-web / case-law",
509
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson",
509
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
510
510
  removedByMove: "M1 removes the opaque `resolved_link` URI (ordinal / record-handle selection); the object shape and the source_type enum survive",
511
511
  },
512
512
  {
@@ -515,7 +515,7 @@ export const E3_BACKLOG = [
515
515
  where: "driver/stages.mjs:3238",
516
516
  surface: "stage-message",
517
517
  evidence: "coverage[]: ONE object per coverage AREA, EXACTLY {\"area\",\"state\",\"note\"}. … state EXACTLY one of: confirmed-clean / coverage-limited / open / not-searched / note.",
518
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; the render owns the coverage panel from these typed states",
518
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; the render owns the coverage panel from these typed states",
519
519
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
520
520
  },
521
521
  {
@@ -533,7 +533,7 @@ export const E3_BACKLOG = [
533
533
  where: "driver/stages.mjs:3304",
534
534
  surface: "stage-message",
535
535
  evidence: "MARK ASSESSMENT … STRUCTURED FORM …: either field may instead be an OBJECT {\"read\":\"…\",\"spectrum\":\"…\",\"per_class\":[{\"class\":\"5\",\"note\":\"…\"}],\"per_market\":[{\"market\":\"CN\",\"note\":\"…\"}],\"counter_registrations\":[{\"mark\":\"…\",\"uri\":\"/mark/…\",\"note\":\"…\"}],\"acquired\":\"<optional>\",\"note\":\"<optional residual>",
536
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; the report collapses the rows behind toggles and the audit workbook renders them",
536
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; the report collapses the rows behind toggles and the audit workbook renders them",
537
537
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
538
538
  },
539
539
  {
@@ -542,7 +542,7 @@ export const E3_BACKLOG = [
542
542
  where: "driver/stages.mjs:3310",
543
543
  surface: "stage-message",
544
544
  evidence: "FOUR ANSWERS …: \"four_answers\": {\"third_party_rights\":{...},\"objection_likelihood\":{...},\"registrability\":{...},\"client_enforceability\":{...}} … Each answer … is {\"read\":\"…\",\"token\":\"…\",\"basis\":\"…\",\"ordinals\":[…]}. Tokens (closed enums …): third_party_rights = strong|moderate|weak; objection_likelih",
545
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson",
545
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
546
546
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
547
547
  },
548
548
  {
@@ -551,7 +551,7 @@ export const E3_BACKLOG = [
551
551
  where: "driver/stages.mjs:3221",
552
552
  surface: "stage-message",
553
553
  evidence: "ACTIONS REGISTER …: emit \"actions\": [...] — ONE object per forward step …, each EXACTLY {\"id\",\"kind\",\"text\",\"ordinals\"} plus an OPTIONAL \"deadline\" and an OPTIONAL \"condition\". … kind: EXACTLY one of — consent / coexistence-agreement / territorial-delimitation / goods-amendment / mark-modification /",
554
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; pipeline applyCoverageFloor legalActions arm derives the delivered disposition from the closed kind enum",
554
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; pipeline applyCoverageFloor legalActions arm derives the delivered disposition from the closed kind enum",
555
555
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
556
556
  },
557
557
  {
@@ -560,7 +560,7 @@ export const E3_BACKLOG = [
560
560
  where: "driver/stages.mjs:3317",
561
561
  surface: "stage-message",
562
562
  evidence: "COVERAGE JUDGMENT …: emit \"coverage_judgment\": {\"sufficient\":<bool>, \"reason\":\"<one line…>\"} — EXACTLY those two keys. Do NOT emit \"rows\": the driver writes that register itself … anything you type there is replaced wholesale.",
563
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson. The \"Do NOT emit rows\" clause is the purest E3 case in the tree — the prompt names a field, dictates its shape and states in the same breath that code overwrites it",
563
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson. The \"Do NOT emit rows\" clause is the purest E3 case in the tree — the prompt names a field, dictates its shape and states in the same breath that code overwrites it",
564
564
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
565
565
  },
566
566
  {
@@ -575,7 +575,7 @@ export const E3_BACKLOG = [
575
575
  // in E3_EVIDENCE_UNRESOLVED instead would have grown the not-checked slice for a row that is
576
576
  // perfectly checkable, which is the avoidable coverage loss conversion 9's note names one row up.
577
577
  evidence: "SEND ONE \"ask_answers\" ENTRY PER ASK, as a TOP-LEVEL field of the findings record you hand to the call (never as a narrative section — the driver renders the labelled line",
578
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson (finding_ask_answer_answer_missing); assembleReportMd · buildAskAnswersSection joins on the retyped ask string",
578
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson (finding_ask_answer_answer_missing); assembleReportMd · buildAskAnswersSection joins on the retyped ask string",
579
579
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
580
580
  },
581
581
  // DELETED at conversion 10, recorded rather than absorbed — E3's own rule for a
@@ -611,7 +611,7 @@ export const E3_BACKLOG = [
611
611
  where: "driver/stages.mjs:3296",
612
612
  surface: "stage-message",
613
613
  evidence: "add it to the top-level \"context_notes\" array — each object EXACTLY {\"type\":\"famous-neighbour-ungrounded\",\"mark\",\"owner\",\"context\"}",
614
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson",
614
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
615
615
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
616
616
  },
617
617
  {
@@ -620,7 +620,7 @@ export const E3_BACKLOG = [
620
620
  where: "driver/stages.mjs:3184",
621
621
  surface: "stage-message",
622
622
  evidence: "- owner: {\"name\",\"country\",\"registrations\":[...]}. … Each registration: {\"uri\", optionally \"classes\":[\"9\",\"41\"],\"status\",\"filed\",\"expiry\",\"jurisdiction\"}. The \"uri\" is the ONLY field that matters: the driver BINDS classes/status/filed/expiry/jurisdiction AND the owner name from the FETCHED record ke",
623
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson + the record-binding join. Six of the seven keys are stated in the prompt and overwritten by code in the same sentence",
623
+ reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson + the record-binding join. Six of the seven keys are stated in the prompt and overwritten by code in the same sentence",
624
624
  removedByMove: "M1 removes the opaque `uri` (ordinal / record-handle selection against the band); the object skeleton and the five overwritten keys survive",
625
625
  },
626
626
  {
@@ -21,7 +21,7 @@
21
21
  // D4 verify.mjs parseCoverageLedgerJson, same shape
22
22
  // D5 verify.mjs:1504 fail(`${unaccounted[0].token}:…`) — token minted in a DATA ROW
23
23
  // D6 verify.mjs:1567 fail(`${violations[0].token}…`) — validatePlanFeasibility in register-plan.mjs
24
- // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2183 disclosureTextByAxis
24
+ // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2270 disclosureTextByAxis
25
25
  // D8 verify.mjs:2470 caseLawLedgerFail fail(caseLawLedgerFail(…)) — token built in case-law-ledger.mjs:195 caseLawLedgerFail
26
26
  //
27
27
  // A partition built on the 60 tokens a regex CAN see would run green while blind to the rest, which is
@@ -139,10 +139,10 @@ export const VOCABULARY = [
139
139
  { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
140
140
  { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
141
141
  { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
142
- { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs coverageFormFail", family: "driver/register-plan.mjs:1860 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
143
- { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1594 validatePlanFeasibility", dynamic: "D6" },
144
- { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1955 searchedJurisdictionsFromPlan", dynamic: "D6" },
145
- { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2183 disclosureTextByAxis", dynamic: "D7" },
142
+ { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs coverageFormFail", family: "driver/register-plan.mjs:1947 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
143
+ { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1681 validatePlanFeasibility", dynamic: "D6" },
144
+ { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2042 searchedJurisdictionsFromPlan", dynamic: "D6" },
145
+ { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2270 disclosureTextByAxis", dynamic: "D7" },
146
146
  { token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs" },
147
147
  { token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
148
148
  { token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
@@ -13,7 +13,7 @@
13
13
  // PASS; ~86% of emitted tokens landed in no artifact, and the attempt that passed is the one that
14
14
  // PATCHED). A cold ladder never patches.
15
15
  //
16
- // THE CURE IS 's, one gate over: the driver writes the row and the model fills in the judgment. The
16
+ // THE CURE IS THE ONE APPLIED ONE GATE OVER: the driver writes the row and the model fills in the judgment. The
17
17
  // axis, the coverage unit, the open crowd blocks with their qids and hit counts, and the deferred slices
18
18
  // with their per-qid receipt reasons are all facts the driver holds BEFORE the stage dispatches. It
19
19
  // writes them. The seat sets `status` and `reason`, and nothing else in the file is the seat's to write.
@@ -856,7 +856,7 @@ export function findCoverageFormViolations(rows, formError = null) {
856
856
  // kind and nothing in it is a string the model had to copy. parseCoverageLedgerFull survives, but only as
857
857
  // the ARCHIVED-RUN reader (loadCoverageLedger's prose fallback): no gate parses this table any more.
858
858
 
859
- const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
859
+ const cell = (s) => String(s ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
860
860
 
861
861
  /** The `## Coverage ledger` section, rendered from the form's rows. "" when nothing is settled. PURE. */
862
862
  export function renderCoverageLedgerSection(rows) {
@@ -97,6 +97,30 @@ export function prepareSample(dir, { repoRoot, tmp } = {}) {
97
97
  }
98
98
 
99
99
 
100
+ /**
101
+ * THE COPIES GO WHEN THEIR PUBLISH IS DONE, AND AT EXIT WHATEVER HAPPENED.
102
+ *
103
+ * Every copy below is made in the system temp directory and nothing removed it: four per `demo --once`,
104
+ * one per demo start, some a full replayed run of about 1,850 files, and 36 GB of them on one machine
105
+ * (measured 2026-09-19). Each root is recorded as it is made. A caller releases them the moment its
106
+ * publish is done, and an exit hook removes whatever is left, on success and on failure. A caller that
107
+ * can be stopped by a signal turns the signal into an exit, so the hook runs then too.
108
+ */
109
+ const copies = new Set();
110
+ let exitHooked = false;
111
+ function tracked(root) {
112
+ copies.add(root);
113
+ if (!exitHooked) { exitHooked = true; process.once("exit", releaseDemoCopies); }
114
+ return root;
115
+ }
116
+ /** Remove every copy made so far. Safe to call more than once. */
117
+ export function releaseDemoCopies() {
118
+ for (const d of [...copies]) {
119
+ try { rmSync(d, { recursive: true, force: true }); } catch { /* going anyway; the exit hook tries again */ }
120
+ copies.delete(d);
121
+ }
122
+ }
123
+
100
124
  /**
101
125
  * The directory a frozen demo should be PUBLISHED from — itself, or a copy when it is part of this tree.
102
126
  *
@@ -117,7 +141,7 @@ export function publishSource(dir, { repoRoot, tmp = tmpdir() } = {}) {
117
141
  const root = resolve(repoRoot ?? "");
118
142
  const here = resolve(dir);
119
143
  if (!root || !(here === root || here.startsWith(root + sep))) return dir;
120
- const copy = join(mkdtempSync(join(tmp, "clearotron-demo-")), "sample");
144
+ const copy = join(tracked(mkdtempSync(join(tmp, "clearotron-demo-"))), "sample");
121
145
  cpSync(here, copy, { recursive: true });
122
146
  return copy;
123
147
  }
@@ -135,7 +159,7 @@ export function publishContainer(root, { repoRoot, tmp = tmpdir() } = {}) {
135
159
  const repo = resolve(repoRoot ?? "");
136
160
  const here = resolve(root);
137
161
  if (!repo || !(here === repo || here.startsWith(repo + sep))) return { dir: root, unusable: demoInventory(root).unusable };
138
- const copy = join(mkdtempSync(join(tmp, "clearotron-demo-")), "sample");
162
+ const copy = join(tracked(mkdtempSync(join(tmp, "clearotron-demo-"))), "sample");
139
163
  mkdirSync(copy, { recursive: true });
140
164
  const { children, unusable } = demoInventory(here);
141
165
  const left = [...unusable];
@@ -288,8 +288,8 @@ export function callsSoFar(dispositionsPath) {
288
288
  export { PARK_AFTER_REFUSALS };
289
289
 
290
290
  /**
291
- * Which rows have been refused past the bound. PURE, and counted PER ROW ACROSS CALLS — the unit 's
292
- * own banner records an earlier investigation dying on, when it joined per-row-final-state instead.
291
+ * Which rows have been refused past the bound. PURE, and counted PER ROW ACROSS CALLS — the unit this
292
+ * module's own banner records an earlier investigation dying on, when it joined per-row-final-state instead.
293
293
  *
294
294
  * A row that is ruled later is never re-parked, because the union only parks rows that are not ruled.
295
295
  * @returns {string[]} row_ids, sorted, so the same ledger always yields the same answer.
@@ -145,7 +145,7 @@ Stages must name **abstract tiers**, not provider aliases. Per-engine maps:
145
145
  | `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `sonnet` | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
146
146
  | `cheap` | saturation-probe | `haiku` | `$CLEAROTRON_OPENAI_MODEL_CHEAP` |
147
147
 
148
- **AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** ( corruption 3, 2026-08-03). This
148
+ **AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** (2026-08-03). This
149
149
  section used to declare two further tiers — `skeptic` → `google/gemini-3-flash-preview` and
150
150
  `refutation` → `together/deepseek-ai/DeepSeek-V4-Pro` — each substituting an anthropic model on the
151
151
  anthropic engine, "grade-moving, validated only in the paid A/B". The substitution never was validated
@@ -191,7 +191,7 @@ disarms the refusal and never the record.
191
191
  | `max` | `max` | `xhigh` |
192
192
 
193
193
  `off` used to map to `minimal` on codex and `low` on claude — a whole rung apart at the bottom, so a
194
- cross-engine effort comparison at `off` was off by one ( corruption 4a).`low` is the anthropic
194
+ cross-engine effort comparison at `off` was off by one. `low` is the anthropic
195
195
  floor, so codex came up to it and codex's `minimal` is deliberately unreachable from the driver's tier
196
196
  vocabulary. `max` is the one sanctioned divergence: it means "this engine's top rung", and codex has no
197
197
  `max`. `engine.anthropic.test.mjs` pins the two tables together and asserts that single exception.
@@ -200,7 +200,7 @@ vocabulary. `max` is the one sanctioned divergence: it means "this engine's top
200
200
  (`assertEffectiveTier`), on the EFFECTIVE tier rather than the declared one: Haiku 4.5 rejects adaptive
201
201
  thinking and the request bounces to sonnet, so the pairing measures sonnet and records haiku. Both ways
202
202
  it can arise are runtime overrides that never touch the stage table (`CLEAROTRON_STAGE_THINKING`, an
203
- `--experiment --model`), which is why the start-of-run scan alone could not see it ( corruption 4b).
203
+ `--experiment --model`), which is why the start-of-run scan alone could not see it.
204
204
 
205
205
  ## 4. `claude -p` invocation (anthropic-agent)
206
206
 
@@ -212,7 +212,7 @@ const RECORDING = Object.freeze({
212
212
  // SECOND — skeptic, same shape ( is the template). Its OWN key, so blind-frame's seat is never
213
213
  // handed record_skeptic and skeptic is never handed record_blind_frame.
214
214
  //
215
- // skeptic's key carries a SECOND tool: search_run_artifacts, the sanctioned read surface ('s
215
+ // skeptic's key carries a SECOND tool: search_run_artifacts, the sanctioned read surface (the
216
216
  // ratification hold, unlock path 1). A READ over the run's own tree, not retrieval — it dials nothing,
217
217
  // writes nothing, and is bounded to CLEAROTRON_BAND_RUN_DIR by construction (skeptic-search.mjs). It
218
218
  // replaces the artifact half of the Bash reads O3c measured; the skill-doc half was already the seeded
@@ -1132,7 +1132,7 @@ serve({
1132
1132
  factors: { type: "array", minItems: 2, maxItems: 4, items: { type: "string" }, description: "2–4 one-line load-bearing observations behind the band." },
1133
1133
  counterFactors: { type: "array", minItems: 1, maxItems: 3, items: { type: "string" }, description: "1–3 one-line statements of what holds this name at this band rather than the next, either way." },
1134
1134
  mitigation: { type: "string", description: "May be \"\" when nothing would move the band — but SEND THE KEY, so a considered \"none\" is not confusable with an omission." },
1135
- assessment: { type: "string", description: "The opening paragraph a reader of THIS MARK'S own report sees first: what the name is, what the landscape looks like, what drives the rating, what to do with it." },
1135
+ assessment: { type: "string", description: "The opening paragraph a reader of THIS MARK'S own report sees first: what the name is, what the landscape looks like, what drives the rating." },
1136
1136
  contextFraming: { type: "string" },
1137
1137
  registerEstimate: { type: "string" },
1138
1138
  parodyNote: { type: ["string", "null"] },