clearotron 0.3.2 → 0.3.3-beta.1

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 (123) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +109 -13
  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 +29 -11
  9. package/driver/CHANGELOG.md +49 -0
  10. package/driver/citation-census.json +3 -3
  11. package/driver/clearance-variants-record.mjs +12 -1
  12. package/driver/common-law-coverage-status.mjs +113 -0
  13. package/driver/config-inventory.mjs +1 -1
  14. package/driver/contract-audit.mjs +1 -1
  15. package/driver/contract-e3-backlog.mjs +37 -37
  16. package/driver/contract-vocabulary.mjs +8 -8
  17. package/driver/coverage-form-io.mjs +3 -1
  18. package/driver/coverage-form.mjs +38 -11
  19. package/driver/coverage-ledger.mjs +37 -7
  20. package/driver/coverage-union.mjs +2 -2
  21. package/driver/crowd-context.mjs +19 -6
  22. package/driver/dev-portal.mjs +3 -3
  23. package/driver/drainer-identity.mjs +1 -1
  24. package/driver/driver.config.mjs +80 -9
  25. package/driver/engine/CONTRACT.md +3 -2
  26. package/driver/engine/anthropic-agent.mjs +34 -7
  27. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  28. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  29. package/driver/engine/mcp/coverage-server.mjs +1 -1
  30. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  31. package/driver/engine/mcp/euipo-server.mjs +2 -0
  32. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  33. package/driver/engine/mcp/gather-config.mjs +8 -2
  34. package/driver/engine/mcp/probe-server.mjs +37 -0
  35. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  36. package/driver/engine/mcp/recording-server.mjs +30 -0
  37. package/driver/engine/mcp/signa-server.mjs +2 -0
  38. package/driver/engine/mcp/supplemental.mjs +89 -12
  39. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  40. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  41. package/driver/engine/openai-agent.mjs +7 -0
  42. package/driver/engine/probe.mjs +67 -14
  43. package/driver/engine/tool-refusal.mjs +16 -0
  44. package/driver/enqueue-schema.mjs +2 -2
  45. package/driver/envelope-settle.mjs +82 -13
  46. package/driver/findings-model.mjs +4 -4
  47. package/driver/gateway.mjs +18 -2
  48. package/driver/manager-groups-verdict.mjs +1 -1
  49. package/driver/matter-frame-record.mjs +24 -7
  50. package/driver/named-band.mjs +1 -1
  51. package/driver/package.json +1 -1
  52. package/driver/partial-payload-baseline.json +12 -3
  53. package/driver/pipeline-knockout.mjs +3 -3
  54. package/driver/pipeline.mjs +154 -50
  55. package/driver/plan-run-agreement-verdict.mjs +49 -0
  56. package/driver/portal-service.mjs +8 -4
  57. package/driver/progress.mjs +14 -3
  58. package/driver/publish/index.mjs +41 -26
  59. package/driver/publish/report-data.mjs +4 -3
  60. package/driver/publish/xlsx.mjs +26 -4
  61. package/driver/queue-markers.mjs +44 -0
  62. package/driver/queue-watch-verdict.mjs +2 -2
  63. package/driver/reference-score.mjs +10 -2
  64. package/driver/register-availability.mjs +2 -2
  65. package/driver/register-plan.mjs +313 -21
  66. package/driver/roster-verdict.mjs +1 -1
  67. package/driver/runner.mjs +26 -2
  68. package/driver/settle-stamp.mjs +10 -3
  69. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  70. package/driver/skills/clearance-register/SKILL.md +44 -3
  71. package/driver/skills/clearance-register/digest.md +5 -5
  72. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  73. package/driver/skills/clearance-register/unit.md +39 -0
  74. package/driver/skills/clearance-variants/SKILL.md +1 -1
  75. package/driver/skills/matter-frame/SKILL.md +4 -2
  76. package/driver/stages.mjs +12 -5
  77. package/driver/status-snapshot.mjs +2 -2
  78. package/driver/suite-census.json +293 -29
  79. package/driver/synthesis-record.mjs +80 -2
  80. package/driver/unit-file-drift.mjs +3 -3
  81. package/driver/unit-inventory.mjs +2 -2
  82. package/driver/unit-state-verdict.mjs +1 -1
  83. package/driver/updater-identity.mjs +2 -3
  84. package/driver/variant-manifest-model.mjs +11 -1
  85. package/driver/verify.mjs +5 -5
  86. package/driver/withheld-families.mjs +104 -0
  87. package/mcp-server/CHANGELOG.md +8 -0
  88. package/mcp-server/lib/brief.mjs +16 -12
  89. package/mcp-server/lib/runs.mjs +1 -1
  90. package/mcp-server/package.json +1 -1
  91. package/mcp-server/server.mjs +3 -2
  92. package/package.json +2 -2
  93. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  94. package/portal-ui/dist/index.html +1 -1
  95. package/portal-ui/package.json +1 -1
  96. package/providers/_shared/count.mjs +2 -2
  97. package/providers/_shared/enumerate.mjs +15 -2
  98. package/providers/_shared/execute-plan.mjs +19 -1
  99. package/providers/_shared/plan-guards.mjs +40 -0
  100. package/providers/clarivate/src/capabilities.js +15 -5
  101. package/providers/clarivate/src/core.js +41 -5
  102. package/providers/corsearch/src/capabilities.js +4 -0
  103. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  104. package/providers/oauth-mcp-bridge/package.json +1 -1
  105. package/providers/signa/src/capabilities.js +22 -8
  106. package/providers/signa/src/core.js +12 -1
  107. package/scripts/demo-evidence.mjs +114 -0
  108. package/scripts/e2e.mjs +1 -1
  109. package/scripts/engine-probe.mjs +6 -5
  110. package/scripts/env-audit.mjs +1 -1
  111. package/scripts/freeze-example-run.mjs +3 -3
  112. package/scripts/live-surface-check.mjs +32 -33
  113. package/scripts/mint-suite-census.mjs +66 -0
  114. package/scripts/package-size-budget.mjs +117 -0
  115. package/scripts/register-plan-shape.mjs +259 -0
  116. package/scripts/release-note-required.mjs +38 -1
  117. package/scripts/repo-writes.mjs +1 -1
  118. package/scripts/report-sections-render-check.mjs +7 -3
  119. package/scripts/score.mjs +7 -1
  120. package/scripts/settings-render-check.mjs +36 -0
  121. package/scripts/travelling-predicates.mjs +1 -1
  122. package/shared/identifier-scan.mjs +22 -5
  123. package/shared/scroll-settle.mjs +67 -0
package/CONTRIBUTING.md CHANGED
@@ -208,6 +208,18 @@ read at all.
208
208
 
209
209
  Branch, commit, open a PR. One change per PR.
210
210
 
211
+ **Keep regenerated demo evidence in its own commit.** `demo/` holds captured runs. Regenerating them
212
+ rewrites hundreds of thousands of lines, and a source diff inside that is a diff nobody reads — one
213
+ release branch was 2,490 files and 632,089 changed lines, almost all of it evidence. So a commit that
214
+ touches `demo/` touches nothing else, and `demo/MANIFEST.json` records what produced it:
215
+
216
+ ```
217
+ node scripts/demo-evidence.mjs --apply # in the same commit that regenerates the evidence
218
+ git diff origin/main... -- . ':!demo' # read a branch WITHOUT the generated evidence
219
+ ```
220
+
221
+ CI checks the first line for you.
222
+
211
223
  The commit **body** is what gets read — not the title. State what changed, why this approach and
212
224
  what you rejected, and how to verify it. "How to verify" should be a command someone else can run.
213
225
 
package/INSTALL.md CHANGED
@@ -115,6 +115,13 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
115
115
  **Installed is not usable.** `npx clearotron install` proves the engine can complete a turn before it
116
116
  writes anything, and `clearotron doctor --probe-engine` re-proves it on a configured box. Both
117
117
  spend one cheap turn; plain `doctor` spends nothing.
118
+
119
+ **The check covers tools.** Every search stage calls tools. On some machines codex's own sandbox
120
+ refuses every one of those calls while the turn reports success, so the check asks the engine to call
121
+ one tool and passes only when that call comes back. If it reports that codex refused it, set
122
+ `CLEAROTRON_CODEX_SANDBOX_BYPASS=1` in the environment file, or use `anthropic-agent`. The setting turns
123
+ off codex's own sandbox, so its tool calls run with the permissions of the user Clearotron runs as, as
124
+ the Anthropic engine's do; set it only where the check reports the refusal.
118
125
  - **A register credential**, and **`PERPLEXITY_API_KEY`**. Both are required for a real run and both
119
126
  fail closed at preflight — before a stage has spent, never at the grid after. The one exception is a
120
127
  Knockout search, which runs keyless: it returns register filing counts and states on the report that
@@ -429,6 +436,7 @@ CLEAROTRON_AI_BILLING=subscription # `subscription` (OAuth, default) | `ap
429
436
  # ANTHROPIC_DEFAULT_OPUS_MODEL=... # optional: hold the opus tier at one model (and _SONNET_, _HAIKU_, _FABLE_)
430
437
  # CLEAROTRON_AI=openai-agent # …or the second adapter: headless `codex exec`
431
438
  # CLEAROTRON_CODEX_PATH= # only to force one copy (default: `codex` on PATH, then the copy setup installed)
439
+ # CLEAROTRON_CODEX_SANDBOX_BYPASS=1 # only where setup or doctor reports that codex refused every tool call
432
440
 
433
441
  # ── Where this install keeps its data ──────────────────────────────────
434
442
  # REQUIRED. CLEAROTRON_REPORTS_DIR has NO default: unset, a run refuses and names it.
package/bin/onboard.mjs CHANGED
@@ -98,7 +98,7 @@ import {
98
98
  // REGISTER_PROVIDER frozen at first import) is the one `preflightCandidate` below already cache-busts
99
99
  // around, and it is cache-busted whether or not this static import happened first.
100
100
  import { config, ENGINE_BINARIES, DEFAULT_ENGINE_ID, RESEARCH_PROVIDERS, SERP_PROVIDERS, resolveEngineProgram, ON_A_WINDOWS_DRIVE,
101
- enginesFolder, engineInstallArgs, engineInstallCommand } from "../driver/driver.config.mjs";
101
+ enginesFolder, engineInstallArgs, engineInstallCommand, olderThanFloor } from "../driver/driver.config.mjs";
102
102
  import { resolveAuthMode, CLOUD_SWITCH, CLOUD_SETTINGS, CLOUD_SECRETS, cloudsSwitchedOn } from "../driver/engine/auth.mjs";
103
103
  import { isInsideCheckout } from "../shared/inside-checkout.mjs"; // — one copy of the rule, and it is testable
104
104
  import { packagedBuild as sharedPackagedBuild } from "../shared/packaged-build.mjs"; // — one reader of build-info.json, reachable from the driver
@@ -1105,6 +1105,25 @@ function programProblem(bin, named) {
1105
1105
  return null;
1106
1106
  }
1107
1107
 
1108
+ // WHAT A COPY BELOW THE FLOOR DOES TO THE MODELS WAS MEASURED ON CLAUDE ALONE (2026-09-22): it refuses the
1109
+ // newest model of a tier and serves the one before. Codex's floor records no such measurement, so an old
1110
+ // Codex is named by its version, the floor and the fix, and nothing is said about which model it runs.
1111
+ export const floorRefusesNewestModel = (eng) => Boolean(eng?.package) && eng.package === ENGINE_BINARIES["anthropic-agent"]?.package;
1112
+
1113
+ /**
1114
+ * What doctor and setup say after the version of a copy below the floor: what it costs, where that was
1115
+ * measured, and the fix. ONE COPY FOR BOTH SCREENS, so the two cannot drift apart again (ruling 285).
1116
+ *
1117
+ * The fix follows where the copy came from. A copy Clearotron installed moves with `update`. Any other
1118
+ * copy wins over one setup installs, by design, so installing another does not replace it: the fix is
1119
+ * to update it, or to name a newer one in the engine's setting.
1120
+ */
1121
+ export function belowFloorTail(eng, bin) {
1122
+ const cost = floorRefusesNewestModel(eng) ? `The newest ${eng.product} models need a newer version. Searches run on an older model instead, or stop at the first step if they name the newest one exactly. ` : "";
1123
+ const fix = bin?.source === "installed" ? `Update it with \`${invoke("update")}\`.` : `Update it, or set ${eng.env} to a newer copy.`;
1124
+ return cost + fix;
1125
+ }
1126
+
1108
1127
  /**
1109
1128
  * What setup found of one engine's program, as its menu row says it; "" when nothing was looked for. A copy
1110
1129
  * that cannot run is a problem, not an absence, and installing another is not always the fix, so the row
@@ -1113,7 +1132,14 @@ function programProblem(bin, named) {
1113
1132
  */
1114
1133
  export function foundWords(eng, bin) {
1115
1134
  if (!bin) return "";
1116
- if (bin.executable && !bin.relative) return bin.version ? `found on this computer (version ${bin.version})` : "found on this computer";
1135
+ if (bin.executable && !bin.relative) {
1136
+ // A COPY THAT RUNS IS NOT NECESSARILY ONE THIS BUILD CAN USE. Below the floor the program refuses
1137
+ // the newest model of a tier and serves the one before it, so the row cannot say "found" and leave
1138
+ // it there; it says which problem, and choosing the engine shows the fix, as the other problems do.
1139
+ if (olderThanFloor(bin.version, eng.floor) === true)
1140
+ return `problem: the copy of ${eng.product} here is version ${bin.version}; Clearotron needs ${eng.floor} or newer — choose it to see the fix`;
1141
+ return bin.version ? `found on this computer (version ${bin.version})` : "found on this computer";
1142
+ }
1117
1143
  const p = programProblem(bin, bin.explicit);
1118
1144
  if (p?.kind === "incomplete") return `problem: the copy of ${eng.product} here is incomplete and won't run — choose it to see the fix`;
1119
1145
  if (p?.kind === "setting") return `problem: this computer is set to use a copy of ${eng.product} that isn't there — choose it to see the fix`;
@@ -1129,6 +1155,12 @@ export function foundWords(eng, bin) {
1129
1155
  */
1130
1156
  export function cannotRunLine(eng, bin, setting = "") {
1131
1157
  const set = namedSetting(eng, setting);
1158
+ // ANSWERED BEFORE THE REST, because a copy below the floor RUNS: it is executable, nothing rejected
1159
+ // it, and every clause below is about a copy that cannot start. Left to them it would fall through to
1160
+ // the general clause and be described as unusable, which sends the reader to look for a broken
1161
+ // install they do not have. The fix here is a version, not a repair.
1162
+ if (bin?.executable && !bin?.relative && olderThanFloor(bin.version, eng.floor) === true)
1163
+ return `${eng.product} on this computer is version ${bin.version}. Clearotron needs ${eng.floor} or newer. ${belowFloorTail(eng, bin)}`;
1132
1164
  const p = programProblem(bin, set);
1133
1165
  if (p?.kind === "incomplete") return `The copy of ${eng.product} at ${p.path} is incomplete: its installation stopped before the program was added. Setup can install a working copy.`;
1134
1166
  if (p?.kind === "setting") return `This computer is set to use ${eng.product} at ${set}, and nothing there can run. Setup can install ${eng.product} and use that instead.`;
@@ -1761,10 +1793,26 @@ export async function runCheck() {
1761
1793
  const bin = resolveEngineBin(binSetting, { engine: engineId });
1762
1794
  // WHICH COPY, AND ITS VERSION, because the machine's own install and the one Clearotron installed
1763
1795
  // are both legitimate and behave differently: the first updates itself, the second moves with
1764
- // `clearotron update`. The version comes from the copy's own package.json when npm installed it;
1765
- // doctor spawns nothing to ask (a vendor's own native installer leaves no package.json to read).
1796
+ // `clearotron update`. The version comes from the copy's own package.json when npm installed it.
1797
+ //
1798
+ // AND IS ASKED FOR WHEN THERE IS NONE TO READ, which is the ordinary case for a copy the machine
1799
+ // installed by another route — a vendor's native installer leaves no package.json. Doctor used to
1800
+ // stop there and print "version not read", which was honest and made the floor comparison below
1801
+ // unanswerable on the route most machines take: this command could not tell an operator whether
1802
+ // their own copy was new enough for the models a run asks for, which is the whole of what it was
1803
+ // asked to check.
1804
+ //
1805
+ // It is the same short call setup's engine menu already makes (menuVersion): `--version`, two
1806
+ // seconds, no session and no network. That is within "calls nobody" — what that promises is no
1807
+ // provider call and no spend, not that nothing on this machine may be asked its own version.
1808
+ const versionOf = (b) => {
1809
+ if (b.version) return b.version;
1810
+ if (!b.executable || b.relative || !b.path) return null;
1811
+ try { return menuVersion(b.path) ?? null; } catch { return null; }
1812
+ };
1813
+ const seen = versionOf(bin);
1766
1814
  const copyWords = (b) => `${b.source === "installed" ? "the copy Clearotron installed"
1767
- : b.source === "explicit" ? `set in ${engSpec.env}` : "on PATH"}${b.version ? `, version ${b.version}` : ", version not read"}`;
1815
+ : b.source === "explicit" ? `set in ${engSpec.env}` : "on PATH"}${seen ? `, version ${seen}` : ", version not read"}`;
1768
1816
  // ── NATIVE WINDOWS IS ANSWERED HERE, BEFORE ANY PATH IS RESOLVED OR REPORTED ──────────────────
1769
1817
  //
1770
1818
  // `resolveEngineBin` tests a candidate with `accessSync(X_OK)` and `isFile()`. Windows has no
@@ -1786,7 +1834,24 @@ export async function runCheck() {
1786
1834
  // and needs no engine, which is why four reports published on that same Windows box.
1787
1835
  const platformRefusal = platformEngineRefusal();
1788
1836
  if (platformRefusal) problem(platformRefusal);
1789
- else if (bin.executable && !bin.relative) ok(`${bin.path} — ${copyWords(bin)}`);
1837
+ // ── AND WHETHER THAT COPY IS NEW ENOUGH FOR THE MODELS IT WILL BE ASKED FOR ──────────────────
1838
+ //
1839
+ // The floor governed the copy setup INSTALLS and nothing else, and a copy already on the machine
1840
+ // wins over that one — so the route most machines actually take was the unchecked one. A program
1841
+ // below the floor does not fail: it refuses the newest model of a tier and serves the previous one,
1842
+ // and the search still finishes and the report still arrives. This is the cheap place to learn it.
1843
+ //
1844
+ // Three outcomes, because the comparison is three-valued: older, not older, and could not be
1845
+ // compared. The last is said rather than passed over — doctor's own contract is that a failure to
1846
+ // look is not a clean result — and it is the ordinary state for a copy the machine installed by
1847
+ // another route, where there is no package.json to read and doctor spawns nothing to ask.
1848
+ else if (bin.executable && !bin.relative && olderThanFloor(seen, engSpec.floor) === true)
1849
+ problem(`${bin.path} — ${copyWords(bin)}. Clearotron needs ${engSpec.floor} or newer. ${belowFloorTail(engSpec, bin)}`);
1850
+ else if (bin.executable && !bin.relative) {
1851
+ ok(`${bin.path} — ${copyWords(bin)}`);
1852
+ if (olderThanFloor(seen, engSpec.floor) === null)
1853
+ info(` Its version could not be checked against the ${engSpec.floor} Clearotron needs.`);
1854
+ }
1790
1855
  // A copy that is there and cannot run is a broken install, not an absence: the vendor's placeholder
1791
1856
  // left by an install that skipped its step, most often. Named with the reason and the fix.
1792
1857
  else if (!binSet && bin.rejected?.length) problem(`no usable \`${engSpec.fallback}\`: ${bin.rejected.map((x) => `${x.path} is ${x.why}`).join("; ")}`);
@@ -1923,7 +1988,16 @@ export async function runCheck() {
1923
1988
  } catch (e) { return { say: problem, text: billingRefusalWords(String(e?.message ?? e)) }; }
1924
1989
  };
1925
1990
  const here = billingOf(engineId, envForResolve);
1926
- here.say(here.text);
1991
+ // A TICK IS A CLAIM ABOUT SOMETHING THAT RESOLVED. Measured on a clean container with no reasoning
1992
+ // CLI and no settings file: the Engine block said demo mode, no program on PATH, nothing to probe —
1993
+ // and this line still printed a green tick for a billing mode. `billingOf` answers from the default
1994
+ // when nothing is set, and the default is not a fact about this machine.
1995
+ //
1996
+ // So where no engine program resolves, the same words are INFORMATION rather than a tick. Nothing
1997
+ // is wrong, which is why it is not a warning either: the rest of that run reads honestly, and this
1998
+ // was the only line claiming a verdict it had not reached.
1999
+ const engineResolved = !!(bin?.path && bin.executable && !bin.relative);
2000
+ (engineResolved || here.say === problem ? here.say : info)(here.text);
1927
2001
  // AND AS THE SERVICES READ IT, when this machine runs them and that reading differs. The line above is
1928
2002
  // this command's configuration; the services read their own file, and a start never replaces a line
1929
2003
  // in it. So a machine whose services pay through a cloud account printed "billing: subscription" here,
@@ -2126,7 +2200,7 @@ export async function runCheck() {
2126
2200
  // One run of this command reported the SAME variable as both set and unset, and concluded a
2127
2201
  // production box was a demo install:
2128
2202
  //
2129
- // ✓ CLEAROTRON_CUSTOMERS_DIR=/home/clearotron/trademark/config/profiles (.env)
2203
+ // ✓ CLEAROTRON_CUSTOMERS_DIR=$HOME/trademark/config/profiles (.env)
2130
2204
  // · profiles resolve from …/node_modules/clearotron/driver/profiles — THE BUNDLED DEMO ROSTER,
2131
2205
  // because CLEAROTRON_CUSTOMERS_DIR is unset.
2132
2206
  //
@@ -3655,7 +3729,7 @@ const confirmOrKey = async (q, def = true, { key = true, what = "key" } = {}) =>
3655
3729
  if (["n", "no"].includes(a)) return { yes: false, value: null };
3656
3730
  if (key && looksLikeAKey(raw)) {
3657
3731
  info(`that looks like the ${what} itself, so it is taken as the answer. It was not shown.`);
3658
- info(`received — ${raw.length} characters, ending …${raw.slice(-4)}`);
3732
+ info(`received — ${raw.length} characters`);
3659
3733
  return { yes: true, value: raw };
3660
3734
  }
3661
3735
  say(" Please answer y or n.");
@@ -3688,10 +3762,23 @@ const askValue = async (q, { def = "", secret = false, skippable = false, skippe
3688
3762
  const a = secret ? await askSecretRaw(prompt) : await askRaw(prompt);
3689
3763
  const v = a || def;
3690
3764
  if (present(v)) {
3691
- // A masked prompt CONFIRMS what it received: the reader cannot see what
3692
- // they typed, and a paste that half-landed looks identical to one that worked. Length and the
3693
- // last four characters are the vendor-dashboard convention for naming a key without showing it.
3694
- if (secret) info(`received — ${v.length} characters, ending …${v.slice(-4)}`);
3765
+ // A masked prompt CONFIRMS what it received: the reader cannot see what they typed, and a paste
3766
+ // that half-landed looks identical to one that worked. The LENGTH is that confirmation now, and
3767
+ // the last four characters are gone.
3768
+ //
3769
+ // THE TAIL WAS FOUR LIVE CHARACTERS OF A KEY ON STDOUT, on every run. The vendor-dashboard
3770
+ // convention it copied is a page you are already signed in to, read once; this is a line that
3771
+ // outlives the moment in a script's output, a CI job, a tee'd install or an assistant's
3772
+ // transcript — ours had to be masked by hand.
3773
+ //
3774
+ // NOT GATED ON A TERMINAL, which is what the passphrase does. That gate is right there and wrong
3775
+ // here: the leak path this closes includes an assistant driving the terminal, and a session like
3776
+ // that has a pty, so `isTTY` is true and the tail would still land in the transcript. A gate that
3777
+ // passes in the case you are defending against is not a defence.
3778
+ //
3779
+ // Length alone still catches the paste this line exists to catch: a truncated paste is a
3780
+ // different number of characters, and that is the failure the reader cannot otherwise see.
3781
+ if (secret) info(`received — ${v.length} characters`);
3695
3782
  return v;
3696
3783
  }
3697
3784
  if (skippable) { if (skipped) info(skipped); return null; }
@@ -3892,6 +3979,15 @@ try {
3892
3979
  if (!bin.executable) { problem(`${bin.path ?? resolve(p)} is ${bin.rejected?.[0]?.why ?? "not an executable file"}.`); continue; }
3893
3980
  }
3894
3981
  ok(`found ${bin.path}${bin.source === "installed" ? `, the copy Clearotron installed${bin.version ? ` (${bin.version})` : ""}` : ""}`);
3982
+ // THE MENU ROW SAID "choose it to see the fix", AND THIS IS WHERE IT IS SHOWN. A copy below the floor
3983
+ // runs, so neither branch above is taken for it, and setup used to print "found" and carry on with the
3984
+ // row's promise unkept. It still carries on — an operator may have a reason to sit below the floor — but
3985
+ // not before saying so. The version is the one the menu already asked for (cached), or the package's.
3986
+ {
3987
+ let seenVersion = bin.version ?? null;
3988
+ if (!seenVersion) { try { seenVersion = menuVersion(bin.path) ?? null; } catch { seenVersion = null; } }
3989
+ if (olderThanFloor(seenVersion, eng.floor) === true) warn(cannotRunLine(eng, { ...bin, version: seenVersion }, process.env[eng.env]));
3990
+ }
3895
3991
  // THE TERMS SENTENCE TRAVELS WITH THE PROGRAM, NOT WITH THE INSTALL OFFER. It was said only when this
3896
3992
  // step offered to install the CLI, and a copy Clearotron installed on an earlier run skips that offer, so it is
3897
3993
  // said here too. Using it, rather than installing it, is what accepts the vendor's terms.
package/bin/start.mjs CHANGED
@@ -1583,7 +1583,7 @@ if (isMain) {
1583
1583
  // Owner, in session, on his first real start: "critical, it started and I still see a demo report in
1584
1584
  // the actual product. Should not be there — should ONLY be in demo. Proper product should have no
1585
1585
  // previous reports." Measured on that box: a fictional clearance sat in
1586
- // /home/clearotron/trademark/pool — the directory that install publishes REAL CLIENT MATTERS into,
1586
+ // the installed pool directory — the one that install publishes REAL CLIENT MATTERS into,
1587
1587
  // written the moment `start` first ran.
1588
1588
  //
1589
1589
  // DORMANT, NOT DELETED, and that is the ruling's own shape rather than a softer reading of it. His
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "c338dded5d7432c6b4b196d023c0cec047eb94bc",
3
- "version": "0.3.2"
2
+ "commit": "d571d0ee20d96c7ce7f9c2c5bee0404db3357467",
3
+ "version": "0.3.3-beta.1"
4
4
  }
@@ -0,0 +1,27 @@
1
+ {
2
+ "note": "What produced the evidence under demo/. Re-recorded by scripts/demo-evidence.mjs --apply in the same commit that regenerates it.",
3
+ "generator": "scripts/freeze-example-run.mjs",
4
+ "recordedFromTree": "7b182e13a3d062a619f74b657b5c824df4deeb39",
5
+ "products": [
6
+ {
7
+ "product": "full-country-search",
8
+ "runId": "tmpdemo2014fullcountrysearch-venqori-2026-09-18-sample-capture",
9
+ "template": "clearance"
10
+ },
11
+ {
12
+ "product": "global-preliminary-search",
13
+ "runId": "tmpdemo2014globalpreliminarysearch-venqori-2026-09-18-sample-capture",
14
+ "template": "clearance"
15
+ },
16
+ {
17
+ "product": "knockout-search",
18
+ "runId": "tmpdemo2014knockoutsearch-venqori-2026-09-18-sample-capture",
19
+ "template": "knockout"
20
+ },
21
+ {
22
+ "product": "multi-country-focus-search",
23
+ "runId": "tmpdemo2014multicountryfocussearch-venqori-2026-09-18-sample-capture",
24
+ "template": "clearance"
25
+ }
26
+ ]
27
+ }
package/docs/INTAKE.md CHANGED
@@ -67,6 +67,14 @@ the default agent's workspace queue.
67
67
  - an unreadable `deadline` → treated as unset, so no envelope is applied (a date typo must not
68
68
  stop a runnable search; a bare `YYYY-MM-DD` is read as the END of that day, UTC)
69
69
  - `jurisdictions` naming the same territory twice → deduped, first spelling wins
70
+ - a goods description under the older spelling `use` → folded onto `goods` when the job is
71
+ assembled for a run
72
+
73
+ **Which field name is read.** A goods description is accepted under `goods` or under the older
74
+ spelling `use`, and `goods` is the field everything downstream reads. The older spelling is folded
75
+ onto it once, when a job is assembled for its run; the queue file keeps whatever was filed. Send
76
+ either, and send only one — where both are present, `goods` wins and `use` is ignored. A value that
77
+ is blank or only whitespace counts as no description at all, under either name.
70
78
 
71
79
  Other consumed fields (see `EXAMPLE_JOB` in `enqueue-schema.mjs` for the full annotated shape):
72
80
  `forwarderEmail`, `forwarderDomain`, `provider`, `marks[] = [{ref,name,classes}]`, `customer`
@@ -124,14 +124,26 @@ through anything containing `/`):
124
124
 
125
125
  | Alias | Full catalog id |
126
126
  |---|---|
127
- | haiku | `anthropic/claude-haiku-4-5` |
128
- | sonnet | `anthropic/claude-sonnet-5` |
129
- | opus | `anthropic/claude-opus-5` |
127
+ | haiku | `anthropic/claude-haiku` |
128
+ | sonnet | `anthropic/claude-sonnet` |
129
+ | opus | `anthropic/claude-opus` |
130
130
  | gemini | `google/gemini-3.1-pro-preview` |
131
131
  | gemini-flash | `google/gemini-3-flash-preview` |
132
132
  | deepseek-v4-pro | `together/deepseek-ai/DeepSeek-V4-Pro` |
133
133
  | azure | `azure-openai/gpt-5.4` |
134
134
 
135
+ The three tiers record a **tier, not a version**, because a tier is what a run asks for: the tier goes
136
+ to the program as the vendor's alias and the vendor answers with its newest model of that tier. This
137
+ id is what a dispatch row and a token-rollup row carry as the model *asked for*; what actually served
138
+ the turn is recorded beside it, and the report names that. A version here would be a claim about a
139
+ request nobody made, and wrong the day a newer model of the tier shipped.
140
+
141
+ One consequence, accepted when this was decided: per-model totals are keyed on what was asked for,
142
+ and the native-language lanes call the API directly, where a model id is required and a tier word is
143
+ not accepted. So one model reached by a stage and by those lanes appears under two keys —
144
+ `anthropic/claude-haiku` and `anthropic/claude-haiku-4-5`. They are different requests, and the split
145
+ says so.
146
+
135
147
  The bottom four are **legacy names that no stage declares and no engine can run** — they resolve at
136
148
  level 1 and then throw at level 2 (below). They are catalogue entries, not available tiers.
137
149
 
@@ -140,12 +152,18 @@ level 1 and then throw at level 2 (below). They are catalogue entries, not avail
140
152
  as aliases, so each tier follows the vendor's newest model; to hold one still, set
141
153
  `ANTHROPIC_DEFAULT_OPUS_MODEL` (or `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`,
142
154
  `ANTHROPIC_DEFAULT_FABLE_MODEL`).
143
- The catalog ids `anthropic/claude-opus-5` and `anthropic/claude-sonnet-5` are passed as those
144
- concrete models; `anthropic/claude-haiku-4-5` goes over as the `haiku` alias, so it follows the
145
- vendor the same way. A bare or dated Anthropic id (`claude-haiku-4-5-20251001`) still resolves to
146
- its family — that is a naming form of a model the CLI can run, not a substitution of a different
147
- one. Telemetry keeps the level-1 catalog id as the model asked for, and the attempt row records the
148
- id the program reports it served.
155
+ An id that names a family and a version — `anthropic/claude-opus-5-5`, `claude-sonnet-5`, the dated
156
+ `claude-haiku-4-5-20251001` — is passed to the CLI as that model, so naming an exact model runs it
157
+ and keeps running it when a newer model of the tier ships. A family with no version
158
+ (`anthropic/claude-opus`) is the tier, not a model: the CLI has no model by that name, so it follows
159
+ the family like the bare alias. Telemetry keeps the level-1 catalog id as the model asked for, and
160
+ the attempt row records the id the program reports it served.
161
+
162
+ **The CLI must be new enough for the model.** Each release carries its own list of accepted models,
163
+ and one that predates a model refuses it outright while the tier alias goes on serving the previous
164
+ generation — a 400 at the first turn, or a run that quietly used an older model. Setup installs
165
+ `2.1.280` or newer for this reason; a copy already on the machine is used as it is, so `doctor`'s
166
+ version is the one to read before assuming which model a tier reaches.
149
167
 
150
168
  **Anything else throws.** There is no regex fall-through to sonnet and no cross-provider
151
169
  substitution: the `gemini`/`gemini-flash`/`deepseek-v4-pro`/`azure` mappings are gone with the
@@ -181,7 +199,7 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
181
199
  | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
182
200
  | ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
183
201
  | `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
184
- | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox helper bypassed, for hosts where that helper cannot spawn. It removes a defence: with it set, the run dir is writable by the seat and only the deny-hook stands between a stage and `_driver/`. Set it because the host forces it, never for convenience. |
202
+ | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox helper bypassed, for hosts where that helper cannot spawn or refuses every tool call (setup and `doctor --probe-engine` report the refusal). It removes a defence: with it set, the run dir is writable by the seat and only the deny-hook stands between a stage and `_driver/`. Set it because the host forces it, never for convenience. |
185
203
 
186
204
  ## Environment variable reference
187
205
 
@@ -360,7 +378,7 @@ cannot be read as one list.
360
378
  | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
361
379
  | `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
362
380
  | `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
363
- | `PORTAL_READ_MODEL` | `claude-sonnet-5` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
381
+ | `PORTAL_READ_MODEL` | `sonnet` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
364
382
  | `CLEAROTRON_ORDER_PROBE_SEED` | unset | Seed for `scripts/band-shape-probe.mjs`, so an ordering probe can be replayed. A diagnostic script's knob, not a run's. |
365
383
  | `PROBE_TERM` | `DELTA` | The mark word `providers/uspto-local/bin/verify-index.mjs` searches when verifying a built local USPTO index. A diagnostic script's knob, not a run's. Change it when a row reports MEASURES NOTHING: that means the term had no exact hit, so the row's timing is not a result. |
366
384
 
@@ -1,5 +1,54 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.3-beta.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: a run can name a Fable model by its published id, not only by the tier word, which used to fail outright.
8
+ - Fixed: the portal's run list shows a run's rating, never the reviewer's sign-off word, while the run is still in progress.
9
+ - Fixed: naming an exact Claude model now runs that model, instead of quietly following the tier to a newer one.
10
+ - Fixed: a run now records the tier it asked for, not a model version nobody chose. The report still names the model that ran.
11
+ - Fixed: setup and doctor now report a Claude program too old for the models a search asks for, instead of passing it as fine.
12
+ - Fixed: setup installs a version of the Claude program new enough to run the current top-tier model, which older versions refuse.
13
+ - Fixed: the assistant's run summary names the rating again, where it had printed "[object object]" in its place.
14
+
15
+ ## 0.3.3-beta.0
16
+
17
+ ### Patch Changes
18
+
19
+ - Fixed: the coverage table names the goods words of a search narrowed by goods, so it no longer reads the same as the main search.
20
+ - Fixed: a list of names searched on Signa is now searched one name at a time, so one crowded name no longer stops the rest.
21
+ - Fixed: a register search the provider refuses as too wide is disclosed at once, with its size, instead of pausing the run to retry it.
22
+ - Fixed: the audit workbook now says when a surface refused a search and why, instead of reading the same as one never run.
23
+ - Fixed: the audit workbook now lists each wider register search the engine chose not to run, with the reason it gave.
24
+ - Fixed: a register search that fails on a provider error is tried once more in the run before it is disclosed as not searched.
25
+ - Fixed: A two-letter mark's register searches, including those narrowed by the client's goods, now run instead of failing where the register's substring search needs three characters.
26
+ - For operators: the run record now distinguishes a question the search step never answered from one it answered with "nothing applies". They previously looked identical, so a step that could not answer looked like a matter with nothing to say.
27
+ - Fixed: a long list of names searched on Clarivate is now split into searches it accepts, not one it refuses as too wide.
28
+ - Fixed: on the OpenAI engine, setup and `doctor --probe-engine` now catch a machine where codex refuses every tool call, before any search is paid for.
29
+
30
+ A search that meets it stops after one attempt and names the setting that fixes it.
31
+ - Fixed: a common-law search no longer repeats a finished stage because its write-up lacked one exact status word.
32
+
33
+ Each coverage status is now recorded as data rather than read from the wording.
34
+ - New: a search whose identical-mark question returns a count rather than a list now narrows that question until the register gives a list, and reads it.
35
+
36
+ It previously left that question unread and searched the wider families instead — compounds, foreign-script forms, neighbour lists — which is where the reading time went.
37
+
38
+ New: the words a search is narrowed to can now be chosen while the search is running, not only when it is first planned.
39
+ - Fixed: doctor no longer ticks a billing mode on a machine where no engine program resolves; it states it as information.
40
+ - Fixed: the identical mark is now read first on every search, before any wider question is asked.
41
+
42
+ New: a search can cover a further category the client's own goods reach, added with a stated reason.
43
+ - Fixed: a search request that describes the goods using the older wording now records those goods. It previously recorded none, so nothing downstream could narrow by what the matter actually covers.
44
+ - Fixed: a search can now be narrowed by what the goods are, which it could not be before. The step that chooses the search words had no way to hand them back, so every search ran without them.
45
+ - Fixed: the worker now reports itself alive throughout a search, not only between searches. Its liveness file went stale for the whole of a long search. A check reading it would call a healthy search dead, and might stop it.
46
+ - For operators: the published package now has a recorded size budget. Nothing about what ships changes; growth past a margin fails the build and names the largest files.
47
+ - Fixed: setup confirms a pasted key by its length alone, so no part of a key reaches a captured install log.
48
+ - Fixed: a territory name that no register can answer is now reported as an uncovered gap instead of being searched.
49
+ - Fixed: the clearance list is now requested at the same time as your sign-in details, instead of waiting for them.
50
+ - Fixed: the delivery email, run list and assistant now give the rating and the report's own conclusion. None of them says a matter is on hold.
51
+
3
52
  ## 0.3.2
4
53
 
5
54
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  {
2
- "citations": 489,
3
- "files": 3945,
4
- "checkable": 126
2
+ "citations": 465,
3
+ "files": 3308,
4
+ "checkable": 129
5
5
  }
@@ -141,6 +141,9 @@ export function renderClearanceVariants(model, scopeRows) {
141
141
 
142
142
  if (model.incumbent_classes?.length) out.push(`Incumbent classes: ${model.incumbent_classes.join(", ")}`, "");
143
143
  if (model.watchlist_owners?.length) out.push("### Watchlists", "", ...model.watchlist_owners.map((o) => `- ${o}`), "");
144
+ // The prose copy restates exactly what the structured sibling holds — a reader of the manifest must
145
+ // be able to see which words the register search was narrowed to without opening the JSON.
146
+ if (model.goods_words?.length) out.push("### Goods words the register search is narrowed to", "", ...model.goods_words.map((w) => `- ${w}`), "");
144
147
  // — the search floor, on the human surface because a reader auditing the run has to see what was
145
148
  // obliged as well as what was done. Rendered ONLY when designated: an empty section would read as a
146
149
  // floor of nothing rather than as no floor, and those are the two states this mechanism must keep apart.
@@ -168,7 +171,7 @@ export function renderClearanceVariants(model, scopeRows) {
168
171
  */
169
172
  /** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
170
173
  const DECLARED = Object.freeze({
171
- "": ["mark", "dominant_element", "elements", "variants", "incumbent_classes", "search_floor", "watchlist_owners", "scope_ledger"],
174
+ "": ["mark", "dominant_element", "elements", "variants", "incumbent_classes", "search_floor", "watchlist_owners", "goods_words", "scope_ledger"],
172
175
  elements: ["value", "kind"],
173
176
  variants: ["value", "category", "rationale", "romanization"],
174
177
  scope_ledger: ["layer", "item", "status", "reason", "reopen_trigger"],
@@ -204,6 +207,10 @@ export function mergeClearanceVariantsCall(stored, received) {
204
207
  incumbent_classes: keepIfAbsent(received?.incumbent_classes, base.incumbent_classes),
205
208
  search_floor: keepIfAbsent(received?.search_floor, base.search_floor),
206
209
  watchlist_owners: keepIfAbsent(received?.watchlist_owners, base.watchlist_owners),
210
+ // Keep-if-absent for the same reason as its neighbours: a repair rung that asks the seat to
211
+ // correct one part, and a seat that sends only that part, would otherwise delete the goods words
212
+ // — and the loss reads as "this matter has none" rather than as a partial call.
213
+ goods_words: keepIfAbsent(received?.goods_words, base.goods_words),
207
214
  };
208
215
  }
209
216
 
@@ -216,6 +223,10 @@ export function acceptClearanceVariants(params) {
216
223
  variants: params?.variants,
217
224
  incumbent_classes: params?.incumbent_classes,
218
225
  watchlist_owners: params?.watchlist_owners,
226
+ // — the words the register search is narrowed to. Carried here because a typed call cannot hand
227
+ // back a key nothing in this chain declares: the tool's schema offers the slot, and this is what
228
+ // moves the answer from the call into the manifest the compiler reads.
229
+ goods_words: params?.goods_words,
219
230
  // — the search-floor axes. Validated by the model parser (closed against REGISTER_AXES), not
220
231
  // here, so there is ONE definition of what a floor may name.
221
232
  search_floor: params?.search_floor,
@@ -0,0 +1,113 @@
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
+ // common-law-coverage-status.mjs — a common-law seat's coverage statuses, recorded as VALUES.
4
+ //
5
+ // ── WHY THIS EXISTS ────────────────────────────────────────────────────────────────────────────────
6
+ //
7
+ // The common-law gate refused a finished findings file unless one of three exact words appeared
8
+ // somewhere in its prose (`no_coverage_status_row`). The research did not change the outcome; the model's
9
+ // phrasing did. On the codex engine, 13 of 22 failed common-law attempts on the test box (16-22 September)
10
+ // were that word, each one a full stage re-run. The register path had already solved the same problem:
11
+ // its seat rules coverage through `record_coverage` and the driver writes the form.
12
+ //
13
+ // This is that shape for the common-law lane. The seat calls `record_coverage_status` (dispositions
14
+ // server) with one {unit, status} entry per ledger row; this module validates each entry, folds it into
15
+ // the half's accumulator and writes the file. The gate reads the file before it looks for the word.
16
+ //
17
+ // ── ONE TOOL, THE DRIVER WRITES THE FILE ─────────────────────────────────────────────────────────────
18
+ //
19
+ // The seat is never told a file name or a JSON shape. A model typing a structured document is how 74
20
+ // correct meaning rulings were once voided by a quote character (dispositions-server.mjs states the
21
+ // case), and the contract audit's E3 check refuses a literal skeleton in a manual for that reason. Here
22
+ // the serialization is ours.
23
+ //
24
+ // ── ONE DERIVATION OF THE PATH ──────────────────────────────────────────────────────────────────────
25
+ //
26
+ // The writer names the file from the grid spec it was handed and the reader from the findings file it is
27
+ // judging; both go through coverageStatusPath(). The findings name the writer derives is the one
28
+ // stages.mjs's paths() dictates, and a test holds the two equal.
29
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
30
+ import { join, dirname, basename } from "node:path";
31
+ import { driverDir } from "../shared/driver-dir.mjs";
32
+
33
+ /**
34
+ * The three statuses a common-law coverage ledger row may carry, as data and as prose alike. Not the
35
+ * register's COVERAGE_STATUSES: that list adds `withheld-by-judgment`, which is the register's reading turn
36
+ * and never a common-law status.
37
+ */
38
+ export const COMMON_LAW_COVERAGE_STATUSES = Object.freeze(["confirmed-clean", "coverage-limited", "deferred"]);
39
+
40
+ /** A findings file's statuses: `_driver/<findings name>.coverage-status.json`, driver-written, never the seat's. */
41
+ export const coverageStatusPath = (findingsPath) =>
42
+ driverDir(dirname(String(findingsPath)), basename(String(findingsPath)).replace(/\.md$/, ".coverage-status.json"));
43
+
44
+ /** The findings file a grid spec's seat writes: the half's, or the canonical file on an unsplit run. */
45
+ export const findingsPathForSpec = (spec) =>
46
+ join(dirname(String(spec.output_path)), spec.half ? `common-law-findings.half-${spec.half}.md` : "common-law-findings.md");
47
+
48
+ const readStatuses = (file) => {
49
+ let doc;
50
+ try { doc = JSON.parse(readFileSync(file, "utf8")); } catch { return null; }
51
+ const rows = doc?.coverage_status;
52
+ return rows && typeof rows === "object" && !Array.isArray(rows) ? rows : null;
53
+ };
54
+
55
+ /**
56
+ * Is this findings file's coverage status on record as data? At least one entry, every value one of
57
+ * COMMON_LAW_COVERAGE_STATUSES. Anything else — absent, unparseable, empty, a value outside the three — is
58
+ * NOT data, and the caller falls back to the prose word, so the file can never turn a passing document into
59
+ * a failing one.
60
+ *
61
+ * THE MERGED FILE IS THE HALVES. The driver writes `common-law-findings.md` by concatenating the halves'
62
+ * findings, so its statuses are theirs: a half that stated its status only as data leaves no word in the
63
+ * merge. For that file the halves' records answer too.
64
+ */
65
+ export function coverageStatusAsData(findingsPath) {
66
+ if (!findingsPath) return false;
67
+ const p = String(findingsPath);
68
+ const valid = (file) => {
69
+ const rows = readStatuses(file);
70
+ const values = rows ? Object.values(rows) : [];
71
+ return values.length > 0 && values.every((v) => COMMON_LAW_COVERAGE_STATUSES.includes(String(v).trim().toLowerCase()));
72
+ };
73
+ if (valid(coverageStatusPath(p))) return true;
74
+ if (basename(p) !== "common-law-findings.md") return false;
75
+ const dir = driverDir(dirname(p));
76
+ let names = [];
77
+ try { names = readdirSync(dir); } catch { return false; }
78
+ return names.filter((n) => /^common-law-findings\.half-[a-z0-9]+\.coverage-status\.json$/.test(n))
79
+ .some((n) => valid(join(dir, n)));
80
+ }
81
+
82
+ /**
83
+ * Record one typed call. `spec` is the DRIVER-WRITTEN grid spec, the same file the grid tool was given, so
84
+ * the seat names no path of its own. Entries that validate are kept even when others in the same call are
85
+ * refused, and statuses accumulate: a later entry for the same unit replaces the earlier one.
86
+ */
87
+ export function recordCoverageStatus(spec, received, { now = () => new Date().toISOString() } = {}) {
88
+ const rows = received?.rows;
89
+ if (!Array.isArray(rows) || rows.length === 0)
90
+ return { ok: false, text: "ERROR: rows is required — one entry per coverage ledger row, each {unit, status}." };
91
+ const accepted = [], refused = [];
92
+ rows.forEach((r, i) => {
93
+ const unit = String(r?.unit ?? "").trim();
94
+ const status = String(r?.status ?? "").trim().toLowerCase();
95
+ if (!unit) refused.push(`entry ${i + 1}: unit is empty — name the coverage unit exactly as your ledger row does`);
96
+ else if (!COMMON_LAW_COVERAGE_STATUSES.includes(status))
97
+ refused.push(`entry ${i + 1} (${unit.slice(0, 80)}): status "${String(r?.status ?? "").slice(0, 40)}" is not one of ${COMMON_LAW_COVERAGE_STATUSES.join(" / ")} — qualifiers belong in the ledger row, not here`);
98
+ else accepted.push([unit, status]);
99
+ });
100
+ const file = coverageStatusPath(findingsPathForSpec(spec));
101
+ if (accepted.length) {
102
+ const statuses = { ...(readStatuses(file) ?? {}), ...Object.fromEntries(accepted) };
103
+ try {
104
+ mkdirSync(dirname(file), { recursive: true });
105
+ writeFileSync(file, JSON.stringify({ coverage_status: statuses, half: spec.half ?? null, recorded_at: now() }, null, 2) + "\n");
106
+ } catch (e) {
107
+ return { ok: false, text: `ERROR: the driver could not record this call (${String(e?.message ?? e).slice(0, 200)}). This is a driver fault, not a fault in your statuses — do not re-type them.` };
108
+ }
109
+ }
110
+ const lines = [`Recorded ${accepted.length} of ${rows.length}.`];
111
+ if (refused.length) lines.push("Refused — send these again, corrected:", ...refused.map((x) => `- ${x}`));
112
+ return { ok: refused.length === 0, text: lines.join("\n") };
113
+ }
@@ -71,7 +71,7 @@ const shown = (names) => names.map((n) => n);
71
71
  * and only one of them describes who gets the invoice.
72
72
  */
73
73
  export function engineInventory(env = process.env) {
74
- // THE SAME EXPRESSION AS THE RUN DOOR, character for character (driver.config.mjs:1880,
74
+ // THE SAME EXPRESSION AS THE RUN DOOR, character for character (driver.config.mjs,
75
75
  // preflightEngineBinary). The obvious rewrite — `String(env.CLEAROTRON_AI ?? "").trim() || DEFAULT` —
76
76
  // reads better and disagrees on a whitespace-only value: it falls back to the default while the door
77
77
  // resolves `""` and refuses with "that is not an engine". The page would then name a known engine that