clearotron 0.3.2-beta.12 → 0.3.2-beta.14

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 (100) hide show
  1. package/CONTRIBUTING.md +6 -5
  2. package/INSTALL.md +3 -4
  3. package/README.md +2 -1
  4. package/bin/clearotron.mjs +7 -1
  5. package/bin/example.mjs +61 -23
  6. package/bin/onboard.mjs +16 -2
  7. package/bin/start.mjs +6 -3
  8. package/build-info.json +2 -2
  9. package/docs/CLIENT-MCP.md +6 -6
  10. package/docs/DELIVERY.md +3 -3
  11. package/docs/ONBOARDING.md +1 -1
  12. package/docs/PORTAL.md +3 -3
  13. package/docs/RELEASES.md +1 -1
  14. package/docs/SECURITY.md +1 -1
  15. package/docs/architecture/04-configuration-reference.md +4 -4
  16. package/docs/architecture/05-config-governance.md +10 -10
  17. package/docs/architecture/06-operations-runbook.md +3 -3
  18. package/docs/architecture/07-quality-and-audit.md +1 -1
  19. package/docs/architecture/08-development-guide.md +2 -2
  20. package/docs/architecture/09-security-and-data.md +1 -1
  21. package/docs/decisions/0002-no-dark-functionality.md +1 -1
  22. package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
  23. package/driver/CHANGELOG.md +21 -0
  24. package/driver/ask-ledger.mjs +1 -1
  25. package/driver/band-shape.mjs +1 -1
  26. package/driver/bundled-demos.mjs +2 -2
  27. package/driver/card-budget.mjs +2 -2
  28. package/driver/case-law-ledger.mjs +2 -2
  29. package/driver/connotation-search.mjs +5 -5
  30. package/driver/contract-e3-backlog.mjs +13 -13
  31. package/driver/coverage-form.mjs +2 -2
  32. package/driver/demo-container.mjs +26 -2
  33. package/driver/disposition-tool.mjs +2 -2
  34. package/driver/engine/CONTRACT.md +3 -3
  35. package/driver/engine/mcp/codex-config.mjs +3 -11
  36. package/driver/engine/mcp/gather-config.mjs +1 -1
  37. package/driver/engine/openai-agent.mjs +2 -2
  38. package/driver/findings-model.mjs +21 -6
  39. package/driver/gateway.mjs +1 -1
  40. package/driver/package.json +1 -1
  41. package/driver/pipeline-knockout.mjs +1 -1
  42. package/driver/pipeline.mjs +4 -4
  43. package/driver/placement-union.mjs +1 -1
  44. package/driver/portal-mcp-client.mjs +1 -1
  45. package/driver/portal-report.mjs +4 -1
  46. package/driver/portal-service.mjs +17 -5
  47. package/driver/predelivery-lint.mjs +9 -4
  48. package/driver/progress.mjs +37 -1
  49. package/driver/publish/render-knockout.mjs +31 -6
  50. package/driver/publish/render.mjs +6 -6
  51. package/driver/record-discard.mjs +1 -1
  52. package/driver/register-count.mjs +1 -1
  53. package/driver/register-digest-record.mjs +1 -1
  54. package/driver/report-card-record.mjs +2 -2
  55. package/driver/roster-verdict.mjs +2 -2
  56. package/driver/search-policy.mjs +1 -1
  57. package/driver/skeptic-record.mjs +1 -1
  58. package/driver/stages.mjs +1 -1
  59. package/driver/suite-census.json +40 -10
  60. package/driver/systemd/README.md +1 -1
  61. package/driver/unit-inventory.mjs +2 -2
  62. package/driver/verify.mjs +1 -1
  63. package/mcp-server/CHANGELOG.md +8 -0
  64. package/mcp-server/CONNECT.md +8 -8
  65. package/mcp-server/lib/runs.mjs +1 -1
  66. package/mcp-server/package.json +1 -1
  67. package/mcp-server/serve.mjs +27 -0
  68. package/package.json +1 -1
  69. package/portal-ui/package.json +1 -1
  70. package/providers/free-tier/src/capabilities.js +2 -2
  71. package/providers/jx/src/core.js +3 -3
  72. package/providers/jx/src/turn-envelope.mjs +1 -1
  73. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  74. package/providers/oauth-mcp-bridge/package.json +1 -1
  75. package/providers/perplexity/README.md +1 -1
  76. package/providers/signa/src/capabilities.js +2 -2
  77. package/providers/signa/src/core.js +2 -2
  78. package/providers/uspto-local/src/core.js +1 -1
  79. package/providers/uspto-local/src/sync.js +1 -1
  80. package/scripts/README.md +2 -6
  81. package/scripts/citation-anchor-report.mjs +1 -1
  82. package/scripts/citation-line-check.mjs +3 -3
  83. package/scripts/e2e.mjs +1 -1
  84. package/scripts/env-audit.mjs +1 -1
  85. package/scripts/env-classify.mjs +1 -1
  86. package/scripts/pack-publishable.mjs +1 -1
  87. package/scripts/release-artifact-seal.mjs +2 -2
  88. package/scripts/settings-render-check.mjs +5 -1
  89. package/scripts/strip-tracker-citations.mjs +4 -4
  90. package/scripts/test-run.mjs +46 -3
  91. package/shared/browser-temp-root.mjs +10 -3
  92. package/shared/client-door.mjs +18 -6
  93. package/shared/connect-clients.mjs +2 -0
  94. package/shared/demo-start-args.mjs +10 -0
  95. package/shared/identifier-scan.mjs +2 -2
  96. package/shared/invocation.mjs +1 -1
  97. package/shared/reference-guard-classes.mjs +5 -3
  98. package/shared/stdio-connect.mjs +80 -28
  99. package/shared/toml-string.mjs +25 -0
  100. package/shared/writing-standard-classes.mjs +2 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.2-beta.12",
3
+ "version": "0.3.2-beta.14",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
+ //
5
+ // serve.mjs — the MCP server over stdio, with the Node check in front of it. The connect lines name
6
+ // this file.
7
+ //
8
+ // server.mjs cannot check the Node it runs on. An ES module's imports are all loaded before any of its
9
+ // code runs, so on a Node below the floor the server fails while loading, with an error that names a
10
+ // module and says nothing about the version. Measured on Node 18.20.8: "SyntaxError: The requested
11
+ // module 'node:util' does not provide an export named 'parseEnv'". On a Windows laptop running the
12
+ // connect line through WSL, the distribution's own Node 18 met "SyntaxError: Unexpected token 'with'"
13
+ // (2026-09-19). This file imports only what an old Node can load, refuses in one plain line, and only
14
+ // then loads the server.
15
+ import { fileURLToPath } from "node:url";
16
+ import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // one floor, read from package.json
17
+
18
+ const floor = nodeFloorVerdict();
19
+ if (!floor.ok) {
20
+ console.error(`clearotron: ${nodeFloorRefusal(floor)}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ // THE SERVER STARTS AS THE ENTRY IT WOULD HAVE BEEN. It starts its stdio transport, and reads the
25
+ // install's settings, only when it is the process's entry file, so the entry is handed over first.
26
+ process.argv[1] = fileURLToPath(new URL("./server.mjs", import.meta.url));
27
+ await import("./server.mjs");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.2-beta.12",
4
+ "version": "0.3.2-beta.14",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.12",
5
+ "version": "0.3.2-beta.14",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -48,9 +48,9 @@
48
48
  // The question that DOES depend on the box — which offices this deployment can reach right now — is
49
49
  // answered in driver/register-availability.mjs, and the answer rides the plan as a disclosed
50
50
  // `deferred_coverage` row. It is deliberately not answered here, and `covered` is deliberately not
51
- // narrowed there either: 's admission gate reads this field to decide which territories a client may
51
+ // narrowed there either: the admission gate reads this field to decide which territories a client may
52
52
  // ORDER, so narrowing it to the configured half would refuse a US-only matter at the door instead of
53
- // disclosing its US gap ('s ruling is that such a matter must START and disclose).
53
+ // disclosing its US gap (the rule is that such a matter must START and disclose).
54
54
  //
55
55
  // ── THE "NO SHAPE FOR HALF OF THIS RAN" RULE IS UNCHANGED — IT MOVED THE SPLIT, NOT REPEALED IT ──────
56
56
  //
@@ -13,10 +13,10 @@
13
13
  // SEARCH MACHINERY (query terms), never registry facts — the never-invent rule binds registry DATA,
14
14
  // not the queries we choose to run.
15
15
  //
16
- // BILLING RIDES THE RUN, NOT THIS FILE (/ at e49868e3, and the transport deleted at).
16
+ // BILLING RIDES THE RUN, NOT THIS FILE.
17
17
  // These lanes used to POST to the Anthropic Messages API on ANTHROPIC_API_KEY at a hardcoded haiku
18
- // tier while the rest of the run used whatever program the customer chose — the mix the owner's D6
19
- // ruling forbids. They now go through `engine.runTurn()` like every other model call, so whatever
18
+ // tier while the rest of the run used whatever program the customer chose — the mix the product's one-provider-per-run
19
+ // rule forbids. They now go through `engine.runTurn()` like every other model call, so whatever
20
20
  // program and billing mode the run is on carries them, and nothing here selects a vendor.
21
21
  //
22
22
  // `callMessagesAPI` — with its own MESSAGES_API_URL, x-api-key header and retry ladder — is DELETED
@@ -3,7 +3,7 @@
3
3
  // turn-envelope.mjs — render a forced-tool request as a CLI-turn prompt, and read the turn's text back
4
4
  // into the envelope the three lane parsers already understand. PURE. No driver import, no network.
5
5
  //
6
- // ── why this exists ( /) ─────────────────────────────────────────────────────────────────
6
+ // ── why this exists ─────────────────────────────────────────────────────────────────
7
7
  //
8
8
  // The jx lanes called the Anthropic Messages API directly with `tool_choice: {type:"tool"}`, on a key,
9
9
  // at a hardcoded tier — regardless of which AI program the customer configured. The owner's standing
@@ -1,5 +1,13 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.2-beta.14
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.3.2-beta.13
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.3.2-beta.12
4
12
 
5
13
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.2-beta.12",
3
+ "version": "0.3.2-beta.14",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -11,7 +11,7 @@ optional: without `PERPLEXITY_API_KEY` the run door refuses by name
11
11
  (`preflightResearchCredential`, `../../driver/driver.config.mjs`) rather than searching less. A
12
12
  KNOCKOUT is the exception and it is deliberate — its register half is a whole product without the
13
13
  sweep, so `../../driver/pipeline-knockout.mjs` SKIPS this adapter and discloses the half it did not
14
- run ( acceptance 6). Nothing is billed for a sweep that never runs.
14
+ run. Nothing is billed for a sweep that never runs.
15
15
  (`CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES` is the $0 dev route to a knockout WITH a sweep.)
16
16
 
17
17
  The header's `index.js` and `build.js` belong to the plugin packaging this core was extracted from;
@@ -20,7 +20,7 @@
20
20
  // * predicates.wildcardInfix — `contains` would appear to serve it and does not. The kernel hands
21
21
  // the infix case its RAW pattern with the asterisks still in it, so the sweep would search the
22
22
  // punctuation. Declaring it would be declaring a capability the executor cannot serve, which is
23
- // the one thing 's criteria forbid. Stays null; the slice defers, disclosed.
23
+ // the one thing a declaration must never do. Stays null; the slice defers, disclosed.
24
24
  // * resultCeiling — there is no single number to put here, and the reason is worth the paragraph.
25
25
  // An unscoped term pages to exhaustion whatever its size, on every predicate — no ceiling. But add
26
26
  // `filters.owner_name` and the SAME term stops dead at the owner-scoped window (400), which the
@@ -194,7 +194,7 @@ export const CAPABILITIES = Object.freeze({
194
194
 
195
195
  // ── WHICH BINDING LAYERS DOES A SEARCH SCOPED TO THIS OFFICE ACTUALLY RETURN? ──────────
196
196
  //
197
- // 's first task, answered for this provider by driving the three scopings against each other rather
197
+ // The first question for every provider, answered here by driving the three scopings against each other rather
198
198
  // than by reading the documentation. One term, one limit, France:
199
199
  //
200
200
  // filters.offices the national register ALONE
@@ -680,8 +680,8 @@ export function toSignaParams(p = {}) {
680
680
  // and this branch is the only thing standing between that and `strategies: ["exact"]`. With
681
681
  // `predicates.default` declared `"contains"` and no mapping here, every unanchored slice would
682
682
  // have gone to the wire as an EXACT search: narrower than the plan asked for, returning fewer
683
- // rows, and answering as though it were the query requested. That is 's defect exactly, and
684
- // declaring a predicate without wiring it is the one thing this issue's criteria forbid.
683
+ // rows, and answering as though it were the query requested. That is the silent narrowing this branch
684
+ // prevents, and declaring a predicate without wiring it is the one thing a declaration must never do.
685
685
  //
686
686
  // A `*` in the term means this is the infix-wildcard case, which shares the empty {} and which
687
687
  // `contains` cannot serve — the kernel hands the RAW pattern, asterisks included, so a contains
@@ -67,7 +67,7 @@ function resolveDbPath(auth) {
67
67
  // marker added by one consumer is a marker the other consumers do not get. Marking at the throw
68
68
  // means every path out of this provider carries it, including ones written later.
69
69
  //
70
- // 's composite backstop caught a THROW and converted it. That backstop never fired against this
70
+ // The composite backstop caught a THROW and converted it. That backstop never fired against this
71
71
  // provider, because four of its five entry points catch their own throw and return a plain error
72
72
  // first — and the test that claimed it worked drove a stub that throws, which this does not do.
73
73
  throw new Error(
@@ -312,7 +312,7 @@ export async function syncIndex({ dbPath, files, ingest = ingestFile, onFile = n
312
312
  await onPhase?.("ingest");
313
313
  // BLOCKS THE EVENT LOOP FOR THE WHOLE REBUILD — node:sqlite is synchronous, and this is one
314
314
  // transaction over every row in the index. Nothing on this thread runs again until it commits,
315
- // which is why 's disk sampler is a worker thread rather than a timer.
315
+ // which is why the disk sampler is a worker thread rather than a timer.
316
316
  rebuildFts(db);
317
317
  await onPhase?.("fts");
318
318
  const rows = db.prepare("SELECT count(*) AS n FROM mark").get().n;
package/scripts/README.md CHANGED
@@ -52,14 +52,10 @@ than its contents, and the whole reason the browser checks exist is that a fix f
52
52
  scrollbars" shipped a report with two scrollbars past 1,500 passing tests. Those checks run in CI's
53
53
  `build-and-verify` job and nowhere else — so **CI, not your machine, is what covers them.**
54
54
 
55
- `render-check.mjs` is a further step out, running in **neither** — and wiring it up is what found out
56
- why that mattered. CI left it out because it measures a PUBLISHED RUN and had no pool to point at;
57
- `--fixture-pool` removed that reason, so wired it into`build-and-verify` as a blocking step.
58
-
59
- **Its first run anywhere reported 6 of 9 assertions failing with `no-probe` and `null`** — not a layout
55
+ **`render-check.mjs`'s first run anywhere reported 6 of 9 assertions failing with `no-probe` and `null`** — not a layout
60
56
  defect, an inability to *measure*. The three assertions that look inside the report frame (the height
61
57
  bridge, the scrollbar loop, the sideways overflow) depend on a probe script in the sandboxed iframe
62
- posting back to the shell. The pool built fine; the harness cannot see inside its own frame. **.**
58
+ posting back to the shell. The pool built fine; the harness cannot see inside its own frame.
63
59
 
64
60
  **The first explanation written down here was wrong, and how it was wrong is the useful part.** This
65
61
  paragraph used to say the message never arrives under `file://` with production's sandbox. Measured in
@@ -113,7 +113,7 @@ export function anchorRows(citations, linesOf) {
113
113
  if (!others.length) { rows.push({ ...c, verdict: "silent", anchor }); continue; }
114
114
  let elsewhere = null;
115
115
  for (const n of others) {
116
- const decl = new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|const|let|var|class)\\s+${n.replace(/\$/g, "\\$")}\\b`);
116
+ const decl = new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|const|let|var|class)\\s+${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
117
117
  for (let i = 0; i < target.length; i++) {
118
118
  if (!decl.test(target[i])) continue;
119
119
  // THE CITATION POINTING INSIDE WHAT IT NAMES IS CORRECT, not drifted — see the header.
@@ -528,7 +528,7 @@ function main() {
528
528
  // WATCHLIST_OWNERS_MAX declared in variant-manifest-model.mjs anything else — `declared` marks it
529
529
  //
530
530
  // A BARE WORD BEFORE `in <file>` IS NOT A SYMBOL CITATION, deliberately, and this is the difference
531
- // between this arm and 's. reads the token beside a number SPECULATIVELY and drops it when it
531
+ // between this arm and the line arm, which reads the token beside a number SPECULATIVELY and drops it when it
532
532
  // is not declared, because "ALREADY computes (coverage-ledger.mjs:180)" would otherwise manufacture a
533
533
  // defect out of emphatic prose. This arm cannot do that: an undeclared symbol has to FAIL or the check
534
534
  // has no teeth at all. So the form is opted into. `the field in scope-ledger.mjs` and `checked in
@@ -923,8 +923,8 @@ export function symbolMisses(citations, readLines) {
923
923
  // WHAT IT CANNOT SEE, and why this stays a slice. A citation that lands on the WRONG NON-BLANK LINE is
924
924
  // invisible to it — that looks identical to a correct one. Three of the eleven citations repointed in
925
925
  // `stages.mjs` under this issue were exactly that: `stages.mjs:1474` pointed at a transliteration `why:`
926
- // row, `pipeline.mjs:2908` at a different function entirely, and neither line was blank. Only 's
927
- // arm can decide those, and only where the citation names a symbol. A clean run here is evidence about
926
+ // row, line 2908 of `pipeline.mjs` at a different function entirely, and neither line was blank. Only the
927
+ // symbol arm can decide those, and only where the citation names a symbol. A clean run here is evidence about
928
928
  // punctuation, not about correctness.
929
929
  //
930
930
  // AN OVERRUN IS NOT THIS FINDING. A span running past the file's last line is already reported as
package/scripts/e2e.mjs CHANGED
@@ -2370,7 +2370,7 @@ const TERMINAL_BY_SUFFIX_RAN = { failed: "failed", done: "delivered", cancelled:
2370
2370
 
2371
2371
  // REPAIR — the live-state vocabulary is IMPORTED, not retyped. This file's first draft spelled it
2372
2372
  // out twice more (an in-flight suffix set and a claim-lock regex), which is exactly the failure
2373
- // queue-markers.mjs was created for: "written down three times and the copies disagreed ". 's
2373
+ // queue-markers.mjs was created for: "written down three times and the copies disagreed". The
2374
2374
  // claim lock in particular is a NORMAL in-flight state — a live token is a claim in progress, a dead one
2375
2375
  // is restored by sweepAbandonedTakeovers — and one more private copy of that rule is one more chance to
2376
2376
  // report a claim race as a stranded job.
@@ -624,7 +624,7 @@ const EXTERNAL_RE = /^#\s*external:\s*(\S.*)$/;
624
624
  //
625
625
  // THE TWO MARKERS DO NOT CLEAR EACH OTHER. `pending` was a single slot, and any comment line that was
626
626
  // not an `# external:` reset it. Adding a second marker to that shape would mean an `# effect:` line
627
- // silently DELETED the `# external:` declaration above it — the row would go orphaned and 's
627
+ // silently DELETED the `# external:` declaration above it — the row would go orphaned and the orphan
628
628
  // guard would red, with the cause three lines away and invisible. Each marker carries its own slot.
629
629
  const EFFECT_RE = /^#\s*effect:\s*(\S.*)$/;
630
630
  // A COMMENTED-OUT ROW STILL COUNTS, the same rule `assigned` uses above and for the same reason: `# X=`
@@ -248,7 +248,7 @@ function namesMergedIntoCandidate(src) {
248
248
  const helpers = new Set([...src.matchAll(/Object\.assign\(\s*candidate\s*,\s*(?:await\s+)?([A-Za-z_$][\w$]*)\s*\(/g)]
249
249
  .map((m) => m[1]));
250
250
  for (const fn of helpers) {
251
- const at = src.search(new RegExp(`\\bfunction\\s+${fn.replace(/\$/g, "\\$")}\\s*\\(`));
251
+ const at = src.search(new RegExp(`\\bfunction\\s+${fn.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`));
252
252
  const refuse = (why) => new Error(`env-classify: bin/onboard.mjs writes what ${fn}() returns, and ${why}, so `
253
253
  + "the names it writes cannot be read. Read as none, they would land in `tuning` and on the deletion "
254
254
  + "population; that is this script failing to look, not a finding about the wizard.");
@@ -123,7 +123,7 @@ async function main() {
123
123
  try {
124
124
  ({ reconcileAndScan: gate } = await import("../cut/packed-artifact.mjs"));
125
125
  } catch (e) {
126
- console.error(" REFUSING (exit 2, could-not-look): cut/packed-artifact.mjs did not load —"
126
+ console.error(" REFUSING (exit 2, could not look): cut/packed-artifact.mjs did not load —"
127
127
  + ` ${String(e?.message ?? e)}.\n`
128
128
  + " That module carries the only rule that says which files may leave this repository, and a\n"
129
129
  + " pack that cannot ask it produces an artifact nobody has checked. An exported tree does not\n"
@@ -106,10 +106,10 @@ function main() {
106
106
  console.error("usage: node scripts/release-artifact-seal.mjs --tarball <path>");
107
107
  process.exit(2);
108
108
  }
109
- // A path that is not there is a could-not-look, never a seal that found nothing to do. Exit 2 is
109
+ // A path that is not there is a check that could not look, never a seal that found nothing to do. Exit 2 is
110
110
  // the house meaning and it keeps this distinguishable from a tarball that was sealed and was clean.
111
111
  if (!existsSync(tarball)) {
112
- console.error(` REFUSING (exit 2, could-not-look): ${tarball} does not exist, so nothing was sealed.`);
112
+ console.error(` REFUSING (exit 2, could not look): ${tarball} does not exist, so nothing was sealed.`);
113
113
  process.exit(2);
114
114
  }
115
115
 
@@ -148,6 +148,10 @@ const sessionId = sess.sessionId
148
148
  const cmd = (method, params = {}) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, sessionId, method, params })) })
149
149
  await cmd('Page.enable')
150
150
  // A probe that throws says so, rather than returning nothing for the assertions after it to misread.
151
+ // A value pasted into code the page evaluates, as a JavaScript string or object literal. JSON.stringify
152
+ // alone leaves `<`, `>`, `/` and the two line separators as they are; escaped, they read the same once
153
+ // parsed and cannot close or break the code they are pasted into.
154
+ const jsLiteral = (v) => JSON.stringify(v).replace(/[<>\/\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`)
151
155
  const evalIn = async (expr) => {
152
156
  const r = (await cmd('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result
153
157
  if (r?.exceptionDetails) console.error(` ! a probe threw: ${r.exceptionDetails.exception?.description ?? r.exceptionDetails.text}`)
@@ -184,7 +188,7 @@ async function reload(ready, what) {
184
188
  }
185
189
 
186
190
  async function setTheme(theme) {
187
- await evalIn(`(() => { document.documentElement.setAttribute('data-theme', ${JSON.stringify(theme)}); try { localStorage.setItem('cordillera-theme', ${JSON.stringify(theme)}) } catch {} return true })()`)
191
+ await evalIn(`(() => { document.documentElement.setAttribute('data-theme', ${jsLiteral(theme)}); try { localStorage.setItem('cordillera-theme', ${jsLiteral(theme)}) } catch {} return true })()`)
188
192
  await sleep(250)
189
193
  }
190
194
 
@@ -2,15 +2,15 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
4
  //
5
- // Removes the internal citation OPENER from comments and prose in the tree (tracker issue 309).
5
+ // Removes the internal citation OPENER from comments and prose in the tree.
6
6
  //
7
7
  // The form is `tracker issue NNN — ` standing at the head of a sentence, where the citation is not part
8
8
  // of what the sentence says but a label in front of it. Stripping the opener leaves the sentence intact:
9
9
  //
10
- // // tracker issue 1149 — the walk must start at the repository root
10
+ // // tracker issue NNNN — the walk must start at the repository root
11
11
  // // the walk must start at the repository root
12
12
  //
13
- // assert.ok(x, "Refs tracker issue 2075 — an absent file is a finding")
13
+ // assert.ok(x, "Refs tracker issue NNNN — an absent file is a finding")
14
14
  // assert.ok(x, "an absent file is a finding")
15
15
  //
16
16
  // WHY THE PATTERN LOOKS OVER-SPECIFIED. Three parts of it are load-bearing and each was measured, not
@@ -20,7 +20,7 @@
20
20
  // and the replacement is `$1`, which puts that opener back. Without the group the sweep deletes the
21
21
  // opening quote of every test name it touches, and a broken string literal is a syntax error in the
22
22
  // lucky cases and a changed assertion in the unlucky ones.
23
- // · `\s+` AFTER THE SEPARATOR, never `\s*`. `tracker issue 1149-12` is an ITEM suffix, not a citation
23
+ // · `\s+` AFTER THE SEPARATOR, never `\s*`. `tracker issue NNNN-12` is an ITEM suffix, not a citation
24
24
  // followed by prose: there is no space after its hyphen. `\s*` eats the item number.
25
25
  // · THE `i` FLAG. `Refs tracker issue NNN` is capitalised at the head of a commit-style line and is a
26
26
  // fifth of the corpus.
@@ -271,8 +271,8 @@ process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
271
271
  // driver/test/*.test.mjs. They are indistinguishable from code defects. An agent who runs the suite on a
272
272
  // branch, sees 295 red, and diffs the failing NAMES against a baseline taken the same way sees zero
273
273
  // regressions and calls the branch clean — and it is, but roughly 190 tests never executed, and a real
274
- // regression inside any of them is invisible by exactly that arithmetic. It has already happened: 's
275
- // first full-suite comparison was taken against a 295-fail baseline.
274
+ // regression inside any of them is invisible by exactly that arithmetic. It has already happened: a
275
+ // full-suite comparison was taken against a 295-fail baseline.
276
276
  //
277
277
  // So: refuse, name what is missing, and name the command. REFUSE RATHER THAN INSTALL — this wrapper is
278
278
  // what CI and scripts/publication-scan.mjs run the suite through, and a wrapper that can start a network
@@ -751,6 +751,37 @@ const repoBefore = snapshotRepo(REPO_ROOT);
751
751
  const RUN_HOME = String(process.env.HOME ?? "").trim() || homedir();
752
752
  const homeBefore = snapshotHome(RUN_HOME);
753
753
 
754
+ // ── AND NOTHING THE RUN MADE IS LEFT IN THE MACHINE'S TEMP DIRECTORY ────────────────────────────────
755
+ //
756
+ // The run's own root is removed on every exit. What escapes it is a child handed an environment without
757
+ // TMPDIR: it falls back to the machine's temp directory, and nothing removes what it made there. Measured
758
+ // 2026-09-19: fifteen `clearotron-demo-*` directories per `npm test`, from the demo's temporary sample
759
+ // copies, and 36 GB accumulated on one box. Named by the product prefix, so the guard reads only what
760
+ // this product makes; owned by this account, so another user's run is not ours to count.
761
+ //
762
+ // THE OUTERMOST RUN WATCHES THE MACHINE'S TEMP ROOTS. A nested run watches only its own base, and only
763
+ // when a test arms it (CT_TEMP_LEAK_GUARD=1): the runner's own tests drive nested runs while the rest of
764
+ // the suite is working, and a nested guard reading the shared root would count its neighbours' files.
765
+ const LEAK_PREFIXES = Object.freeze(["clearotron-demo-"]);
766
+ const OUTERMOST = !String(process.env.CT_TEST_MACHINE_TMP ?? "").trim();
767
+ const LEAK_ROOTS = OUTERMOST
768
+ ? [...new Set([MACHINE_TMP, REAL_TMP, ...PLATFORM_TMP].map((p) => resolve(p)))]
769
+ : String(process.env.CT_TEMP_LEAK_GUARD ?? "") === "1" ? [resolve(REAL_TMP)] : [];
770
+ function tempLeftovers() {
771
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
772
+ const out = new Set();
773
+ for (const r of LEAK_ROOTS) {
774
+ let names;
775
+ try { names = readdirSync(r); } catch { continue; }
776
+ for (const n of names) {
777
+ if (!LEAK_PREFIXES.some((p) => n.startsWith(p))) continue;
778
+ try { if (uid == null || statSync(join(r, n)).uid === uid) out.add(join(r, n)); } catch { /* gone already */ }
779
+ }
780
+ }
781
+ return out;
782
+ }
783
+ const tempBefore = tempLeftovers();
784
+
754
785
 
755
786
  child = spawn(argv[0], argv.slice(1), {
756
787
  stdio: "inherit",
@@ -808,8 +839,20 @@ child.on("close", (code, signal) => {
808
839
  if (wrote.length) for (const line of explainRepoWrites(wrote)) console.error(line);
809
840
  const wroteHome = repoWrites(homeBefore, snapshotHome(RUN_HOME), RUN_HOME);
810
841
  if (wroteHome.length) for (const line of explainHomeWrites(wroteHome, RUN_HOME)) console.error(line);
842
+ const leftInTemp = [...tempLeftovers()].filter((p) => !tempBefore.has(p)).sort();
843
+ if (leftInTemp.length) {
844
+ console.error("");
845
+ console.error(`[test-run] THIS RUN LEFT ${leftInTemp.length} DIRECTOR${leftInTemp.length === 1 ? "Y" : "IES"} IN THE MACHINE'S TEMP DIRECTORY:`);
846
+ for (const p of leftInTemp) console.error(` + ${p}`);
847
+ console.error("");
848
+ console.error(" A child handed an environment without TMPDIR puts its temporary files in the machine's temp");
849
+ console.error(" directory, where nothing removes them. Pass `TMPDIR: tmpdir()` in that child's env, so they land");
850
+ console.error(" in this run's own root, which is removed on every exit.");
851
+ console.error("");
852
+ console.error(" (Another run on this account making the same directories at the same time prints this too.)");
853
+ }
811
854
  // THIS MAY TURN A GREEN RUN RED. IT MUST NEVER TURN A RED RUN GREEN — a failing suite keeps its own
812
855
  // exit code, because what the tests found matters more than what they wrote while finding it.
813
856
  const childCode = code ?? 1;
814
- process.exit(childCode !== 0 ? childCode : (wrote.length || wroteHome.length ? 1 : 0));
857
+ process.exit(childCode !== 0 ? childCode : (wrote.length || wroteHome.length || leftInTemp.length ? 1 : 0));
815
858
  });
@@ -111,13 +111,20 @@ export function browserTempRoot() {
111
111
  }
112
112
 
113
113
  /**
114
- * The environment a browser must be spawned with so its singleton lock lands under `root`.
114
+ * The environment a browser must be spawned with so everything it writes lands under `root`.
115
115
  *
116
- * `TMPDIR` is the whole mechanism, so this refuses rather than passing a root that cannot work.
116
+ * `TMPDIR` puts its singleton lock there, so this refuses rather than passing a root that cannot work.
117
+ * AND A HOME OF ITS OWN. A profile directory does not hold everything a browser writes: it keeps its
118
+ * font cache, certificate store, desktop settings and crash folder under the user's home, so a suite run
119
+ * left `.cache/fontconfig`, `.local/share/pki`, `.local/share/applications` and
120
+ * `.config/google-chrome/Crash Reports` in the real home of whoever ran it (measured on a fresh home,
121
+ * 2026-09-19). The home and the three XDG folders now sit inside the root, and go with it.
117
122
  */
118
123
  export function browserEnv(root, env = process.env) {
119
124
  assertRootFits(root);
120
- return { ...env, TMPDIR: root };
125
+ const home = join(root, "home");
126
+ for (const d of [home, join(home, ".config"), join(home, ".cache"), join(home, ".local", "share")]) mkdirSync(d, { recursive: true });
127
+ return { ...env, TMPDIR: root, HOME: home, XDG_CONFIG_HOME: join(home, ".config"), XDG_CACHE_HOME: join(home, ".cache"), XDG_DATA_HOME: join(home, ".local", "share") };
121
128
  }
122
129
 
123
130
  /**
@@ -214,7 +214,7 @@ export const clientDoorAddress = (env = {}) => `http://127.0.0.1:${clientDoorPor
214
214
  * the fence off accepts no account key, and the fence on with nothing listening is a setting with no
215
215
  * server. Reporting "standing" on half of it would send a reader to paste an address at nothing.
216
216
  */
217
- export function clientDoorState({ env = {}, unitDir, exists, active = null, listening = null, activeState = null, subState = null } = {}) {
217
+ export function clientDoorState({ env = {}, unitDir, exists, active = null, listening = null, ownListener = null, activeState = null, subState = null } = {}) {
218
218
  const fenceOn = String(env.CLIENT_MCP_ACCOUNT_ACCESS ?? "").trim() === "1";
219
219
  const unitInstalled = Boolean(exists(join(unitDir, CLIENT_DOOR_UNIT)));
220
220
  // `standing` IS UNCHANGED AND STILL MEANS CONFIGURED — a file on disk and a fence flag. Two callers
@@ -238,7 +238,7 @@ export function clientDoorState({ env = {}, unitDir, exists, active = null, list
238
238
  // so a crash loop (`activating/auto-restart`) and a unit that was never started (`inactive/dead`)
239
239
  // reduce to the same `false` and printed the same sentence — one is a fault to read the journal for,
240
240
  // the other is a connect that stopped half-way.
241
- listening, activeState, subState, standing, fenceOn, unitInstalled, active, serving: standing && active === true };
241
+ listening, ownListener, activeState, subState, standing, fenceOn, unitInstalled, active, serving: standing && active === true };
242
242
  }
243
243
 
244
244
  /**
@@ -300,10 +300,17 @@ export function describeDoorState(door, {
300
300
  // reader whose foreground door was up to run the command they had just run — and it is NOT enough
301
301
  // to call it theirs: the product's ports are fixed defaults, so on a shared box the answer may be
302
302
  // another install's door entirely. Caught by 2145's arm on a machine where exactly that was true.
303
+ //
304
+ // AND WHOSE IT IS IS ASKED, NOT GUESSED. On a shared box the port is often another account's door,
305
+ // and "something is listening on the client door's address for this environment" read as this
306
+ // install's (measured 2026-09-19). `ownListener` is true only when this install's own record says
307
+ // it holds the port; anything else is said as exactly what was measured, a process on the port.
308
+ if (door.ownListener === true)
309
+ return { level: "info",
310
+ text: `this install's client door is running in the foreground — it stops when that terminal does; `
311
+ + `\`${startCmd} --background\` installs the unit.` };
303
312
  return { level: "info",
304
- text: `something is listening on the client door's address for this environment — no ${unit} is `
305
- + "installed, so either this install is running in the foreground (it stops when that terminal "
306
- + `does; \`${startCmd} --background\` installs the unit) or another install holds the port.` };
313
+ text: "a process holds the client door's port, and nothing here shows it is this install's door." };
307
314
  }
308
315
  return { level: "info",
309
316
  text: `the client door is not set up here — ${missing}. \`${startCmd}\` writes both. Since the `
@@ -313,11 +320,16 @@ export function describeDoorState(door, {
313
320
  if (door.active === null) {
314
321
  // THE PROBE STILL COUNTS HERE. Not asking systemd is not the same as knowing nothing: if the port
315
322
  // answers, the door is serving whatever systemd would have said.
316
- if (door.listening === true) {
323
+ if (door.listening === true && door.ownListener === true) {
317
324
  return { level: "ok",
318
325
  text: `${unit} is installed and account access is enabled, and the client door's port is `
319
326
  + "answering — systemd was not asked, so this is the port's word rather than the unit's" };
320
327
  }
328
+ if (door.listening === true) {
329
+ return { level: "info",
330
+ text: `${unit} is installed and account access is enabled — whether it is RUNNING was not checked. `
331
+ + "A process holds the client door's port, and nothing here shows it is this install's door." };
332
+ }
321
333
  return { level: "info",
322
334
  text: `${unit} is installed and account access is enabled — whether it is RUNNING was not checked, `
323
335
  + "so this says the door is set up, not that it answers" };
@@ -429,6 +429,8 @@ const splitSides = (step) => {
429
429
  return variants.map((v) => ({
430
430
  ...step,
431
431
  text: v.heading,
432
+ // A side that needs something filled in before it is pasted says so under its own copy.
433
+ hint: v.hint ?? step.hint,
432
434
  copy: { ...step.copy, text: v.text, stdio: { ...step.copy.stdio, text: v.text, variants: null } },
433
435
  }));
434
436
  };
@@ -12,6 +12,16 @@
12
12
  * `--demo-own-base` says it is the demo's default, which is what lets the supervisor remove the folder
13
13
  * when the window closes. `--keep` is the reader's, and it travels as given.
14
14
  */
15
+ /**
16
+ * The Node flag the demo runs under, so Node's one-time warning that its built-in SQLite is experimental
17
+ * does not land on the demo's first screen. Named once: the launcher puts it on the demo's own process,
18
+ * and `bin/example.mjs` hands it to the services it starts through NODE_OPTIONS.
19
+ */
20
+ export const EXPERIMENTAL_WARNING_OFF = "--disable-warning=ExperimentalWarning";
21
+
22
+ /** The same flag added to whatever NODE_OPTIONS the reader already has, which is kept. PURE. */
23
+ export const withWarningOff = (nodeOptions) => [nodeOptions, EXPERIMENTAL_WARNING_OFF].filter(Boolean).join(" ");
24
+
15
25
  export function demoStartArgs({ demoBase, readerBase = false, keep = false, port = null, noOpen = false } = {}) {
16
26
  const args = ["--demo", "--base", demoBase];
17
27
  if (!readerBase) args.push("--demo-own-base");
@@ -152,7 +152,7 @@ export function firesOn(name, line, suffixable) {
152
152
  * @returns {{start: number, end: number}[]}
153
153
  */
154
154
  export function matchSpans(name, line, suffixable) {
155
- const body = name.replace(/[&]/g, "\\&").replace(/ /g, SEPARATOR_CLASS);
155
+ const body = name.replace(/[.*+?^${}()|[\]\\&]/g, "\\$&").replace(/ /g, SEPARATOR_CLASS);
156
156
  const tail = suffixable.has(name) ? "" : "(?![A-Za-z0-9])";
157
157
  const re = new RegExp(`(?<![A-Za-z0-9])${body}${tail}`, "gi");
158
158
  const out = [];
@@ -332,7 +332,7 @@ const COMMENT_LINE = /^\s*(\/\/|#|\*|<!--)/;
332
332
  // this repository is about to start printing addresses at — an allowlist for those mailboxes would
333
333
  // have been inert, because nothing ever examined them.
334
334
  //
335
- // WHY THE RULED MAILBOXES JOIN ROLE_LOCALPART RATHER THAN THE DOMAIN GETTING AN EXEMPTION. 's
335
+ // WHY THE RULED MAILBOXES JOIN ROLE_LOCALPART RATHER THAN THE DOMAIN GETTING AN EXEMPTION. The
336
336
  // ruling says `security` joins ROLE_LOCALPART "in the same PR, never a bypass", and that is the
337
337
  // right shape: exempting the whole domain would also pass a named person's address at it, which is
338
338
  // exactly the thing nobody should paste into a public README. Measured before widening rather than
@@ -312,7 +312,7 @@ export function reachableCommand(verb, { argv1 = process.argv[1] ?? "", env = pr
312
312
  /**
313
313
  * The verb by NAME ONLY — no prefix, no path, no `npx`.
314
314
  *
315
- * ✕ A DELIBERATE EXCEPTION TO "ONE TREATMENT", AND THE ONE SURFACE THAT NEEDS IT. 's
315
+ * ✕ A DELIBERATE EXCEPTION TO "ONE TREATMENT", AND THE ONE SURFACE THAT NEEDS IT. The
316
316
  * second requirement is that the choice is made once rather than per site, so an exception has to be
317
317
  * named rather than quietly spelled differently somewhere.
318
318
  *
@@ -85,10 +85,12 @@ export const withoutColourValues = (line) => String(line).replace(HEX_COLOUR, (m
85
85
  });
86
86
 
87
87
  /** Strip the spans where a `#NNN` is an address rather than a reference. */
88
- export const withoutLinkTargets = (line) => String(line)
88
+ export const withoutLinkTargets = (line) => withoutAngleSpans(String(line)
89
89
  .replace(/\]\([^)]*\)/g, "]()") // markdown link targets, anchors included
90
- .replace(/https?:\/\/\S+/g, "") // bare URLs and their fragments
91
- .replace(/<[^>]*>/g, ""); // angle-bracket autolinks
90
+ .replace(/https?:\/\/\S+/g, "")); // bare URLs and their fragments
91
+
92
+ /** Angle-bracket spans (autolinks, tags), removed until none is left: one pass can reassemble one. */
93
+ const withoutAngleSpans = (s) => { for (let prev = null; prev !== s;) { prev = s; s = s.replace(/<[^>]*>/g, ""); } return s; };
92
94
 
93
95
  // A `#` COMMENT IS A COMMENT WHEREVER THE FILE FORMAT SAYS SO, not only in YAML. Extensionless is
94
96
  // deliberate: a systemd unit or a dotfile often has no extension worth matching, so the KNOWN