clearotron 0.3.2 → 0.3.3-beta.0

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 (105) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +29 -7
  4. package/bin/start.mjs +1 -1
  5. package/build-info.json +2 -2
  6. package/demo/MANIFEST.json +27 -0
  7. package/docs/INTAKE.md +8 -0
  8. package/docs/architecture/04-configuration-reference.md +1 -1
  9. package/driver/CHANGELOG.md +37 -0
  10. package/driver/clearance-variants-record.mjs +12 -1
  11. package/driver/common-law-coverage-status.mjs +113 -0
  12. package/driver/contract-audit.mjs +1 -1
  13. package/driver/contract-e3-backlog.mjs +37 -37
  14. package/driver/contract-vocabulary.mjs +8 -8
  15. package/driver/coverage-form-io.mjs +3 -1
  16. package/driver/coverage-form.mjs +38 -11
  17. package/driver/coverage-ledger.mjs +37 -7
  18. package/driver/coverage-union.mjs +2 -2
  19. package/driver/crowd-context.mjs +19 -6
  20. package/driver/drainer-identity.mjs +1 -1
  21. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  22. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  23. package/driver/engine/mcp/coverage-server.mjs +1 -1
  24. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  25. package/driver/engine/mcp/euipo-server.mjs +2 -0
  26. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  27. package/driver/engine/mcp/gather-config.mjs +8 -2
  28. package/driver/engine/mcp/probe-server.mjs +28 -0
  29. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  30. package/driver/engine/mcp/recording-server.mjs +30 -0
  31. package/driver/engine/mcp/signa-server.mjs +2 -0
  32. package/driver/engine/mcp/supplemental.mjs +89 -12
  33. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  34. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  35. package/driver/engine/openai-agent.mjs +7 -0
  36. package/driver/engine/probe.mjs +67 -14
  37. package/driver/engine/tool-refusal.mjs +16 -0
  38. package/driver/enqueue-schema.mjs +2 -2
  39. package/driver/envelope-settle.mjs +82 -13
  40. package/driver/findings-model.mjs +4 -4
  41. package/driver/gateway.mjs +18 -2
  42. package/driver/manager-groups-verdict.mjs +1 -1
  43. package/driver/matter-frame-record.mjs +24 -7
  44. package/driver/named-band.mjs +1 -1
  45. package/driver/package.json +1 -1
  46. package/driver/partial-payload-baseline.json +12 -3
  47. package/driver/pipeline.mjs +141 -37
  48. package/driver/publish/index.mjs +40 -25
  49. package/driver/publish/xlsx.mjs +26 -4
  50. package/driver/queue-markers.mjs +44 -0
  51. package/driver/queue-watch-verdict.mjs +2 -2
  52. package/driver/register-availability.mjs +2 -2
  53. package/driver/register-plan.mjs +313 -21
  54. package/driver/roster-verdict.mjs +1 -1
  55. package/driver/runner.mjs +26 -2
  56. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  57. package/driver/skills/clearance-register/SKILL.md +44 -3
  58. package/driver/skills/clearance-register/digest.md +5 -5
  59. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  60. package/driver/skills/clearance-register/unit.md +39 -0
  61. package/driver/skills/clearance-variants/SKILL.md +1 -1
  62. package/driver/skills/matter-frame/SKILL.md +4 -2
  63. package/driver/stages.mjs +12 -5
  64. package/driver/status-snapshot.mjs +1 -1
  65. package/driver/suite-census.json +236 -14
  66. package/driver/synthesis-record.mjs +80 -2
  67. package/driver/unit-file-drift.mjs +3 -3
  68. package/driver/unit-inventory.mjs +2 -2
  69. package/driver/unit-state-verdict.mjs +1 -1
  70. package/driver/updater-identity.mjs +2 -3
  71. package/driver/variant-manifest-model.mjs +11 -1
  72. package/driver/verify.mjs +5 -5
  73. package/driver/withheld-families.mjs +104 -0
  74. package/mcp-server/CHANGELOG.md +4 -0
  75. package/mcp-server/lib/brief.mjs +5 -7
  76. package/mcp-server/lib/runs.mjs +1 -1
  77. package/mcp-server/package.json +1 -1
  78. package/mcp-server/server.mjs +3 -2
  79. package/package.json +1 -1
  80. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  81. package/portal-ui/dist/index.html +1 -1
  82. package/portal-ui/package.json +1 -1
  83. package/providers/_shared/count.mjs +2 -2
  84. package/providers/_shared/enumerate.mjs +15 -2
  85. package/providers/_shared/execute-plan.mjs +19 -1
  86. package/providers/_shared/plan-guards.mjs +40 -0
  87. package/providers/clarivate/src/capabilities.js +15 -5
  88. package/providers/clarivate/src/core.js +41 -5
  89. package/providers/corsearch/src/capabilities.js +4 -0
  90. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  91. package/providers/oauth-mcp-bridge/package.json +1 -1
  92. package/providers/signa/src/capabilities.js +22 -8
  93. package/providers/signa/src/core.js +12 -1
  94. package/scripts/demo-evidence.mjs +114 -0
  95. package/scripts/engine-probe.mjs +6 -5
  96. package/scripts/env-audit.mjs +1 -1
  97. package/scripts/freeze-example-run.mjs +3 -3
  98. package/scripts/live-surface-check.mjs +26 -19
  99. package/scripts/mint-suite-census.mjs +66 -0
  100. package/scripts/package-size-budget.mjs +117 -0
  101. package/scripts/register-plan-shape.mjs +259 -0
  102. package/scripts/release-note-required.mjs +38 -1
  103. package/scripts/settings-render-check.mjs +36 -0
  104. package/scripts/travelling-predicates.mjs +1 -1
  105. package/shared/identifier-scan.mjs +22 -5
@@ -90,6 +90,9 @@ export async function clarivateFetch(apiKey, base, path, { body = null, method =
90
90
  attempts = i + 1;
91
91
  resp = await fetch(url, init);
92
92
  if (resp.ok || resp.status < 500 || i === retries) break;
93
+ // A 500 wrapping the search service's 400 is a refusal of the query as written, not weather: the
94
+ // same request is refused the same way, so it is not sent again (countFailureReason below).
95
+ if (WRAPPED_REFUSAL.test(await resp.clone().text().catch(() => ""))) break;
93
96
  await new Promise((r) => setTimeout(r, 1500 * (i + 1)));
94
97
  }
95
98
  } catch (err) {
@@ -1113,13 +1116,45 @@ export async function doSearch(apiKey, base, params, tctx) {
1113
1116
  }
1114
1117
 
1115
1118
  // ── Count ─────────────────────────────────────────────────────────────────────────────────────────
1116
- // POST /count takes the SAME SearchRequest body as /search, is cheap, works at ANY magnitude (209012
1117
- // returned without complaint) and returns PER-OFFICE counts in one call. This is the enumerate
1119
+ // POST /count takes the SAME SearchRequest body as /search, is cheap, works at ANY magnitude and
1120
+ // returns PER-OFFICE counts in one call. This is the enumerate
1118
1121
  // kernel's countProbe:"endpoint" dependency, so it returns the kernel's plain probe shape
1119
1122
  // { ok, total, reason } — plus `per_office`, which is richer than corsearch can offer and rides into
1120
1123
  // the crowd descriptor for judgment.
1121
1124
 
1122
1125
  /** ONE /count round trip over already-resolved params. Returns the kernel's plain probe shape. */
1126
+ // ── A REFUSED QUERY IS NAMED AS ONE, with the size of what was sent ─────────────────────────────────
1127
+ //
1128
+ // The vendor answers a query its search service cannot take with that service's 400, delivered inside
1129
+ // its own 500: `INTERNAL_SERVER_ERROR - 400 on POST request for "…/search"`. Passed on as a bare HTTP 500
1130
+ // it reads as an outage and names nothing an operator can act on. A query that is too wide is the common
1131
+ // case: the service's bound is the number of OR terms in one field, which is about 496, not the query's
1132
+ // length.
1133
+ // The reason says so: the widest OR stack's term count and characters, and this provider's declared
1134
+ // width. It avoids the words `HTTP 400`, which providerRejectedTheQuery reads as a cue to retry an owner
1135
+ // stack on the caller's own term; that behaviour is unchanged.
1136
+ //
1137
+ // AND IT IS A CAPABILITY GAP. The service refused the query as written and will refuse the identical query
1138
+ // every time, so the reason carries CAPABILITY_GAP_MARKER: the plan executor defers the slice as a
1139
+ // disclosed gap at once, as it does a query buildSearchRequest refuses before sending, instead of sending
1140
+ // it round the recovery ladder. On production that ladder re-sent a refused 500-term query and spent a
1141
+ // recovery park each time. Every other HTTP failure keeps the transient reading.
1142
+ const WRAPPED_REFUSAL = /\b400\s+on\s+POST\s+request\b/;
1143
+ const widestOrStack = (body) => (body?.searchFields ?? []).reduce((best, f) => {
1144
+ const v = String(f?.value ?? "");
1145
+ const terms = v ? v.split(" OR ").length : 0;
1146
+ return terms > best.terms ? { terms, chars: v.length } : best;
1147
+ }, { terms: 0, chars: 0 });
1148
+ export function countFailureReason(status, words, body) {
1149
+ if (status !== 500 || !WRAPPED_REFUSAL.test(String(words ?? ""))) return `HTTP ${status}${words ? `: ${words}` : ""}`;
1150
+ const w = widestOrStack(body);
1151
+ // THE NUMBERS COME LAST. The executor clips a failed slice's reason to its head and tail, and its own
1152
+ // prefixes fill the head, so a count placed early is elided; the tail is what a run's record keeps.
1153
+ return `${CAPABILITY_GAP_MARKER} register_refused_query: the register refused this query as malformed (a 400 inside its `
1154
+ + `HTTP 500). Provider's words: ${words}. Sent ${w.terms} OR terms (${w.chars} characters); this provider's declared `
1155
+ + `width is ${CAPABILITIES.maxOrWidth} terms.`;
1156
+ }
1157
+
1123
1158
  async function countOnce(apiKey, base, p, tctx) {
1124
1159
  let body;
1125
1160
  try { body = buildSearchRequest(p); }
@@ -1129,11 +1164,12 @@ async function countOnce(apiKey, base, p, tctx) {
1129
1164
  // Marked as a capability gap so the plan executor DEFERS the slice — a disclosed coverage row the
1130
1165
  // lawyer reads — instead of stamping a transient error and grinding the whole run to a fan-in
1131
1166
  // StageFailure over an answer that will never differ. A real HTTP failure below keeps the transient
1132
- // reading and rides the repair ladder, exactly as before.
1167
+ // reading and rides the repair ladder, exactly as before, except a query the service refused as written
1168
+ // (countFailureReason above).
1133
1169
  catch (e) { return { ok: false, total: null, per_office: null, reason: `${CAPABILITY_GAP_MARKER} ${e.message}` }; }
1134
1170
 
1135
1171
  const r = await clarivateFetch(apiKey, base, "/count", { body, tctx: { ...tctx, target: String(echoOf(p)).slice(0, 200) } });
1136
- if (!r.ok) return { ok: false, total: null, per_office: null, reason: `HTTP ${r.status}${errText(r) ? `: ${errText(r)}` : ""}` };
1172
+ if (!r.ok) return { ok: false, total: null, per_office: null, reason: countFailureReason(r.status, errText(r), body) };
1137
1173
  // Named rather than folded into the "no counts{}" branch below: both refuse (total null, never 0),
1138
1174
  // but someone repairing a run needs to know the response was CUT, not that the provider answered in
1139
1175
  // a shape we did not expect.
@@ -1734,7 +1770,7 @@ export async function doEnumerate(apiKey, base, params, tctx) {
1734
1770
  // dictated a count instead of an enumerate. POST /search FAILS LOUD past 30000 (tooManyResults),
1735
1771
  // so routing the count probe through it would return an ERROR string, the executor would stamp the
1736
1772
  // block error:true, and a SANCTIONED CROWD would be counted MISSING — doctrine 5 inverted. POST
1737
- // /count never fails on magnitude (209012 returned without complaint) and carries per-office truth.
1773
+ // /count never fails on magnitude and carries per-office truth.
1738
1774
  // `countParams` is therefore {}: buildSearchRequest has no `limit` knob to honour, and passing a
1739
1775
  // dead parameter would only imply one exists.
1740
1776
  //
@@ -133,6 +133,10 @@ export const CAPABILITIES = Object.freeze({
133
133
  // Several goods terms become several `product:` clauses, and within one field this query language
134
134
  // ORs them implicitly — the same "any of these words" the explicit OR writes elsewhere.
135
135
  goodsTextListOr: true,
136
+ // The shortest term the contains form accepts: none known, so none declared. The vendor publishes no
137
+ // public reference for this API (none was found on 2026-09-22), and no short-term refusal has been
138
+ // measured here. `null` keeps the contains form at every length; see clarivate/capabilities.js.
139
+ containsMinLength: null,
136
140
  // ── WHICH FORM OF A NON-LATIN MARK DOES THE INDEX HOLD? ──────────────────────────────────────────
137
141
  // `true` = the CHARACTERS. A native-script term is a legitimate, productive query here and MUST be
138
142
  // sent — the shared executor's script-form refusal (providers/_shared/script-form.mjs) is switched
@@ -1,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.3-beta.0
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.2
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.2",
3
+ "version": "0.3.3-beta.0",
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).",
@@ -154,6 +154,23 @@ export const CAPABILITIES = Object.freeze({
154
154
  // So the compiler does not build a multi-term goods entry for this provider, and it must never fall
155
155
  // back to a space-joined string. No coverage is lost: the broad class-wide sweep still runs.
156
156
  goodsTextListOr: false,
157
+ // ── THE SHORTEST TERM THE CONTAINS FORM ACCEPTS: 3 folded characters, documented ────────────────
158
+ //
159
+ // Signa's API reference, "Search trademarks", section "Match modes" (docs.signa.so/api-reference/
160
+ // trademarks/search-trademarks, read 2026-09-22): "Deterministic modes accept a single-character `q`
161
+ // (one non-empty folded char), whereas `similar` requires at least 2 folded chars and `contains`
162
+ // requires at least 3." The same page's validation rules repeat it. A shorter term on `contains` is a
163
+ // hard 400 ("match=contains requires q to be at least 3 characters (after case/accent folding) to
164
+ // avoid an over-broad substring scan").
165
+ //
166
+ // The compiler reads this: a goods-narrowed question or a saturation probe whose term is shorter is
167
+ // asked on the exact form instead, with the same class and goods filters, and the plan entry says so.
168
+ // Exact goes out as the ranked `strategies: ["exact"]`, which `similar`'s 2-character floor governs,
169
+ // so a two-letter mark is answered; a one-letter mark is below both floors and stays refused.
170
+ containsMinLength: 3,
171
+ // The ranked shape's floor: "`similar` requires at least 2 folded chars" (same page). An exact question
172
+ // shorter than this goes out on the deterministic `match`, which takes one; from here up it stays ranked.
173
+ rankedMinLength: 2,
157
174
  // Search rows already carry status / nice_classes / owner_name → screening is inline, zero extra calls.
158
175
  screenSource: "search-row",
159
176
  // No hard result ceiling, and no total to compare one against.
@@ -168,8 +185,6 @@ export const CAPABILITIES = Object.freeze({
168
185
  //
169
186
  // strategies[] exact | phonetic | fuzzy | prefix — ranked, several per call
170
187
  // match similar | exact | starts_with | ends_with | contains — deterministic, one only
171
- //
172
- // Every value below was run against the live API with the resulting total recorded.
173
188
  predicates: Object.freeze({
174
189
  exact: "exact", // strategies[] — the deterministic shape, the ranked one for audit continuity
175
190
  // `contains` IS the unanchored mode this contract said did not exist. The old header
@@ -177,12 +192,11 @@ export const CAPABILITIES = Object.freeze({
177
192
  // contains slice is a substring sweep, and they return different sets. It never needed to be
178
193
  // fuzzy; it needed the deterministic shape, which nothing here had.
179
194
  //
180
- // ONE LIVE CONSTRAINT NOT IN THE PUBLISHED SPECIFICATION: deterministic modes take a
181
- // query of 1 character or more. `contains` in fact demands THREE — "match=contains requires q to
182
- // be at least 3 characters (after case/accent folding) to avoid an over-broad substring scan."
183
- // A one- or two-character element therefore cannot ride this predicate, and it fails loud (400)
184
- // rather than quietly returning a narrower set. Recorded because it is the second place in this
185
- // file where the document and the wire disagree, and the wire is the one that answers queries.
195
+ // A LENGTH FLOOR THE VENDOR DOCUMENTS: deterministic modes take a query of 1 character or more,
196
+ // and `contains` demands THREE — "match=contains requires q to be at least 3 characters (after
197
+ // case/accent folding) to avoid an over-broad substring scan." A one- or two-character element
198
+ // therefore cannot ride this predicate, and it fails loud (400) rather than quietly returning a
199
+ // narrower set. `containsMinLength` above declares it, and the compiler asks such a term exactly.
186
200
  default: "contains",
187
201
  // `starts_with`, NOT the `prefix` strategy. Both exist and the executor uses this one:
188
202
  // planPredicateParams emits `match_mode: "starts_with"` for a trailing-`*` entry and never emits
@@ -30,6 +30,9 @@ import { nonAnswerBodyError, parseJsonBody, unparsedBodyError } from "../../_sha
30
30
  import { makeEnumerate, isOwnerScoped } from "../../_shared/enumerate.mjs";
31
31
  import { makeExecutePlan } from "../../_shared/execute-plan.mjs";
32
32
  import CAPABILITIES, { SIGNA_OFFICE_KEYS, OWNER_SCOPED_WINDOW } from "./capabilities.js";
33
+ // The register's own measure of a term's length: case and accents folded, spaces not counted — the same
34
+ // measure its documented floors are stated in (the compiler's foldedTermLength counts the same way).
35
+ const foldedLength = (q) => [...String(q ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, "")].length;
33
36
  import { SIGNA_OFFICE_SNAPSHOT } from "./offices.generated.js";
34
37
 
35
38
  export const DEFAULT_BASE = "https://api.signa.so";
@@ -660,8 +663,16 @@ export function toSignaParams(p = {}) {
660
663
  // `match`. exact/phonetic stay on strategies, which is what this provider has always sent and what
661
664
  // its fixtures were captured with; the anchored and unanchored modes can only be expressed by
662
665
  // `match`, so they select that shape and drop strategies.
666
+ //
667
+ // EXACT BELOW THE RANKED FLOOR ONLY. The ranked shape takes `similar`'s floor of two folded
668
+ // characters, so a one-letter exact question is refused there. The deterministic `match: "exact"`
669
+ // takes one, but it asks a narrower question: "`exact` requires the whole mark to equal `q`", where
670
+ // the ranked `exact` strategy also returns longer marks that contain the term, which is the recall an
671
+ // identical question relies on. So exact stays ranked from the floor up, and only a term below it
672
+ // goes deterministic, where it is answered rather than refused.
663
673
  const mode = String(p.match_mode ?? "").trim();
664
- if (mode === "exact" || mode === "phonetic" || mode === "prefix") out.strategies = [mode];
674
+ if (mode === "exact" && foldedLength(out.query) < CAPABILITIES.rankedMinLength) out.match = "exact";
675
+ else if (mode === "exact" || mode === "phonetic" || mode === "prefix") out.strategies = [mode];
665
676
  else if (mode === "starts_with" || mode === "ends_with" || mode === "contains") out.match = mode;
666
677
  // ── `default` IS THE COUNT LANE'S WORD FOR THE SAME UNANCHORED QUERY THE PLAN LANE CALLS `{}` ────
667
678
  //
@@ -0,0 +1,114 @@
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
+ // demo-evidence.mjs — THE HAND-WRITTEN DIFF MUST BE REVIEWABLE ON ITS OWN.
5
+ //
6
+ // One integration branch carried 37 commits, 2,490 files, 221,105 additions and 410,984 deletions, and
7
+ // most of it was regenerated demo evidence. A reviewer cannot find the source changes inside that, so
8
+ // in practice nobody reads them — the review happens, and it reviews nothing. The evidence is not the
9
+ // problem: it is generated output, it is meant to change wholesale, and it is right that it ships.
10
+ // Mixing it into the same commits as hand-written code is the problem.
11
+ //
12
+ // TWO HALVES, because a convention nobody checks is a convention that lasts one beta:
13
+ //
14
+ // --check a commit that touches `demo/` touches nothing else, across a range
15
+ // --apply re-record `demo/MANIFEST.json` — generator, tree, and the run each product replays
16
+ //
17
+ // The manifest is what makes the regeneration commit self-explaining: without it a reviewer skipping
18
+ // the demo diff is taking on trust that it IS regenerated output rather than a hand edit hidden in
19
+ // 200,000 lines. With it, the thing they are skipping says what produced it.
20
+
21
+ import { execFileSync } from "node:child_process";
22
+ import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
27
+ export const MANIFEST = join(ROOT, "demo", "MANIFEST.json");
28
+ export const GENERATOR = "scripts/freeze-example-run.mjs";
29
+
30
+ const git = (...args) => execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim();
31
+
32
+ /** Every product directory under `demo/`, with the run it replays. */
33
+ export function products({ root = ROOT } = {}) {
34
+ const dir = join(root, "demo");
35
+ if (!existsSync(dir)) return [];
36
+ return readdirSync(dir)
37
+ .filter((name) => statSync(join(dir, name)).isDirectory())
38
+ .sort()
39
+ .map((name) => {
40
+ let meta = {};
41
+ try { meta = JSON.parse(readFileSync(join(dir, name, "meta.json"), "utf8")); } catch { /* absent is an answer */ }
42
+ return { product: name, runId: meta.runId ?? null, template: meta.template ?? null };
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Is this a commit that mixes generated evidence with hand-written source?
48
+ * Returns the offending paths, or an empty array. PURE given the file list.
49
+ */
50
+ export function mixedPaths(files) {
51
+ const demo = files.filter((f) => f.startsWith("demo/"));
52
+ if (!demo.length) return []; // touches no evidence — nothing to say
53
+ return files.filter((f) => !f.startsWith("demo/"));
54
+ }
55
+
56
+ if (import.meta.url === `file://${process.argv[1]}`) {
57
+ const apply = process.argv.includes("--apply");
58
+
59
+ if (apply) {
60
+ writeFileSync(MANIFEST, `${JSON.stringify({
61
+ note: "What produced the evidence under demo/. Re-recorded by scripts/demo-evidence.mjs --apply in the same commit that regenerates it.",
62
+ generator: GENERATOR,
63
+ recordedFromTree: git("rev-parse", "HEAD"),
64
+ products: products(),
65
+ }, null, 2)}\n`);
66
+ console.log(`recorded ${products().length} demo product(s) against ${git("rev-parse", "--short", "HEAD")}`);
67
+ process.exit(0);
68
+ }
69
+
70
+ // --check: every commit in the range keeps its evidence to itself.
71
+ const base = process.argv[process.argv.indexOf("--base") + 1] || "origin/main";
72
+
73
+ // ── A BASE THIS CHECKOUT CANNOT RESOLVE IS A FAILURE TO LOOK, AND IT SAYS SO ────────────────────
74
+ //
75
+ // A shallow clone fetches one branch, so `origin/main` is not a ref in it and `rev-list base..HEAD`
76
+ // dies on an ambiguous argument — a stack trace naming a line of this file, which reads as a broken
77
+ // script rather than as a checkout that cannot answer the question. It is neither a clean range nor
78
+ // a mixed one: nothing was compared. Falling back to another base would be worse, because a range
79
+ // measured against something the caller did not ask for reports a clean result for a population it
80
+ // never examined.
81
+ try { git("rev-parse", "--verify", "--quiet", `${base}^{commit}`); }
82
+ catch {
83
+ console.error(`demo-evidence: this checkout has no "${base}", so no commit was compared.`);
84
+ console.error("");
85
+ console.error("Nothing is known about the range either way — this is not a clean result. A shallow");
86
+ console.error("checkout carries only the branch it fetched; fetch the base ref, or name one this");
87
+ console.error("clone holds with --base.");
88
+ process.exit(2);
89
+ }
90
+
91
+ const shas = git("rev-list", `${base}..HEAD`).split("\n").filter(Boolean);
92
+ const bad = [];
93
+ for (const sha of shas) {
94
+ const files = git("show", "--name-only", "--pretty=format:", sha).split("\n").filter(Boolean);
95
+ const mixed = mixedPaths(files);
96
+ if (mixed.length) bad.push({ sha, mixed });
97
+ }
98
+
99
+ if (!bad.length) {
100
+ console.log(`demo-evidence: ${shas.length} commit(s) against ${base}; none mixes generated evidence with source.`);
101
+ process.exit(0);
102
+ }
103
+
104
+ console.error("demo-evidence: generated evidence is mixed with hand-written source.\n");
105
+ console.error("A reviewer cannot find the source diff inside a regeneration, so in practice nobody reads it.\n");
106
+ for (const { sha, mixed } of bad) {
107
+ console.error(` ${sha.slice(0, 9)} also touches ${mixed.length} non-demo path(s):`);
108
+ for (const f of mixed.slice(0, 5)) console.error(` ${f}`);
109
+ if (mixed.length > 5) console.error(` …and ${mixed.length - 5} more`);
110
+ }
111
+ console.error("\nSplit the regeneration into its own commit:");
112
+ console.error(" git reset HEAD~ && git add demo && git commit && git add -A && git commit");
113
+ process.exit(1);
114
+ }
@@ -10,9 +10,9 @@
10
10
  // ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────────────
11
11
  //
12
12
  // `probeEngineTurn` is the smallest thing that genuinely exercises an engine: one Haiku-tier turn at low
13
- // effort on a six-word prompt, through the adapter's own spawn path, with no MCP config, no tools, no
14
- // skills dir and no run dir. It answers the one question an operator asks before spending anything —
15
- // CAN this box run a turn, and under the billing mode it thinks it is using.
13
+ // effort, through the adapter's own spawn path, asked to call the one tool on the probe's own server, with
14
+ // no skills dir and no run dir. It answers the one question an operator asks before spending anything —
15
+ // CAN this box run a turn that uses a tool, and under the billing mode it thinks it is using.
16
16
  //
17
17
  // It had no way to be asked. Its only callers were `bin/onboard.mjs` (the setup wizard, which asks it
18
18
  // once, inside a flow) and `preflightEngineTurn` in `pipeline.mjs` (which asks it at the start of a run,
@@ -28,8 +28,9 @@
28
28
  // ── WHAT A GREEN PROBE DOES NOT PROVE, from the module's own header ─────────────────────────────────
29
29
  //
30
30
  // The cheap tier only. Both tiers ride one credential, so AUTH is proven for all of them — a per-tier
31
- // model entitlement or per-tier quota is not. And it sets up no tools, no gather MCP, no skills and no
32
- // run dir, so none of that path is touched. A probe that says `ok` says the door opens.
31
+ // model entitlement or per-tier quota is not. It calls one tool on its own server, so the tool path is
32
+ // proven and no stage's tool server is; it sets up no skills and no run dir. A probe that says `ok` says
33
+ // the door opens.
33
34
 
34
35
  // FIRST IMPORT, AND IT HAS TO BE. This entry statically reaches driver.config.mjs, which captures
35
36
  // env at module top — and every static import evaluates before this file's own body runs, so applying the
@@ -557,7 +557,7 @@ export function auditEnv(root = ROOT) {
557
557
  // 4 read RIGHT NOW through an idiom no `env`-prefixed regex can reach —
558
558
  // envValue("CLEAROTRON_MAX_RETRIES") and
559
559
  // envValue("CLEAROTRON_RATE_LIMIT_DEFAULT_BACKOFF_MS") (driver.config.mjs:486,
560
- // :494), envOn("CLEAROTRON_DUMP_JSON") (gateway.mjs:1423), and
560
+ // :494), envOn("CLEAROTRON_DUMP_JSON") (runStageLadder() in gateway.mjs), and
561
561
  // `export const API_KEY_ENV = "USPTO_API_KEY"` (providers/uspto-local/src/sync.js:77).
562
562
  // The name is a STRING LITERAL in every one; it just never sits beside an
563
563
  // `env` token. USPTO_API_KEY is a credential.
@@ -69,8 +69,8 @@ const FROZEN_FILES = [
69
69
  { path: "_driver/senior-rights.json", why: "publish/index.mjs:787 seniorRights" },
70
70
  { path: "_driver/verdict.json", why: "publish/index.mjs:792 verdictInfo" },
71
71
  { path: "_driver/framework.json", why: "publish/index.mjs, the frozen band vocabulary the run was rated under" },
72
- { path: "_driver/register-plan.json", why: "publish/index.mjs:901 scopeBasis" },
73
- { path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
72
+ { path: "_driver/register-plan.json", why: "publish/index.mjs:910 scopeBasis" },
73
+ { path: "_driver/instructed-scope.json", why: "publish/index.mjs:911 searchedJurisdictions, the fallback for register-plan" },
74
74
  { path: "_driver/enforcer-signals.json", why: "`esPath` declared in index.mjs" },
75
75
  { path: "_driver/predelivery-lint.json", why: "publish/index.mjs:172 lintSink" },
76
76
  { path: "_driver/escalation-state.json", why: "publish/index.mjs:173 escSink" },
@@ -118,7 +118,7 @@ const KNOCKOUT_FILES = [
118
118
  // knockout report render empty (publish/knockout.mjs:140-155). Named by stages-knockout.mjs:32,41.
119
119
  { path: "_driver/register-counts.json", why: "publish/knockout.mjs:140-155 counted figures + the Register column" },
120
120
  { path: "_driver/register-records.json", why: "stages-knockout.mjs:41 the terms behind the close-variation axis" },
121
- { path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
121
+ { path: "_driver/instructed-scope.json", why: "publish/index.mjs:911 searchedJurisdictions, the fallback for register-plan" },
122
122
  ];
123
123
 
124
124
  /** The allowlist for a template. One place, so a new template cannot half-exist. */
@@ -500,8 +500,8 @@ if (!snapshot) {
500
500
  catch { return null; }
501
501
  })();
502
502
  const { groups: managerGroups, why } = readUserManagerGroups(uid);
503
- const { state, message } = managerGroupsVerdict({ idGroups, managerGroups, user, uid, why });
504
- ({ pass, fail, skip })[state]("user manager groups are current", message);
503
+ const v = managerGroupsVerdict({ idGroups, managerGroups, user, uid, why });
504
+ record("user manager groups are current", v.state, v.message, v.blocked === true);
505
505
  }
506
506
 
507
507
  // 1b. — HOW THIS BOX DIFFERS FROM PRODUCTION on the flags that change output without saying so.
@@ -582,9 +582,9 @@ try {
582
582
  try { caller = (await import("../driver/portal-service.mjs")).opsTokenPosture(OPS_TOKEN); }
583
583
  catch { /* stays unreadable — the verdict reports what it could not compare */ }
584
584
  if (bundledDemos) {
585
- const { state, message } = rosterVerdict({ keys, onDisk, bundledDemos, caller,
585
+ const v = rosterVerdict({ keys, onDisk, bundledDemos, caller,
586
586
  expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
587
- ({ pass, fail, skip })[state]("roster resolves", message);
587
+ record("roster resolves", v.state, v.message, v.blocked === true);
588
588
  }
589
589
  // A COMPANY IN THE STORE THAT THIS KEY CANNOT START is its own finding, not a roster disagreement. A
590
590
  // warning, not a failure: the portal re-takes its credential at the start of every call, so a Start for
@@ -639,7 +639,7 @@ try {
639
639
  effectiveMode: (process.env.TRADEMARK_MCP_AUTH_MODE || "").trim().toLowerCase() === "token" ? "token" : "cf-access",
640
640
  allowedHosts: (process.env.TRADEMARK_MCP_ALLOWED_HOSTS || "").split(",").map((h) => h.trim()).filter(Boolean),
641
641
  });
642
- record("the ops door's auth mode was chosen for it", posture.state, posture.message);
642
+ record("the ops door's auth mode was chosen for it", posture.state, posture.message, posture.blocked === true);
643
643
  }
644
644
  } catch (e) {
645
645
  if (e?.message === "__door_unset__") skip("ops-MCP reachable", "this instance does not say where its ops-MCP is — NOT PROBED");
@@ -827,11 +827,12 @@ else {
827
827
  // process that is in no unit, so this arm derives from the PROCESS TABLE and from a stamp the drainer
828
828
  // writes about itself.
829
829
  //
830
- // IT FAILS ON COULD-NOT-LOOK, and that is deliberate. This script exits non-zero on `fail` only —
831
- // `skip` does not move the exit code — so recording an absent stamp as a skip would let the deploy
832
- // report a build live having never established what the executing process holds, which is the exact
833
- // state the incident's drainer was in. The fourth criterion of that issue is that the deploy does not
834
- // report a build live until this arm has looked; a skip here would be that criterion silently unmet.
830
+ // A COULD-NOT-LOOK HERE MOVES THE EXIT CODE, and that is deliberate. An ordinary skip does not, so
831
+ // recording an absent stamp as one would let the deploy report a build live having never established
832
+ // what the executing process holds, which is the exact state the incident's drainer was in. It is
833
+ // recorded through `blocked`, which exits 3 rather than the 1 a drift gives: the deploy still does not
834
+ // report a build live until this arm has looked, and the reader is told it could not look rather than
835
+ // that something drifted.
835
836
  {
836
837
  let workspaceRoot = null, resolveError = null;
837
838
  try { workspaceRoot = config.workspaceRoot; }
@@ -937,7 +938,7 @@ else {
937
938
  // which tree this deploy is. Reading it twice is how they would come to.
938
939
  deployClone,
939
940
  });
940
- record("the updater that deploys this box is the current one", v.state, v.message);
941
+ record("the updater that deploys this box is the current one", v.state, v.message, v.blocked === true);
941
942
  }
942
943
  }
943
944
  }
@@ -947,10 +948,10 @@ else {
947
948
  // inline string equality over a two-word vocabulary, which is why a `Type=oneshot` doing its job read as
948
949
  // a fault on the deploy's final gate. The decision now lives in driver/unit-state-verdict.mjs, where a
949
950
  // test can reach it — same move made for the roster arm, for the same reason.
950
- // `record`, not the ({pass, fail, skip})[state] shorthand used above: `warn` is a real outcome here.
951
+ // `record`, not a ({pass, fail, skip})[state] shorthand: `warn` is a real outcome here.
951
952
  {
952
953
  const v = unitsActiveVerdict({ units: clones, probe: unitProbe });
953
- record("units active", v.state, v.message);
954
+ record("units active", v.state, v.message, v.blocked === true);
954
955
  }
955
956
 
956
957
  // 10. — THE UNIT A BOX RUNS versus the unit the deployed commit SHIPS. The deploy syncs code, not
@@ -1013,7 +1014,7 @@ else {
1013
1014
  return { unit: fragName ?? c.unit, live, tracked, dropIns };
1014
1015
  });
1015
1016
  const v = unitFileDriftVerdict({ units: rows, probe: unitProbe });
1016
- record("units match the deployed commit", v.state, v.message);
1017
+ record("units match the deployed commit", v.state, v.message, v.blocked === true);
1017
1018
  }
1018
1019
 
1019
1020
  // 10a. — IS EVERY UNIT THIS BOX RUNS ACCOUNTED FOR AT ALL?
@@ -1082,7 +1083,7 @@ else {
1082
1083
  const box = deploymentBox(); // — the shared rule, so /portal/health cannot disagree with this
1083
1084
  const v = unitInventoryVerdict({ live: liveUnits, files: walk.files, collisions: walk.collisions,
1084
1085
  filesError: walk.error, box, probe, boxNames: DEPLOYMENT_BOXES });
1085
- record("every live unit is declared", v.state, v.message);
1086
+ record("every live unit is declared", v.state, v.message, v.blocked === true);
1086
1087
 
1087
1088
  // — AND WHETHER ANYTHING STILL STARTS THE TIMER-DRIVEN ONES. Reported separately from the line above
1088
1089
  // because it answers a different question: that one says a declared unit exists and is not adrift,
@@ -1090,7 +1091,7 @@ else {
1090
1091
  // fails the second, and reads `inactive` for both — which is why one line could not carry both.
1091
1092
  const t = declaredTimers();
1092
1093
  const tv = timerVerdict(t.rows, { probeFailed: t.probeFailed });
1093
- record("every declared timer is still armed", tv.state, tv.message);
1094
+ record("every declared timer is still armed", tv.state, tv.message, tv.blocked === true);
1094
1095
  }
1095
1096
 
1096
1097
  // ── — EVERY QUEUE THIS DEPLOYMENT WOULD DRAIN IS WATCHED BY SOMETHING ──────────────────────────
@@ -1116,7 +1117,7 @@ else {
1116
1117
  // job's life, and two readers of one unit file is how the deploy tick and a door come to different
1117
1118
  // conclusions about the same box. The unit path now has one home.
1118
1119
  const v = probeQueueWatch({ queueDirs, resolveError });
1119
- record("every queue this deployment would drain is watched", v.state, v.message);
1120
+ record("every queue this deployment would drain is watched", v.state, v.message, v.blocked === true);
1120
1121
  }
1121
1122
 
1122
1123
  // ── report ───────────────────────────────────────────────────────────────────────────────────────────
@@ -1129,8 +1130,14 @@ const couldNotLook = results.filter((r) => r.blocked);
1129
1130
  // — extracted for the reason roster-verdict and unit-state-verdict were: this file is a program, and a
1130
1131
  // decision that can only be reached by running it is a decision nobody can drive.
1131
1132
 
1133
+ // ONE DECISION, READ BY BOTH SURFACES. `--json` used to print `ok: failed.length === 0`, so a run that
1134
+ // could not look at a surface and found nothing else wrong told a machine reader `ok: true` while the
1135
+ // terminal said COULD NOT LOOK and the process exited 3. The human surface was fixed and the one a script
1136
+ // believes silently was not. `ok` is now the exit code's own answer, and `exit` carries which of the three.
1137
+ const code = exitFor({ failed: failed.length, couldNotLook: couldNotLook.length });
1138
+
1132
1139
  if (asJson) {
1133
- console.log(JSON.stringify({ ok: failed.length === 0, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
1140
+ console.log(JSON.stringify({ ok: code === 0, exit: code, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
1134
1141
  } else {
1135
1142
  const mark = { pass: " ok ", fail: " FAIL ", warn: " warn ", skip: " skip " };
1136
1143
  console.log(`\n== live surface check — ${POOL_ROOT} ==\n`);
@@ -1148,4 +1155,4 @@ if (asJson) {
1148
1155
  }
1149
1156
  }
1150
1157
 
1151
- process.exit(exitFor({ failed: failed.length, couldNotLook: couldNotLook.length }));
1158
+ process.exit(code);
@@ -148,6 +148,60 @@ export function laidPathVerdict({ laid = 0, laidPaths = [], cutRecordPresent = f
148
148
  * A tree with no HEAD cannot say what it published. That is a failure to look, and it exits rather
149
149
  * than reading as "nothing is laid here", which is the permissive answer and the one that passes.
150
150
  */
151
+ /**
152
+ * — A HALF-FINISHED MERGE CANNOT SAY WHAT ITS SUITE IS, and it does not know that it cannot.
153
+ *
154
+ * The refusal above catches one of the two ways a mid-merge tree lies to this script: a path arriving
155
+ * from the other parent is in the index and not in HEAD, so it is laid, named and refused. It cannot
156
+ * catch the other. A file both parents changed IS in HEAD, so nothing about it is laid — it is counted,
157
+ * and what is counted is a working copy holding `<<<<<<<`, `=======` and `>>>>>>>` as though they were
158
+ * source. Measured 2026-09-21 on a tree conflicted in one test file: the mint read all 980 driver
159
+ * files, counted the conflicted one, printed "unchanged — no file added, removed, grown, shrunk or
160
+ * newly skipped", and exited 0. The operator's confirmation and the defect are the same sentence.
161
+ *
162
+ * THE STATE IS THE TEST, NOT THE MARKERS. Scanning files for marker lines would find this instance and
163
+ * would also refuse a test that legitimately contains one in a string, while missing a conflict
164
+ * resolved into plausible nonsense. Git already publishes the fact: an operation is half-finished, so
165
+ * no tree it produced is a population anyone should stamp. That covers both mechanisms at once, and
166
+ * covers a rebase and a cherry-pick, where the same two lies are available for the same reason.
167
+ *
168
+ * REFUSES RATHER THAN REPAIRS. Reading the index instead of HEAD would fix the counting and would still
169
+ * mint over markers, so it is the weaker of the two. There is a correct tree a few seconds away —
170
+ * finish the operation and mint on the result — and the census is a deliberate act whose whole value is
171
+ * that somebody looked at the diff.
172
+ *
173
+ * Pure, and takes the list rather than reading it, so both branches can be driven without a test
174
+ * having to conflict a real tree.
175
+ */
176
+ export const MID_OPERATION_STATES = Object.freeze([
177
+ ["MERGE_HEAD", "merge"],
178
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
179
+ ["REVERT_HEAD", "revert"],
180
+ ["rebase-merge", "rebase"],
181
+ ["rebase-apply", "rebase"],
182
+ ]);
183
+
184
+ export function midOperationVerdict({ inProgress = [] } = {}) {
185
+ if (!inProgress.length) return { refuse: false, message: null };
186
+ const what = [...new Set(inProgress.map(([, label]) => label))].join(" and a ");
187
+ return { refuse: true,
188
+ message: `mint-suite-census: this tree is in the middle of a ${what}, so the files in it are not a\n`
189
+ + " population anybody published. A path from the other parent is missing from HEAD and would be\n"
190
+ + " dropped from the count; a file both sides changed is in HEAD and would be counted WITH its\n"
191
+ + " conflict markers. Either way this would print \"unchanged\" for a population it had just\n"
192
+ + ` misread. Finish the ${what} and mint on the tree that comes out.` };
193
+ }
194
+
195
+ /** Which half-finished operations this worktree's own git directory is carrying. */
196
+ export function operationsInProgress(root, exists = existsSync) {
197
+ let gitDir;
198
+ try {
199
+ gitDir = execFileSync("git", ["-C", root, "rev-parse", "--absolute-git-dir"], { encoding: "utf8" }).trim();
200
+ } catch { return []; }
201
+ if (!gitDir) return [];
202
+ return MID_OPERATION_STATES.filter(([name]) => exists(join(gitDir, name)));
203
+ }
204
+
151
205
  /**
152
206
  * The paths the private overlay says it laid over this clone, or null where it laid nothing.
153
207
  *
@@ -219,6 +273,18 @@ export function buildCensus(root = ROOT) {
219
273
  const readManifest = (rel) => { try { return JSON.parse(readFileSync(join(ROOT, rel), "utf8")); } catch { return null; } };
220
274
 
221
275
  function main() {
276
+ // ── BEFORE EVEN THAT — IS THIS TREE A POPULATION AT ALL? ────────────────────────────────────
277
+ //
278
+ // First, because every check below reads files from this working tree, and mid-merge those files are
279
+ // not what either parent published. A census that is internally consistent about a tree nobody
280
+ // committed passes every other refusal in this file, `--check` included.
281
+ const mid = midOperationVerdict({ inProgress: operationsInProgress(ROOT) });
282
+ if (mid.refuse) {
283
+ console.error(`\nREFUSING — ${mid.message}`);
284
+ process.exitCode = 2;
285
+ return;
286
+ }
287
+
222
288
  // ── ITEM 1 — BEFORE ANY COUNTING, DOES THIS CENSUS DESCRIBE WHAT THE RUNNER RUNS? ───────────
223
289
  //
224
290
  // FIRST, and it refuses rather than warns. Everything below counts files inside a corpus this list