clearotron 0.4.0-beta.0 → 0.4.0-beta.2

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 (293) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +4 -31
  3. package/README.md +5 -5
  4. package/bin/brandowner.mjs +1 -1
  5. package/bin/onboard.mjs +18 -74
  6. package/bin/start.mjs +58 -14
  7. package/bin/update.mjs +10 -2
  8. package/build-info.json +2 -2
  9. package/demo/full-country-search/run/_driver/search-policy.json +1 -1
  10. package/demo/full-country-search/run/status.json +1 -1
  11. package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
  12. package/demo/global-preliminary-search/run/status.json +1 -1
  13. package/demo/knockout-search/run/_driver/search-policy.json +1 -1
  14. package/demo/knockout-search/run/status.json +1 -1
  15. package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
  16. package/demo/multi-country-focus-search/run/status.json +1 -1
  17. package/docs/README.md +1 -0
  18. package/docs/SECURITY-OWASP.md +298 -0
  19. package/docs/SECURITY.md +18 -13
  20. package/docs/architecture/02-architecture.md +5 -5
  21. package/docs/architecture/03-run-lifecycle.md +28 -29
  22. package/docs/architecture/04-configuration-reference.md +5 -10
  23. package/docs/architecture/05-config-governance.md +5 -7
  24. package/docs/architecture/06-operations-runbook.md +0 -12
  25. package/docs/architecture/07-quality-and-audit.md +12 -13
  26. package/docs/architecture/09-security-and-data.md +9 -4
  27. package/driver/CHANGELOG.md +70 -0
  28. package/driver/ask-ledger.mjs +4 -2
  29. package/driver/authority-trees.mjs +90 -8
  30. package/driver/band-shape.mjs +13 -13
  31. package/driver/blind-frame-model.mjs +1 -1
  32. package/driver/case-law-ledger.mjs +19 -3
  33. package/driver/claim-liveness.mjs +30 -1
  34. package/driver/clearance-variants-record.mjs +10 -3
  35. package/driver/common-law-receipts.mjs +64 -14
  36. package/driver/commonlaw-carry.mjs +2 -2
  37. package/driver/compare.mjs +2 -2
  38. package/driver/connotation-search.mjs +33 -104
  39. package/driver/contract-arm2-baseline.json +6 -5
  40. package/driver/contract-e3-backlog.mjs +62 -62
  41. package/driver/contract-vocabulary.mjs +38 -31
  42. package/driver/coverage-form-io.mjs +13 -1
  43. package/driver/coverage-form.mjs +42 -4
  44. package/driver/coverage-ledger.mjs +3 -5
  45. package/driver/cross-check-wait.mjs +43 -0
  46. package/driver/declination-call.mjs +34 -10
  47. package/driver/declination-tool.mjs +27 -15
  48. package/driver/deferral-row.mjs +139 -0
  49. package/driver/degraded-parts.mjs +377 -0
  50. package/driver/deliver-trigger.sh +1 -1
  51. package/driver/demo-container.mjs +4 -1
  52. package/driver/dev-portal.mjs +5 -5
  53. package/driver/dispatch-record.mjs +2 -2
  54. package/driver/driver.config.mjs +193 -96
  55. package/driver/e2e/README.md +1 -1
  56. package/driver/effective-scope.mjs +58 -2
  57. package/driver/engine/anthropic-agent.mjs +103 -27
  58. package/driver/engine/auth.mjs +2 -2
  59. package/driver/engine/child-record.mjs +11 -0
  60. package/driver/engine/cli-version.mjs +4 -3
  61. package/driver/engine/common.mjs +17 -2
  62. package/driver/engine/deny-authority-write.mjs +3 -1
  63. package/driver/engine/engine-env.mjs +174 -0
  64. package/driver/engine/engine-spawn.mjs +179 -0
  65. package/driver/engine/mcp/band-server.mjs +1 -1
  66. package/driver/engine/mcp/clarivate-server.mjs +2 -1
  67. package/driver/engine/mcp/codex-config.mjs +112 -3
  68. package/driver/engine/mcp/corsearch-server.mjs +1 -0
  69. package/driver/engine/mcp/declination-server.mjs +9 -8
  70. package/driver/engine/mcp/dispositions-server.mjs +2 -1
  71. package/driver/engine/mcp/euipo-server.mjs +1 -0
  72. package/driver/engine/mcp/fetch-server.mjs +14 -11
  73. package/driver/engine/mcp/free-tier-server.mjs +1 -1
  74. package/driver/engine/mcp/gather-config.mjs +23 -11
  75. package/driver/engine/mcp/perplexity-server.mjs +97 -17
  76. package/driver/engine/mcp/probe-server.mjs +43 -21
  77. package/driver/engine/mcp/public-fetch.mjs +195 -0
  78. package/driver/engine/mcp/recording-server.mjs +79 -14
  79. package/driver/engine/mcp/signa-server.mjs +1 -0
  80. package/driver/engine/mcp/supplemental.mjs +14 -14
  81. package/driver/engine/mcp/unit-note-server.mjs +50 -1
  82. package/driver/engine/mcp/uspto-local-server.mjs +1 -0
  83. package/driver/engine/openai-agent.mjs +109 -28
  84. package/driver/engine/probe.mjs +152 -19
  85. package/driver/enqueue-schema.mjs +5 -5
  86. package/driver/feedback-issues.mjs +1 -1
  87. package/driver/feedback-store.mjs +1 -1
  88. package/driver/findings-model.mjs +67 -16
  89. package/driver/flag-snapshot.mjs +1 -1
  90. package/driver/floor-duty.mjs +23 -7
  91. package/driver/form-neighbourhood.mjs +52 -17
  92. package/driver/frame-diff-model.mjs +3 -3
  93. package/driver/framework-method.mjs +304 -0
  94. package/driver/gateway.mjs +49 -21
  95. package/driver/hand-off-exits.mjs +129 -0
  96. package/driver/jx-lanes.mjs +1 -1
  97. package/driver/jx.mjs +8 -5
  98. package/driver/knockout-assess-record.mjs +7 -1
  99. package/driver/knockout-frame-record.mjs +96 -34
  100. package/driver/knockout-review-record.mjs +7 -2
  101. package/driver/log.mjs +2 -2
  102. package/driver/matter-frame-record.mjs +31 -2
  103. package/driver/methodology-witness.mjs +6 -3
  104. package/driver/named-band.mjs +1 -1
  105. package/driver/owner-use-check.mjs +45 -10
  106. package/driver/package.json +1 -1
  107. package/driver/partial-payload-baseline.json +4 -0
  108. package/driver/phase0.mjs +2 -2
  109. package/driver/pipeline-knockout.mjs +256 -129
  110. package/driver/pipeline.mjs +417 -614
  111. package/driver/placement-carry.mjs +22 -3
  112. package/driver/placement-form-io.mjs +18 -4
  113. package/driver/placement-form.mjs +53 -4
  114. package/driver/placement-union.mjs +32 -5
  115. package/driver/portal-report.mjs +3 -3
  116. package/driver/portal-service.mjs +6 -5
  117. package/driver/portal-static.mjs +11 -18
  118. package/driver/predelivery-lint.mjs +10 -10
  119. package/driver/profile-page.html +5 -5
  120. package/driver/profiles.mjs +5 -5
  121. package/driver/progress.mjs +6 -4
  122. package/driver/provider-usage.mjs +25 -2
  123. package/driver/publish/index.mjs +97 -25
  124. package/driver/publish/knockout.mjs +136 -14
  125. package/driver/publish/profiles-page.mjs +2 -0
  126. package/driver/publish/publish-inputs.mjs +10 -4
  127. package/driver/publish/render-knockout.mjs +48 -21
  128. package/driver/publish/render.mjs +13 -8
  129. package/driver/publish/report-data.mjs +2 -0
  130. package/driver/publish/search-depth.mjs +77 -4
  131. package/driver/publish/templates/report.css +1 -1
  132. package/driver/publish/xlsx.mjs +34 -15
  133. package/driver/reasoning-tripwires.mjs +2 -160
  134. package/driver/recall-receipt.mjs +27 -0
  135. package/driver/recall-reconciliation.mjs +1 -1
  136. package/driver/record-carry.mjs +14 -14
  137. package/driver/recording-agreement.mjs +2 -2
  138. package/driver/reference-score.mjs +169 -47
  139. package/driver/register-count.mjs +61 -4
  140. package/driver/register-digest-record.mjs +14 -1
  141. package/driver/register-plan.mjs +161 -80
  142. package/driver/register-records.mjs +37 -4
  143. package/driver/registration-scripts.mjs +22 -0
  144. package/driver/registry-fidelity.mjs +4 -4
  145. package/driver/repair-composers.mjs +23 -7
  146. package/driver/repair-contract.mjs +1 -1
  147. package/driver/repairs.mjs +5 -2
  148. package/driver/reviewer-open-points.mjs +33 -0
  149. package/driver/rule-shape.mjs +8 -8
  150. package/driver/run-economics.mjs +3 -3
  151. package/driver/run-integrity.mjs +2 -2
  152. package/driver/runner.mjs +17 -5
  153. package/driver/scope-facts.mjs +89 -12
  154. package/driver/score-redaction.mjs +247 -0
  155. package/driver/screen-gate.mjs +1 -1
  156. package/driver/search-policy.mjs +17 -0
  157. package/driver/skills/README.md +2 -2
  158. package/driver/skills/blind-frame/SKILL.md +2 -2
  159. package/driver/skills/clearance-common-law/SKILL.md +29 -36
  160. package/driver/skills/clearance-common-law/perplexity-prompts.md +8 -30
  161. package/driver/skills/clearance-register/SKILL.md +15 -16
  162. package/driver/skills/clearance-register/digest.md +15 -31
  163. package/driver/skills/clearance-register/register-recipes.md +14 -56
  164. package/driver/skills/clearance-register/unit.md +37 -48
  165. package/driver/skills/clearance-search/SKILL.md +6 -8
  166. package/driver/skills/clearance-search/delivery-contract.md +0 -7
  167. package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
  168. package/driver/skills/clearance-search/phase2-execution.md +5 -6
  169. package/driver/skills/clearance-search/report-prose.md +5 -5
  170. package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
  171. package/driver/skills/clearance-search/synthesis-rules.md +9 -9
  172. package/driver/skills/clearance-search/template-formatting.md +2 -2
  173. package/driver/skills/clearance-variants/SKILL.md +8 -8
  174. package/driver/skills/clearance-variants/transliteration-scripts.md +2 -2
  175. package/driver/skills/frame-diff/SKILL.md +2 -2
  176. package/driver/skills/knockout-assess/SKILL.md +40 -10
  177. package/driver/skills/knockout-frame/SKILL.md +42 -10
  178. package/driver/skills/matter-frame/SKILL.md +12 -5
  179. package/driver/skills/matter-frame/watchlist-reference.md +1 -1
  180. package/driver/skills/narrative-refutation/SKILL.md +5 -3
  181. package/driver/skills/placement-inquiry/SKILL.md +5 -1
  182. package/driver/stage-context.mjs +27 -6
  183. package/driver/stages-knockout.mjs +92 -79
  184. package/driver/stages.mjs +111 -68
  185. package/driver/stray-artifacts.mjs +9 -5
  186. package/driver/suite-census.json +696 -150
  187. package/driver/synthesis-record.mjs +26 -4
  188. package/driver/systemd/README.md +5 -6
  189. package/driver/systemd/clearotron-client-mcp.service +24 -0
  190. package/driver/systemd/clearotron-mcp-face.service +24 -0
  191. package/driver/systemd/clearotron-portal.service +24 -0
  192. package/driver/systemd/clearotron-worker.service +29 -0
  193. package/driver/tokens.mjs +10 -9
  194. package/driver/turnaround-bands.mjs +1 -1
  195. package/driver/unit-inventory.mjs +24 -43
  196. package/driver/variant-manifest-model.mjs +22 -3
  197. package/driver/verify-knockout.mjs +163 -4
  198. package/driver/verify.mjs +42 -16
  199. package/driver/web-grid.mjs +150 -0
  200. package/driver/whatif-memo-run.mjs +1 -1
  201. package/driver/withheld-families.mjs +113 -4
  202. package/driver/worker-heartbeat.mjs +13 -2
  203. package/mcp-server/CHANGELOG.md +8 -0
  204. package/mcp-server/README.md +1 -1
  205. package/mcp-server/lib/knockout.mjs +1 -1
  206. package/mcp-server/lib/ops.mjs +10 -10
  207. package/mcp-server/lib/scrub.mjs +1 -1
  208. package/mcp-server/lib/trace.mjs +3 -2
  209. package/mcp-server/package.json +1 -1
  210. package/mcp-server/server.mjs +3 -3
  211. package/package.json +1 -1
  212. package/portal-ui/dist/assets/{index-DVtz44vH.js → index-BPAUjcI0.js} +3 -2
  213. package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
  214. package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
  215. package/portal-ui/dist/index.html +2 -15
  216. package/portal-ui/package.json +1 -1
  217. package/providers/_shared/README.md +1 -1
  218. package/providers/_shared/answer-memory.mjs +199 -0
  219. package/providers/_shared/enumerate.mjs +155 -50
  220. package/providers/_shared/execute-plan.mjs +62 -16
  221. package/providers/_shared/ledger-path.mjs +1 -1
  222. package/providers/_shared/ledger.mjs +48 -6
  223. package/providers/_shared/plan-guards.mjs +7 -0
  224. package/providers/_shared/script-form.mjs +24 -5
  225. package/providers/_shared/term-shape.mjs +5 -5
  226. package/providers/clarivate/src/capabilities.js +11 -0
  227. package/providers/clarivate/src/core.js +193 -18
  228. package/providers/corsearch/README.md +1 -2
  229. package/providers/corsearch/src/core.js +2 -2
  230. package/providers/jx-subclass/lookup.mjs +1 -1
  231. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  232. package/providers/oauth-mcp-bridge/README.md +8 -7
  233. package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
  234. package/providers/oauth-mcp-bridge/package.json +1 -1
  235. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  236. package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
  237. package/providers/perplexity/src/core.js +246 -21
  238. package/providers/signa/src/capabilities.js +24 -0
  239. package/providers/signa/src/core.js +93 -25
  240. package/scripts/README.md +2 -1
  241. package/scripts/added-reference-check.mjs +2 -1
  242. package/scripts/backup-recall-stores.mjs +5 -5
  243. package/scripts/demo-evidence.mjs +2 -1
  244. package/scripts/deprecate-below.mjs +114 -2
  245. package/scripts/drive-env-check.mjs +2 -1
  246. package/scripts/e2e-first-time.mjs +469 -0
  247. package/scripts/e2e-scenario-ops.mjs +441 -0
  248. package/scripts/e2e.mjs +104 -16
  249. package/scripts/env-audit.mjs +8 -0
  250. package/scripts/freeze-example-run.mjs +7 -7
  251. package/scripts/generated-files-are-current.mjs +2 -1
  252. package/scripts/hand-off-exits-probe.mjs +75 -0
  253. package/scripts/import-cycle-check.mjs +2 -1
  254. package/scripts/merge-shape-check.mjs +2 -1
  255. package/scripts/package-size-budget.mjs +2 -1
  256. package/scripts/record-carry-probe.mjs +1 -1
  257. package/scripts/release-entry-catch-up.mjs +77 -8
  258. package/scripts/release-install-check.mjs +4 -1
  259. package/scripts/release-note-required.mjs +139 -9
  260. package/scripts/release-rehearsal-version.mjs +60 -0
  261. package/scripts/release-sbom.mjs +104 -0
  262. package/scripts/release-visible-check.mjs +7 -5
  263. package/scripts/render-check.mjs +5 -2
  264. package/scripts/report-offline-render-check.mjs +169 -0
  265. package/scripts/score.mjs +79 -8
  266. package/scripts/strip-titles-and-attributions.mjs +2 -1
  267. package/scripts/strip-tracker-citations.mjs +2 -1
  268. package/scripts/test-full.mjs +2 -1
  269. package/scripts/test-run.mjs +10 -3
  270. package/scripts/third-party-notices.mjs +3 -1
  271. package/scripts/travelling-predicates.mjs +1 -1
  272. package/scripts/writing-standard-check.mjs +2 -1
  273. package/shared/brand-fonts.mjs +59 -0
  274. package/shared/brand.mjs +6 -6
  275. package/shared/browser-temp-root.mjs +3 -2
  276. package/shared/doctrine-overlay.mjs +1 -1
  277. package/shared/driver-dir.mjs +47 -11
  278. package/shared/fonts/OFL-fira-code.txt +93 -0
  279. package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
  280. package/shared/fonts/README.md +37 -0
  281. package/shared/fonts/fira-code.woff2 +0 -0
  282. package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
  283. package/shared/names-in-force.mjs +2 -0
  284. package/shared/npm-cli.mjs +23 -0
  285. package/shared/os-advice.mjs +14 -2
  286. package/shared/path-seps.mjs +42 -0
  287. package/shared/process-table.mjs +71 -3
  288. package/shared/reference-guard-classes.mjs +30 -6
  289. package/shared/root-doc-commands.mjs +6 -2
  290. package/shared/running-start.mjs +24 -7
  291. package/shared/scope.mjs +2 -2
  292. package/shared/wsl.mjs +1 -1
  293. package/driver/known-conflicts.mjs +0 -327
@@ -164,7 +164,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
164
164
  `CLEAROTRON_OPENAI_MODEL_CHEAP` (all gpt-5.6-sol),
165
165
  `CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
166
166
  `CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
167
- `CLEAROTRON_KNOCKOUT_PRESET` (pro-search), `CLEAROTRON_MAX_BUDGET_USD` (unset).
167
+ `CLEAROTRON_MAX_BUDGET_USD` (unset).
168
168
 
169
169
  The Claude program's own cloud settings — `CLAUDE_CODE_USE_FOUNDRY`, `CLAUDE_CODE_USE_VERTEX` and
170
170
  `CLAUDE_CODE_USE_BEDROCK`, each cloud's own settings, the gateway pair and the model pins — are the
@@ -206,7 +206,7 @@ move once the same window has been read across more runs.
206
206
  `CLEAROTRON_PLAN_DISPATCH`, `CLEAROTRON_SATPROBE_CODESIDE`,
207
207
  `CLEAROTRON_BAND_TRUTH_GATE` (**never disable in prod — restores the fabrication**),
208
208
  `CLEAROTRON_FRAME_REOPEN` (+`CLEAROTRON_FRAME_REOPEN_MAX`=1, `CLEAROTRON_REOPEN_MAX_FETCH`=150),
209
- `CLEAROTRON_REGISTER_GAP_CLAMP`, `CLEAROTRON_RECALL_PROBES`, `CLEAROTRON_RECALL_TRIPWIRE`, `CLEAROTRON_WARM_RETRY`,
209
+ `CLEAROTRON_REGISTER_GAP_CLAMP`, `CLEAROTRON_WARM_RETRY`,
210
210
  `CLEAROTRON_MODEL_WIRE_CHECK` (fails a turn whose
211
211
  provider reports a different model FAMILY than the driver asked for; disarming it silences the refusal
212
212
  and never the record: `modelActual`/`modelMismatch` keep landing on every dispatch row), `CLEAROTRON_FORM_REPAIR`
@@ -277,8 +277,8 @@ edited — that box runs pre-rebuild code and genuinely still sets it.
277
277
 
278
278
  ### 5.7 Delivery & comms — T3/T4
279
279
 
280
- `CLEAROTRON_DEFAULT_AGENT` (clawdi), `CLEAROTRON_SEND_TOOL_PREFIX`
281
- (clawdi_send_), `CLEAROTRON_AGENT_WHATSAPP`, `CLEAROTRON_OUTBOX_BACKOFF_BASE_SEC` (60) / `CLEAROTRON_OUTBOX_BACKOFF_CAP_SEC` (900) /
280
+ `CLEAROTRON_DEFAULT_AGENT` (localagent),
281
+ `CLEAROTRON_AGENT_WHATSAPP`, `CLEAROTRON_OUTBOX_BACKOFF_BASE_SEC` (60) / `CLEAROTRON_OUTBOX_BACKOFF_CAP_SEC` (900) /
282
282
  `CLEAROTRON_OUTBOX_BACKOFF_MAX_RETRIES` (5) / `CLEAROTRON_OUTBOX_GIVEUP_COOLDOWN_SEC` (3600 — note this one
283
283
  drops BACKOFF from the prefix, which the elided spelling this row used to carry actively misled about),
284
284
  `CLEAROTRON_OUTBOX_NOPROGRESS_MAX`, `TRADEMARK_MSGID_DOMAIN` (enqueue.local).
@@ -456,9 +456,7 @@ Dev cockpit: `PORTAL_PORT`, `PORTAL_HOST`.
456
456
 
457
457
  Diagnostics and fixtures: `CLEAROTRON_DUMP_JSON`,
458
458
  `CLEAROTRON_REPLAY_SNAPSHOT`, `CLEAROTRON_REPLAY_ROOTS`, `CLEAROTRON_JX_FIXTURES`,
459
- `CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES`, `SIGNA_FIXTURES_DIR` (and its accepted alias
460
- `CLAWDI_SIGNA_FIXTURES_DIR` — `providers/signa/src/core.js` reads the plain name first and falls back
461
- to the prefixed one, so an operator who sets only the alias must be able to find it here),
459
+ `CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES`, `SIGNA_FIXTURES_DIR`,
462
460
  `PORT` (smoke test), and the `MOCK_*` fixture vars. Those are part of
463
461
  the harness-only set `scripts/env-audit.mjs` counts separately and this register deliberately omits.
464
462
 
@@ -110,18 +110,6 @@ and the environment file holding the secrets.
110
110
  - **A scheduled gateway restart** (chat-channel session and plugin-loader hygiene) should *defer*
111
111
  off live work rather than skip: wait while a run is live, up to a bound, then restart anyway.
112
112
  `XDG_RUNTIME_DIR` must be set for `systemctl --user` to work from cron.
113
- - **Pin the agent id before upgrading an install made before 0.2.2.** The default agent id changed
114
- from `clawdi` to `localagent`, and that id is a path segment: runs live under
115
- `<workspaceRoot>/workspace-<agent>/studio/clearance-search/`. An install that never set one starts
116
- reading an empty workspace, and empty reads as "no runs" rather than as an error. Set **both**
117
- variables in the environment file — the gather servers read their own:
118
-
119
- ```
120
- CLEAROTRON_DEFAULT_AGENT=clawdi
121
- CLEAROTRON_GATHER_AGENT=clawdi
122
- ```
123
-
124
- An install that already sets them is unaffected, and a fresh install needs neither.
125
113
  - **Integrator-platform upgrades** go through that platform's own guarded upgrade path — dry-run,
126
114
  exact-version confirm, stop → update → start. Never `npm i -g` / `pnpm add -g` directly.
127
115
 
@@ -92,9 +92,15 @@ under-grading ships a wrong CLEAR. The reviewer's context includes code-computed
92
92
  and a mandatory plan-execution check. CONDITIONAL/BLOCKING triggers corrective re-synthesis, a
93
93
  freshness gate proving the named corrections actually reached `findings.json`, and a warm verdict
94
94
  re-check that can only fall back to the *entry* verdict. A terminal BLOCKING first gets the
95
- degenerate-artifact refusal (a "BLOCKING" with zero cited defects is re-asked fresh, once), then
96
- fails the run — a report the reviewer won't stand behind never ships looking finished
97
- ("delivered with open questions" is retired). The reviewer's verdict is treated as documented-noisy;
95
+ degenerate-artifact refusal (a "BLOCKING" with zero cited defects is re-asked fresh, once), and
96
+ then the run **delivers**. Ruling 2026-08-26, verbatim: "Deliver always, with open points printed.
97
+ The refusal on a blocking review goes." The corrective ladder is still the fix arm and still runs
98
+ first; what changed is only what happens once it is exhausted. "Never ships" and "never ships
99
+ *looking finished*" were treated as one requirement and are two — only the second was ever
100
+ load-bearing, and it is met by the open-points section being code-built from the review. Ruling
101
+ 2026-09-24 settled where that section goes: reviewer notes never reach the client page, and a
102
+ report the reviewer still refuses ships with its rating and nothing added, the points recorded
103
+ beside the review for the reviewing lawyer. The reviewer's verdict is treated as documented-noisy;
98
104
  that is precisely why every *honesty* signal below derives from ledgers and receipts, not review
99
105
  prose.
100
106
 
@@ -138,16 +144,10 @@ What makes the coverage statement trustworthy:
138
144
  - **The clamps** (`applyCoverageFloor`) only ever raise CLEAR → CONDITIONAL, never lower, never
139
145
  manufacture BLOCKING: live condition actions, the lawyer's own `coverage_judgment.sufficient:
140
146
  false` (sufficiency is judgment's call — code only carries it), frame residuals, register gaps
141
- (deferred rows ∪ taint axes ∪ material recall regressions — independent of the model's
142
- self-report), and in-window deadlines delivered without their dates. The verdict sidecar
147
+ (deferred rows ∪ taint axes — independent of the model's self-report). The verdict sidecar
143
148
  (`_driver/verdict.json`) is then the single label authority for every surface; one shared
144
149
  predicate keeps the validator and the auto-correct judging "unconditional proceed" with the same
145
150
  eyes.
146
- - **Recall memory**: confirmed conflicts persist per mark in a workspace store
147
- (`_known-conflicts/`, rows are only added — never deleted or rewritten, save the machine-provenance
148
- `terminal` a delivered run may upgrade — human edits always win, non-Latin marks handled); the next run
149
- folds them in as deterministic plan probes, and a prior-confirmed conflict that neither resurfaces
150
- nor gets a recorded justification is a *material regression* that clamps.
151
151
 
152
152
  Register budgets deserve one honest sentence: the per-worker call budgets in the skill prose
153
153
  (enumerates, phoneme ≤ 5, image ≤ 10) are **observed, prompt-level budgets, not code-enforced
@@ -194,9 +194,8 @@ pass over inherited artifacts is visible as such.
194
194
  `reasoning-tripwires.mjs` is the mechanical net under principles held holistically in the skills:
195
195
  recall floor (a live identical registration dropped in the noise rows but not carried), seed
196
196
  neutrality (upstream artifacts must state facts, never grades), probative grading, matrix ceilings
197
- (legacy scale only), status honesty (clean headline over material gaps), deadline urgency and
198
- deadline carry, unresolved disagreements, orphan register findings, uncross-checked demotions,
199
- recall regression. All of it lands in `_driver/reasoning-integrity.json` and surfaces on the audit
197
+ (legacy scale only), status honesty (clean headline over material gaps), deadline urgency,
198
+ unresolved disagreements, orphan register findings, uncross-checked demotions. All of it lands in `_driver/reasoning-integrity.json` and surfaces on the audit
200
199
  workbook and quality pages — never as a banner on the rendered report.
201
200
  Two of these pure functions *also* feed the verdict clamps (register gap, deadline gap) — but via
202
201
  the clamp path on ledger facts, never as a tripwire "failure". `rule-shape.mjs` (rating decided by
@@ -3,10 +3,15 @@
3
3
  > Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
4
4
  > integrator contract are in [`driver/README.md`](../../driver/README.md).
5
5
 
6
- The security posture in one sentence: **nothing listens on the network except loopback services
7
- behind an authenticated tunnel; agents that touch the outside world cannot execute anything; the
8
- process that executes everything is not an agent; and secrets reach exactly the processes that need
9
- them, by inheritance, never by file-copy into configs.**
6
+ The security posture in brief: **nothing listens on the network except loopback services behind an
7
+ authenticated tunnel. A stage on Claude is offered no tool that runs a command. A stage on Codex runs its
8
+ commands inside Codex's own sandbox, or with the permissions of the account Clearotron runs as where a
9
+ machine sets the bypass. The process that runs everything else is ordinary code, not an agent. Each
10
+ stage's AI program starts with a named list of settings, and the key that signs access keys is handed
11
+ only to the portal and the two doors. On Claude, a stage's file tools can read nothing outside its run folder, its
12
+ instruction folders and any folder the machine's own Claude settings add. On Codex with its sandbox on,
13
+ a stage's commands can read nothing outside its run folder, its instruction folders, the temporary
14
+ folders, and the system and program files a command needs to run.**
10
15
 
11
16
  ## Trust boundaries
12
17
 
@@ -1,5 +1,75 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.4.0-beta.2
4
+
5
+ ### Minor Changes
6
+
7
+ - New: A knockout search now also asks whether each name is already in use in the client's field, such as a game character or a drink.
8
+ - New: Searching by judgment.
9
+
10
+ This release changes how a clearance decides what to search. Until now the engine followed fixed rules: a set number of spellings, a fixed list of stores, a stop after the first identical mark. Fixed rules are fast and cheap, but they miss things. On a recent matter, a lawyer's review found marks the engine had counted but never read, and others it had raised that did not matter.
11
+
12
+ So the approach changes. Wherever the engine holds a pile of results, it now looks at what is there and decides what a lawyer would raise. It carries that forward and writes down what it set aside, and why. Nothing is dropped silently. Every wider search, every spelling set aside and every store left out appears in the audit workbook with its reason.
13
+
14
+ What you will notice:
15
+ - Searches widen where it could change the advice, and narrow where it cannot.
16
+ - Near spellings no buyer would confuse with the mark are set aside, with the reason recorded.
17
+ - Searches in other scripts run only in markets that file marks in that script.
18
+ - A large company's register is read for the marks that share the searched name. The rest is counted, not fetched.
19
+ - Marketplace searches cover the stores that sell the client's goods, and say which stores were left out and why.
20
+ - The report no longer opens with the internal reviewer's notes. They reach the reviewing lawyer separately.
21
+ - A large marketplace search no longer fails because its results were too big to return in one piece.
22
+ - Where a company's framework rates through named inputs, the report shows them beside each band.
23
+
24
+ A word on regressions. While testing this line we found two. A crowded search failed before delivering its report, and a knockout's ratings moved one step away from the lawyer's. Both were caught by running the same matters against a lawyer's answers, and both are fixed here. Every build is now tested that way before it ships. We are tuning for three things at once: a report that is right the first time, delivered fast, at a sensible cost. They pull against each other, and each release is our best current balance.
25
+
26
+ ### Patch Changes
27
+
28
+ - For operators: on the Anthropic engine, a search can no longer read the install's settings or other private files.
29
+ - For operators: stages on the Anthropic engine can no longer run commands on the server; every tool that runs a command is removed from them.
30
+ - Fixed: In a crowded field, the search now reads the spellings a lawyer would raise, rather than the ones that fit under a record limit.
31
+ - Fixed: When a knockout's search for filings fails, the report and the audit workbook no longer show the raw error.
32
+ - Fixed: On Windows, a failed finding card no longer shows as covered in the audit workbook.
33
+ - Fixed: When a search could not be made, the report says so instead of giving a wrong reason.
34
+ - Fixed: A register search limited to the client's goods now searches every other office when one office cannot filter by goods.
35
+ - Fixed: a right the report keeps is no longer dropped because other registrations of the same right were ruled out.
36
+ - Fixed: A knockout search no longer drops a store listing or fan wiki page its web search found; the rating now reads every result.
37
+ - Fixed: A knockout's audit workbook also lists a part that failed as a whole, such as its filings listing or plain-language review. A filing shown without its link now says why.
38
+ - Fixed: on a Mac, `clearotron start` no longer offers to run in the background, which only works on Linux.
39
+ - Fixed: on a Mac, a search step could write into Clearotron's own protected folders by naming them in a different letter case. Those writes are now refused.
40
+ - Fixed: A register search that times out across many countries is asked again in smaller parts; one that still times out is reported as incomplete.
41
+ - Fixed: on the OpenAI engine, Codex's own sandbox no longer refuses the register searches a clearance runs.
42
+ - For operators: on the OpenAI engine with Codex's sandbox on, the commands a search runs can no longer read the install's settings or other private files.
43
+ - Fixed: the audit workbook's What was searched tab no longer shows a web or marketplace search that found similar listings as clean.
44
+ - Fixed: Worldwide searches are no longer narrowed to an account's default territories, and results too crowded to read in full are no longer reported clean.
45
+ - Fixed: A report no longer offers a native-language investigation to a client who ordered one, and says when that investigation was not completed.
46
+ - Fixed: When a knockout's lookup of a filing owner fails, its read now says the lookup did not answer, instead of saying it found nothing.
47
+ - New: Clearotron runs natively on Windows, from PowerShell: no WSL2, no Git and no administrator rights.
48
+ - Fixed: a full country search's report no longer says court decisions were searched when that search failed or its source was down.
49
+ - New: Global preliminary, multi-country and full country searches now search the web in more depth and keep more results from each search.
50
+ - Fixed: opening a report no longer contacts any font service; the report carries its typeface, now Plus Jakarta Sans, inside itself.
51
+ - Fixed: searches no longer reuse earlier results.
52
+ - Fixed: skipped searches are no longer shown as unfinished in the report.
53
+ - For operators: each stage's AI program now receives only the settings it needs, and never the key that signs access keys.
54
+ - Fixed: The audit workbook now lists, on Coverage & gaps, any part of a report that could not be completed on the run. A knockout's Audit Trail also lists an owner lookup that got no answer.
55
+ - Fixed: on the OpenAI engine, a Full country search now reads case law from CourtListener and Legal Data Hunter, not EUR-Lex alone.
56
+ - Fixed: setup, `clearotron doctor` and the start of every search now check that the engine can write a file where a search writes its results.
57
+ - New: The meaning and reputation search now asks the questions the matter's framing names, in the languages whose markets matter. Where the framing names none, the report says no meaning search ran.
58
+ - For operators: the old default agent name is gone from the product. An install upgrading from a version before 0.2.2 must set its own agent name to keep seeing its earlier runs.
59
+ - Fixed: The operator tool no longer accepts instructions, which never reached the run; a call sending them is refused.
60
+ - Fixed: on the OpenAI engine, the tool that reads web pages now refuses addresses on the server's own network, such as cloud metadata addresses.
61
+ - For operators: the background worker no longer receives the key that signs access keys. Run `clearotron start --background` once to update an installed worker.
62
+
63
+ ## 0.4.0-beta.1
64
+
65
+ ### Patch Changes
66
+
67
+ - Fixed: Clarivate clearances no longer count look-alike spellings that mix Latin with Greek or Cyrillic letters as searches that could not be completed.
68
+ - For operators: Clarivate clearances no longer fetch a record, or repeat a count, search or owner lookup, that the same run already made.
69
+ - For operators: Knockouts on Signa and Clarivate take their name and close-variation counts from the listing, so each is asked of the register once.
70
+ - Fixed: Knockouts on the Signa register now say how many register hits the listed filings were drawn from.
71
+ - Fixed: Clearances on the Signa register no longer search look-alike spellings that mix Latin with Greek or Cyrillic letters; those searches found only unrelated marks.
72
+
3
73
  ## 0.4.0-beta.0
4
74
 
5
75
  ### Minor Changes
@@ -179,7 +179,7 @@ function recallEnding(name, qid, join, ts) {
179
179
  if (discharges) return { ending, handoff: null };
180
180
  return {
181
181
  ending: null,
182
- handoff: `the probe ran and ${why} — a recall probe discharges on what came back, not on having run (#1349). `
182
+ handoff: `the probe ran and ${why} — a recall probe discharges on what came back, not on having run. `
183
183
  + `Ships OPEN in the audit's ask ledger for the reviewing lawyer.`,
184
184
  };
185
185
  }
@@ -428,6 +428,8 @@ export function deriveAsks({
428
428
  }
429
429
 
430
430
  // 9 ── cross-checks: the common-law→register recovery nets (xcheck + recall) — directives carry
431
+ // (the recall receipt exists only on runs from before the recall store's removal, 2026-09-24;
432
+ // on a newer run `recall` is null and this family lists the cross-checks alone)
431
433
  // qids (computed executed); over-cap rows are real unanswered questions (OPEN → closure/lawyer).
432
434
  for (const [name, doc] of [["xcheck", xcheck], ["recall", recall]]) {
433
435
  for (const d of doc?.directives ?? []) {
@@ -601,7 +603,7 @@ const normMark = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
601
603
  * as immaterial only when the mark it went looking for is in the client's findings. Otherwise the mark
602
604
  * is owed a finding and the ask stays open.
603
605
  *
604
- * The defect this answers: a recall probe found OSLER DELPHI, the closure stage pointed at
606
+ * The defect this answers: a recall probe found HALVER KORPHI, the closure stage pointed at
605
607
  * `register-findings.md` — the run's own working sheet, where the mark genuinely IS reasoned — and the
606
608
  * ask closed. The sheet is a citable file and the citation was true. It is simply not the document the
607
609
  * client reads, and nothing checked that.
@@ -44,17 +44,97 @@
44
44
  // set-GID scar on the pool root). The existing detect-and-journal sweep STAYS as corroboration; its
45
45
  // never-kill-a-run property is sound and survives.
46
46
 
47
- import { resolve, sep } from "node:path";
47
+ import { realpathSync, statSync } from "node:fs";
48
+ import { resolve, win32, posix } from "node:path";
48
49
  import { driverDir } from "../shared/driver-dir.mjs"; //
49
50
 
50
- /** Is `child` inside `root` — by path segment, never by string prefix? PURE. */
51
- export function isInside(root, child) {
52
- const r = resolve(String(root ?? ""));
53
- const c = resolve(String(child ?? ""));
51
+ /**
52
+ * Is `child` inside `root` — by path segment, never by string prefix? PURE.
53
+ *
54
+ * ON WINDOWS BY WINDOWS RULES. There a path may use either separator and a name matches in any case, so
55
+ * `c:/users/x/SKILLS/a.md` is a file inside `C:\Users\x\skills`. Compared as Linux compares, the
56
+ * write lands in the protected tree and the boundary lets it through. `platform` is a parameter so that
57
+ * branch runs on a Linux CI. `foldCase` says whether to ignore letter case: always on Windows, and
58
+ * elsewhere as foldsCaseAt finds the protected folder's disk, which denyReason asks for each tree.
59
+ */
60
+ export function isInside(root, child, { platform = process.platform, foldCase = platform === "win32" } = {}) {
61
+ const P = platform === "win32" ? win32 : posix;
62
+ const fold = foldCase ? (s) => s.toLowerCase() : (s) => s;
63
+ const r = fold(P.resolve(String(root ?? "")));
64
+ const c = fold(P.resolve(String(child ?? "")));
54
65
  if (!r || r === "." || !c) return false;
55
66
  // `/a/skills-backup` is NOT inside `/a/skills`. A `startsWith` without the separator says it is, and
56
67
  // that is the classic form of this check being wrong in the direction that blocks real work.
57
- return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
68
+ return c === r || c.startsWith(r.endsWith(P.sep) ? r : r + P.sep);
69
+ }
70
+
71
+ /**
72
+ * The one spelling of a Windows path: no device prefix, and the long names Windows itself reports.
73
+ *
74
+ * WINDOWS REACHES ONE FILE BY SEVERAL NAMES, and the boundary compared the one it was handed. Measured on
75
+ * a Windows runner, 2026-09-23: a write spelled `\\?\C:\…\skills\x.md`, and a write spelled with the
76
+ * long name of a folder the policy had recorded by its 8.3 short name (`RUNNER~1` against
77
+ * `runneradmin`), both reached the protected folder and were allowed. The device prefix (`\\?\`,
78
+ * `\\.\`) is taken off, and the longest part of the path that exists is asked of Windows itself, which
79
+ * answers with the long names and follows a junction, so a junction pointing into a protected folder is
80
+ * inside it too. The part that does not exist yet is joined back as written. `realpath` is injected so
81
+ * this runs on Linux.
82
+ */
83
+ export function canonicalWindowsPath(p, { realpath = realpathSync.native } = {}) {
84
+ const bare = String(p ?? "").replace(/^[\\/]{2}[?.][\\/]UNC[\\/]/i, "\\\\").replace(/^[\\/]{2}[?.][\\/]/, "");
85
+ const abs = win32.resolve(bare);
86
+ const rest = [];
87
+ for (let cur = abs; ;) {
88
+ try { return win32.join(realpath(cur), ...rest); }
89
+ catch {
90
+ const parent = win32.dirname(cur);
91
+ if (parent === cur) return abs;
92
+ rest.unshift(win32.basename(cur));
93
+ cur = parent;
94
+ }
95
+ }
96
+ }
97
+
98
+ const swapCase = (s) => [...s].map((ch) => (ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase())).join("");
99
+ const foldMemo = new Map();
100
+
101
+ /**
102
+ * Does the disk holding `root` ignore letter case? Asked of THAT folder, never of the platform.
103
+ *
104
+ * A Mac's default volume ignores case, so `P/SKILLS/x.md` is a file inside `P/skills`, and a boundary
105
+ * comparing letter for letter let the write through: measured on the macOS runner, 2026-09-23. But a Mac
106
+ * volume can be formatted to mind case, and a Linux folder can be set to ignore it, so the platform does
107
+ * not answer. The folder is stat'ed under its own name and under that name with every letter's case
108
+ * swapped: the same file under both means this disk ignores case. It writes nothing, so it adds nothing to
109
+ * a protected tree.
110
+ *
111
+ * WINDOWS ALWAYS FOLDS, as it did before this was asked, because NTFS ignores case unless a folder is set
112
+ * otherwise. Where the question cannot be put (the folder does not exist yet, or its name has no letters)
113
+ * the answer is the platform's default disk, Windows and macOS folding, so a Mac errs toward refusing. The
114
+ * answer is kept per folder for the life of the process. `stat` is injected so every branch runs on Linux.
115
+ */
116
+ export function foldsCaseAt(root, { platform = process.platform, stat = statSync } = {}) {
117
+ if (platform === "win32") return true;
118
+ const abs = posix.resolve(String(root ?? ""));
119
+ const key = `${platform}|${abs}`;
120
+ if (foldMemo.has(key) && stat === statSync) return foldMemo.get(key);
121
+ const byDefault = platform === "darwin";
122
+ const name = posix.basename(abs);
123
+ let answer = byDefault;
124
+ const other = swapCase(name);
125
+ if (name && other !== name) {
126
+ try {
127
+ const own = stat(abs);
128
+ try {
129
+ const swapped = stat(posix.join(posix.dirname(abs), other));
130
+ answer = own.dev === swapped.dev && own.ino === swapped.ino;
131
+ } catch (e) {
132
+ answer = e?.code === "ENOENT" || e?.code === "ENOTDIR" ? false : byDefault;
133
+ }
134
+ } catch { answer = byDefault; }
135
+ }
136
+ if (stat === statSync) foldMemo.set(key, answer);
137
+ return answer;
58
138
  }
59
139
 
60
140
  /**
@@ -87,11 +167,13 @@ export function authorityTrees({ skillsRoots = [], profilesDir = null, runDir =
87
167
  * why it is refused, that a retry gets the same answer, and where stage output lives. No second person, no
88
168
  * imperative. The seat decides what to do with a fact, which is the whole difference.
89
169
  */
90
- export function denyReason(targetPath, trees) {
170
+ export function denyReason(targetPath, trees, { platform = process.platform, foldsCase = (root) => foldsCaseAt(root, { platform }),
171
+ canonical = platform === "win32" ? (p) => canonicalWindowsPath(p) : (p) => p } = {}) {
91
172
  const t = String(targetPath ?? "").trim();
92
173
  if (!t) return null;
174
+ const target = canonical(t);
93
175
  for (const tree of trees ?? []) {
94
- if (!isInside(tree.path, t)) continue;
176
+ if (!isInside(canonical(tree.path), target, { platform, foldCase: foldsCase(tree.path) })) continue;
95
177
  return `REFUSED by the driver's write boundary: ${t} is inside ${tree.why} (${tree.path}). `
96
178
  + `That tree is authored at deploy time and never by a running stage, so the refusal is configuration `
97
179
  + `rather than a permission prompt, and a retry of the same path returns this same answer. Stage `
@@ -65,7 +65,7 @@ const tokensOf = (s) => String(s ?? "").split(/[^\p{L}\p{N}]+/u).map((t) => norm
65
65
 
66
66
  /**
67
67
  * The ALTERNATIVE NAMES a label carries — `VENZAL / VENZALMONO / VENZALKOMB` is one relabelled entry,
68
- * `CHROMA & Device` is one mark plus a device note.
68
+ * `LUMIVANE & Device` is one mark plus a device note.
69
69
  *
70
70
  * The separator class is reference-score's `labelAliases`, character for character, and the two are
71
71
  * meant to stay that way: the scorer and the engine must agree about what a relabelling is, or a record
@@ -130,7 +130,7 @@ export function classifyRecord(markText, targets) {
130
130
  if (!norm) {
131
131
  // ── — A MARK WITH NO LATIN SKELETON CAN STILL BE IDENTICAL, AND IT WAS (2026-08-14) ──────────
132
132
  //
133
- // R1 retrieved TEN exact-match 色度 registrations — four REGISTERED, three in class 9, all in the
133
+ // R1 retrieved TEN exact-match 澜珀 registrations — four REGISTERED, three in class 9, all in the
134
134
  // matter's own classes — and the delivered report did not mention the token once. This line is
135
135
  // where they went. `normalizeElement` keeps only Latin characters, so a Han mark normalises to the
136
136
  // empty string, fell straight to `unclassifiable`, and `unclassifiable` is not one of the two tiers
@@ -139,7 +139,7 @@ export function classifyRecord(markText, targets) {
139
139
  // weighed. Retrieved, banded, positioned, and never an obligation.
140
140
  //
141
141
  // The classifier could not COMPARE them; that is true, and it is not the same as could not MATCH
142
- // them. 色度 equals 色度 by inspection. The precedent is in this repo already: reference-score's
142
+ // them. 澜珀 equals 澜珀 by inspection. The precedent is in this repo already: reference-score's
143
143
  // matchesReference makes RAW comparison its rule 1 for exactly this case, because normalizeElement
144
144
  // keeps only [a-z0-9] and a CJK mark folds to the empty string that every Latin rule would match
145
145
  // against everything. The scorer has had that rule since R1's Chinese registration was specced. The
@@ -147,13 +147,13 @@ export function classifyRecord(markText, targets) {
147
147
  //
148
148
  // EQUALITY ONLY, DELIBERATELY. The floors are mechanical obligations a lawyer must answer row by
149
149
  // row, so every member has to be defensible without judgment: raw-equal after NFKC (which folds
150
- // full-width and compatibility forms of the same characters) is that. Containment is not — 色度計
151
- // contains 色度 and is a different mark — and whether it matters is judgment's call, reached
150
+ // full-width and compatibility forms of the same characters) is that. Containment is not — 澜珀計
151
+ // contains 澜珀 and is a different mark — and whether it matters is judgment's call, reached
152
152
  // through the same-family tiers and the crowd descriptors, not by widening a floor.
153
153
  //
154
154
  // THIS PARAGRAPH USED TO SAY "narrower than the scorer's rule", and it was right when it was
155
155
  // written: matchesReference's script rule was unowned containment in both directions until,
156
- // which scored gold 色度 as retrieved off a different proprietor's 色度花间. The scorer now takes
156
+ // which scored gold 澜珀 as retrieved off a different proprietor's longer mark that contains it. The scorer now takes
157
157
  // equality too, with one owner-gated containment escape the floors deliberately do not have. The
158
158
  // two agree; neither is the looser one any more.
159
159
  const raw = text.normalize("NFKC").trim();
@@ -175,7 +175,7 @@ export function classifyRecord(markText, targets) {
175
175
 
176
176
  // ── member 2 — A RELABELLED RECORD IS THE SAME MARK, AND IT WAS TIERING BELOW THE FLOOR ────────
177
177
  //
178
- // Measured against the scorer on `origin/main`: `VENZAL / VENZALMONO / VENZALKOMB` and `CHROMA &
178
+ // Measured against the scorer on `origin/main`: `VENZAL / VENZALMONO / VENZALKOMB` and `LUMIVANE &
179
179
  // Device` are `alias` to reference-score and `same-family / token-identical` here. Not dropped —
180
180
  // tiered BELOW the line. The floors take `identical` and `near-identical` only, so a register record
181
181
  // that IS the mark under a relabelling never became a row a lawyer had to answer. Same consequence as
@@ -184,7 +184,7 @@ export function classifyRecord(markText, targets) {
184
184
  // ALIAS SEPARATORS ONLY, and the distinction is reference-score's, kept deliberately identical
185
185
  // because the two must agree about what a relabelling IS: `/ , · & |` separate ALTERNATIVE NAMES for
186
186
  // one record; whitespace and hyphens separate WORDS WITHIN one name. Conflating them makes every
187
- // multi-word mark match its own first word — `TIKI TWIST` would become `TIKI`, which is a different
187
+ // multi-word mark match its own first word — `WAVO TWIST` would become `WAVO`, which is a different
188
188
  // proprietor's different mark and the collision the scorer's own doc block warns about. Whitespace is
189
189
  // NOT in the class here for exactly that reason.
190
190
  //
@@ -213,7 +213,7 @@ export function classifyRecord(markText, targets) {
213
213
  // The candidate set of the retrieved→judgment reconciliation: every LIVE, in-scope, SCREEN-SURFACED
214
214
  // record whose mark carries the manifest's dominant element — as a standalone token
215
215
  // (`token-identical`), a token one edit away (`token-edit-1`), or CONCATENATED inside a longer
216
- // squashed word (`concatenation` — the TIKITONK class: same-family in substance, "other" to the
216
+ // squashed word (`concatenation` — the WAVOTONK class: same-family in substance, "other" to the
217
217
  // token classifier, and the proven silent-death seam). Screen-surfaced only (screen_verdict
218
218
  // surface:*): the obligation this feeds is "everything the screen surfaced ends somewhere a reader
219
219
  // can see"; drop:* records were already ended by the screen with a policed drop row.
@@ -227,7 +227,7 @@ export function classifyRecord(markText, targets) {
227
227
  // itself never spells. They rank last so they never reached the top slice, but they inflate the
228
228
  // residual a declared crowd count must cover — a count written one short blocks the run (the
229
229
  // reviewer reproduced 378 declared vs 379 residual → blocked). A concatenation is a SQUASHED WORD
230
- // (TIKITONK), which is a property of one token.
230
+ // (WAVOTONK), which is a property of one token.
231
231
  export const MIN_CONCAT_LEN = 4;
232
232
 
233
233
  /**
@@ -323,8 +323,8 @@ export function dominantElementComposites(band, { dominantElement, inScopeClasse
323
323
  // identity, and the arithmetic becomes a cross-reference instead.
324
324
  // (2) owner identity is compared on a CORE + LEGAL-FORM pair, not on the suffix-stripped core
325
325
  // alone. Stripping entity words is what makes "Fairmile Snack Foods Corp." = "Fairmile Snack Foods
326
- // Corporation" work, but on the same band it also collapsed "Tiki Group Limited", "Tiki AG"
327
- // and "Tiki Corporation" to the bare core "tiki" and merged three distinct registrants on the
326
+ // Corporation" work, but on the same band it also collapsed "Wavo Group Limited", "Wavo AG"
327
+ // and "Wavo Corporation" to the bare core "wavo" and merged three distinct registrants on the
328
328
  // mark-owner edge alone. So: same core AND compatible legal forms (one side's form set a
329
329
  // SUBSET of the other's — a register that simply omits the form still matches, two different
330
330
  // stated forms never do).
@@ -359,7 +359,7 @@ export function ownerIdentical(a, b) {
359
359
  }
360
360
 
361
361
  /** The union-find bucket key for the mark+owner edge — core + the SORTED form set, so
362
- * "Tiki AG" and "Tiki Corporation" land in different buckets while "X Corp."/"X Corporation" share
362
+ * "Wavo AG" and "Wavo Corporation" land in different buckets while "X Corp."/"X Corporation" share
363
363
  * one. The subset relaxation (a register omitting the form) is applied by the second pass below. */
364
364
  const ownerKey = (s) => { const { core, forms } = ownerIdentity(s); return core ? `${core}|${[...forms].sort().join("+")}` : ""; };
365
365
  const normOwner = (s) => ownerIdentity(s).core;
@@ -13,7 +13,7 @@
13
13
  // PURE (no node imports) → tests offline. Mirrors coverage-ledger.mjs / findings-model.mjs.
14
14
 
15
15
  // add/drop = the both-directions element neighbours (VELTRIN→VELTRI is "drop"); phonetic/homophone =
16
- // sound-alikes; neighbour = a one-keystroke real-word / famous-mark neighbour (CHROME on NOVAPULSE);
16
+ // sound-alikes; neighbour = a one-keystroke real-word / famous-mark neighbour (NOVAPULSO on NOVAPULSE);
17
17
  // composite = the element inside a larger mark (VELTRI-composite cluster).
18
18
  export const VARIANT_DIRECTIONS = ["add", "drop", "phonetic", "homophone", "neighbour", "composite"];
19
19
  // the spine is ranked by goods-overlap with the actual product, not by class number + registration status.
@@ -36,7 +36,18 @@
36
36
  // "no node imports → tests offline" invariant, exactly as connotation-search.mjs already does.
37
37
  import { abbrev } from "./repair-contract.mjs";
38
38
 
39
- export const CASE_LAW_LEDGER_SCHEMA_VERSION = 1;
39
+ // Version 2 is the one whose instruction tells the model to write `null` for a query it could not send,
40
+ // and never 0. At version 1 a 0 could mean either, so a reader may not count one as a search that ran.
41
+ export const CASE_LAW_LEDGER_SCHEMA_VERSION = 2;
42
+
43
+ // WHAT A RECORD THAT NAMES NO VERSION IS. The oldest, never the newest. Defaulting an unversioned record
44
+ // to whatever this build happens to be at hands it every promise the current instruction makes, which is
45
+ // exactly backwards: the records that name no version are the ones written before the field was asked
46
+ // for, and their zeros are the ambiguous kind. Read low, and a record only gains a promise by claiming it.
47
+ export const CASE_LAW_LEDGER_OLDEST_SCHEMA = 1;
48
+
49
+ /** Version 2 and up separate "sent and returned nothing" (0) from "never sent" (null). */
50
+ export const ledgerSeparatesUnsentQueries = (ledger) => Number(ledger?.schema_version) >= 2;
40
51
 
41
52
  /**
42
53
  * How far a proceeding got. `read` is the only state that discharges a depth dive, and the distinction
@@ -91,11 +102,16 @@ export function parseCaseLawLedger(raw) {
91
102
  return {
92
103
  error: null,
93
104
  ledger: {
94
- schema_version: Number(parsed.schema_version) || CASE_LAW_LEDGER_SCHEMA_VERSION,
105
+ schema_version: Number(parsed.schema_version) || CASE_LAW_LEDGER_OLDEST_SCHEMA,
95
106
  queries: queries.filter((q) => q && typeof q === "object").map((q) => ({
96
107
  query: str(q.query),
97
108
  jurisdiction: str(q.jurisdiction),
98
- results: Number.isFinite(Number(q.results)) ? Number(q.results) : null,
109
+ // NULL STAYS NULL, and this is load-bearing from version 2 on. `Number(null)` is 0 and
110
+ // `Number("")` is 0, both finite, so the obvious coercion turns the record's "I could not send
111
+ // this query" into "I sent it and got nothing" — the very two the version exists to separate,
112
+ // collapsed by the parser before any reader sees them. Only a value that genuinely is a number
113
+ // becomes one; everything else is null, which is the record declining to give a count.
114
+ results: q.results == null || q.results === "" || Number.isNaN(Number(q.results)) ? null : Number(q.results),
99
115
  })),
100
116
  citations: citations.filter((c) => c && typeof c === "object").map((c) => ({
101
117
  proceeding: str(c.proceeding),
@@ -35,6 +35,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
35
35
  import { spawnSync } from "node:child_process"; // — the birth stamp where there is no /proc
36
36
  import { join } from "node:path";
37
37
  import { config } from "./driver.config.mjs";
38
+ import { windowsProcessScript, parseWindowsRows, defaultRunPowerShell } from "../shared/process-table.mjs"; // the birth stamp on Windows
38
39
 
39
40
  // ── B2 — fail-safe claim liveness ────────────────────────────────────────────────────────────────────
40
41
  // The .pid sidecar records "<pid>:<starttime>" (starttime = field 22 of /proc/<pid>/stat, the kernel's
@@ -42,7 +43,7 @@ import { config } from "./driver.config.mjs";
42
43
  // impersonate a dead claimer and rot its `.processing` forever. Legacy bare-pid sidecars still parse
43
44
  // (starttime null → pid-aliveness is all we have, today's behavior). All exported for unit tests.
44
45
  export function procStarttime(pid, readStat = undefined,
45
- { platform = process.platform, readPsStart = defaultReadPsStart } = {}) {
46
+ { platform = process.platform, readPsStart = defaultReadPsStart, runPowerShell = defaultRunPowerShell } = {}) {
46
47
  // AN INJECTED READER IS THE CALLER'S STATEMENT ABOUT HOW TO READ, and it outranks the platform.
47
48
  //
48
49
  // Measured on macOS (the verification run): the arm that pins the field-22
@@ -61,6 +62,7 @@ export function procStarttime(pid, readStat = undefined,
61
62
  return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/)[19] ?? null;
62
63
  } catch { return null; }
63
64
  }
65
+ if (platform === "win32") return windowsStarttime(pid, runPowerShell);
64
66
  // ── — THE SAME DEFENCE WHERE THERE IS NO /proc ─────────────────────────
65
67
  //
66
68
  // Returning null here was not a neutral degradation. `claimToken` announces it and falls back to a
@@ -82,6 +84,33 @@ export function procStarttime(pid, readStat = undefined,
82
84
  } catch { return null; }
83
85
  }
84
86
 
87
+ // ── WINDOWS: NEITHER /proc NOR ps ────────────────────────────────────────────────────────────────────
88
+ //
89
+ // Without a stamp on Windows the portal said nothing was draining the queue while the worker ran, which
90
+ // is the false alarm described above, and a claim held by a dead runner stayed held until the max-claim-age
91
+ // ceiling. That second cost is larger on Windows than anywhere else, because Windows hands a freed pid to
92
+ // the next process soon after. So the stamp is read from the system's process list, through PowerShell:
93
+ // the creation time as epoch milliseconds, the same reader `processTable` uses, so a stamp written from
94
+ // one is compared with a stamp read by the same code.
95
+ //
96
+ // ONE PROCESS IS REMEMBERED: this one. Its start cannot change while it runs, and the heartbeat and the
97
+ // status stamp ask for it on every tick, each a PowerShell start. Every other pid is read afresh, for the
98
+ // reason above: a remembered stamp is the dead process's, handed to whatever now wears its number.
99
+ let ownWindowsStamp;
100
+ function windowsStarttime(pid, runPowerShell) {
101
+ if (!Number.isInteger(pid) || pid <= 0) return null;
102
+ const own = pid === process.pid && runPowerShell === defaultRunPowerShell;
103
+ if (own && ownWindowsStamp) return ownWindowsStamp;
104
+ let row = null;
105
+ try {
106
+ const r = runPowerShell(windowsProcessScript(pid));
107
+ if (r && r.status === 0) row = parseWindowsRows(r.stdout).find((p) => p.pid === pid) ?? null;
108
+ } catch { return null; }
109
+ const stamp = row && Number.isFinite(row.startedAt) ? String(row.startedAt) : null;
110
+ if (own && stamp) ownWindowsStamp = stamp;
111
+ return stamp;
112
+ }
113
+
85
114
  /** `ps` is POSIX and is on macOS. Empty stdout (no such pid) parses to NaN above, which is `null`. */
86
115
  function defaultReadPsStart(pid) {
87
116
  const r = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" });
@@ -130,8 +130,15 @@ export function renderClearanceVariants(model, scopeRows) {
130
130
  const out = ["# Variant manifest", ""];
131
131
  out.push(`## Mark: ${model.mark}`, "", `Dominant element: ${model.dominant_element}`, "");
132
132
 
133
- out.push("### Elements", "", "| Value | Kind |", "|---|---|");
134
- for (const e of model.elements) out.push(`| ${e.value} | ${e.kind} |`);
133
+ // The famous-mark column appears only when an element carries the flag, so a manifest with none reads
134
+ // exactly as it did before the flag existed.
135
+ if (model.elements.some((e) => e.famous_mark_flag)) {
136
+ out.push("### Elements", "", "| Value | Kind | Famous-mark check |", "|---|---|---|");
137
+ for (const e of model.elements) out.push(`| ${e.value} | ${e.kind} | ${e.famous_mark_flag ? "yes" : ""} |`);
138
+ } else {
139
+ out.push("### Elements", "", "| Value | Kind |", "|---|---|");
140
+ for (const e of model.elements) out.push(`| ${e.value} | ${e.kind} |`);
141
+ }
135
142
  out.push("");
136
143
 
137
144
  out.push(`### Variants (${model.variants.length})`, "", "| Value | Category | Romanisation | Rationale |", "|---|---|---|---|");
@@ -172,7 +179,7 @@ export function renderClearanceVariants(model, scopeRows) {
172
179
  /** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
173
180
  const DECLARED = Object.freeze({
174
181
  "": ["mark", "dominant_element", "elements", "variants", "incumbent_classes", "search_floor", "watchlist_owners", "goods_words", "scope_ledger"],
175
- elements: ["value", "kind"],
182
+ elements: ["value", "kind", "famous_mark_flag"],
176
183
  variants: ["value", "category", "rationale", "romanization"],
177
184
  scope_ledger: ["layer", "item", "status", "reason", "reopen_trigger"],
178
185
  });