clearotron 0.3.2-beta.7 → 0.3.2-beta.9

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 (272) hide show
  1. package/.env.example +58 -26
  2. package/CONTRIBUTING.md +8 -8
  3. package/INSTALL.md +148 -81
  4. package/README.md +3 -3
  5. package/SECURITY.md +3 -3
  6. package/bin/brandowner.mjs +3 -3
  7. package/bin/framework-preflight.mjs +1 -1
  8. package/bin/onboard.mjs +637 -216
  9. package/bin/start.mjs +151 -27
  10. package/bin/update.mjs +58 -11
  11. package/build-info.json +2 -2
  12. package/docs/DELIVERY.md +2 -1
  13. package/docs/INTAKE.md +1 -1
  14. package/docs/ONBOARDING.md +1 -1
  15. package/docs/architecture/03-run-lifecycle.md +6 -6
  16. package/docs/architecture/04-configuration-reference.md +32 -14
  17. package/docs/architecture/05-config-governance.md +23 -8
  18. package/docs/architecture/05-customer-profiles.md +2 -2
  19. package/docs/architecture/06-operations-runbook.md +3 -3
  20. package/docs/architecture/08-development-guide.md +6 -6
  21. package/docs/configuration.md +5 -5
  22. package/docs/decisions/0003-credential-model.md +1 -1
  23. package/docs/writing-standard.md +4 -0
  24. package/driver/CHANGELOG.md +124 -0
  25. package/driver/README.md +3 -3
  26. package/driver/band-size.mjs +59 -0
  27. package/driver/binding-layers.mjs +1 -1
  28. package/driver/citation-census.json +3 -3
  29. package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
  30. package/driver/common-law-receipts.mjs +2 -2
  31. package/driver/company-bundle.mjs +3 -3
  32. package/driver/compose-read.mjs +8 -14
  33. package/driver/config-inventory.mjs +112 -9
  34. package/driver/consumption-ledger.mjs +2 -2
  35. package/driver/contract-arm2-baseline.json +2 -5
  36. package/driver/contract-dictation-registry.mjs +19 -19
  37. package/driver/contract-e3-backlog.mjs +43 -43
  38. package/driver/contract-e3-baseline.json +14 -14
  39. package/driver/contract-vocabulary.mjs +68 -27
  40. package/driver/deliver-trigger.sh +16 -16
  41. package/driver/demo-container.mjs +3 -3
  42. package/driver/dev-portal.mjs +3 -3
  43. package/driver/disposition-call.mjs +1 -1
  44. package/driver/door-gates.mjs +41 -7
  45. package/driver/doubt-ledger.mjs +2 -2
  46. package/driver/drainer-identity.mjs +34 -8
  47. package/driver/driver.config.mjs +367 -104
  48. package/driver/engine/CONTRACT.md +10 -3
  49. package/driver/engine/README.md +2 -2
  50. package/driver/engine/anthropic-agent.mjs +77 -21
  51. package/driver/engine/auth.mjs +129 -10
  52. package/driver/engine/jx-turn.mjs +7 -6
  53. package/driver/engine/mcp/README.md +1 -1
  54. package/driver/engine/mcp/dispositions-server.mjs +3 -3
  55. package/driver/engine/mcp/gather-config.mjs +9 -9
  56. package/driver/engine/mcp/perplexity-server.mjs +2 -2
  57. package/driver/engine/mcp/recording-server.mjs +18 -5
  58. package/driver/engine/openai-agent.mjs +4 -2
  59. package/driver/engine/probe.mjs +110 -23
  60. package/driver/enqueue-schema.mjs +6 -2
  61. package/driver/findings-model.mjs +6 -3
  62. package/driver/flag-snapshot.mjs +34 -8
  63. package/driver/form-neighbourhood.mjs +54 -7
  64. package/driver/framework.mjs +4 -4
  65. package/driver/gateway.mjs +36 -24
  66. package/driver/jx-lanes.mjs +23 -4
  67. package/driver/jx-units.mjs +7 -4
  68. package/driver/jx.mjs +34 -4
  69. package/driver/knockout-review-record.mjs +56 -4
  70. package/driver/known-conflicts.mjs +1 -1
  71. package/driver/matter-frame-record.mjs +90 -1
  72. package/driver/named-band.mjs +1 -1
  73. package/driver/ordinary-words.mjs +51 -0
  74. package/driver/outbox-backoff.mjs +31 -16
  75. package/driver/package.json +1 -1
  76. package/driver/partial-payload-baseline.json +2 -2
  77. package/driver/phase0.mjs +3 -3
  78. package/driver/pipeline-knockout.mjs +5 -5
  79. package/driver/pipeline.mjs +396 -81
  80. package/driver/placement-form.mjs +77 -1
  81. package/driver/placement-model.mjs +1 -1
  82. package/driver/portal-config-view.mjs +30 -1
  83. package/driver/portal-report.mjs +107 -6
  84. package/driver/portal-service.mjs +80 -14
  85. package/driver/portal-upstream.mjs +1 -1
  86. package/driver/predelivery-lint.mjs +12 -2
  87. package/driver/preserve-merge.mjs +3 -3
  88. package/driver/product-rows.mjs +2 -2
  89. package/driver/products.mjs +1 -1
  90. package/driver/profiles/README.md +3 -3
  91. package/driver/profiles/demo-brand-owner.json +2 -2
  92. package/driver/profiles.mjs +55 -17
  93. package/driver/progress.mjs +18 -8
  94. package/driver/provider-usage.mjs +8 -8
  95. package/driver/publish/index.mjs +154 -8
  96. package/driver/publish/knockout.mjs +39 -5
  97. package/driver/publish/pool-admin.mjs +1 -1
  98. package/driver/publish/publish-inputs.mjs +18 -2
  99. package/driver/publish/render-knockout.mjs +184 -31
  100. package/driver/publish/render.mjs +323 -93
  101. package/driver/publish/report-data.mjs +4 -1
  102. package/driver/publish/report-topbar.mjs +58 -0
  103. package/driver/publish/search-depth.mjs +133 -4
  104. package/driver/publish/templates/report.css +78 -4
  105. package/driver/publish/xlsx.mjs +20 -1
  106. package/driver/queue-order.mjs +2 -2
  107. package/driver/recording-agreement.mjs +1 -1
  108. package/driver/reference-score.mjs +1 -1
  109. package/driver/register-availability.mjs +2 -2
  110. package/driver/register-count.mjs +50 -5
  111. package/driver/register-coverage.mjs +161 -1
  112. package/driver/register-digest-record.mjs +236 -11
  113. package/driver/register-grant-vocabulary.mjs +1 -1
  114. package/driver/register-plan.mjs +189 -2
  115. package/driver/registry-fidelity.mjs +3 -3
  116. package/driver/repair-composers.mjs +1 -1
  117. package/driver/repair-contract.mjs +1 -1
  118. package/driver/replay-archive.mjs +6 -6
  119. package/driver/report-overview-record.mjs +2 -2
  120. package/driver/result-noun-fields.mjs +2 -2
  121. package/driver/run-economics.mjs +41 -10
  122. package/driver/run-requirements.mjs +173 -9
  123. package/driver/runner.mjs +5 -5
  124. package/driver/scope-facts.mjs +20 -5
  125. package/driver/scope-ledger.mjs +5 -5
  126. package/driver/search-policy.mjs +22 -12
  127. package/driver/skills/README.md +15 -15
  128. package/driver/skills/blind-frame/SKILL.md +2 -2
  129. package/driver/skills/case-law-citation/SKILL.md +4 -4
  130. package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
  131. package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
  132. package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
  133. package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
  134. package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
  135. package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
  136. package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
  137. package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
  138. package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
  139. package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
  140. package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
  141. package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
  142. package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
  143. package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
  144. package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
  145. package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
  146. package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
  147. package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
  148. package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
  149. package/driver/skills/frame-diff/SKILL.md +1 -1
  150. package/driver/skills/knockout-assess/SKILL.md +10 -7
  151. package/driver/skills/matter-frame/SKILL.md +3 -3
  152. package/driver/skills/narrative-refutation/SKILL.md +9 -9
  153. package/driver/skills/placement-inquiry/SKILL.md +5 -5
  154. package/driver/stage-context.mjs +1 -1
  155. package/driver/stages-knockout.mjs +4 -4
  156. package/driver/stages.mjs +65 -61
  157. package/driver/status-snapshot.mjs +2 -2
  158. package/driver/suite-census.json +340 -136
  159. package/driver/surface-exit-verdict.mjs +58 -0
  160. package/driver/systemd/README.md +9 -6
  161. package/driver/systemd/clearotron-worker.service +1 -1
  162. package/driver/terminal-clamp.mjs +109 -1
  163. package/driver/tokens.mjs +169 -3
  164. package/driver/unit-environment.mjs +42 -15
  165. package/driver/unit-inventory.mjs +19 -2
  166. package/driver/usage-ledger.mjs +1 -1
  167. package/driver/variant-manifest-model.mjs +4 -4
  168. package/driver/verify-knockout.mjs +27 -0
  169. package/driver/verify.mjs +94 -6
  170. package/driver/whatif-queue.mjs +1 -1
  171. package/driver/wordlists/en.txt +63906 -0
  172. package/mcp-server/CHANGELOG.md +8 -0
  173. package/mcp-server/README.md +1 -1
  174. package/mcp-server/lib/README.md +1 -1
  175. package/mcp-server/lib/options.mjs +8 -7
  176. package/mcp-server/lib/plan.mjs +18 -2
  177. package/mcp-server/lib/runs.mjs +1 -1
  178. package/mcp-server/lib/usage.mjs +3 -3
  179. package/mcp-server/lib/whatif.mjs +1 -1
  180. package/mcp-server/package.json +1 -1
  181. package/mcp-server/server.mjs +18 -1
  182. package/package.json +12 -11
  183. package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
  184. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
  185. package/portal-ui/dist/index.html +2 -2
  186. package/portal-ui/package.json +1 -1
  187. package/providers/README.md +1 -1
  188. package/providers/_shared/enumerate.mjs +6 -6
  189. package/providers/_shared/execute-plan.mjs +3 -3
  190. package/providers/_shared/ledger.mjs +119 -5
  191. package/providers/_shared/provider-text.mjs +2 -2
  192. package/providers/_shared/screen.mjs +2 -2
  193. package/providers/_shared/script-form.mjs +3 -3
  194. package/providers/_shared/territory-codes.mjs +23 -3
  195. package/providers/clarivate/README.md +1 -1
  196. package/providers/clarivate/src/capabilities.js +12 -12
  197. package/providers/clarivate/src/core.js +37 -43
  198. package/providers/corsearch/README.md +1 -1
  199. package/providers/corsearch/src/capabilities.js +5 -5
  200. package/providers/corsearch/src/core.js +3 -3
  201. package/providers/jx/README.md +2 -1
  202. package/providers/jx/src/turn-envelope.mjs +8 -3
  203. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  204. package/providers/oauth-mcp-bridge/package.json +1 -1
  205. package/providers/perplexity/src/core.js +1 -1
  206. package/providers/signa/README.md +1 -1
  207. package/providers/signa/src/capabilities.js +42 -49
  208. package/providers/signa/src/core.js +106 -29
  209. package/providers/uspto-local/README.md +1 -1
  210. package/providers/uspto-local/src/sync.js +1 -1
  211. package/scripts/README.md +1 -0
  212. package/scripts/ask-ai-render-check.mjs +127 -1
  213. package/scripts/authority-boundary-probe.mjs +8 -6
  214. package/scripts/backfill-started-at.mjs +2 -2
  215. package/scripts/census-merge-driver.mjs +33 -2
  216. package/scripts/citation-anchor-report.mjs +181 -0
  217. package/scripts/dead-names.mjs +1 -1
  218. package/scripts/deprecate-below.mjs +66 -8
  219. package/scripts/drain-preflight.mjs +1 -1
  220. package/scripts/e2e.mjs +174 -0
  221. package/scripts/env-audit.mjs +39 -6
  222. package/scripts/env-classify.mjs +20 -2
  223. package/scripts/freeze-example-run.mjs +61 -18
  224. package/scripts/generated-files-are-current.mjs +69 -4
  225. package/scripts/live-surface-check.mjs +124 -41
  226. package/scripts/markdown-link-check.mjs +1 -1
  227. package/scripts/merge-shape-check.mjs +242 -0
  228. package/scripts/mint-names-in-force.mjs +5 -5
  229. package/scripts/mint-offered-territories.mjs +72 -0
  230. package/scripts/mint-public-residue.mjs +2 -2
  231. package/scripts/mint-reference-strip-backlog.mjs +2 -2
  232. package/scripts/mint-suite-census.mjs +75 -2
  233. package/scripts/mint-writing-standard-backlog.mjs +2 -2
  234. package/scripts/purge-runs.mjs +7 -7
  235. package/scripts/reconcile-runs.mjs +2 -2
  236. package/scripts/release-approve-parked.mjs +20 -2
  237. package/scripts/release-await-cut.mjs +120 -1
  238. package/scripts/release-note-required.mjs +76 -8
  239. package/scripts/report-header-render-check.mjs +164 -0
  240. package/scripts/settings-render-check.mjs +75 -2
  241. package/scripts/test-full.mjs +96 -3
  242. package/scripts/test-run.mjs +10 -0
  243. package/shared/brand.mjs +27 -0
  244. package/shared/connect-clients.mjs +39 -11
  245. package/shared/deployment-box.mjs +7 -2
  246. package/shared/driver-dir.mjs +1 -1
  247. package/shared/env-aliases.mjs +1 -1
  248. package/shared/identifier-scan.mjs +65 -9
  249. package/shared/identifier-sentinels.mjs +22 -0
  250. package/shared/names-in-force.mjs +4 -2
  251. package/shared/offered-territories.json +738 -0
  252. package/shared/pre-rename-spellings.mjs +53 -0
  253. package/shared/reference-guard-classes.mjs +40 -2
  254. package/shared/stdio-connect.mjs +39 -4
  255. package/shared/tree-commit.mjs +48 -0
  256. /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
  257. /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
  258. /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
  259. /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
  260. /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
  261. /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
  262. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
  263. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
  264. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
  265. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
  266. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
  267. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
  268. /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
  269. /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
  270. /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
  271. /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
  272. /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
@@ -34,7 +34,7 @@ import { MODEL_FILE as BLIND_FRAME_MODEL_FILE } from "./blind-frame-record.mjs";
34
34
  import { FLAGS_FILE as SKEPTIC_FLAGS_FILE } from "./skeptic-record.mjs";
35
35
  import { MODEL_FILE as FRAME_DIFF_MODEL_FILE, PROSE_FILE as FRAME_DIFF_PROSE_FILE } from "./frame-diff-record.mjs";
36
36
  import { MATTER_CONTEXT_FILE } from "./matter-frame-record.mjs";
37
- import { MODEL_FILE as VARIANT_MODEL_FILE, PROSE_FILE as VARIANT_PROSE_FILE } from "./prelim-variants-record.mjs";
37
+ import { MODEL_FILE as VARIANT_MODEL_FILE, PROSE_FILE as VARIANT_PROSE_FILE } from "./clearance-variants-record.mjs";
38
38
  import { PROSE_FILE as REPORT_OVERVIEW_FILE } from "./report-overview-record.mjs";
39
39
  import { NARRATIVE_FILE, FINDINGS_FILE, refusalsFor } from "./synthesis-record.mjs";
40
40
  import { FINDINGS_FILE as REGISTER_FINDINGS_FILE, refusalsFor as registerDigestRefusalsFor } from "./register-digest-record.mjs";
@@ -96,8 +96,8 @@ export const TOOL_WRITTEN_ARTIFACTS = new Map([
96
96
  // scope-ledger.json is deliberately ABSENT: it was already driver-written before this conversion (the
97
97
  // driver derived it), so it is outside this conversion's claim. What changed is where its values come
98
98
  // from, not who writes it.
99
- [VARIANT_MODEL_FILE, { tool: "record_prelim_variants", what: "the variant manifest" }],
100
- [VARIANT_PROSE_FILE, { tool: "record_prelim_variants", what: "the variant manifest" }],
99
+ [VARIANT_MODEL_FILE, { tool: "record_clearance_variants", what: "the variant manifest" }],
100
+ [VARIANT_PROSE_FILE, { tool: "record_clearance_variants", what: "the variant manifest" }],
101
101
  // Conversion 4 — ONE basename, and the first row whose artifact a CLIENT reads. report-overview.md is
102
102
  // the delivered report's front-matter and its Actions section; assembleReportMd splices the code-built
103
103
  // sections into it and publishes the result. So a repair that arrives naming this file and gets handed
@@ -261,6 +261,7 @@ import { unionDispositionForm, formSidecarPath } from "./disposition-union.mjs";
261
261
  import { unionCoverageForm } from "./coverage-union.mjs";
262
262
  import { coverageFormStamp, readCoverageForm, readCoverageFormInput, writeCoverageForm } from "./coverage-form-io.mjs";
263
263
  import { unionPlacementForm } from "./placement-union.mjs";
264
+ import { placementRenderAccount } from "./placement-form.mjs";
264
265
  import { placementFormStamp, readPlacementForm, readPlacementFormInput, readSubmittedPlacementForm, writePlacementForm, renderPlacementsFile } from "./placement-form-io.mjs";
265
266
  // The register-axis vocabulary, quoted verbatim into the coverage-form axis hint. ONE source: the same
266
267
  // constant `rowIsSettled` refuses against, so the hint can never name a set the gate does not accept.
@@ -741,7 +742,12 @@ export function syncPlacementForm(files) {
741
742
  // never put an unparseable deliverable on disk, and on a throw the previous file is left alone.
742
743
  const r = renderPlacementsFile(runDir, u.form.rows);
743
744
  if (!r.ok) note(`[placement-form] placements.json NOT re-rendered: ${r.error} — the previous file stands and the omission is on the form`);
744
- return { ...u, rendered: r.ok ? r.placements : null, render_error: r.error };
745
+ // WHO OWES EACH ROW THE RENDER LEFT OUT — the same account validators.placement judges with, carried onto
746
+ // the attempt row so an omission is never read without its cause.
747
+ const a = placementRenderAccount(u.form.rows);
748
+ const account = { unjudged: a.unjudged.length, registerSelected: a.register_selected,
749
+ registerRendered: a.register_rendered, registerFacts: a.register_facts };
750
+ return { ...u, rendered: r.ok ? r.placements : null, render_error: r.error, account };
745
751
  }
746
752
  return null;
747
753
  }
@@ -817,7 +823,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
817
823
  followup = false, // #5b: this run is a warm-resume / followup (escalation, envelope close, frame-reopen
818
824
  // sweep) — a hard-wall timeout breaks after ONE attempt (a 1.5× extension can't fit
819
825
  // an already-over-budget resume; the caller records the coverage-limited deferral).
820
- excludeTools, bandSize, // copper-lattice re-route: tool names dropped from this stage's allowedTools
826
+ excludeTools, bandSize, derivedLimit = null, // copper-lattice re-route: tool names dropped from this stage's allowedTools
821
827
  } = opts;
822
828
  if (!message) throw new Error(`runStage(${name}): message is required`);
823
829
  if (!sessionKey) throw new Error(`runStage(${name}): sessionKey is required`);
@@ -1145,17 +1151,17 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1145
1151
  // it; the log was already saying what the code believed.
1146
1152
  //
1147
1153
  // TWO FIELDS, NEVER COLLAPSED INTO ONE:
1148
- // modelUsed — the requested resolution. Unchanged in meaning and unchanged in value, because
1149
- // run-economics.mjs and tokens.mjs both read it and a field that quietly changes
1150
- // what it means is its own corruption.
1151
- // modelActual — the id the WIRE reported (engine tuple `modelWire`), or NULL when the stream
1152
- // never said: an engine that does not emit one (codex), a turn killed before any
1153
- // event, a spawn error. It NEVER falls back to the requested alias.
1154
- // `modelBasis` names which of the two the row can defend: "actual" or "unknown". There is no third
1155
- // state in which a requested value is dressed as an observed one.
1154
+ // modelUsed — the requested resolution, unchanged in meaning and value: run-economics.mjs and
1155
+ // tokens.mjs both read it, and a field that quietly changes meaning is its own corruption.
1156
+ // modelActual — the id the WIRE reported (engine tuple `modelWire`), or NULL when the stream never
1157
+ // said: an engine that does not emit one (codex), a turn killed before any event, a
1158
+ // spawn error. It NEVER falls back to the requested alias, and `modelBasis` ("actual" or
1159
+ // "unknown") never dresses a requested value as an observed one. `providerReported` is
1160
+ // the provider word the same stream gave (tuple `providerWire`), null on the same terms.
1156
1161
  const modelRequested = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model);
1157
1162
  const modelActual = (typeof turn.modelWire === "string" && turn.modelWire) ? turn.modelWire : null;
1158
1163
  const modelBasis = modelActual ? "actual" : "unknown";
1164
+ const providerReported = (typeof turn.providerWire === "string" && turn.providerWire) ? turn.providerWire : null;
1159
1165
  // WHETHER THE OBSERVED ID NAMES A FIXED BUILD. `modelBasis: "actual"` says the provider answered,
1160
1166
  // not that the answer is pinned: two of the three tiers come back as undated aliases the provider
1161
1167
  // may repoint, and recorded beside a dated one they read identically. null when there is nothing to
@@ -1171,9 +1177,13 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1171
1177
  // "written before anybody asked", which is the distinction the field exists for. One spawn per
1172
1178
  // binary per process; a probe never throws, because taking down a dispatch to record a version
1173
1179
  // would be a worse defect than the gap it closes.
1180
+ // `source` says WHICH copy served (explicit / path / installed), and it is attached here, outside the
1181
+ // probe's cache: that cache is keyed by the file, and one file can be reached by more than one route.
1174
1182
  const cli = (() => {
1175
- try { return probeCliVersion(preflightEngineBinary(process.env)?.resolved ?? null); }
1176
- catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160) }; }
1183
+ try {
1184
+ const pre = preflightEngineBinary(process.env);
1185
+ return { ...probeCliVersion(pre?.resolved ?? null), source: pre?.source ?? null };
1186
+ } catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160), source: null }; }
1177
1187
  })();
1178
1188
  if (modelActual) lastModelWire = modelActual; // — never overwritten with null
1179
1189
  // The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
@@ -1294,7 +1304,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1294
1304
  attempt, key, agent, model,
1295
1305
  modelUsed: (lastModelUsed = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model)),
1296
1306
  // Same billing stamp as the attempt row, written on the same terms — see the note there.
1297
- engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
1307
+ engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1298
1308
  code: rt.code, wall: rt.wall, timeoutSec: effTimeout,
1299
1309
  // — same rename as the attempt row above. This row already carries the driver's verdict as
1300
1310
  // `repairOutcome` (only "repaired" is success), so it needs no `ok`; what it lacked was any mark
@@ -1638,8 +1648,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1638
1648
  // modelMismatch — true/false when both sides name a family, null when either does not.
1639
1649
  // Written even on the rows where they are null, so "this engine cannot report" stays visibly
1640
1650
  // different from "this record predates the gauge".
1641
- modelActual, modelBasis, modelSnapshot, modelMismatch,
1642
- cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1651
+ modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
1652
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
1643
1653
  // W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
1644
1654
  // records INTENT (the mode the engine was configured to bill under), not independent billing evidence
1645
1655
  // — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
@@ -1653,7 +1663,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1653
1663
  // "every telemetry field is written unconditionally, so 'did not happen' stays distinguishable
1654
1664
  // from 'not recorded'" (instrumentation-house-rule.test.mjs). A run must be able to STATE that it
1655
1665
  // billed subscription, not merely fail to state that it billed API.
1656
- engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
1666
+ engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1657
1667
  // build 2 — THIS attempt is the fresh dispatch bought by discarding a warm session that
1658
1668
  // reproduced its own failure. Exact, not cumulative: a `warmEscalatedAt > 0` test would mark
1659
1669
  // every later attempt too the moment the ladder is deepened, and the row would stop meaning
@@ -1695,7 +1705,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1695
1705
  // destroyed. `unresolved` counts selections the fold does not hold. RECORDING ONLY.
1696
1706
  placements: lastPlacementUnion ? { settled: lastPlacementUnion.settled, outstanding: lastPlacementUnion.outstanding,
1697
1707
  carried: lastPlacementUnion.carried, total: lastPlacementUnion.total, seatRows: lastPlacementUnion.seat_rows,
1698
- unresolved: lastPlacementUnion.unresolved, rendered: lastPlacementUnion.rendered } : undefined,
1708
+ unresolved: lastPlacementUnion.unresolved, rendered: lastPlacementUnion.rendered, account: lastPlacementUnion.account } : undefined,
1699
1709
  //: how many in-dispatch form repairs THIS attempt bought (absent = none). Its own rows
1700
1710
  // sit immediately above with the defect each one was dispatched to fix.
1701
1711
  formRepairs: formRepairsThisAttempt || undefined,
@@ -1743,6 +1753,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1743
1753
  // null (not 0, and not absent) on an engine that cannot report them, so "this adapter does not
1744
1754
  // measure" stays visibly different from "this turn called no tools" — see toolGauge.
1745
1755
  ...toolGauge(turn), band: bandSize ?? undefined,
1756
+ inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
1746
1757
  // AD-4 emitted-vs-landed, UNCONDITIONAL (was success-only, which made a failed attempt's mid-write
1747
1758
  // artifact invisible): `output` = what LANDED on disk after this attempt (null when the stage has no
1748
1759
  // expected file); `wrote` = whether THIS attempt emitted it (see the computation above the runDir
@@ -1776,8 +1787,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1776
1787
  event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: !fail, fail: fail ?? null,
1777
1788
  //: the spine carries the same pair as the per-stage log, or the two disagree about what
1778
1789
  // ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
1779
- model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
1780
- cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1790
+ model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
1791
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
1781
1792
  wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
1782
1793
  rescued: rescued ?? undefined, killed: killed || undefined,
1783
1794
  quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
@@ -1789,7 +1800,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1789
1800
  // archived runs actually reads, and the question "has this box ever billed API" could not be
1790
1801
  // answered from it because the pair was only ever on the per-stage log. Written unconditionally,
1791
1802
  // like everything else here: a subscription run states `false`.
1792
- authMode: auth.mode, apiBilled: auth.apiBilled === true,
1803
+ authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1793
1804
  //: the spine carries the POINTER and the sha, not the text — enough to find the file and
1794
1805
  // to tell two attempts apart without opening either.
1795
1806
  dispatch: dispatch?.file ?? null, dispatchSha: dispatch?.sha ?? null,
@@ -1806,6 +1817,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1806
1817
  wall, outputTokens: usage?.output ?? null, tokensPerSec: tokensPerSec(usage, wall),
1807
1818
  // — the spine carries them too, or a round has to join two files to ask why a stage was slow.
1808
1819
  ...toolGauge(turn), band: bandSize ?? undefined,
1820
+ inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
1809
1821
  formRepairs: formRepairsThisAttempt || undefined, //, see the stage row above
1810
1822
  });
1811
1823
  } catch { /* telemetry best-effort — never fail a turn over a journal line */ }
@@ -2082,7 +2094,7 @@ function refusalsInWindow(files, runDir, from, to) {
2082
2094
  }
2083
2095
 
2084
2096
  function rel(p) {
2085
- const i = p.indexOf("/prelim-search/");
2097
+ const i = Math.max(p.indexOf("/clearance-search/"), p.indexOf("/prelim-search/")); // either spelling of the studio segment
2086
2098
  return i >= 0 ? p.slice(i + 1) : p;
2087
2099
  }
2088
2100
 
@@ -183,10 +183,10 @@ export function decideJxLanes({ job, profile, searchPolicy } = {}) {
183
183
  *
184
184
  * ── THE GATE THAT WAS DECIDING, AND DECIDING OFF ────────────────────────────────────────────────────
185
185
  *
186
- * This function opened with `resolvedPolicy.level !== "prelim"`. `resolveSearchPolicy` now returns
186
+ * This function opened with `resolvedPolicy.level !== "clearance"`. `resolveSearchPolicy` now returns
187
187
  * `level` = THE PRODUCT ID for all four searches, so that leg was false on every live run and the whole
188
188
  * recommendation was dead — silently, with both callers (pipeline.mjs and runner.mjs) live and 3,754
189
- * driver tests green, because the one test that covered it hand-fed `{ level: "prelim" }`, a value
189
+ * driver tests green, because the one test that covered it hand-fed `{ level: "clearance" }`, a value
190
190
  * nothing produces any more. A retired slug does not stop deciding when it is retired; it starts
191
191
  * deciding one way.
192
192
  *
@@ -280,6 +280,25 @@ export const jxBillingStamp = (executorSource, result = null) => {
280
280
  // NO VENDOR ON THE RESULT MEANS NO DISPATCH HAPPENED. A fixture, an injected executor, or a
281
281
  // configuration the engine door refused all return without one, and none of them is provider-billed.
282
282
  // Saying so in its own words beats "unknown", which is indistinguishable from an unstamped legacy row.
283
- if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed" };
284
- return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed" };
283
+ if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed", cloud: null };
284
+ return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed", cloud: result.cloud ?? null }; // cloud: which account a cloud mode bills
285
+ };
286
+
287
+ // ── What a jx ledger row says about the model ─────────────────────────────────────────────────────────
288
+ // The `model` on these rows is the id the program reported for the turn (jx-turn.mjs reads it off the
289
+ // wire), not a tier asked for, so a row that names one also carries it as `modelActual`, the name every
290
+ // attempt row uses for a served id. That is what the report's list of models reads; without it, a model
291
+ // that did only this work was left off the report.
292
+ //
293
+ // A TURN THAT RAN AND NAMED NO MODEL STILL RAN. The Claude program answered it itself, or the stream never
294
+ // said, or the turn was killed first: the program reports no id. Such a row used to carry neither field,
295
+ // and every reader took a row with no `model` for a call that was never made, so the turn's attempt and
296
+ // any tokens it reported fell out of the run's totals, and a run made only of such turns read as one where
297
+ // nothing was looked at. It now records `modelActual: null`, the stage rows' own words for "a turn ran and
298
+ // named no model", and tokens.mjs counts it as an attempt. Only a real dispatch writes it, by the rule
299
+ // jxBillingStamp states: no vendor on the result means no dispatch, so a fixture, an injected executor or
300
+ // a configuration the engine door refused writes neither field.
301
+ export const jxModelFields = (executorSource, result = null) => {
302
+ if (result?.model) return { model: result.model, modelActual: result.model };
303
+ return jxBillingStamp(executorSource, result).engine === "not-provider-billed" ? {} : { modelActual: null };
285
304
  };
@@ -34,7 +34,7 @@
34
34
  import { readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
35
35
  import { join } from "node:path";
36
36
  import { driverDir } from "../shared/driver-dir.mjs"; //
37
- import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp } from "./jx-lanes.mjs";
37
+ import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
38
38
  import { jxKey, MAX_LANE_ATTEMPTS } from "./jx.mjs";
39
39
  import { abbrev } from "./repair-contract.mjs";
40
40
  import { kebab } from "./search-policy.mjs";
@@ -137,11 +137,14 @@ function foldRetryable(ctx, lane) {
137
137
  // spend real tokens that no per-run total ever sees (they did, until 2026-07-28).
138
138
  //
139
139
  // …and the BILLING PATH rides with them — see jxBillingStamp in jx-lanes.mjs.
140
+ //
141
+ // …and so does what the turn said about the model: the id it reported, or that it ran and named none —
142
+ // see jxModelFields in jx-lanes.mjs.
140
143
  function ledgerRow(runDir, row) {
141
144
  try { appendFileSync(driverDir(runDir, "jx-completions.jsonl"), JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
142
145
  }
143
146
 
144
- const runPrefix = (run) => `prelim-${run?.slug ?? "run"}-${run?.codename ?? "local"}-`;
147
+ const runPrefix = (run) => `clearance-${run?.slug ?? "run"}-${run?.codename ?? "local"}-`;
145
148
 
146
149
  // ── Executor chains (the resolveJxExecutor idiom: injected → CLEAROTRON_JX_FIXTURES → live) ─────────────
147
150
  export function resolveSerpExecutor(opts, { mark, lane }) {
@@ -392,7 +395,7 @@ export async function runJxSerpGrid(ctx, job, opts = {}, { runLog = () => {}, no
392
395
  ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "serp-judge", executor: judgeSource,
393
396
  ...jxBillingStamp(judgeSource, jr),
394
397
  took_ms: jr?.tookMs ?? (Date.now() - started), ok: Boolean(jr?.ok), judged: jr?.ok ? (jr.judgments?.length ?? 0) : 0,
395
- ...(jr?.model ? { model: jr.model } : {}),
398
+ ...jxModelFields(judgeSource, jr),
396
399
  ...(jr?.usage ? { usage: jr.usage } : {}), ...(jr?.ok ? {} : { cause: String(jr?.cause ?? "unknown").slice(0, 300) }) });
397
400
  if (!jr?.ok) { judgeDegraded = String(jr?.cause ?? "unknown").slice(0, 200); break; }
398
401
  const byId = new Map(jr.judgments.map((j) => [j.id, j]));
@@ -524,7 +527,7 @@ export async function runJxNativeread(ctx, job, opts = {}, { runLog = () => {},
524
527
  ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "nativeread", executor: source,
525
528
  ...jxBillingStamp(source, r),
526
529
  took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok), items: r?.ok ? (r.items?.length ?? 0) : 0,
527
- ...(r?.model ? { model: r.model } : {}),
530
+ ...jxModelFields(source, r),
528
531
  ...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) });
529
532
  if (!r?.ok) {
530
533
  degradeUnit(run.runDir, key, st.attempts, r?.cause ?? "unknown");
package/driver/jx.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  import { readFileSync, writeFileSync, renameSync, appendFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { driverDir } from "../shared/driver-dir.mjs"; //
17
- import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp } from "./jx-lanes.mjs";
17
+ import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
18
18
  import { cnipaSubgroupsForClasses, cnipaEditionLabel } from "./jx-subclass.mjs"; // — replaces the hand-written seed table
19
19
  import { kebab } from "./search-policy.mjs";
20
20
  import { JX_PROVIDERS } from "./driver.config.mjs";
@@ -186,6 +186,13 @@ export function deriveJxSliceStatement({ sidecar, units = null, env = process.en
186
186
  //
187
187
  // PURE. Takes the parsed sidecar and the slice statement `deriveJxSliceStatement` just produced.
188
188
  export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
189
+ // EITHER SHAPE, AND THAT IS NOT POLITENESS. This reads `slices.candidates`, and the publish path handed
190
+ // it `deriveJxSliceStatement`'s whole return — `{executes, slices}` — so the lookup found nothing and
191
+ // every lane reported `ran: null` whatever it had done. Nothing threw and nothing was logged: the
192
+ // failure arrived as a delivered client report saying a search had not run when it had. The two shapes
193
+ // are told apart with certainty rather than guessed at — a statement carries a `slices` object and a
194
+ // slice map carries slice names — so a caller cannot pass the wrong one and be quietly misread.
195
+ const bySlice = slices?.slices && typeof slices.slices === "object" ? slices.slices : slices;
189
196
  const declared = sidecar?.lanes ?? {};
190
197
  const out = {};
191
198
  for (const lane of Object.keys(declared)) {
@@ -194,10 +201,10 @@ export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
194
201
  // lane that gains a slice later is covered without this function being edited — and so a lane that
195
202
  // has none is a measured fact rather than a hardcoded assumption about ja/ko.
196
203
  const deep = JX_SLICES.filter((s) => s.slice > 1 && s.lane === lane);
197
- const deepStates = deep.map((s) => ({ name: s.name, state: slices?.[s.name]?.state ?? null, why: slices?.[s.name]?.why ?? null }));
204
+ const deepStates = deep.map((s) => ({ name: s.name, state: bySlice?.[s.name]?.state ?? null, why: bySlice?.[s.name]?.why ?? null }));
198
205
  const deepRan = deepStates.filter((d) => d.state === "ran").map((d) => d.name);
199
206
  // Slice 1 is the multi-lane one: its per-lane state lives in `.lanes`, never in a scalar `.state`.
200
- const candidatesState = slices?.candidates?.lanes?.[lane] ?? null;
207
+ const candidatesState = bySlice?.candidates?.lanes?.[lane] ?? null;
201
208
 
202
209
  const ran = deepRan.length ? "full" : candidatesState === "ran" ? "candidates" : null;
203
210
  const shortfall = asked === "full" && ran !== "full";
@@ -225,6 +232,27 @@ export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
225
232
  return out;
226
233
  }
227
234
 
235
+ /**
236
+ * THE VERDICT A PUBLISHED REPORT SHOULD CARRY: the one the RUN stated, not a fresh one. PURE.
237
+ *
238
+ * `stateJxFold` writes `fold.depth` at delivery, derived from this run's own environment — which is the
239
+ * reason the seam exists: the arms are environment, so a verdict derived on some later box speaks for
240
+ * that box and not for the run. Publishing derived its own, and the two disagreed on a delivered client
241
+ * report: the run said the ja lane ran as a candidate lane, the publisher said no lane ran, and the page
242
+ * printed "Not run this run" over a search that had happened.
243
+ *
244
+ * Deriving stays, for a run delivered before the stamp existed — an old artifact still gets the best
245
+ * answer its record supports, rather than none.
246
+ *
247
+ * @param {object|null} sidecar parsed _driver/jx-lanes.json
248
+ * @param {object|null} units parsed _driver/jx/units.json, or null
249
+ */
250
+ export function laneDepthOfRun({ sidecar, units = null, env = process.env } = {}) {
251
+ const stamped = sidecar?.fold?.depth;
252
+ if (stamped && typeof stamped === "object" && !Array.isArray(stamped)) return stamped;
253
+ return deriveLaneDepthVerdicts({ sidecar, slices: deriveJxSliceStatement({ sidecar, units, env }) });
254
+ }
255
+
228
256
  const flag = (name) => ["1", "true", "yes", "on"].includes(String(process.env[name] ?? "").trim().toLowerCase());
229
257
  // — the private copy is gone; laneArmed (driver.config.mjs) is the one reader.
230
258
 
@@ -512,7 +540,9 @@ export async function runJxCandidateFold(ctx, job, opts = {}, { runLog = () => {
512
540
  const row = { ts: new Date().toISOString(), lane, mark: markName, executor: source, ...jxBillingStamp(source, r),
513
541
  took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok),
514
542
  candidates: r?.ok ? (r.candidates?.length ?? 0) : 0,
515
- ...(r?.model ? { model: r.model } : {}),
543
+ // the model the turn reported, as `model` and `modelActual`, or `modelActual: null` for a turn
544
+ // that ran and named none — see jxModelFields.
545
+ ...jxModelFields(source, r),
516
546
  ...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) };
517
547
  try { appendFileSync(ledgerPath, JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
518
548
  // Before the degraded-lane `continue` below: a lane that FAILED still ran a turn, and the model
@@ -43,6 +43,9 @@ import { captureCall, stampVerdict } from "./call-capture.mjs";
43
43
  import { refuseUndeclared as refuseUndeclaredShared, lastAccepted, acceptedEnvelope } from "./preserve-merge.mjs";
44
44
  import { knockoutVisibleProse } from "./predelivery-lint.mjs"; // the walk is the authority on what a reader sees
45
45
  import { plainRegisterFlags } from "./plain-register.mjs"; // the pinned rule, one copy
46
+ // THE HINT'S fold, and it is imported rather than rebuilt — see nearestAddress. It is a comparison key
47
+ // for saying WHICH line was probably meant; it never decides whether an address resolves.
48
+ import { queryKey } from "./connotation-search.mjs";
46
49
 
47
50
  const SCHEMA_VERSION = 1;
48
51
 
@@ -142,6 +145,42 @@ export function addressKey(at) {
142
145
  return [a.field, a.mark ?? "", a.index ?? "", a.ordinal ?? ""].join(ADDRESS_SEP);
143
146
  }
144
147
 
148
+ /**
149
+ * THE ADDRESS A SEAT PROBABLY MEANT, FOR THE REFUSAL TO NAME — and for nothing else.
150
+ *
151
+ * The lookup above stays EXACT and that is a ruling, not an oversight (owner, 2026-09-16). Punctuation
152
+ * can be part of a mark's identity, so two marks differing by an apostrophe form are not assumed to be
153
+ * one mark and a rewrite is never applied by analogy. What was wrong was the refusal, not the matching:
154
+ * a seat that re-typed a name with a right single quotation mark where the record has an apostrophe was
155
+ * told its rewrite "names no line on this record", which describes the record rather than the difference,
156
+ * and the rewrite was dropped with nobody able to see why. One character, invisible in most fonts.
157
+ *
158
+ * So this names the nearest and changes nothing. It folds ONLY typographic form — `queryKey`, the same
159
+ * key the meaning gate was folded onto after a production clearance stopped on exactly this character —
160
+ * and it is imported rather than copied, because two private folds are how the two gates drift apart.
161
+ * That key deliberately does not fold accents, so it cannot quietly propose that Café and Cafe are the
162
+ * same mark.
163
+ *
164
+ * It requires an EXACT match on every other part of the address — field, index, ordinal — so the hint is
165
+ * always the same line of the same record, differing only in how the mark's name was typed. A candidate
166
+ * whose mark is byte-identical is not a hint: the caller only asks when the exact lookup already failed,
167
+ * and pointing at the address that was just refused would say nothing.
168
+ *
169
+ * @returns {object|null} the address on this record, or null when nothing differs by form alone
170
+ */
171
+ export function nearestAddress(at, knownAts) {
172
+ const want = at ?? {};
173
+ if (want.mark == null) return null; // no name to have mistyped
174
+ const same = (a, b) => (a ?? "") === (b ?? "");
175
+ for (const k of knownAts ?? []) {
176
+ if (!k || k.field !== want.field) continue;
177
+ if (!same(k.index, want.index) || !same(k.ordinal, want.ordinal)) continue;
178
+ const kn = String(k.mark ?? ""), wn = String(want.mark ?? "");
179
+ if (kn !== wn && queryKey(kn) === queryKey(wn)) return k;
180
+ }
181
+ return null;
182
+ }
183
+
145
184
  /** Is this a well-formed address for a field the pass may reach? The reason, or null. */
146
185
  export function addressFault(at) {
147
186
  if (!at || typeof at !== "object" || Array.isArray(at)) return "an address must be an object";
@@ -296,15 +335,21 @@ export function validateKnockoutReviewFile(file, text) {
296
335
  try { merged = JSON.parse(readFileSync(findingsFile, "utf8")); }
297
336
  catch (e) { return { ok: false, reason: `the merged record will not parse, so no address could be checked: ${e.message}` }; }
298
337
 
299
- const known = new Set(knockoutVisibleProse(merged).map((v) => addressKey(v.at)));
338
+ const knownAts = knockoutVisibleProse(merged).map((v) => v.at);
339
+ const known = new Set(knownAts.map((a) => addressKey(a)));
300
340
  const owned = new Set(knockoutVisibleProse(merged).filter((v) => v.at?.engineOwned).map((v) => addressKey(v.at)));
341
+ // Appended to a refusal, never consulted before one: the address still has to match exactly.
342
+ const hint = (at) => {
343
+ const n = nearestAddress(at, knownAts);
344
+ return n ? ` — this record carries "${n.mark}" at that line, which differs from "${at.mark}" only in how it is typed; the two are not assumed to be the same mark, so re-send the rewrite naming the record's spelling` : "";
345
+ };
301
346
  for (const r of doc.rewrites ?? []) {
302
347
  const key = addressKey(r.at);
303
348
  if (owned.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field} names a caveat the engine wrote, not a seat — it is not offered and cannot be changed` };
304
- if (!known.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field}${r.at.mark ? ` on "${r.at.mark}"` : ""} names no line on this record` };
349
+ if (!known.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field}${r.at.mark ? ` on "${r.at.mark}"` : ""} names no line on this record${hint(r.at)}` };
305
350
  }
306
351
  for (const d of doc.declined ?? []) {
307
- if (!known.has(addressKey(d.at))) return { ok: false, reason: `the declined row for ${d.at.field}${d.at.mark ? ` on "${d.at.mark}"` : ""} names no line on this record` };
352
+ if (!known.has(addressKey(d.at))) return { ok: false, reason: `the declined row for ${d.at.field}${d.at.mark ? ` on "${d.at.mark}"` : ""} names no line on this record${hint(d.at)}` };
308
353
  }
309
354
  return { ok: true };
310
355
  }
@@ -340,7 +385,14 @@ export function applyKnockoutReview(merged, review) {
340
385
  for (const r of review?.rewrites ?? []) {
341
386
  const key = addressKey(r.at);
342
387
  const seen = byKey.get(key);
343
- if (!seen) { unresolved.push({ at: r.at, why: "no such line on this record" }); continue; }
388
+ if (!seen) {
389
+ // Same hint as the validator's, for the same reason: the apply path is reached on a record this
390
+ // pass did not validate, and an unresolved row that cannot say WHY is the state that cost a
391
+ // rewrite silently. It still changes nothing — `unresolved` is a report.
392
+ const near = nearestAddress(r.at, [...byKey.values()].map((v) => v.at));
393
+ unresolved.push({ at: r.at, why: near ? `no such line on this record — it carries "${near.mark}" there, differing from "${r.at.mark}" only in how it is typed` : "no such line on this record" });
394
+ continue;
395
+ }
344
396
  if (seen.at?.engineOwned) { refused.push({ at: r.at, why: "the engine wrote this caveat, not a seat" }); continue; }
345
397
  const at = r.at, text = String(r.text);
346
398
  // THE WALK IS THE BOUNDS CHECK. `seen` came from this same record, so a resolved address names a
@@ -8,7 +8,7 @@
8
8
  // (copper-causeway could not see teal-conduit's VENERET: different noref slugs for the same VENZY).
9
9
  // The store therefore lives one level up, keyed by the MARK:
10
10
  //
11
- // <studioRoot>/_known-conflicts/<kebab(mark)>.json (studioRoot = workspace-<agent>/studio/prelim-search)
11
+ // <studioRoot>/_known-conflicts/<kebab(mark)>.json (studioRoot = workspace-<agent>/studio/clearance-search)
12
12
  //
13
13
  // One file per searched mark name; workspace-per-agent keeps customers separated. Each file keeps the
14
14
  // EXACT inner shape the tripwire already reads ({schema_version, marks:{"<mark key>":[rows]}}), so
@@ -141,6 +141,16 @@ export function renderMatterFrame(model) {
141
141
  // is owed against — so the frame states it where a reader can see it rather than only in a field.
142
142
  if ((model.ratified_forms ?? []).length > 1)
143
143
  out.push(`- **Ratified forms:** ${model.ratified_forms.join(", ")}`);
144
+ // THE EXCLUSION IS EVIDENCED ON THE DOCUMENT A READER SEES, and it is stated as a PROPOSAL because
145
+ // that is what it is at this point in the run. A reader meeting "excluded" here would believe the
146
+ // search was narrowed on the frame's authority; the driver's verification has not run yet, and if it
147
+ // cannot confirm the client's ownership the element is searched in full and this line is the only
148
+ // place the question was ever raised.
149
+ if (model.house_element_candidate)
150
+ out.push(`- **Client's own element, proposed for exclusion:** ${model.house_element_candidate.element}`
151
+ + ` — the analysis would be limited to ${model.house_element_candidate.remainder}.`
152
+ + ` Basis: ${model.house_element_candidate.owner_basis}.`
153
+ + " Excluded only if the driver confirms the client's own registrations on the register; otherwise searched in full.");
144
154
  out.push("");
145
155
 
146
156
  // `Search channels:` — domains only; the grid site-restricts to them and the general web is always
@@ -172,9 +182,10 @@ export function renderMatterFrame(model) {
172
182
  */
173
183
  /** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
174
184
  const DECLARED = Object.freeze({
175
- "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms"],
185
+ "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms", "house_element_candidate"],
176
186
  intake_asks: ["ask", "owner"],
177
187
  identified_classes: ["class", "reason"],
188
+ house_element_candidate: ["element", "remainder", "owner_basis"],
178
189
  });
179
190
 
180
191
  /** Refuse an undeclared key by path, at depth. Shared walk; the table above is what is this tool's. */
@@ -218,6 +229,26 @@ export function frameRatifiedForms(runDir) {
218
229
  return (Array.isArray(rows) ? rows : []).map((r) => String(r ?? "").trim()).filter(Boolean);
219
230
  }
220
231
 
232
+ /**
233
+ * The house element this run's frame PROPOSED, or null. IMPURE (reads the run's own accepted call).
234
+ *
235
+ * A PROPOSAL, AND THE CALLER MUST TREAT IT AS ONE. Nothing here has been checked against the register:
236
+ * the frame runs before the plan, holds no band tool, and is reporting how it reads the matter. The
237
+ * caller verifies ownership by owner-scoped lookup and writes its own receipt; the plan excludes on
238
+ * that receipt. A caller that excluded on this return would be dropping an element from the search on a
239
+ * model's say-so, which is the one direction that reaches a client as a clean answer over unswept
240
+ * ground rather than as a visible failure.
241
+ *
242
+ * Null on every archived and replayed run whose accepted call predates the field, so none of them moves.
243
+ */
244
+ export function frameHouseElementCandidate(runDir) {
245
+ const h = lastAcceptedMatterFrame(runDir)?.house_element_candidate;
246
+ const element = String(h?.element ?? "").trim();
247
+ const remainder = String(h?.remainder ?? "").trim();
248
+ if (!element || !remainder) return null;
249
+ return { element, remainder, owner_basis: String(h?.owner_basis ?? "").trim() };
250
+ }
251
+
221
252
  /** The last ACCEPTED call for this run, or null. */
222
253
  export function lastAcceptedMatterFrame(runDir) {
223
254
  return lastAccepted(matterFrameCallPaths(String(runDir ?? "")).accepted, readFileSync);
@@ -252,6 +283,15 @@ export function mergeMatterFrameCall(stored, received) {
252
283
  // omission here is a repair that did not mention them, never a decision to withdraw them.
253
284
  identified_classes: keepIfAbsent(received?.identified_classes, base.identified_classes),
254
285
  ratified_forms: keepIfAbsent(received?.ratified_forms, base.ratified_forms),
286
+ // KEEP-IF-ABSENT, and the direction of its failure is the OPPOSITE of the two above — which is
287
+ // worth saying, because the reasoning that protects them does not transfer and a reader who assumed
288
+ // it did would mis-rank this key. Dropping the identified classes NARROWS the next compile, towards
289
+ // missing rights. Dropping this one WIDENS it: the house element goes back to being searched as a
290
+ // conflict axis, which is the band this field exists to shrink, so a partial call that lost it costs
291
+ // a slower run and the report's one sentence explaining what was excluded and why — never coverage.
292
+ // It is kept because a repair turn that did not mention the element is not a withdrawal of it, which
293
+ // is the same rule, reached by a different road.
294
+ house_element_candidate: keepIfAbsent(received?.house_element_candidate, base.house_element_candidate),
255
295
  };
256
296
  }
257
297
 
@@ -346,6 +386,54 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
346
386
  ratified_forms.push(form);
347
387
  }
348
388
 
389
+ // ── THE CLIENT'S OWN HOUSE ELEMENT — A CANDIDATE, NEVER A DECISION ───────────────────────────────
390
+ //
391
+ // THE DEFECT (production run, 2026-09-16). The mark was the client's own famous house mark followed by
392
+ // a tagline, and the plan treated the house element as a conflict axis: exact, variants, one-letter
393
+ // mutations, transliterations, incumbent checks. Over half the band came from that element. The
394
+ // reviewing lawyer's method for the same matter was three queries — the whole phrase, the shorter
395
+ // phrase, the last word alone — because an element the client already owns outright is not what the
396
+ // analysis is about. The engine planned thirty-six.
397
+ //
398
+ // WHY THE FIELD IS NAMED `candidate`, AND WHY THE NAME IS LOAD-BEARING. This frame CANNOT verify
399
+ // ownership: `BAND_READING_STAGES` is placement-inquiry, register-digest and synthesis, and the band
400
+ // does not exist yet when the frame runs. So everything here is the seat's reading of the matter, and
401
+ // an exclusion taken on a seat's say-so is an unsearched element justified by an assertion — a clean
402
+ // report over ground nobody swept, which is the one failure that reaches a client as a wrong answer
403
+ // rather than as no answer. The driver verifies against the register by owner and writes the receipt;
404
+ // the plan excludes on the RECEIPT and never on this field. Requirement 4 ("when the frame cannot
405
+ // verify, it does not exclude, and says so") is then the write order rather than a branch someone has
406
+ // to remember: no receipt, no exclusion.
407
+ //
408
+ // TYPED RATHER THAN PARSED, for the reason `identified_classes` gives and measures: a list derived
409
+ // from judgment prose dropped the primary entry in 19 of 21 runs.
410
+ let house_element_candidate = null;
411
+ if (params?.house_element_candidate !== undefined && params?.house_element_candidate !== null) {
412
+ const h = params.house_element_candidate;
413
+ const element = str(h?.element), remainder = str(h?.remainder), owner_basis = str(h?.owner_basis);
414
+ if (!element)
415
+ return { ok: false, reason: "matterframe_house_element_empty: name the element of the mark the client already owns, or omit the field entirely — a blank row is not an answer" };
416
+ if (!owner_basis)
417
+ return { ok: false, reason: `matterframe_house_element_basis_missing:${element} — say why you read this as the client's own registered element. It is not taken on your word (the driver verifies it against the register by owner), but the reader of the report is owed the ground, and an unverifiable basis is how a wrong exclusion would be argued for` };
418
+ // ── THE FLOOR, AND IT IS ON THE POPULATION RATHER THAN ON THE RULE ────────────────────────────
419
+ //
420
+ // The catastrophic direction here is naming too MUCH as the house element: mark "ACME WIDGETS",
421
+ // element "ACME WIDGETS", remainder nothing — and the plan becomes three queries that do not exist.
422
+ // Ownership can verify perfectly in that case, so requirement 4 does not catch it and no refusal
423
+ // downstream would either: "queries on the house element: 0" is satisfied by a plan with no queries
424
+ // at all. So the remainder is checked for being something a search can be built on, here, where the
425
+ // claim is made.
426
+ if (!remainder)
427
+ return { ok: false, reason: `matterframe_house_element_no_remainder:${element} — excluding it would leave nothing to search. The remainder is what the analysis is about; if the mark IS the client's own element with nothing distinctive after it, there is no exclusion to make and the field is omitted` };
428
+ if (remainder.toLowerCase() === element.toLowerCase())
429
+ return { ok: false, reason: `matterframe_house_element_remainder_same:${element} — the remainder must be the part of the mark that is NOT the house element` };
430
+ // The whole mark cannot be the house element by another spelling: an element that swallows the
431
+ // remainder leaves the same empty plan, arriving as two fields that merely look different.
432
+ if (element.toLowerCase().includes(remainder.toLowerCase()))
433
+ return { ok: false, reason: `matterframe_house_element_swallows_remainder:${element} — the element you named contains the remainder, so excluding it excludes the whole mark` };
434
+ house_element_candidate = { element, remainder, owner_basis };
435
+ }
436
+
349
437
  const model = {
350
438
  schema_version: SCHEMA_VERSION,
351
439
  instructed_scope: instructedScope ?? null,
@@ -358,6 +446,7 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
358
446
  intake_asks,
359
447
  identified_classes,
360
448
  ratified_forms,
449
+ house_element_candidate,
361
450
  };
362
451
  return { ok: true, model, content: renderMatterFrame(model) };
363
452
  }
@@ -137,7 +137,7 @@ export function parseNamedBand(raw) {
137
137
  // byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
138
138
  // run: four capability-gap blocks carried `error:true, deferred:true` into this function and
139
139
  // reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
140
- // COUNT for this slice". register-plan.mjs:1439 validatePlanFeasibility already enforces the same rule one layer up
140
+ // COUNT for this slice". register-plan.mjs:1594 validatePlanFeasibility already enforces the same rule one layer up
141
141
  // ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
142
142
  // RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
143
143
  // Conditional like the four keys above, so old bands carry neither key and nothing shifts.