clearotron 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.env.example +36 -37
  2. package/CONTRIBUTING.md +8 -4
  3. package/INSTALL.md +24 -4
  4. package/README.md +7 -6
  5. package/bin/example.mjs +6 -5
  6. package/bin/onboard.mjs +175 -9
  7. package/bin/start.mjs +66 -4
  8. package/build-info.json +2 -2
  9. package/demo/README.md +1 -1
  10. package/docs/GLOSSARY.md +85 -0
  11. package/docs/README.md +1 -0
  12. package/docs/architecture/01-product-overview.md +21 -9
  13. package/docs/architecture/05-config-governance.md +5 -0
  14. package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
  15. package/driver/CHANGELOG.md +34 -0
  16. package/driver/README.md +25 -6
  17. package/driver/connotation-search.mjs +1 -1
  18. package/driver/contract-audit.mjs +5 -1
  19. package/driver/contract-e3-baseline.json +11 -11
  20. package/driver/doubt-selection.mjs +1 -1
  21. package/driver/drainer-identity.mjs +1 -1
  22. package/driver/effort-model.mjs +2 -2
  23. package/driver/engine/probe.mjs +45 -5
  24. package/driver/gateway.mjs +2 -2
  25. package/driver/outbox-backoff.mjs +1 -1
  26. package/driver/package.json +1 -1
  27. package/driver/pipeline.mjs +206 -101
  28. package/driver/plain-register.mjs +16 -2
  29. package/driver/portal-config-view.mjs +30 -1
  30. package/driver/portal-local-auth.mjs +5 -1
  31. package/driver/portal-service.mjs +53 -2
  32. package/driver/predelivery-lint.mjs +54 -25
  33. package/driver/publish/render.mjs +109 -14
  34. package/driver/search-policy.mjs +1 -1
  35. package/driver/stage-context.mjs +13 -0
  36. package/driver/stages.mjs +51 -4
  37. package/driver/suite-census.json +97 -31
  38. package/driver/systemd/clearotron-worker.service +3 -3
  39. package/driver/tokens.mjs +1 -1
  40. package/driver/unit-inventory.mjs +34 -4
  41. package/mcp-server/CHANGELOG.md +2 -0
  42. package/mcp-server/package.json +1 -1
  43. package/package.json +4 -10
  44. package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
  45. package/portal-ui/dist/index.html +1 -1
  46. package/portal-ui/package.json +1 -1
  47. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  48. package/providers/oauth-mcp-bridge/package.json +1 -1
  49. package/scripts/added-reference-check.mjs +45 -89
  50. package/scripts/e2e.mjs +2 -2
  51. package/scripts/generated-files-are-current.mjs +125 -0
  52. package/scripts/mint-names-in-force.mjs +4 -2
  53. package/scripts/mint-public-residue.mjs +74 -0
  54. package/scripts/mint-reference-strip-backlog.mjs +12 -1
  55. package/scripts/mint-suite-census.mjs +5 -2
  56. package/scripts/render-check.mjs +13 -2
  57. package/scripts/report-frame-check.mjs +8 -1
  58. package/scripts/report-screenshot.mjs +6 -3
  59. package/scripts/revisit-render-check.mjs +6 -3
  60. package/scripts/score.mjs +1 -1
  61. package/scripts/strip-tracker-citations.mjs +9 -1
  62. package/scripts/test-full.mjs +226 -0
  63. package/shared/README.md +2 -1
  64. package/shared/browser-temp-root.mjs +142 -0
  65. package/shared/checkout-move.mjs +1 -1
  66. package/shared/client-door.mjs +4 -3
  67. package/shared/identifier-scan.mjs +1 -1
  68. package/shared/install-auth.mjs +35 -0
  69. package/shared/invocation.mjs +5 -1
  70. package/shared/listen.mjs +55 -3
  71. package/shared/names-in-force.mjs +4 -1
  72. package/shared/os-advice.mjs +91 -0
  73. package/shared/reap-on-exit.mjs +42 -0
  74. package/shared/reference-guard-classes.mjs +351 -0
  75. package/shared/suite-census.mjs +31 -3
  76. package/shared/withheld-paths-access.mjs +36 -18
@@ -14,6 +14,7 @@
14
14
  // CI runs. Without it, the table is rewritten.
15
15
  import { readFileSync, writeFileSync } from "node:fs";
16
16
  import { execFileSync } from "node:child_process";
17
+ import { publishedOf } from "../shared/reference-guard-classes.mjs";
17
18
  import { join, dirname } from "node:path";
18
19
  import { fileURLToPath } from "node:url";
19
20
  import { SIGNATURES, censusOf } from "../driver/reference-strip-signatures.mjs";
@@ -21,8 +22,18 @@ import { SIGNATURES, censusOf } from "../driver/reference-strip-signatures.mjs";
21
22
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
22
23
  const TABLE = join(ROOT, "driver/test/fixtures/reference-strip-backlog.json");
23
24
 
24
- const tracked = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 })
25
+ // THE PUBLISHED POPULATION, NOT THE INDEX. `git ls-files` reads the index, and an overlay run stages
26
+ // the withheld corpus over a clone without committing it — so minting there wrote withheld hits into
27
+ // this public table with nothing reporting it. `publishedOf` filters the tracked list down to what
28
+ // HEAD carries; one helper, shared with the residue floor, because two spellings of this rule would
29
+ // disagree under the overlay and `--check` would report a difference that is only the two
30
+ // instruments asking different questions.
31
+ const all = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 })
25
32
  .split("\n").filter(Boolean);
33
+ const p = publishedOf(all, ROOT);
34
+ if (p.error) { console.error(`mint-reference-strip-backlog: ${p.error}`); process.exit(2); }
35
+ if (p.laid) console.log(`mint-reference-strip-backlog: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
36
+ const tracked = p.files;
26
37
  const minted = censusOf(ROOT, tracked, (f) => readFileSync(join(ROOT, f), "utf8"));
27
38
 
28
39
  if (process.argv.includes("--check")) {
@@ -36,6 +36,9 @@ import { join, dirname } from "node:path";
36
36
  import { fileURLToPath } from "node:url";
37
37
  import { CENSUS_WORKSPACES, CENSUS_ROOT_SCRIPTS, countTestSites, collectionFromManifests, censusDisagreements,
38
38
  rootScriptDisagreements, censusBuckets } from "../shared/suite-census.mjs";
39
+ // THE MEASUREMENT SIDE of the root-script check: the census states the globs it expects and this
40
+ // supplies what the corpus actually resolves to, so the two can disagree.
41
+ import { providerTestFiles } from "./test-full.mjs";
39
42
  import { withheldEntryFor, announceWithheldMode } from "../shared/withheld-paths-access.mjs"; // — withheld is a stated absence, not a loss.: the record does not ship, and without it every absence is a loss
40
43
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
41
44
 
@@ -129,7 +132,7 @@ function main() {
129
132
  // — the same question of the ROOT scripts. Narrowing `test:providers`' glob silences a third of
130
133
  // that population at exit 0, and no per-workspace check can see it: `providers` is not a workspace.
131
134
  const disagreements = [...censusDisagreements(collectionFromManifests(readManifest)),
132
- ...rootScriptDisagreements(readManifest)];
135
+ ...rootScriptDisagreements(readManifest, undefined, () => providerTestFiles())];
133
136
  if (disagreements.length) {
134
137
  // — the message names BOTH declared sources now. It used to say "the census and `npm run
135
138
  // test:full` disagree" whatever had moved, so a root-script disagreement sent the reader to look at
@@ -150,7 +153,7 @@ function main() {
150
153
  try { prev = JSON.parse(readFileSync(CENSUS, "utf8")); } catch { /* first mint */ }
151
154
 
152
155
  const lost = [];
153
- // THE THIRD BUCKET, ruled by overwatch 2026-09-06.
156
+ // THE THIRD BUCKET, decided 2026-09-06.
154
157
  //
155
158
  // A reasoned skip is neither a pass nor a loss, and this census had no place to put one. Two guards
156
159
  // that are each right about their own failure had come to disagree: `a-bail-on-an-unmeetable-
@@ -59,7 +59,8 @@
59
59
  // coupling ever bites, that is the moment to mount the real component instead of copying it.
60
60
 
61
61
  import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
62
- import { reapOnExit } from "../shared/reap-on-exit.mjs"; // — a detached group dies with this script
62
+ import { reapOnExit, removeOnExit } from "../shared/reap-on-exit.mjs";
63
+ import { assertRootFits, browserEnv } from "../shared/browser-temp-root.mjs"; // — a detached group dies with this script
63
64
  import { execFileSync, spawn } from "node:child_process";
64
65
  import { createServer } from "node:http";
65
66
  import { basename, extname } from "node:path";
@@ -76,6 +77,16 @@ const has = (n) => process.argv.includes(`--${n}`);
76
77
 
77
78
  const ZOOMS = arg("zoom", "1.0,1.25").split(",").map((z) => z.trim()).filter(Boolean);
78
79
  const WORK = join(tmpdir(), `render-check-${process.pid}`);
80
+ // WORK is this run's browser temp root as well as its scratch: the browser is spawned with TMPDIR
81
+ // pointing here, so the process-singleton lock it creates lands inside and leaves with it. Asserted
82
+ // rather than assumed, because the budget is on the PATH and nothing about this one is fixed.
83
+ assertRootFits(WORK);
84
+ // The removal below runs on the paths somebody wrote a branch for. This one also covers the exits
85
+ // nobody writes a branch for, which is where the leaked locks came from.
86
+ const stopRootSweep = removeOnExit(WORK);
87
+ // ...unless the caller asked to keep it. Read here rather than at the cleanup below, because the
88
+ // sweep is registered at import time and `--keep` has to be honoured before any exit can happen.
89
+ if (has("keep")) stopRootSweep();
79
90
 
80
91
  /**
81
92
  * Replay the committed frozen run into a fresh pool and return its path.
@@ -412,7 +423,7 @@ async function measureAtZoom(zoom, deadlineMs = 90_000) {
412
423
  // — DETACHED so Chrome LEADS A PROCESS GROUP. Its renderer, GPU and zygote processes are
413
424
  // separate PIDs; without a group there is nothing to signal them with, and the teardown below could
414
425
  // only ever reach the parent.
415
- ], { stdio: ["ignore", "ignore", "ignore"], detached: true });
426
+ ], { stdio: ["ignore", "ignore", "ignore"], detached: true, env: browserEnv(WORK) });
416
427
  // — and the group dies with THIS script, on every exit it can observe.
417
428
  // The teardown below runs on the paths somebody wrote a branch for; a cancelled CI job (SIGTERM),
418
429
  // a Ctrl-C, or a throw elsewhere in this file are not among them — and that is where the measured
@@ -29,6 +29,8 @@ import { tmpdir } from "node:os";
29
29
  import { join, dirname } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
32
+ import { removeOnExit } from "../shared/reap-on-exit.mjs";
33
+ import { assertRootFits, browserEnv } from "../shared/browser-temp-root.mjs";
32
34
 
33
35
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
34
36
 
@@ -48,6 +50,11 @@ function main() {
48
50
  console.log(`shipped sandbox: ${tokens.join(" ")}`);
49
51
 
50
52
  const work = mkdtempSync(join(tmpdir(), "frame-check-"));
53
+ // The browser inherits TMPDIR from here, so its process-singleton lock lands under `work` and goes
54
+ // with it. `removeOnExit` covers the exits the `finally` below cannot see — a cancelled job, a
55
+ // Ctrl-C — which are the ones that left locks behind in the shared temp directory.
56
+ assertRootFits(work);
57
+ removeOnExit(work);
51
58
  try {
52
59
  // The page builds the frame with the SHIPPED string and reports what the browser kept.
53
60
  writeFileSync(join(work, "verify.html"), `<!doctype html><title>pending</title><body>
@@ -62,7 +69,7 @@ function main() {
62
69
  "--headless=new", "--disable-gpu", "--no-sandbox",
63
70
  `--user-data-dir=${join(work, "prof")}`,
64
71
  "--virtual-time-budget=8000", "--dump-dom", `file://${join(work, "verify.html")}`,
65
- ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120000 });
72
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120000, env: browserEnv(work) });
66
73
 
67
74
  // ── IS THIS OUR PAGE, OR CHROME'S ERROR PAGE? (tracker issue 227) ───────────────────────────────
68
75
  //
@@ -20,10 +20,11 @@
20
20
  // "fix" either to match the other.
21
21
  import { spawn } from "node:child_process";
22
22
  import { assertPageLoaded, cjkCharsIn, cjkVerdict, fontsCovering } from "./headless-page.mjs"; // did chrome open the report, or its own error page?
23
- import { mkdtempSync, writeFileSync, existsSync } from "node:fs";
23
+ import { writeFileSync, existsSync } from "node:fs";
24
24
  import { tmpdir } from "node:os";
25
25
  import { join, dirname, resolve } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
+ import { browserRun } from "../shared/browser-temp-root.mjs";
27
28
 
28
29
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
29
30
  const OUT = resolve(process.argv[3] ?? join(ROOT, "docs", "assets", "example-report.png"));
@@ -43,12 +44,14 @@ if (!existsSync(page)) {
43
44
  process.exit(2);
44
45
  }
45
46
 
46
- const userDir = mkdtempSync(join(tmpdir(), "report-shot-"));
47
+ // The profile goes inside a run root whose TMPDIR the browser inherits, so the singleton
48
+ // lock it writes there leaves with the root instead of accumulating in the shared one.
49
+ const { profile: userDir, env: chromeEnv } = browserRun("report-shot-");
47
50
  const chrome = spawn("google-chrome", [
48
51
  "--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars",
49
52
  `--user-data-dir=${userDir}`, `--window-size=${WIDTH},${HEIGHT}`,
50
53
  "--remote-debugging-port=0", `file://${page}`,
51
- ], { stdio: ["ignore", "pipe", "pipe"] });
54
+ ], { stdio: ["ignore", "pipe", "pipe"], env: chromeEnv });
52
55
 
53
56
  let stderr = "";
54
57
  const wsUrl = await new Promise((res, rej) => {
@@ -61,12 +61,13 @@
61
61
 
62
62
  import { navigateOrRefuse } from './headless-page.mjs' // Page.navigate returns an errorText, and nothing read it
63
63
  import { createServer } from 'node:http'
64
- import { readFileSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
64
+ import { readFileSync, existsSync, writeFileSync, rmSync } from 'node:fs'
65
65
  import { join, extname, dirname } from 'node:path'
66
66
  import { fileURLToPath } from 'node:url'
67
67
  import { spawn } from 'node:child_process'
68
68
  import { tmpdir } from 'node:os'
69
69
  import { reapOnExit } from '../shared/reap-on-exit.mjs' // — a detached group dies with this script
70
+ import { browserRun } from "../shared/browser-temp-root.mjs";
70
71
 
71
72
  const HERE = dirname(fileURLToPath(import.meta.url))
72
73
  const DIST = join(HERE, '..', 'portal-ui', 'dist')
@@ -174,11 +175,13 @@ const origin = `http://127.0.0.1:${server.address().port}`
174
175
 
175
176
  // ── chrome ──────────────────────────────────────────────────────────────────────────────────────────
176
177
 
177
- const profile = mkdtempSync(join(tmpdir(), 'revisit-check-'))
178
+ // The profile goes inside a run root whose TMPDIR the browser inherits, so the singleton
179
+ // lock it writes there leaves with the root instead of accumulating in the shared one.
180
+ const { profile, env: chromeEnv } = browserRun("revisit-check-")
178
181
  const chrome = spawn('google-chrome', [
179
182
  '--headless=new', '--disable-gpu', '--no-sandbox', '--remote-debugging-port=0',
180
183
  `--user-data-dir=${profile}`, '--window-size=1440,900', 'about:blank',
181
- ], { stdio: ['ignore', 'ignore', 'pipe'] })
184
+ ], { stdio: ['ignore', 'ignore', 'pipe'], env: chromeEnv })
182
185
  reapOnExit(chrome)
183
186
 
184
187
  const wsUrl = await new Promise((resolve, reject) => {
package/scripts/score.mjs CHANGED
@@ -583,7 +583,7 @@ function print(id, ref, run, s, delta, refPath) {
583
583
  // workspace archive instead of the pool — and the lane fact is visible on the `lane:` line anyway.
584
584
  console.log(row("withheld", "n/a", s.collapseReason));
585
585
  } else {
586
- // — THE SCOPE ON THE LINE, not in a footnote. `role-e2e` calls this "the bucket that changes
586
+ // — THE SCOPE ON THE LINE, not in a footnote. The testing doctrine calls this "the bucket that changes
587
587
  // what you fix", so a bare `0` beside that sentence reads as "no seam defect this round" — and on
588
588
  // the round that measured it, two live in-class rights the run retrieved and dropped were outside
589
589
  // the reference and could not have raised it. The footnote saying `withheld` is a reference-entry
@@ -58,6 +58,7 @@
58
58
  // wants a guard against reintroduction rather than a backlog. Different residue, different repair.
59
59
  import { readFileSync, writeFileSync } from "node:fs";
60
60
  import { execFileSync } from "node:child_process";
61
+ import { publishedOf } from "../shared/reference-guard-classes.mjs";
61
62
  import { join, dirname } from "node:path";
62
63
  import { fileURLToPath } from "node:url";
63
64
 
@@ -122,8 +123,15 @@ export function surveyOf(files, read) {
122
123
  }
123
124
 
124
125
  if (import.meta.url === `file://${process.argv[1]}`) {
125
- const tracked = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 })
126
+ // THE PUBLISHED POPULATION, NOT THE INDEX. The counts this prints are read as a statement about the
127
+ // public tree, and under an overlay `git ls-files` would have made them a statement about the
128
+ // withheld corpus instead. Same helper as the residue floor and the backlog minter.
129
+ const all = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 })
126
130
  .split("\n").filter(Boolean);
131
+ const pub = publishedOf(all, ROOT);
132
+ if (pub.error) { console.error(`strip-tracker-citations: ${pub.error}`); process.exit(2); }
133
+ if (pub.laid) console.log(`${pub.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not read`);
134
+ const tracked = pub.files;
127
135
  const s = surveyOf(tracked, (f) => readFileSync(join(ROOT, f), "utf8"));
128
136
  // BEFORE ANYTHING ELSE, because every number under it is about the files that COULD be read.
129
137
  if (s.unreadable.length) {
@@ -0,0 +1,226 @@
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
+ // RUN EVERY CORPUS, AND REFUSE TO BE SILENT ABOUT ONE THAT WAS NOT RUN.
6
+ //
7
+ // node scripts/test-full.mjs # every corpus
8
+ // node scripts/test-full.mjs --only providers # one of them
9
+ // node scripts/test-full.mjs --list # the plan, run nothing
10
+ //
11
+ // ── WHAT THIS REPLACES, AND WHY A FLAG WAS NOT ENOUGH ────────────────────────────────────────────
12
+ //
13
+ // `npm run test:full --workspaces --if-present` reported this suite green while running part of it.
14
+ // Two separate holes, and only the smaller one is about the flag:
15
+ //
16
+ // - `providers/` holds twelve directories of register-adapter tests. ELEVEN OF THEM ARE NOT
17
+ // WORKSPACES — they have no package.json at all — so no `--workspaces` command can reach them,
18
+ // with or without `--if-present`. That is the larger half and no flag fixes it.
19
+ // - `--if-present` turns "this workspace declares no such script" into a silent skip. An absence
20
+ // reading as a pass is the shape this repository refuses everywhere else.
21
+ //
22
+ // So the population is declared here rather than inferred from a flag, and every member of it is
23
+ // either run or named. A corpus that was not run is a line on stdout, never an omission.
24
+ //
25
+ // ── THE RULE, AND THE ONE THING THAT MAY SOFTEN IT ───────────────────────────────────────────────
26
+ //
27
+ // A declared workspace that does not define `test:full` FAILS this command. That is the permissive
28
+ // half of the old gate written the other way round.
29
+ //
30
+ // One softening, because refusing here would be a false refusal: a workspace whose tests are already
31
+ // run by another corpus is COVERED, not missing. `providers/oauth-mcp-bridge` is a workspace with no
32
+ // scripts block whose ten tests run every time the providers corpus does. Failing it would refuse a
33
+ // workspace whose tests demonstrably execute, and giving it a script of its own would split one
34
+ // corpus across two mechanisms and run those files twice.
35
+ //
36
+ // THE EXEMPTION IS CHECKED, NEVER TRUSTED. For each entry below, this command asserts that the
37
+ // covering corpus's own file list actually reaches that workspace, and that the workspace has test
38
+ // files at all. Both halves matter: an intersection with an empty set is empty, so a workspace that
39
+ // lost its tests would satisfy a reachability check that only asked "is nothing missing". If someone
40
+ // narrows the providers file list until it no longer reaches this workspace, the exemption stops
41
+ // being true and this command says so instead of going quiet.
42
+ import { spawnSync } from "node:child_process";
43
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
44
+ import { join, dirname } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+
47
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
48
+
49
+ /**
50
+ * Every `*.test.mjs` under `providers/`, at any depth, deduplicated and ordered.
51
+ *
52
+ * RECURSIVE, AND THAT IS THE POINT. This read `providers/<dir>/test/*.test.mjs` and nothing else, one
53
+ * level deep — and the exemption check below counted a covered workspace's own files with the SAME
54
+ * shape. So a test at `providers/x/y.test.mjs`, or at `providers/x/test/sub/y.test.mjs`, was in no
55
+ * corpus at all, both sides missed it together and therefore agreed, and every check reported clean
56
+ * under a command whose whole claim is that it runs every corpus. Two derived sides sharing one
57
+ * assumption cannot disagree about it; that is this file's own argument, and it applied here.
58
+ *
59
+ * `node_modules` is excluded because an installed dependency's tests are not this repository's corpus.
60
+ * Nothing else is: a file is either collected or it is a fault, never quietly outside the shape.
61
+ */
62
+ export function providerTestFiles(root = ROOT) {
63
+ const base = join(root, "providers");
64
+ if (!existsSync(base)) return [];
65
+ const out = new Set();
66
+ const walk = (dir, rel) => {
67
+ for (const d of readdirSync(dir, { withFileTypes: true })) {
68
+ if (d.name === "node_modules") continue;
69
+ const here = `${rel}/${d.name}`;
70
+ if (d.isDirectory()) walk(join(dir, d.name), here);
71
+ else if (d.name.endsWith(".test.mjs")) out.add(here);
72
+ }
73
+ };
74
+ walk(base, "providers");
75
+ // DEDUPLICATED ON PURPOSE. The shell form this replaced listed `providers/_shared/test/*.test.mjs`
76
+ // and then `providers/*/test/*.test.mjs`, which matches `_shared` as well — 51 paths for 37 files.
77
+ // Measured before it was called a defect: `node --test` runs a repeated path once, so the old form
78
+ // executed nothing twice. Building the list here makes that a property rather than a coincidence.
79
+ return [...out].sort();
80
+ }
81
+
82
+ /**
83
+ * A workspace with no `test:full`, whose tests another corpus already runs.
84
+ * `reaches` is the claim this command verifies on every run — not documentation.
85
+ */
86
+ export const COVERED_ELSEWHERE = [
87
+ {
88
+ workspace: "providers/oauth-mcp-bridge",
89
+ corpus: "providers",
90
+ why: "a workspace for its dependencies, not for a suite of its own: its tests sit in "
91
+ + "providers/<name>/test like every other adapter's and run with them. A script here would "
92
+ + "split one corpus across two mechanisms.",
93
+ },
94
+ ];
95
+
96
+ /**
97
+ * The corpora, discovered from the manifest rather than listed.
98
+ *
99
+ * `providerFiles` is injectable for one reason: the exemption check below compares the covering
100
+ * corpus's file list against the workspace's own directory, and if both are always read from the
101
+ * same place the comparison can never fail — coverage in appearance with nothing underneath. The
102
+ * state worth refusing is a NARROWED corpus, which is an edit to `providerTestFiles`, so an arm has
103
+ * to be able to hand in a narrowed list and see the refusal.
104
+ */
105
+ export function plan(root = ROOT, { providerFiles } = {}) {
106
+ const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
107
+ const workspaces = manifest.workspaces ?? [];
108
+ const corpora = [];
109
+ const faults = [];
110
+
111
+ for (const ws of workspaces) {
112
+ let scripts = null;
113
+ try { scripts = JSON.parse(readFileSync(join(root, ws, "package.json"), "utf8")).scripts ?? {}; }
114
+ catch { scripts = null; }
115
+ // A DECLARED WORKSPACE WITH NO READABLE MANIFEST IS A FAULT, not a skip. The old command could
116
+ // not tell this apart from a workspace that simply had nothing to run.
117
+ if (scripts === null) { faults.push(`${ws} is a declared workspace and its package.json could not be read`); continue; }
118
+
119
+ if (scripts["test:full"]) {
120
+ corpora.push({ name: ws, kind: "workspace", argv: ["npm", "run", "test:full", "-w", ws] });
121
+ continue;
122
+ }
123
+
124
+ const cover = COVERED_ELSEWHERE.find((c) => c.workspace === ws);
125
+ if (!cover) {
126
+ faults.push(`${ws} is a declared workspace and defines no \`test:full\` script. Either give it `
127
+ + `one, or add it to COVERED_ELSEWHERE in scripts/test-full.mjs naming the corpus that runs `
128
+ + `its tests — which is checked, so the claim has to be true.`);
129
+ continue;
130
+ }
131
+ corpora.push({ name: ws, kind: "covered", cover });
132
+ }
133
+
134
+ const files = providerFiles ?? providerTestFiles(root);
135
+ corpora.push({
136
+ name: "providers",
137
+ kind: "files",
138
+ files,
139
+ argv: ["node", "scripts/test-run.mjs", "node", "--test", ...files],
140
+ });
141
+
142
+ // ── THE EXEMPTIONS, CHECKED ────────────────────────────────────────────────────────────────────
143
+ for (const c of corpora.filter((x) => x.kind === "covered")) {
144
+ const covering = corpora.find((x) => x.name === c.cover.corpus);
145
+ if (!covering || !Array.isArray(covering.files)) {
146
+ faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, and this `
147
+ + `command has no such corpus with a file list to check that against.`);
148
+ continue;
149
+ }
150
+ const own = covering.files.filter((f) => f.startsWith(c.name + "/"));
151
+ // BOTH HALVES. A workspace that lost its own tests would pass a check that only asked whether
152
+ // the covering corpus was missing any of them, because nothing is missing from nothing.
153
+ const here = existsSync(join(root, c.name, "test"))
154
+ ? readdirSync(join(root, c.name, "test")).filter((f) => f.endsWith(".test.mjs")).length : 0;
155
+ if (!here) {
156
+ faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, but it has `
157
+ + `no test files of its own, so that claim covers nothing. Remove the entry or restore the tests.`);
158
+ } else if (own.length !== here) {
159
+ faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, which reaches `
160
+ + `${own.length} of its ${here} test file(s). The exemption is no longer true.`);
161
+ }
162
+ c.reaches = own.length;
163
+ c.hasTests = here;
164
+ }
165
+
166
+ return { corpora, faults };
167
+ }
168
+
169
+ function main() {
170
+ const only = (() => { const i = process.argv.indexOf("--only"); return i === -1 ? null : process.argv[i + 1]; })();
171
+ const listOnly = process.argv.includes("--list");
172
+ const { corpora, faults } = plan();
173
+
174
+ // FAULTS BEFORE ANYTHING RUNS. A missing corpus discovered after a green suite reads as an
175
+ // afterthought; discovered first, it is the answer.
176
+ if (faults.length) {
177
+ console.error(`\ntest-full: ${faults.length} declared workspace(s) this command cannot account for:\n`);
178
+ for (const f of faults) console.error(` - ${f}`);
179
+ console.error("");
180
+ process.exit(1);
181
+ }
182
+
183
+ const chosen = only ? corpora.filter((c) => c.name === only || c.name.endsWith("/" + only)) : corpora;
184
+ if (only && !chosen.length) {
185
+ console.error(`test-full: no corpus named ${only}. Known: ${corpora.map((c) => c.name).join(", ")}`);
186
+ process.exit(2);
187
+ }
188
+
189
+ if (listOnly) {
190
+ for (const c of chosen) {
191
+ console.log(c.kind === "covered"
192
+ ? ` ${c.name}: covered by the ${c.cover.corpus} corpus (${c.reaches}/${c.hasTests} file(s))`
193
+ : ` ${c.name}: ${c.kind === "files" ? `${c.files.length} file(s)` : "workspace script"}`);
194
+ }
195
+ return;
196
+ }
197
+
198
+ const ran = [];
199
+ for (const c of chosen) {
200
+ if (c.kind === "covered") { ran.push({ ...c, status: "covered" }); continue; }
201
+ console.log(`\n──── ${c.name} ────`);
202
+ // STDIO INHERITED, NOT CAPTURED. CI reads the child stream for the corpus-guard markers, and a
203
+ // runner that buffered its children would take those markers out of the log the check greps.
204
+ const r = spawnSync(c.argv[0], c.argv.slice(1), { cwd: ROOT, stdio: "inherit" });
205
+ ran.push({ ...c, status: r.status === 0 ? "ok" : "FAILED", code: r.status });
206
+ }
207
+
208
+ // ── THE SUMMARY, AT THE END, ON STDOUT ─────────────────────────────────────────────────────────
209
+ // The point of the whole file. A reader who scrolls to the bottom of a long run sees every corpus
210
+ // this command knows about and what became of it, so "the full suite is green" is a claim they can
211
+ // check rather than one they have to trust.
212
+ console.log(`\n──── what test-full ran ────`);
213
+ for (const c of ran) {
214
+ if (c.status === "covered") {
215
+ console.log(` covered ${c.name} — ${c.hasTests} file(s), run by the ${c.cover.corpus} corpus: ${c.cover.why}`);
216
+ } else {
217
+ const n = c.kind === "files" ? `${c.files.length} file(s)` : "workspace suite";
218
+ console.log(` ${c.status === "ok" ? "ran " : "FAILED "} ${c.name} — ${n}${c.code ? ` (exit ${c.code})` : ""}`);
219
+ }
220
+ }
221
+ const bad = ran.filter((c) => c.status === "FAILED");
222
+ console.log(bad.length ? `\n${bad.length} corpus/corpora failed.` : `\nEvery corpus above was run or accounted for.`);
223
+ if (bad.length) process.exit(1);
224
+ }
225
+
226
+ if (import.meta.url === `file://${process.argv[1]}`) main();
package/shared/README.md CHANGED
@@ -25,7 +25,8 @@ resolves the running commit for artifact provenance, but it cannot be the shared
25
25
  | `listen.mjs` | The one place a bind failure becomes a sentence rather than a stack trace. |
26
26
  | `driver-dir.mjs` | Where a run's `_driver/` is: the name, the path, and the one creation call. Product code no longer builds it by hand, so the directory's location — and one day its mode — is a decision in one place rather than a convention in 1123. |
27
27
  | `site-nav.mjs`, `anon-overlay.mjs` | The single top nav shared by the internal staff pages, and the display-only overlay that masks client names and marks so those pages can be screen-shared. |
28
- | `identifier-scan.mjs`, `identifier-blocklist.mjs`, `vetted-identities.mjs` | One matcher, one name table, one list of declared exceptions — shared by the guard that sweeps the tracked tree and the scan that sweeps the whole history. |
28
+ | `identifier-scan.mjs`, `identifier-sentinels.mjs`, `vetted-identities.mjs` | One matcher, one name table, one list of declared exceptions — shared by the guard that sweeps the tracked tree and the scan that sweeps the whole history. The real table is private; the sentinels are what make a zero from that sweep mean something. |
29
+ | `reference-guard-classes.mjs` | What a public tree must not acquire, as one table: citations, account names, home directories on the build machines, private repository names, this project's own words for how it is organised, and both attribution trailers. Read by the diff guard in `scripts/added-reference-check.mjs` and by the per-file floor beside it. |
29
30
 
30
31
  `brand/assets/` holds the vendored logo SVGs. `tools/` holds the two brand-system generators — see
31
32
  [`tools/README.md`](tools/README.md).
@@ -0,0 +1,142 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // browser-temp-root.mjs — give a browser launch its own temp root, so an untidy exit leaks nothing shared.
4
+ //
5
+ // ── WHAT LEAKS, AND WHY IT IS NOT THE PROFILE DIRECTORY ───────────────────────────────────────────
6
+ //
7
+ // Chrome does not keep its process-singleton lock in `--user-data-dir`. It creates a directory named
8
+ // `com.google.Chrome.XXXXXX` in the system temp directory, holding `SingletonCookie` and a
9
+ // `SingletonSocket` unix socket, and symlinks the profile's entry at it. The reason is length: a
10
+ // profile path may be too long for a unix socket, so the lock is shortened through the temp root.
11
+ //
12
+ // Every launcher here already removes its own profile directory. None of them could remove that one,
13
+ // because none of them knows its name. Measured on a development machine 2026-09-09: 4,855 such
14
+ // directories under one shared temp root, the oldest five days old, still accumulating — the residue
15
+ // of runs that ended without running their cleanup, mostly cancelled continuous-integration jobs.
16
+ //
17
+ // The fix is not to find and delete them. It is to stop them being shared: the directory is created in
18
+ // `GetTempDir()`, which honours `TMPDIR`, so a run that exports its own temp root gets its own copy of
19
+ // the lock inside it and takes it away when the root goes.
20
+ //
21
+ // ── THE BYTE BUDGET, WHICH IS WHY THIS FILE REFUSES RATHER THAN TRUSTS ────────────────────────────
22
+ //
23
+ // A unix socket address is a fixed 108-byte field, so the socket path must fit in 107 bytes plus a
24
+ // terminator. The lock directory and socket name add a fixed 41 characters to the temp root, leaving
25
+ // 66 for the root itself. One character more and Chrome does not degrade — it aborts, with a fatal
26
+ // log line about the socket path and a core dump.
27
+ //
28
+ // That failure is why the check below exists. A crash in a continuous-integration step, reported as a
29
+ // core dump, is not something a reader connects to the length of a checkout path; and the length is
30
+ // not a constant of this project, because a run can be rooted anywhere a caller or a runner puts it.
31
+ // So the budget is asserted at the point the root is chosen, and the refusal states the length it
32
+ // measured, the limit, and the path — which is the whole of what a reader needs to fix it.
33
+ //
34
+ // The boundary is measured, not derived: walking root lengths across it, 66 starts and 67 aborts.
35
+ import { mkdtempSync, mkdirSync } from "node:fs";
36
+ import { tmpdir } from "node:os";
37
+ import { join } from "node:path";
38
+ import { removeOnExit } from "./reap-on-exit.mjs";
39
+
40
+ /** A unix socket address field is 108 bytes; the last is the terminator. */
41
+ export const SUN_PATH_MAX = 107;
42
+
43
+ /** `/com.google.Chrome.XXXXXX/SingletonSocket` — what the lock adds to the temp root. */
44
+ export const LOCK_SUFFIX = "/com.google.Chrome.XXXXXX/SingletonSocket";
45
+
46
+ /** The longest temp root a browser can be launched under. */
47
+ export const MAX_ROOT_LENGTH = SUN_PATH_MAX - LOCK_SUFFIX.length;
48
+
49
+ /**
50
+ * The root's own prefix, and it is SHORT ON PURPOSE — this is the one lever on the budget.
51
+ *
52
+ * Every character here is spent on every browser run, and roots nest: under the suite runner the
53
+ * ambient temp directory already carries one `ct-testrun-XXXXXX` level before this is appended.
54
+ * Measured on a development machine 2026-09-09, ambient temp 18 characters:
55
+ *
56
+ * root length worst case with a 22-character name in the root with this prefix
57
+ * ambient 18 47 (19 to spare) 29 (37 to spare)
58
+ * one runner 36 65 (ONE to spare) 47 (19 to spare)
59
+ * nested 54 83 (refuses) 65 (1 to spare)
60
+ *
61
+ * The middle row is why this is not cosmetic. Naming the root after the check spent the budget on a
62
+ * label, and left an ordinary single-level suite run one character from refusing — a host with a
63
+ * temp directory one character longer, or this one renamed a level deeper, and every browser check
64
+ * would refuse at once while the tree looked broken. So the root is named for what it IS, and the
65
+ * check that made it is named by the directory INSIDE it, where length costs nothing.
66
+ *
67
+ * A leaked root is still sweepable by name, which was the point of naming it at all, and now it is
68
+ * attributable too: open it and the profile directory says which check left it.
69
+ */
70
+ export const ROOT_PREFIX = "ctb-";
71
+
72
+ /**
73
+ * Why `dir` cannot be a browser temp root, or `null` if it can.
74
+ *
75
+ * Returned rather than thrown so a caller can decide: a check that has already produced a result
76
+ * should say this and exit, not lose the result to an exception.
77
+ */
78
+ export function rootRefusal(dir) {
79
+ if (typeof dir !== "string" || dir === "") return "a browser temp root must be a non-empty path";
80
+ if (dir.length <= MAX_ROOT_LENGTH) return null;
81
+ return (
82
+ `browser temp root is ${dir.length} characters and the limit is ${MAX_ROOT_LENGTH}: ${dir}\n` +
83
+ ` A browser writes its process-singleton socket at <root>${LOCK_SUFFIX}, and a unix socket ` +
84
+ `address holds ${SUN_PATH_MAX} characters. Over that the browser aborts with a fatal error about ` +
85
+ `the socket path rather than reporting anything this check could read.\n` +
86
+ ` Run from a shorter path, or set TMPDIR to one.`
87
+ );
88
+ }
89
+
90
+ /** The refusal as an exception, for a caller with nothing to lose by throwing. */
91
+ export function assertRootFits(dir) {
92
+ const why = rootRefusal(dir);
93
+ if (why) throw new Error(why);
94
+ return dir;
95
+ }
96
+
97
+ /**
98
+ * A temp root for one browser run, removed when this process exits by any route it can observe.
99
+ *
100
+ * Rooted at the ambient temp directory rather than anywhere relative to the checkout, and that is a
101
+ * decision rather than a convenience: the ambient root is short by construction, while a checkout,
102
+ * a worktree or a per-session scratch directory is routinely past the budget above. Nesting is
103
+ * still correct — under a test runner that has already exported its own root, this lands inside it
104
+ * and is carried away with it.
105
+ */
106
+ export function browserTempRoot() {
107
+ const root = mkdtempSync(join(tmpdir(), ROOT_PREFIX));
108
+ assertRootFits(root);
109
+ removeOnExit(root);
110
+ return root;
111
+ }
112
+
113
+ /**
114
+ * The environment a browser must be spawned with so its singleton lock lands under `root`.
115
+ *
116
+ * `TMPDIR` is the whole mechanism, so this refuses rather than passing a root that cannot work.
117
+ */
118
+ export function browserEnv(root, env = process.env) {
119
+ assertRootFits(root);
120
+ return { ...env, TMPDIR: root };
121
+ }
122
+
123
+ /**
124
+ * A temp root and a profile directory inside it, plus the environment to spawn with.
125
+ *
126
+ * The profile goes INSIDE the root so one removal covers both. Several call sites used to make the
127
+ * profile directly in the shared temp directory and remove it on their success path; that left the
128
+ * singleton lock behind on every other path, which is the leak this module exists for.
129
+ */
130
+ export function browserRun(label = "browser") {
131
+ const root = mkdtempSync(join(tmpdir(), ROOT_PREFIX));
132
+ assertRootFits(root);
133
+ const keep = removeOnExit(root);
134
+ // The CALLER'S NAME GOES HERE, inside the root, where it costs nothing against the socket budget.
135
+ // Callers pass the name they used to prefix the root with, trailing dash and all, so this reads as
136
+ // `<root>/home-check` and an operator opening a leaked root learns which check left it.
137
+ const profile = join(root, label.replace(/-+$/, "") || "browser");
138
+ mkdirSync(profile, { recursive: true });
139
+ // `keep` deregisters the root from the exit sweep. A caller with a `--keep` flag MUST call it, or
140
+ // the flag reads as working while the directory it promised goes at exit.
141
+ return { root, profile, env: browserEnv(root), keep };
142
+ }
@@ -9,7 +9,7 @@
9
9
  // so that one line decides which tree the whole deployment executes — and connect said nothing about
10
10
  // having written it.
11
11
  //
12
- // Driven on testuser: a lane made a worktree to test a branch, ran `connect` from it once, and
12
+ // Driven on the test deployment: a lane made a worktree to test a branch, ran `connect` from it once, and
13
13
  //
14
14
  // · the next deploy tick fast-forwarded the DETACHED worktree and failed with "You are not currently
15
15
  // on a branch" — a message about branches, for a box that was silently a merge behind; and