clearotron 0.2.2 → 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 +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- 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/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- 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 +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- 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/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
// EVERY GENERATED FILE IN THIS TREE IS CURRENT, or this says which one is not.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/generated-files-are-current.mjs
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THIS EXISTS, AND WHY IT DISCOVERS ITS OWN POPULATION ─────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// Each `scripts/mint-*.mjs` writes a file that is committed and derived, and each takes `--check` to
|
|
12
|
+
// say whether the committed copy still matches the tree. Until now CI ran exactly one of them. The
|
|
13
|
+
// other three could go stale on `main` and nothing here would say so — the failure surfaced instead
|
|
14
|
+
// in a private control that refuses to start on a stale fixture, which is the most expensive place
|
|
15
|
+
// to learn it and the furthest from whoever caused it.
|
|
16
|
+
//
|
|
17
|
+
// So this enumerates `scripts/mint-*.mjs` rather than naming them. A list would have to be extended
|
|
18
|
+
// by whoever adds the fifth minter, and the whole reason the fourth went unchecked is that nobody
|
|
19
|
+
// extended anything. A discovered population is covered by construction.
|
|
20
|
+
//
|
|
21
|
+
// AN EMPTY POPULATION IS A FAULT, NOT A PASS. If the glob matches nothing, this file has been moved
|
|
22
|
+
// or the naming convention has changed, and reporting "all current" over zero checks is exactly the
|
|
23
|
+
// shape of green this guard exists to refuse.
|
|
24
|
+
import { readdirSync } from "node:fs";
|
|
25
|
+
import { spawnSync, execFileSync } from "node:child_process";
|
|
26
|
+
import { join, dirname } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const ROOT = join(HERE, "..");
|
|
31
|
+
|
|
32
|
+
/** Every minter in `scripts/`, by filename, ordered. */
|
|
33
|
+
export function minters(dir = HERE) {
|
|
34
|
+
return readdirSync(dir).filter((f) => f.startsWith("mint-") && f.endsWith(".mjs")).sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What the working tree looks like, so a minter that WROTE during `--check` can be caught writing.
|
|
39
|
+
*
|
|
40
|
+
* Returns null when the tree cannot be read — outside a checkout, say — and a null is carried as a
|
|
41
|
+
* stated limit rather than as a pass: the contract simply goes unchecked and the run says so.
|
|
42
|
+
*/
|
|
43
|
+
export function treeState(root = ROOT) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync("git", ["-C", root, "status", "--porcelain"], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run every minter's `--check` and sort the answers.
|
|
51
|
+
*
|
|
52
|
+
* `dir` and `root` are parameters so the arms can drive all three outcomes over throwaway minters.
|
|
53
|
+
* The alternative — dirtying a real generated file and restoring it — is a shared-file mutation, and
|
|
54
|
+
* the test runner runs files in parallel, so it would be a race that reddens somebody else's arm.
|
|
55
|
+
*/
|
|
56
|
+
export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree = () => treeState(root) } = {}) {
|
|
57
|
+
const found = minters(dir);
|
|
58
|
+
if (!found.length) return { found, stale: [], unreadable: [], wrote: [], empty: true };
|
|
59
|
+
|
|
60
|
+
const stale = [];
|
|
61
|
+
const unreadable = [];
|
|
62
|
+
const wrote = [];
|
|
63
|
+
for (const m of found) {
|
|
64
|
+
// ── `--check` IS A CONTRACT, AND NOTHING WAS VERIFYING IT ──────────────────────────────────────
|
|
65
|
+
//
|
|
66
|
+
// Every minter here is trusted to treat `--check` as "report, change nothing". A minter that
|
|
67
|
+
// ignores the flag and re-mints repairs the drift and then reports `current` — so the file is
|
|
68
|
+
// quietly fixed on whoever's machine ran it, the commit does not carry the repair, and this guard
|
|
69
|
+
// logs a clean line over a check that never happened. That is this file's own sentence one level
|
|
70
|
+
// in: a pass from a minter that did not look is not a pass either.
|
|
71
|
+
//
|
|
72
|
+
// The probe is the contract stated directly. `--check` must not write, so if the tree moved while
|
|
73
|
+
// one ran, it wrote. THE LIMIT, SAID RATHER THAN LEFT: this catches the harmful inert form, the
|
|
74
|
+
// one that silently repairs. A minter that ignores the flag and does nothing at all still reports
|
|
75
|
+
// `current`, and no probe from out here can tell that from a file that really is current.
|
|
76
|
+
const before = readTree();
|
|
77
|
+
const r = spawnSync(process.execPath, [join(dir, m), "--check"], { cwd: root, encoding: "utf8" });
|
|
78
|
+
const after = readTree();
|
|
79
|
+
const out = ((r.stdout || "") + (r.stderr || "")).trim();
|
|
80
|
+
if (before !== null && after !== null && before !== after) {
|
|
81
|
+
wrote.push({ m, out });
|
|
82
|
+
log(` WROTE ${m} (during --check)`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// 0 is current, 1 is stale, anything else is a minter that could not look — reported separately,
|
|
86
|
+
// because "I could not read the tree" and "the file is out of date" need different things done.
|
|
87
|
+
if (r.status === 0) { log(` current ${m}`); continue; }
|
|
88
|
+
if (r.status === 1) { stale.push({ m, out }); log(` STALE ${m}`); continue; }
|
|
89
|
+
unreadable.push({ m, out, code: r.status });
|
|
90
|
+
log(` ? ${m} (exit ${r.status})`);
|
|
91
|
+
}
|
|
92
|
+
return { found, stale, unreadable, wrote, empty: false, contractChecked: readTree() !== null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function main() {
|
|
96
|
+
const { found, stale, unreadable, wrote, empty, contractChecked } = checkAll();
|
|
97
|
+
if (empty) {
|
|
98
|
+
console.error("generated-files-are-current: no scripts/mint-*.mjs found. Either they moved or the "
|
|
99
|
+
+ "naming changed — and a pass over nothing is not a pass.");
|
|
100
|
+
process.exit(2);
|
|
101
|
+
}
|
|
102
|
+
console.log(`\ngenerated-files-are-current: checked ${found.length} minter(s)`
|
|
103
|
+
+ (contractChecked ? "" : "; the tree could not be read, so nothing verified that `--check` changed nothing"));
|
|
104
|
+
|
|
105
|
+
for (const { m, out } of [...wrote, ...stale, ...unreadable]) {
|
|
106
|
+
console.error(`\n──── ${m} ────\n${out}`);
|
|
107
|
+
}
|
|
108
|
+
if (wrote.length) {
|
|
109
|
+
console.error(`\n${wrote.length} minter(s) CHANGED THE TREE while running \`--check\`. \`--check\` `
|
|
110
|
+
+ `reports and changes nothing; one that re-mints repairs the drift on whoever ran it, leaves the `
|
|
111
|
+
+ `commit without the repair, and reports current over a check that did not happen. Fix the minter.`);
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
if (unreadable.length) {
|
|
115
|
+
console.error(`\n${unreadable.length} minter(s) could not look. That is not a pass; fix the minter first.`);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
if (stale.length) {
|
|
119
|
+
console.error(`\n${stale.length} generated file(s) are out of date. Re-mint each one named above and `
|
|
120
|
+
+ `commit the result — a derived file that drifts is a check reporting on a tree that no longer exists.`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (import.meta.url === `file://${process.argv[1]}`) main();
|
|
@@ -90,7 +90,7 @@ import { unitsActiveVerdict } from "../driver/unit-state-verdict.mjs";
|
|
|
90
90
|
import { deploymentBox } from "../shared/deployment-box.mjs"; // — extracted; one allowlist, two readers
|
|
91
91
|
import { unitFileDriftVerdict } from "../driver/unit-file-drift.mjs"; //
|
|
92
92
|
import { placeholdersIn, resolveValues, renderUnit } from "../driver/systemd/render-units.mjs"; //
|
|
93
|
-
import { CHECKED_UNITS, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory, unitClone } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
93
|
+
import { CHECKED_UNITS, CHECKED_TIMERS, timerVerdict, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory, unitClone } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
94
94
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // the ONE ExecStart parser — a unit says which module it runs
|
|
95
95
|
import { treeOfRunning } from "../shared/checkout-move.mjs"; // …and the live argv says which tree that module came from
|
|
96
96
|
import { findUnitFiles, unitFilePath } from "../driver/unit-files.mjs"; //
|
|
@@ -226,6 +226,35 @@ function userBusEnv() {
|
|
|
226
226
|
// Returns {clones, probe}. `probe` is the honest answer to "could I look at all?" — {ok, why} — and it is
|
|
227
227
|
// SEPARATE from the clones list on purpose. "enumerated, nothing to compare" and "could not enumerate"
|
|
228
228
|
// were the same empty array before, and this suite exists because absences were read as successes.
|
|
229
|
+
/**
|
|
230
|
+
* Ask each declared TIMER about itself, as a timer.
|
|
231
|
+
*
|
|
232
|
+
* Deliberately not folded into the loop below. That loop asks a service for its WorkingDirectory and
|
|
233
|
+
* MainPID and attributes a running process to a checkout — none of which a `.timer` has, so putting
|
|
234
|
+
* timers through it would produce rows whose every attribution field is empty for a reason that is not
|
|
235
|
+
* a finding. Two questions, two calls, and each says what it could not answer.
|
|
236
|
+
*/
|
|
237
|
+
function declaredTimers() {
|
|
238
|
+
if (!CHECKED_TIMERS.length) return { rows: [], probeFailed: null };
|
|
239
|
+
const env = userBusEnv();
|
|
240
|
+
const rows = [];
|
|
241
|
+
let probeFailed = null;
|
|
242
|
+
for (const unit of CHECKED_TIMERS) {
|
|
243
|
+
try {
|
|
244
|
+
const shown = execFileSync("systemctl", ["--user", "show", unit, "-p", "ActiveState", "-p", "LoadState"],
|
|
245
|
+
{ encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
|
|
246
|
+
const f = Object.fromEntries(shown.split("\n").filter(Boolean).map((l) => { const [k, ...v] = l.split("="); return [k, v.join("=")]; }));
|
|
247
|
+
// An EMPTY answer is the no-session-bus case, and it must not read as a stopped timer — that is
|
|
248
|
+
// the shape that turns "could not ask" into a finding about the box.
|
|
249
|
+
if (!f.ActiveState) { probeFailed = probeFailed ?? `systemctl --user answered nothing for ${unit}`; continue; }
|
|
250
|
+
rows.push({ unit, active: f.ActiveState || null, load: f.LoadState || null });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
probeFailed = probeFailed ?? String(e?.message ?? e).slice(0, 120);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { rows, probeFailed };
|
|
256
|
+
}
|
|
257
|
+
|
|
229
258
|
function serviceClones() {
|
|
230
259
|
// — the list is DECLARED, not written here. It used to be eight names inline, which put a unit
|
|
231
260
|
// inside the drift guarantee or outside it by omission: `client-access` was live on production and in
|
|
@@ -889,6 +918,14 @@ else {
|
|
|
889
918
|
const v = unitInventoryVerdict({ live: liveUnits, files: walk.files, collisions: walk.collisions,
|
|
890
919
|
filesError: walk.error, box, probe });
|
|
891
920
|
record("every live unit is declared", v.state, v.message);
|
|
921
|
+
|
|
922
|
+
// — AND WHETHER ANYTHING STILL STARTS THE TIMER-DRIVEN ONES. Reported separately from the line above
|
|
923
|
+
// because it answers a different question: that one says a declared unit exists and is not adrift,
|
|
924
|
+
// this one says its timer is still armed. A service whose timer was stopped satisfies the first and
|
|
925
|
+
// fails the second, and reads `inactive` for both — which is why one line could not carry both.
|
|
926
|
+
const t = declaredTimers();
|
|
927
|
+
const tv = timerVerdict(t.rows, { probeFailed: t.probeFailed });
|
|
928
|
+
record("every declared timer is still armed", tv.state, tv.message);
|
|
892
929
|
}
|
|
893
930
|
|
|
894
931
|
// ── — EVERY QUEUE THIS DEPLOYMENT WOULD DRAIN IS WATCHED BY SOMETHING ──────────────────────────
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// ── THE ORACLE THIS DERIVATION IS CHECKED AGAINST ────────────────────────────────────────────────
|
|
22
22
|
//
|
|
23
23
|
// A derivation that is only ever compared against itself proves nothing, which is the shape this repo
|
|
24
|
-
// keeps paying for. So
|
|
24
|
+
// keeps paying for. So scripts/generated-files-are-current.mjs, in CI's guards job, holds this output to the
|
|
25
25
|
// THIRTEEN dead names Cart measured in the production install's own env file on 2026-09-05 — a
|
|
26
26
|
// population this script never saw and cannot influence. All thirteen must resolve to a replacement.
|
|
27
27
|
// If a future sweep breaks the suffix rule, that arm reds on real-world data rather than on a
|
|
@@ -82,7 +82,9 @@ function render(names) {
|
|
|
82
82
|
//
|
|
83
83
|
// node scripts/mint-names-in-force.mjs
|
|
84
84
|
//
|
|
85
|
-
//
|
|
85
|
+
// CI refuses a stale copy, from the guards job: scripts/generated-files-are-current.mjs runs every
|
|
86
|
+
// minter in scripts/ with --check. (This line used to name a test file that does not exist in this
|
|
87
|
+
// tree, so the only thing that ever caught this drifting was a private control refusing to start.)
|
|
86
88
|
|
|
87
89
|
export const NAMES_IN_FORCE = Object.freeze([
|
|
88
90
|
${names.map((n) => ` "${n}",`).join("\n")}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
// RE-MINT THE PUBLIC RESIDUE BACKLOG — the per-file, per-class count that may only go down.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/mint-public-residue.mjs # report the delta, change nothing
|
|
8
|
+
// node scripts/mint-public-residue.mjs --check # ... and exit 1 if the file is out of date
|
|
9
|
+
// node scripts/mint-public-residue.mjs --apply # write it
|
|
10
|
+
//
|
|
11
|
+
// THE ONLY REASON TO RUN `--apply` IS THAT THE NUMBER WENT DOWN. Nothing here refuses to write a
|
|
12
|
+
// higher one — a table that could not record growth would be unable to describe a tree somebody
|
|
13
|
+
// widened a class over — but the floor arm in driver/test/the-public-residue-is-a-floor.test.mjs
|
|
14
|
+
// refuses the growth itself, and it reads the committed file rather than this script's output. So an
|
|
15
|
+
// author who mints upward has recorded the regression rather than absorbed it, and the suite says so
|
|
16
|
+
// on the next run. The two halves are deliberately not the same program.
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { CLASSES, censusOf, publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
21
|
+
import { trackedFiles, skipReason } from "../shared/tracked-files.mjs";
|
|
22
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
23
|
+
|
|
24
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
const FIXTURE = join(ROOT, "driver/test/fixtures/public-residue-backlog.json");
|
|
26
|
+
const GUARD = "public-residue-backlog";
|
|
27
|
+
|
|
28
|
+
/** The tree's current census, as the fixture records it. */
|
|
29
|
+
export function mint() {
|
|
30
|
+
const tracked = trackedFiles(GUARD, { root: ROOT });
|
|
31
|
+
if (tracked === null) return null;
|
|
32
|
+
// THE SAME POPULATION THE FLOOR READS, from the same helper. A mint over the index and a floor over
|
|
33
|
+
// HEAD would disagree under the overlay, and `--check` would report a difference that is only the
|
|
34
|
+
// two instruments asking different questions.
|
|
35
|
+
const p = publishedOf(tracked, ROOT);
|
|
36
|
+
if (p.error) { console.error(`mint-public-residue: ${p.error}`); process.exit(2); }
|
|
37
|
+
if (p.laid) console.log(`mint-public-residue: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
|
|
38
|
+
const c = censusOf(p.files, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
39
|
+
return { classes: CLASSES.map((x) => x.id), total: c.total, files: c.files };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Per-class totals, for a reader who wants to know WHICH number moved. */
|
|
43
|
+
export const byClass = (table) =>
|
|
44
|
+
CLASSES.map((c, i) => [c.id, Object.values(table.files).reduce((a, v) => a + v[i], 0)]);
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const apply = process.argv.includes("--apply");
|
|
48
|
+
const check = process.argv.includes("--check");
|
|
49
|
+
|
|
50
|
+
const now = mint();
|
|
51
|
+
// A COULD-NOT-LOOK EXITS 2, never 0. Outside a checkout there is no corpus, and a mint that wrote an
|
|
52
|
+
// empty table here would replace the whole backlog with nothing and call it a repair.
|
|
53
|
+
if (now === null) { console.error(`mint-public-residue: ${skipReason(GUARD)}`); process.exit(2); }
|
|
54
|
+
|
|
55
|
+
let was = null;
|
|
56
|
+
try { was = JSON.parse(readFileSync(FIXTURE, "utf8")); } catch { /* first mint */ }
|
|
57
|
+
|
|
58
|
+
console.log(`public residue: ${now.total} line(s) across ${Object.keys(now.files).length} file(s)`);
|
|
59
|
+
for (const [id, n] of byClass(now)) console.log(` ${String(n).padStart(5)} ${id}`);
|
|
60
|
+
|
|
61
|
+
const next = JSON.stringify(now, null, 2) + "\n";
|
|
62
|
+
const same = was && JSON.stringify(was, null, 2) + "\n" === next;
|
|
63
|
+
if (same) { console.log("the backlog is current"); return; }
|
|
64
|
+
|
|
65
|
+
if (was) {
|
|
66
|
+
const delta = now.total - was.total;
|
|
67
|
+
console.log(`\ntotal ${was.total} → ${now.total} (${delta >= 0 ? "+" : ""}${delta})`);
|
|
68
|
+
}
|
|
69
|
+
if (apply) { writeFileSync(FIXTURE, next); console.log("written"); return; }
|
|
70
|
+
console.log("\nre-run with --apply to write it");
|
|
71
|
+
if (check) process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isEntrypoint(import.meta.url)) main();
|
|
@@ -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-
|
|
@@ -151,8 +151,6 @@ export function noticeBody({ runUrl, sha, subject, failedJobs = [], lastGreenAt,
|
|
|
151
151
|
+ "passes — a run that never started cannot make the platform green."
|
|
152
152
|
: "This thread is reused for every red night. Close it once the platform is green again — the next "
|
|
153
153
|
+ "red opens a new one.",
|
|
154
|
-
"",
|
|
155
|
-
"Agent: role-dev · bergface",
|
|
156
154
|
].join("\n");
|
|
157
155
|
}
|
|
158
156
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// The install refuses an unsupported Node BEFORE anything is written — tracker issue 364.
|
|
5
|
+
//
|
|
6
|
+
// Three declarations already stated the requirement and none of them bound anyone: `engines.node` is a
|
|
7
|
+
// WARNING unless the person installing has set `engine-strict`, `.nvmrc` is read by nvm and nothing
|
|
8
|
+
// else, and the install guide is prose. An outside report arrived as "some node issues, not as obvious
|
|
9
|
+
// as the README made it seem" — which is what a warning inside an install log looks like from outside.
|
|
10
|
+
//
|
|
11
|
+
// So this is a `preinstall` hook, node builtins only, because nothing is installed when it runs.
|
|
12
|
+
//
|
|
13
|
+
// IT NAMES THE LATER FAILURE, not just the version. The whole defect is that an unsupported Node
|
|
14
|
+
// installs cleanly and then fails somewhere else saying nothing about Node, so a message that only
|
|
15
|
+
// prints a semver range leaves the reader where they started.
|
|
16
|
+
import { declaredRange, floorOf, meetsFloor, nodeFloorRefusal } from "../shared/node-floor.mjs";
|
|
17
|
+
|
|
18
|
+
const range = declaredRange();
|
|
19
|
+
if (!meetsFloor(process.versions.node, floorOf(range))) {
|
|
20
|
+
process.stderr.write(`\n${nodeFloorRefusal({ current: process.versions.node, required: floorOf(range).join("."), range })}\n`);
|
|
21
|
+
process.stderr.write(" Nothing has been installed.\n\n nvm install 22 && nvm use 22\n\n");
|
|
22
|
+
process.stderr.write(" Without nvm, take the current 22.x from https://nodejs.org/.\n\n");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
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
|
|
|
@@ -77,6 +78,15 @@ export const EXCLUDED = [
|
|
|
77
78
|
"driver/test/a-bare-reference-added-in-a-diff-is-refused.test.mjs",
|
|
78
79
|
"driver/test/prompt-payload-names-no-tracker-issue.test.mjs",
|
|
79
80
|
"driver/test/euipo-environment-doctrine.test.mjs",
|
|
81
|
+
// THIS RULE'S OWN DEFINITION, and it caught itself. Both files below QUOTE the residue in order to
|
|
82
|
+
// define it — the specimens the sweep is tested against, and the worked examples in the header above.
|
|
83
|
+
// Sweeping them rewrites the corpus this instrument is checked by, and it would then pass on something
|
|
84
|
+
// it had stopped checking: the arms would assert that a stripped line equals a stripped line. The
|
|
85
|
+
// sibling module that counts the earlier strip's residue learned the same lesson and names its own
|
|
86
|
+
// three files for the same reason. Found by running the sweep after fixing its report, which is the
|
|
87
|
+
// only reason it surfaced: before that, these lines were counted and never listed.
|
|
88
|
+
"scripts/strip-tracker-citations.mjs",
|
|
89
|
+
"driver/test/the-citation-strip-removes-openers-and-nothing-else.test.mjs",
|
|
80
90
|
// Pinned at a content hash; a prose repair is not worth spending a freeze on.
|
|
81
91
|
"driver/publish/render.mjs",
|
|
82
92
|
];
|
|
@@ -86,28 +96,49 @@ export const isScannable = (f) =>
|
|
|
86
96
|
|
|
87
97
|
/** Per-file classification. PURE, and `read` is injected so an arm can drive it over a synthetic tree. */
|
|
88
98
|
export function surveyOf(files, read) {
|
|
89
|
-
const stripped = {}, handoff = [];
|
|
99
|
+
const stripped = {}, handoff = [], unreadable = [];
|
|
90
100
|
let strippedTotal = 0, remainingTotal = 0;
|
|
91
101
|
for (const f of files.filter(isScannable)) {
|
|
92
102
|
let text;
|
|
93
|
-
|
|
103
|
+
// AN UNREADABLE FILE IS A FINDING, NOT A QUIET SKIP. This used to `continue` into no counter and no
|
|
104
|
+
// error channel, so "I could not open this" and "there was nothing to do here" produced identical
|
|
105
|
+
// output — in the report whose whole job is to tell somebody a hundred-file rewrite is safe. The
|
|
106
|
+
// count is reported before anything else when it is non-zero.
|
|
107
|
+
try { text = read(f); } catch (e) { unreadable.push({ file: f, why: String(e?.message ?? e).slice(0, 120) }); continue; }
|
|
94
108
|
let n = 0;
|
|
109
|
+
// CLASSIFY AFTER REPLACING, NOT INSTEAD OF IT. The replacement is not global, so a line carrying an
|
|
110
|
+
// opener AND a second citation further along was stripped once, counted as done, and never reached
|
|
111
|
+
// the hand-off list a person is told to read — its survivor was invisible in the one place it should
|
|
112
|
+
// have been named. Test the RESULT: a line can be both stripped and still owed to a reader.
|
|
95
113
|
const out = text.split("\n").map((line, i) => {
|
|
96
|
-
|
|
97
|
-
if (ANY_CITATION.test(
|
|
98
|
-
return
|
|
114
|
+
const after = OPENER.test(line) ? (n++, line.replace(OPENER, "$1")) : line;
|
|
115
|
+
if (ANY_CITATION.test(after)) handoff.push({ file: f, line: i + 1, text: after.trim() });
|
|
116
|
+
return after;
|
|
99
117
|
});
|
|
100
118
|
if (n) { stripped[f] = n; strippedTotal += n; }
|
|
101
119
|
remainingTotal += out.filter((l) => ANY_CITATION.test(l)).length;
|
|
102
120
|
if (APPLY && n) writeFileSync(join(ROOT, f), out.join("\n"));
|
|
103
121
|
}
|
|
104
|
-
return { strippedTotal, remainingTotal, stripped, handoff };
|
|
122
|
+
return { strippedTotal, remainingTotal, stripped, handoff, unreadable };
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
108
|
-
|
|
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 })
|
|
109
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;
|
|
110
135
|
const s = surveyOf(tracked, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
136
|
+
// BEFORE ANYTHING ELSE, because every number under it is about the files that COULD be read.
|
|
137
|
+
if (s.unreadable.length) {
|
|
138
|
+
console.log(`${s.unreadable.length} file(s) COULD NOT BE READ — every count below excludes them:`);
|
|
139
|
+
for (const u of s.unreadable) console.log(` ${u.file} ${u.why}`);
|
|
140
|
+
console.log("");
|
|
141
|
+
}
|
|
111
142
|
console.log(`${APPLY ? "stripped" : "would strip"} ${s.strippedTotal} opener(s) across ${Object.keys(s.stripped).length} file(s)`);
|
|
112
143
|
console.log(`${s.handoff.length} citation(s) are NOT openers and need a reader — the hand-off list:`);
|
|
113
144
|
for (const h of s.handoff) console.log(` ${h.file}:${h.line} ${h.text.slice(0, 110)}`);
|