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
@@ -10415,6 +10415,7 @@ var api = {
10415
10415
  accountNames: Object.fromEntries(Object.entries(asRecord(b["accountNames"])).filter(([, v]) => typeof v === "string" && v)),
10416
10416
  concurrentRuns: asNumber(b["concurrentRuns"]),
10417
10417
  engineMode: b["engineMode"] === "demo" ? "demo" : b["engineMode"] === "engine-unproven" ? "engine-unproven" : null,
10418
+ engineProgramDisputed: b["engineProgramDisputed"] === true ? true : b["engineProgramDisputed"] === false ? false : null,
10418
10419
  setupRoute: b["setupRoute"] === "packaged" ? "packaged" : b["setupRoute"] === "checkout" ? "checkout" : null,
10419
10420
  brand: typeof b["brand"] === "string" ? b["brand"] : "",
10420
10421
  stopControl: (() => {
@@ -10716,7 +10717,9 @@ var api = {
10716
10717
  apiBilled: bill["apiBilled"] === true,
10717
10718
  missing: asStrings(bill["missing"])
10718
10719
  },
10719
- binaryPresent: e["binaryPresent"] === true
10720
+ binaryPresent: e["binaryPresent"] === true,
10721
+ program: asString(e["program"]),
10722
+ install: asString(e["install"])
10720
10723
  };
10721
10724
  })() : null,
10722
10725
  providers: Array.isArray(b["providers"]) ? asArray(b["providers"]).map((p) => {
@@ -14548,6 +14551,42 @@ function Missing({ go, reason }) {
14548
14551
  });
14549
14552
  }
14550
14553
  //#endregion
14554
+ //#region src/contract/engineState.ts
14555
+ function engineRowFaults(engine, { programDisputed = false } = {}) {
14556
+ return [
14557
+ ...engine.known ? [] : [`This build does not ship an engine called ${engine.id}.`],
14558
+ ...engine.billing.missing.length ? [`Set to bill an API key, and ${engine.billing.missing.join(" and ")} is not set — a run is refused rather than billed to the subscription.`] : [],
14559
+ ...engine.binaryPresent ? [] : engine.program && engine.install ? [`The engine program \`${engine.program}\` cannot be found or run on this machine. Install it with \`${engine.install}\`, then restart this service so it re-reads its PATH.`] : ["The engine program cannot be found or run on this machine."],
14560
+ ...programDisputed && engine.binaryPresent ? ["The engine program is on this machine, but the engine could not find it when it last started — so a new search will refuse. Restart the engine service, or install the CLI where the service can see it."] : []
14561
+ ];
14562
+ }
14563
+ function engineNotice({ programDisputed }) {
14564
+ if (programDisputed === true) return {
14565
+ state: "disputed",
14566
+ headline: "The engine program is on this machine, but the engine could not find it when it last started.",
14567
+ before: "So a new search will refuse. Restart the engine service, or install the CLI where the service can see it — installing it again where it already is will not change this.",
14568
+ namesSetupCommand: false,
14569
+ after: ""
14570
+ };
14571
+ return {
14572
+ state: "absent",
14573
+ headline: "No search engine is attached to this install.",
14574
+ before: "Everything else works — the example report, its audit trail and the assistant connection are live right now. To start new searches, install a reasoning CLI and sign in, then run the setup wizard again",
14575
+ namesSetupCommand: true,
14576
+ after: "Then restart the service: this reading was taken when it started, and it will not notice a new install until it starts again."
14577
+ };
14578
+ }
14579
+ function engineRow(engine, { programDisputed = false } = {}) {
14580
+ const faults = engineRowFaults(engine, { programDisputed });
14581
+ return {
14582
+ ok: faults.length === 0,
14583
+ name: engine.vendor ?? engine.id,
14584
+ mono: engine.id,
14585
+ state: engine.billing.apiBilled ? "API key" : "Subscription",
14586
+ faults
14587
+ };
14588
+ }
14589
+ //#endregion
14551
14590
  //#region src/contract/compose.ts
14552
14591
  function parseNames(raw) {
14553
14592
  return raw.split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
@@ -14781,6 +14820,8 @@ var isKnownTerritory = (entry) => {
14781
14820
  const e = String(entry).trim().toLowerCase();
14782
14821
  return e.length > 0 && ALL_TERRITORIES$1.some((t) => t.toLowerCase() === e);
14783
14822
  };
14823
+ var isTerritoryCode = (entry) => /^[A-Za-z]{2}$/.test(String(entry).trim());
14824
+ var isTerritoryEntry = (entry) => isKnownTerritory(entry) || isTerritoryCode(entry);
14784
14825
  function machineryFor(d, product) {
14785
14826
  const knockout = product?.pipeline === "knockout";
14786
14827
  return {
@@ -16590,6 +16631,8 @@ function NewClearance({ ctx }) {
16590
16631
  busy,
16591
16632
  demoMode: ctx.me.engineMode === "demo",
16592
16633
  setupRoute: ctx.me.setupRoute,
16634
+ programDisputed: ctx.me.engineProgramDisputed,
16635
+ onSettings: ctx.me.role === "staff" ? () => ctx.go("/portal/admin/config") : null,
16593
16636
  saveOpen,
16594
16637
  saveName,
16595
16638
  saveText,
@@ -16761,7 +16804,7 @@ function NameWall({ count, allowed, first, canScreen, screenName, onScreenAll })
16761
16804
  }) : null]
16762
16805
  });
16763
16806
  }
16764
- function Footer({ startedFrom, tier, detail, units, cost, duration, runs, ready, busy, demoMode, setupRoute, saveOpen, saveName, saveText, saveNote, saveDone, editing, canSave, blockedBy, onSaveOpen, onSaveName, onSaveText, onSaveCancel, onSave, onReview, onSeeSaved }) {
16807
+ function Footer({ startedFrom, tier, detail, units, cost, duration, runs, ready, busy, demoMode, setupRoute, programDisputed, onSettings, saveOpen, saveName, saveText, saveNote, saveDone, editing, canSave, blockedBy, onSaveOpen, onSaveName, onSaveText, onSaveCancel, onSave, onReview, onSeeSaved }) {
16765
16808
  return (0, import_jsx_runtime.jsxs)("div", {
16766
16809
  className: "composer-footer",
16767
16810
  children: [
@@ -16974,21 +17017,30 @@ function Footer({ startedFrom, tier, detail, units, cost, duration, runs, ready,
16974
17017
  })
16975
17018
  ]
16976
17019
  }) : null,
16977
- demoMode ? (0, import_jsx_runtime.jsxs)("div", {
16978
- className: "footer-demo-note",
16979
- role: "status",
16980
- style: {
16981
- fontSize: 12.5,
16982
- lineHeight: 1.45
16983
- },
16984
- children: [
16985
- (0, import_jsx_runtime.jsx)("strong", { children: "No search engine is attached to this install." }),
16986
- " ",
16987
- "Everything else works — the example report, its audit trail and the assistant connection are live right now. To start new searches, install a reasoning CLI and sign in, then run the setup wizard again",
16988
- " ",
16989
- (0, import_jsx_runtime.jsx)(SetupCommand, { route: setupRoute })
16990
- ]
16991
- }) : (0, import_jsx_runtime.jsxs)("span", {
17020
+ demoMode ? (() => {
17021
+ const n = engineNotice({ programDisputed });
17022
+ return (0, import_jsx_runtime.jsxs)("div", {
17023
+ className: "footer-demo-note",
17024
+ role: "status",
17025
+ style: {
17026
+ fontSize: 12.5,
17027
+ lineHeight: 1.45
17028
+ },
17029
+ children: [
17030
+ (0, import_jsx_runtime.jsx)("strong", { children: n.headline }),
17031
+ " ",
17032
+ n.before,
17033
+ n.namesSetupCommand ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [" ", (0, import_jsx_runtime.jsx)(SetupCommand, { route: setupRoute })] }) : null,
17034
+ n.after ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [" ", n.after] }) : null,
17035
+ onSettings ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [" ", (0, import_jsx_runtime.jsx)("button", {
17036
+ type: "button",
17037
+ className: "link-btn",
17038
+ onClick: onSettings,
17039
+ children: "See what this install has"
17040
+ })] }) : null
17041
+ ]
17042
+ });
17043
+ })() : (0, import_jsx_runtime.jsxs)("span", {
16992
17044
  style: {
16993
17045
  display: "inline-flex",
16994
17046
  alignItems: "center",
@@ -18647,10 +18699,10 @@ var PROFILE_FIELDS = [
18647
18699
  commaSeparated: true,
18648
18700
  picker: "territories",
18649
18701
  item: {
18650
- ok: isKnownTerritory,
18651
- expected: "a territory from the picker below"
18702
+ ok: isTerritoryEntry,
18703
+ expected: "a territory from the picker below, or a two-letter code"
18652
18704
  },
18653
- hint: "One per line or comma-separated — pick from the list below. A region counts as one entry: European Union covers its member states, so there is no need to add them. Anything not in the list is kept and flagged rather than refused, because a search can name a territory this list does not carry."
18705
+ hint: "One per line or comma-separated — pick from the list below, or type a two-letter code like US, EU or GB; both are understood. A region counts as one entry: European Union covers its member states, so there is no need to add them. Anything not in the list is kept and flagged rather than refused, because a search can name a territory this list does not carry."
18654
18706
  },
18655
18707
  {
18656
18708
  key: "platforms",
@@ -20683,20 +20735,7 @@ function Auth({ auth }) {
20683
20735
  }) : null] });
20684
20736
  }
20685
20737
  function Engine({ engine, programDisputed = false }) {
20686
- const billed = engine.billing.apiBilled ? "API key" : "Subscription";
20687
- const faults = [
20688
- ...engine.known ? [] : [`This build does not ship an engine called ${engine.id}.`],
20689
- ...engine.billing.missing.length ? [`Set to bill an API key, and ${engine.billing.missing.join(" and ")} is not set — a run is refused rather than billed to the subscription.`] : [],
20690
- ...engine.binaryPresent ? [] : ["The engine program cannot be found or run on this machine."],
20691
- ...programDisputed && engine.binaryPresent ? ["The engine program is on this machine, but the engine could not find it when it last started — so a new search will refuse. Restart the engine service, or install the CLI where the service can see it."] : []
20692
- ];
20693
- return (0, import_jsx_runtime.jsx)(Row$1, {
20694
- ok: faults.length === 0,
20695
- name: engine.vendor ?? engine.id,
20696
- mono: engine.id,
20697
- state: billed,
20698
- faults
20699
- });
20738
+ return (0, import_jsx_runtime.jsx)(Row$1, { ...engineRow(engine, { programDisputed }) });
20700
20739
  }
20701
20740
  function Provider({ p }) {
20702
20741
  const faults = p.remedy ? [p.remedy] : p.configured ? [] : p.provider === null ? [`No register is selected. Set ${p.missing.join(" and ")}.`] : !p.known ? [`This build does not ship a provider called ${p.provider}.`] : [`Set ${p.missing.join(" and ")}.`];
@@ -49,7 +49,7 @@
49
49
  -->
50
50
  <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
51
51
  <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
52
- <script type="module" crossorigin src="/portal/assets/index-CEYvXdqH.js"></script>
52
+ <script type="module" crossorigin src="/portal/assets/index-KFAHMgdT.js"></script>
53
53
  <link rel="stylesheet" crossorigin href="/portal/assets/index-1ziUJX1E.css">
54
54
  </head>
55
55
  <body>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.3",
5
+ "version": "0.2.4",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -1,5 +1,7 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.2.4
4
+
3
5
  ## 0.2.3
4
6
 
5
7
  ## 0.2.2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -2,110 +2,59 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
4
  //
5
- // A BARE `#NNN` ADDED IN A DIFF IS REFUSED. What is already here is not this check's business.
5
+ // WHAT THIS CHANGE ADDS IS REFUSED. What is already here is not this check's business.
6
6
  //
7
7
  // node scripts/added-reference-check.mjs [--base <ref>]
8
8
  //
9
9
  // ── WHY THIS IS DIFF-SHAPED AND NOT A SWEEP ──────────────────────────────────────────────────────
10
10
  //
11
- // This tree carries 3,828 of these tokens across 542 files. They are opaque numbers into an archived
12
- // tracker no client data and no private name and they are the residual the export's ratchet
13
- // accepted, not a leak. A guard that refused them all would refuse every pull request from its first
14
- // day, and a guard everybody bypasses protects nothing. So this one asks a smaller question that has a
15
- // clean answer: did THIS change add another one.
11
+ // The classes below have a standing population in this tree: citations into a tracker nobody outside
12
+ // can open, account names, home directories, and the words this project uses for how it is built. A
13
+ // guard that refused them all would refuse every pull request from its first day, and a guard
14
+ // everybody bypasses protects nothing. So this one asks a smaller question that has a clean answer:
15
+ // did THIS change add another.
16
16
  //
17
- // The 3,828 are somebody's work, filed and costed. When they go, the allowlist below goes with them and
18
- // this check widens to the whole tree in the same pull request.
17
+ // The standing population is not therefore accepted. It is counted, per file and per class, in
18
+ // `driver/test/fixtures/public-residue-backlog.json`, and the floor beside it refuses any file that
19
+ // grows. Between the two, the number can only fall: this check stops the inflow and the floor stops
20
+ // the backsliding. Neither works alone — a diff guard with no floor watches the total drift upward one
21
+ // repaired-and-reintroduced line at a time, and a floor with no diff guard is a number that goes stale
22
+ // the first time somebody adds a file.
19
23
  //
20
24
  // ── WHAT IT READS, AND WHAT IT DELIBERATELY DOES NOT ─────────────────────────────────────────────
21
25
  //
22
26
  // COMMENTS AND PROSE ONLY. A hash followed by digits is not always a reference: a three-digit one is
23
27
  // also a CSS colour, and a composite key or a fixture string can hold anything. Reading only comment
24
- // markdown keeps the check away from every context where the token means something else, which is what
25
- // lets it refuse without a table of exceptions that would rot.
28
+ // and markdown text keeps the check away from every context where the token means something else,
29
+ // which is what lets it refuse without a table of exceptions that would rot.
26
30
  //
27
- // SO A TEST NAME IS OUT OF SCOPE, and that is a real hole rather than an oversight: 2,650 of the 3,828
28
- // are test names, which are string literals. Named here so the next reader does not have to rediscover
29
- // it — widening to string literals means deciding what to do about CSS and composite keys first, and
30
- // that decision belongs with the cleanup, not with this.
31
+ // SO A TEST NAME IS OUT OF SCOPE, and that is a real hole rather than an oversight: most of the
32
+ // standing citation population is test names, which are string literals. Named here so the next reader
33
+ // does not have to rediscover it — widening to string literals means deciding what to do about CSS and
34
+ // composite keys first, and that decision belongs with the cleanup, not with this.
31
35
  //
32
36
  // LINK TARGETS ARE EXEMPT. A markdown anchor and a URL fragment are addresses, not references.
33
37
  //
34
- // AND `tracker issue NNN` PASSES, because it carries no `#` at all. That is the form this project
35
- // writes, and the guard exists to make the wrong form loud rather than to ban the number.
36
- import { execFileSync } from "node:child_process";
37
-
38
- // THE TWELVE FILES RESTORED VERBATIM FROM THE FROZEN TIP AND KEPT. Thirteen were restored; the
39
- // duplicate-skip arms were dropped in the same branch, so twelve reach main. They came across
40
- // byte-exact, which is what made their arms trustworthy and also brought their references with them.
41
- // Exempted for that one merge and removed by the cleanup; see the note above.
42
- // RESTORED_VERBATIM IS GONE (tracker issue 188). It exempted twelve files by literal path so that a
43
- // byte-for-byte restoration could land without the guard refusing its own restored text. Their tokens
44
- // are now retired like everything else, so the list exempts nothing real — and a stale exemption list is
45
- // worse than none: it silently covers files nobody is checking any more, and it is invisible in a diff
46
- // that does not touch this file.
47
-
48
- const TOKEN = /#[0-9]{3,}/g;
49
-
50
- // A `#` COMMENT IS A COMMENT WHEREVER THE FILE FORMAT SAYS SO, not only in YAML (tracker issue 188).
51
- //
52
- // This read `#` as a comment for YAML alone, so the same sentence was refused in a .yml file and waved
53
- // through in .env.example, a systemd unit or a shell script. Those comments are exactly as publicly
54
- // visible, and `# REQUIRED — tracker issue 774 removed the code default` was sitting in .env.example on the public
55
- // tree while this guard reported the tree clean. Found while measuring the class for the retirement pass:
56
- // the guard's own rule flagged 359 tokens, and thousands more sat in files it had never classified.
57
- //
58
- // Extensionless is deliberate: a systemd unit or a dotfile often has no extension worth matching, so the
59
- // KNOWN `#`-comment names are listed and everything else keeps the source rule.
60
- const HASH_COMMENT = /(^|\/)(\.env[^/]*|[^/]*\.(ya?ml|sh|bash|service|timer|path|socket|conf|ini|toml|properties)|Dockerfile[^/]*|Makefile|\.gitignore|\.gitattributes)$/;
61
-
62
- /** Is this added line one the check reads at all? Comments in source, everything in markdown. */
63
- export const isProse = (path, line) => {
64
- if (/\.mde?$/.test(path) || path.endsWith(".md")) return true;
65
- const t = line.trim();
66
- if (HASH_COMMENT.test(path)) return t.startsWith("#");
67
- return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
68
- };
69
-
70
- // ── A COLOUR IS NOT A CITATION, AND THE DIGITS ALONE CANNOT SAY WHICH ───────────────────────────
71
- //
72
- // `TOKEN` matches digits only, so a six-digit hex colour is read as its leading digits and refused as a
73
- // reference. An earlier sweep acted on exactly that reading and rewrote sixteen colour literals as issue
74
- // text, two of them live mermaid `classDef` directives — so the documentation's diagrams rendered
75
- // broken, and thirteen comments stated a value that was no longer there. Restoring them then hit this
76
- // guard, whose refusal told the author to write the very text that had caused it.
38
+ // `demo/**` AND `driver/skills/**` ARE NOT READ. The reasons are in shared/reference-guard-classes.mjs
39
+ // beside the rule itself, because that is where the next person changing it will be looking.
77
40
  //
78
- // TWO RULES, and between them they settle every case without a table of exceptions:
41
+ // ── WHERE THE TABLE LIVES ────────────────────────────────────────────────────────────────────────
79
42
  //
80
- // 1. A hex colour may contain a-f; an issue number is decimal. A token carrying a letter cannot be a
81
- // reference whatever its length, and no reading of the digits is needed to know it.
82
- // 2. For the all-digit case — three digits is both a short colour and a plausible issue number — the
83
- // SITE settles it: a value whose property is a colour is a colour. Same rule and the same property
84
- // list as the guard in `driver/test/prompt-payload-names-no-tracker-issue.test.mjs`, which already
85
- // plants both and requires them told apart.
86
- //
87
- // This does NOT widen to bare digits in prose. A reference in a comment is still refused, which is the
88
- // whole point of the check, and the arm asserts that direction too.
89
- const HEX_COLOUR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
90
- const COLOUR_PROPERTY = /(?:^|[;{\s(,])(?:color|background|background-color|border|border-color|fill|stroke|outline|box-shadow|text-shadow)\s*:\s*$/i;
91
-
92
- /** Strip hex colours, so what remains is only tokens that could be a reference. */
93
- export const withoutColourValues = (line) => String(line).replace(HEX_COLOUR, (m, offset, whole) => {
94
- if (/[a-fA-F]/.test(m.slice(1))) return ""; // letters ⇒ not a decimal number
95
- return COLOUR_PROPERTY.test(whole.slice(0, offset)) ? "" : m; // all digits ⇒ the property decides
96
- });
43
+ // shared/reference-guard-classes.mjs, with the census that reads the same table. Two definitions of
44
+ // "what a public tree must not acquire" is one definition and one imitation of it, and the imitation
45
+ // is whichever the reader did not run.
46
+ import { execFileSync } from "node:child_process";
47
+ import {
48
+ CLASSES, offendingClasses, isProse, isScannable,
49
+ withoutColourValues, withoutLinkTargets,
50
+ } from "../shared/reference-guard-classes.mjs";
97
51
 
98
- /** Strip the spans where a `#NNN` is an address rather than a reference. */
99
- export const withoutLinkTargets = (line) => line
100
- .replace(/\]\([^)]*\)/g, "]()") // markdown link targets, anchors included
101
- .replace(/https?:\/\/\S+/g, "") // bare URLs and their fragments
102
- .replace(/<[^>]*>/g, ""); // angle-bracket autolinks
52
+ // Re-exported because scripts/retire-bare-refs.mjs and the arms beside this file import them from
53
+ // here, and moving the table should not move every caller in the same commit.
54
+ export { CLASSES, offendingClasses, isProse, isScannable, withoutColourValues, withoutLinkTargets };
103
55
 
104
56
  /** Every offending token on one added line, or an empty array. */
105
- export const offendingTokens = (path, line) => {
106
- if (!isProse(path, line)) return [];
107
- return [...withoutColourValues(withoutLinkTargets(line)).matchAll(TOKEN)].map((m) => m[0]);
108
- };
57
+ export const offendingTokens = (path, line) => offendingClasses(path, line).map((c) => c.token);
109
58
 
110
59
  const baseArg = () => {
111
60
  const i = process.argv.indexOf("--base");
@@ -143,14 +92,21 @@ function main() {
143
92
  // difference between "looked and found nothing" and "had nothing to look at".
144
93
  const hits = [];
145
94
  for (const { path, line } of added) {
146
- for (const token of offendingTokens(path, line)) hits.push({ path, token, line: line.trim().slice(0, 100) });
95
+ for (const c of offendingClasses(path, line)) hits.push({ path, ...c, line: line.trim().slice(0, 100) });
147
96
  }
148
97
  console.log(`added-reference-check: read ${added.length} added line(s) against ${base}`);
149
98
  if (!hits.length) return;
150
- console.error(`\n${hits.length} bare reference(s) added in comments or prose:\n`);
151
- for (const h of hits) console.error(` ${h.path}: ${h.token}\n ${h.line}`);
152
- console.error("\nWrite `tracker issue NNN` instead. A bare `#NNN` linkifies into whatever repository "
153
- + "renders it, which is not the one the number belongs to, and it lives on in public history.");
99
+
100
+ // GROUPED BY CLASS, because the remedy is per class and a flat list makes the reader derive it eight
101
+ // times. Each heading is said once, then the lines it applies to.
102
+ console.error(`\n${hits.length} line(s) added that a public tree must not carry:\n`);
103
+ for (const { id, why } of CLASSES) {
104
+ const mine = hits.filter((h) => h.id === id);
105
+ if (!mine.length) continue;
106
+ console.error(` ${id} — ${why}`);
107
+ for (const h of mine) console.error(` ${h.path}: ${h.token}\n ${h.line}`);
108
+ console.error("");
109
+ }
154
110
  process.exit(1);
155
111
  }
156
112
 
package/scripts/e2e.mjs CHANGED
@@ -630,7 +630,7 @@ export function bandForScenario(scenario, resolve = resolveForDoor) {
630
630
  /**
631
631
  * — WHICH ENGINE COMMIT THIS RUN LOADED, read from the run's own artifacts.
632
632
  *
633
- * `role-e2e`'s handover requires the commit on every run, and until this existed it was reconstructed
633
+ * the testing handover requires the commit on every run, and until this existed it was reconstructed
634
634
  * by joining a checkout's reflog against `startedAt`. That reconstruction expires with the reflog,
635
635
  * dies with a re-clone, and is simply wrong for any run made while the checkout sat detached — it
636
636
  * produced two near-miss wrong certifications in two days.
@@ -1354,7 +1354,7 @@ function evalAssertion(a, runDir) {
1354
1354
  // ── — THIS IS A DELIVERY CONTRACT, AND A RUN THAT DID NOT DELIVER NEVER ENTERED IT ────────
1355
1355
  //
1356
1356
  // Owner ruling, 2026-08-22, verbatim: "clean up the failed runs. they owe the client nothing."
1357
- // (Relayed by role-overwatch; recorded here because the rule this line encodes is a product
1357
+ // (Recorded here because the rule this line encodes is a product
1358
1358
  // decision, not a harness preference.)
1359
1359
  //
1360
1360
  // Measured before the ruling: `sendPending` is carried by 25 of 25 delivered runs and 0 of 29
@@ -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();
@@ -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 the arm in driver/test/retired-env-spellings.test.mjs holds this output to the
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
- // driver/test/retired-env-spellings.test.mjs regenerates this and refuses a stale copy.
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();