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.
- package/.env.example +36 -37
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +24 -4
- package/README.md +7 -6
- package/bin/example.mjs +6 -5
- package/bin/onboard.mjs +175 -9
- package/bin/start.mjs +66 -4
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/GLOSSARY.md +85 -0
- package/docs/README.md +1 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/05-config-governance.md +5 -0
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +34 -0
- package/driver/README.md +25 -6
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/probe.mjs +45 -5
- package/driver/gateway.mjs +2 -2
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +206 -101
- package/driver/plain-register.mjs +16 -2
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-service.mjs +53 -2
- package/driver/predelivery-lint.mjs +54 -25
- package/driver/publish/render.mjs +109 -14
- package/driver/search-policy.mjs +1 -1
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +51 -4
- package/driver/suite-census.json +97 -31
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +34 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/package.json +1 -1
- package/package.json +4 -10
- package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/e2e.mjs +2 -2
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +9 -1
- package/scripts/test-full.mjs +226 -0
- package/shared/README.md +2 -1
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +5 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +4 -1
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/suite-census.mjs +31 -3
- 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
|
-
|
|
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,
|
|
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-
|
package/scripts/render-check.mjs
CHANGED
|
@@ -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";
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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-
|
|
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
|
+
}
|
package/shared/checkout-move.mjs
CHANGED
|
@@ -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
|
|
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
|