clearotron 0.3.1-beta.2 → 0.3.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/bin/grant.mjs +21 -22
  2. package/build-info.json +2 -2
  3. package/driver/CHANGELOG.md +33 -0
  4. package/driver/citation-census.json +3 -3
  5. package/driver/common-law-receipts.mjs +11 -1
  6. package/driver/connotation-search.mjs +56 -5
  7. package/driver/contract-e3-backlog.mjs +31 -31
  8. package/driver/contract-vocabulary.mjs +59 -58
  9. package/driver/engine/mcp/gather-config.mjs +1 -1
  10. package/driver/gateway.mjs +29 -2
  11. package/driver/matter-frame-record.mjs +54 -1
  12. package/driver/package.json +1 -1
  13. package/driver/portal-config-view.mjs +26 -1
  14. package/driver/portal-report.mjs +16 -2
  15. package/driver/portal-service.mjs +327 -8
  16. package/driver/publish/render.mjs +0 -45
  17. package/driver/publish/templates/report.css +1 -30
  18. package/driver/skills/knockout-assess/SKILL.md +35 -6
  19. package/driver/skills/knockout-frame/SKILL.md +2 -1
  20. package/driver/skills/prelim-search/risk-framework-triage.md +3 -3
  21. package/driver/stages.mjs +19 -5
  22. package/driver/suite-census.json +112 -16
  23. package/driver/verify-knockout.mjs +58 -0
  24. package/driver/verify.mjs +83 -7
  25. package/mcp-server/CHANGELOG.md +11 -0
  26. package/mcp-server/lib/audit.mjs +96 -3
  27. package/mcp-server/lib/http-handler.mjs +76 -3
  28. package/mcp-server/lib/runs.mjs +42 -2
  29. package/mcp-server/package.json +1 -1
  30. package/mcp-server/packs/client/CONNECT.md +10 -6
  31. package/mcp-server/server.mjs +37 -3
  32. package/package.json +1 -1
  33. package/portal-ui/dist/assets/{index-D8ITW-aD.js → index-ChIQsMYp.js} +1375 -817
  34. package/portal-ui/dist/assets/{index-D5WAoLZI.css → index-DBIs21e4.css} +37 -0
  35. package/portal-ui/dist/index.html +2 -2
  36. package/portal-ui/package.json +1 -1
  37. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  38. package/providers/oauth-mcp-bridge/package.json +1 -1
  39. package/scripts/ask-ai-render-check.mjs +359 -0
  40. package/scripts/citation-line-check.mjs +53 -2
  41. package/scripts/deprecate-below.mjs +146 -0
  42. package/scripts/test-run.mjs +24 -1
  43. package/shared/connect-clients.mjs +52 -16
  44. package/shared/driver-dir.mjs +1 -1
  45. package/shared/grants-edit.mjs +99 -2
@@ -15,9 +15,9 @@
15
15
  // several times that, because EIGHT sites build the token dynamically and hand back a string the
16
16
  // extractor never sees:
17
17
  //
18
- // D1 verify.mjs:898 fail(`connotation_${reason}:…`) — reason iterates the table at line 894
18
+ // D1 verify.mjs:940 fail(`connotation_${reason}:…`) — reason iterates the table at line 894
19
19
  // D2 verify.mjs:704 fail(String(e.message)) — parseFindingsJson throws token-first
20
- // D3 verify.mjs:1123 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
20
+ // D3 verify.mjs:1165 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
21
21
  // D4 verify.mjs:1603 parseCoverageLedgerJson, same shape
22
22
  // D5 verify.mjs:1504 fail(`${unaccounted[0].token}:…`) — token minted in a DATA ROW
23
23
  // D6 verify.mjs:1567 fail(`${violations[0].token}…`) — validatePlanFeasibility in register-plan.mjs
@@ -68,51 +68,52 @@ export const VOCABULARY = [
68
68
  { token: "missing", stages: ["matter-frame", "prelim-variants", "common-law", "common-law-half", "placement-inquiry", "register-digest", "doubt-closure", "report-overview", "report-card"], site: "driver/verify.mjs:164" },
69
69
 
70
70
  // ── common-law / common-law-half ───────────────────────────────────────────────────────────────────
71
- { token: "declared_unavailable", stages: CL, site: "driver/verify.mjs:231" },
71
+ { token: "declared_unavailable", stages: CL, site: "driver/verify.mjs" },
72
72
  { token: "grid_spec_unreadable", stages: CL, site: "driver/verify.mjs:257" },
73
73
  { token: "grid_ledger_missing", stages: CL, site: "driver/verify.mjs:297" },
74
74
  { token: "grid_ledger_unparseable", stages: CL, site: "driver/verify.mjs:266" },
75
- { token: "connotation_query_unrecorded", stages: ["common-law-half"], site: "driver/verify.mjs:269" },
76
- { token: "profile_unparseable", stages: ["common-law"], site: "driver/verify.mjs:301" },
75
+ { token: "connotation_query_unrecorded", stages: ["common-law-half"], site: "driver/verify.mjs" },
76
+ { token: "profile_unparseable", stages: ["common-law"], site: "driver/verify.mjs" },
77
77
  { token: "grid_join_missing", stages: CL, site: "driver/verify.mjs:318" },
78
78
  { token: "platform_identity_error", stages: CL, site: "driver/verify.mjs:323" },
79
79
  { token: "platforms_missing", stages: CL, site: "driver/verify.mjs:334" },
80
- { token: "connotation_search_missing", stages: ["common-law"], site: "driver/verify.mjs:362" },
81
- { token: "receipts_short", stages: ["common-law"], site: "driver/verify.mjs:388" },
80
+ { token: "connotation_search_missing", stages: ["common-law"], site: "driver/verify.mjs" },
81
+ { token: "receipts_short", stages: ["common-law"], site: "driver/verify.mjs" },
82
82
  { token: "half_path_unrecognized", stages: ["common-law-half"], site: "driver/verify.mjs:395" },
83
83
  { token: "no_coverage_status_row", stages: [...CL, "register-digest"], site: "driver/verify.mjs:452, 1416" },
84
84
  // B — `connotation_form_unparseable` and `connotation_form_untouched` are GONE: they
85
85
  // were states only a hand-authored document could be in, and the form path is deleted (ruling
86
86
  // 2026-08-17). Their subject matter is carried by the call tokens under the D1 family row below.
87
- { token: "connotation_form_damaged", stages: CL, site: "driver/verify.mjs:889" },
88
- { token: "connotation_", stages: CL, site: "driver/verify.mjs:898", family: "driver/connotation-search.mjs:894 (CONNOTATION_FORM_REASONS, exported at :819)", dynamic: "D1" },
89
- { token: "connotation_quote_unbound", stages: CL, site: "driver/verify.mjs:914" },
87
+ { token: "connotation_form_damaged", stages: CL, site: "driver/verify.mjs" },
88
+ { token: "connotation_", stages: CL, site: "driver/verify.mjs", family: "driver/connotation-search.mjs CONNOTATION_FORM_REASONS", dynamic: "D1" },
89
+ { token: "connotation_quote_unbound", stages: CL, site: "driver/verify.mjs" },
90
90
 
91
91
  // ── synthesis ──────────────────────────────────────────────────────────────────────────────────────
92
92
  { token: "framework_manifest_unreadable", stages: ["synthesis"], site: "driver/verify.mjs:691" },
93
93
  { token: "framework_manifest_missing_for_v4", stages: ["synthesis"], site: "driver/verify.mjs:706" },
94
- { token: "finding_use_check_source_missing", stages: ["synthesis"], site: "driver/verify.mjs:713" },
95
- { token: "finding_own_rights_source_missing", stages: ["synthesis"], site: "driver/verify.mjs:715" },
96
- { token: "finding_basis_source_missing", stages: ["synthesis"], site: "driver/verify.mjs:779" },
97
- { token: "finding_use_check_missing", stages: ["synthesis"], site: "driver/verify.mjs:729" },
98
- { token: "intake_ask_unanswered", stages: ["synthesis"], site: "driver/verify.mjs:1636" },
99
- { token: "coverage_recommendation", stages: ["synthesis"], site: "driver/verify.mjs:1648" },
100
- { token: "coverage_gap_unexplained", stages: ["synthesis"], site: "driver/verify.mjs:1649" },
94
+ { token: "finding_use_check_source_missing", stages: ["synthesis"], site: "driver/verify.mjs" },
95
+ { token: "finding_own_rights_source_missing", stages: ["synthesis"], site: "driver/verify.mjs" },
96
+ { token: "finding_basis_source_missing", stages: ["synthesis"], site: "driver/verify.mjs" },
97
+ { token: "finding_use_check_missing", stages: ["synthesis"], site: "driver/verify.mjs" },
98
+ { token: "intake_ask_unanswered", stages: ["synthesis"], site: "driver/verify.mjs" },
99
+ { token: "ratified_form_unread", stages: ["synthesis"], site: "driver/verify.mjs" },
100
+ { token: "coverage_recommendation", stages: ["synthesis"], site: "driver/verify.mjs" },
101
+ { token: "coverage_gap_unexplained", stages: ["synthesis"], site: "driver/verify.mjs" },
101
102
  { token: "finding", stages: ["synthesis"], site: "driver/verify.mjs:704", family: "driver/findings-model.mjs (token-first throws; `finding_*` and `findings_*`)", dynamic: "D2" },
102
103
 
103
104
  // ── register-digest ────────────────────────────────────────────────────────────────────────────────
104
- { token: "coverage_form_damaged", stages: ["register-digest"], site: "driver/verify.mjs:976" },
105
- { token: "coverage_form_engine_vocabulary", stages: ["register-digest"], site: "driver/verify.mjs:1021" },
106
- { token: "coverage_form_axis_invalid", stages: ["register-digest"], site: "driver/verify.mjs:1029" },
107
- { token: "coverage_no_status", stages: ["register-digest"], site: "driver/verify.mjs:1062" },
108
- { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs:1445" },
109
- { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs:1449" },
105
+ { token: "coverage_form_damaged", stages: ["register-digest"], site: "driver/verify.mjs" },
106
+ { token: "coverage_form_engine_vocabulary", stages: ["register-digest"], site: "driver/verify.mjs" },
107
+ { token: "coverage_form_axis_invalid", stages: ["register-digest"], site: "driver/verify.mjs" },
108
+ { token: "coverage_no_status", stages: ["register-digest"], site: "driver/verify.mjs" },
109
+ { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
110
+ { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
110
111
  { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
111
112
  { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1504 coverageFormFail", family: "driver/register-plan.mjs:1642 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
112
113
  { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1417 validatePlanFeasibility", dynamic: "D6" },
113
114
  { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1766 searchedJurisdictionsFromPlan", dynamic: "D6" },
114
115
  { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2018 disclosureTextByAxis", dynamic: "D7" },
115
- { token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs:1578" },
116
+ { token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs" },
116
117
  { token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
117
118
  { token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
118
119
  { token: "coverage_axis_", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
@@ -122,14 +123,14 @@ export const VOCABULARY = [
122
123
 
123
124
  // ── matter-frame / prelim-variants / blind-frame / frame-diff ──────────────────────────────────────
124
125
  { token: "stagecontracts_invalid", stages: ["matter-frame", "prelim-variants", "placement-inquiry", "case-law"], site: "driver/verify.mjs:1539, 1170, 1366, 1677" },
125
- { token: "meaning_angles_missing", stages: ["matter-frame"], site: "driver/verify.mjs:1103" },
126
+ { token: "meaning_angles_missing", stages: ["matter-frame"], site: "driver/verify.mjs" },
126
127
  { token: "frame_scope_missing", stages: ["matter-frame"], site: "driver/verify.mjs:1119" },
127
- { token: "variantmodel_romanization_missing", stages: ["prelim-variants"], site: "driver/verify.mjs:1237" },
128
- { token: "variantmodel_family_incomplete", stages: ["prelim-variants"], site: "driver/verify.mjs:1196" },
129
- { token: "variantmodel_term_markup", stages: ["prelim-variants"], site: "driver/verify.mjs:1263" },
128
+ { token: "variantmodel_romanization_missing", stages: ["prelim-variants"], site: "driver/verify.mjs" },
129
+ { token: "variantmodel_family_incomplete", stages: ["prelim-variants"], site: "driver/verify.mjs" },
130
+ { token: "variantmodel_term_markup", stages: ["prelim-variants"], site: "driver/verify.mjs" },
130
131
  { token: "variantmodel_missing", stages: ["prelim-variants"], site: "driver/verify.mjs:1153, 1212" },
131
132
  // Recovered during E2 authoring, absent from the draft census: variant-manifest.json is strict-parsed
132
- // through checkSiblingJson (verify.mjs:1136) → checkJson (:742), so the WHOLE variantmodel_* family
133
+ // through checkSiblingJson (verify.mjs:1178) → checkJson (:742), so the WHOLE variantmodel_* family
133
134
  // reaches prelim-variants, not just the four literal tokens above.
134
135
  // CONVERSION 3 widened this family's SOURCE without widening its prefix. `acceptPrelimVariants` raises
135
136
  // `variantmodel_scope_layer_invalid`, `_scope_status_invalid`, `_scope_item_missing` and `_scope_pipe`
@@ -142,7 +143,7 @@ export const VOCABULARY = [
142
143
  // `{ok: false, reason}` instead — measured, all four extract ZERO tokens. So this row is authored, not
143
144
  // extracted, and nothing re-derives it if the module grows a member. Filed as.
144
145
  { token: "variantmodel_", stages: ["prelim-variants"], site: "driver/verify.mjs:1153 → 742 (JSON family); driver/prelim-variants-record.mjs acceptPrelimVariants (scope-ledger transport family)", family: "driver/variant-manifest-model.mjs (token-first throws) + driver/prelim-variants-record.mjs", dynamic: "D3" },
145
- { token: "blindframe_", stages: ["blind-frame"], site: "driver/verify.mjs:1221 → 742", family: "driver/blind-frame-model.mjs", dynamic: "D3" },
146
+ { token: "blindframe_", stages: ["blind-frame"], site: "driver/verify.mjs blindFrame → checkJson", family: "driver/blind-frame-model.mjs", dynamic: "D3" },
146
147
  // — THE SKEPTIC TRANSPORT FAMILY, WHICH HAD NO ROW AT ALL. Nine tokens minted by acceptSkeptic
147
148
  // and not one of them was covered here: the conversion that moved them to the acceptance boundary moved
148
149
  // them out of a census that could not read that boundary, so nothing went red. Two of the nine are
@@ -164,16 +165,16 @@ export const VOCABULARY = [
164
165
  // the frame never reaches disk. They are declared here for the same reason the two families below are —
165
166
  // verify.mjs reaches the module, so a family that could grow a member no row covers must be readable.
166
167
  { token: "matterframe_", stages: ["matter-frame"], site: "driver/matter-frame-record.mjs acceptMatterFrame", family: "driver/matter-frame-record.mjs", dynamic: "D3" },
167
- { token: "framediff_model_missing", stages: ["frame-diff"], site: "driver/verify.mjs:1224" },
168
+ { token: "framediff_model_missing", stages: ["frame-diff"], site: "driver/verify.mjs" },
168
169
  { token: "framediff_", stages: ["frame-diff"], site: "driver/verify.mjs:1224 → 742", family: "driver/frame-diff-model.mjs", dynamic: "D3" },
169
170
 
170
171
  // ── register-unit ──────────────────────────────────────────────────────────────────────────────────
171
- { token: "declared_not_executed", stages: ["register-unit"], site: "driver/verify.mjs:1236" },
172
+ { token: "declared_not_executed", stages: ["register-unit"], site: "driver/verify.mjs" },
172
173
  { token: "tool_timeout", stages: ["register-unit"], site: "driver/verify.mjs:1788" },
173
- { token: "named_band_missing", stages: ["register-unit"], site: "driver/verify.mjs:1731" },
174
- { token: "named_band_invalid", stages: ["register-unit"], site: "driver/verify.mjs:1736" },
175
- { token: "named_band_collapsed", stages: ["register-unit"], site: "driver/verify.mjs:1747" },
176
- { token: "band_block_unplanned", stages: ["register-unit"], site: "driver/verify.mjs:1761" },
174
+ { token: "named_band_missing", stages: ["register-unit"], site: "driver/verify.mjs" },
175
+ { token: "named_band_invalid", stages: ["register-unit"], site: "driver/verify.mjs" },
176
+ { token: "named_band_collapsed", stages: ["register-unit"], site: "driver/verify.mjs" },
177
+ { token: "band_block_unplanned", stages: ["register-unit"], site: "driver/verify.mjs" },
177
178
  { token: "named_band_", stages: ["register-unit"], site: "driver/verify.mjs:1736 → 742", family: "driver/named-band.mjs", dynamic: "D3" },
178
179
 
179
180
  // ── placement-inquiry ──────────────────────────────────────────────────────────────────────────────
@@ -182,10 +183,10 @@ export const VOCABULARY = [
182
183
 
183
184
  // ── case-law / narrative-refutation ────────────────────────────────────────────────────────────────
184
185
  { token: "caselaw_ledger_missing", stages: ["case-law"], site: "driver/verify.mjs:1682, 1685" },
185
- { token: "caselaw_ledger_unparseable", stages: ["case-law"], site: "driver/verify.mjs:1745" },
186
+ { token: "caselaw_ledger_unparseable", stages: ["case-law"], site: "driver/verify.mjs" },
186
187
  { token: "caselaw_ledger", stages: ["case-law"], site: "driver/verify.mjs:1692", family: "driver/case-law-ledger.mjs:204 (census reasons)", dynamic: "D8" },
187
- { token: "no_verdict_line", stages: ["narrative-refutation"], site: "driver/verify.mjs:1754" },
188
- { token: "plan_audit_missing", stages: ["narrative-refutation"], site: "driver/verify.mjs:1771" },
188
+ { token: "no_verdict_line", stages: ["narrative-refutation"], site: "driver/verify.mjs" },
189
+ { token: "plan_audit_missing", stages: ["narrative-refutation"], site: "driver/verify.mjs" },
189
190
 
190
191
  // ── delivery ───────────────────────────────────────────────────────────────────────────────────────
191
192
  // EMPTY SINCE, and that is the finding rather than an omission: the only delivery-side token was
@@ -193,7 +194,7 @@ export const VOCABULARY = [
193
194
  // its own send. Delivery is code and a packet now, so there is no stage to emit a delivery token.
194
195
 
195
196
  // ── the gateway wrapper, emittable by EVERY stage and not in verify.mjs at all ─────────────────────
196
- { token: "missing_file", stages: ALL_STAGES, site: "driver/gateway.mjs:1037" },
197
+ { token: "missing_file", stages: ALL_STAGES, site: "driver/gateway.mjs" },
197
198
  ];
198
199
 
199
200
  /**
@@ -299,9 +300,9 @@ export const ARM1_EXEMPTIONS = [
299
300
  * than silently enjoyed, and so a validator that LATER gains a stage is noticed.
300
301
  */
301
302
  export const STAGE_UNREACHABLE_VALIDATORS = [
302
- { validator: "findings", site: "driver/verify.mjs:1661", reason: "no STAGES entry names it; synthesis validates findings.json through validators.narrative's sibling checks instead" },
303
- { validator: "report", site: "driver/verify.mjs:1719", reason: "report.md is ASSEMBLED by the driver (assembleReportMd); the validator survives as a post-assembly structural gate outside the stage table" },
304
- { validator: "audit", site: "driver/verify.mjs:1728", reason: "reached only through the MCP server's dynamic validators[key] lookup, which declares it required" },
303
+ { validator: "findings", site: "driver/verify.mjs validators.findings", reason: "no STAGES entry names it; synthesis validates findings.json through validators.narrative's sibling checks instead" },
304
+ { validator: "report", site: "driver/verify.mjs validators.report", reason: "report.md is ASSEMBLED by the driver (assembleReportMd); the validator survives as a post-assembly structural gate outside the stage table" },
305
+ { validator: "audit", site: "driver/verify.mjs validators.audit", reason: "reached only through the MCP server's dynamic validators[key] lookup, which declares it required" },
305
306
  { validator: "clientSummary", site: "driver/verify.mjs:1736", reason: "the client-summary STAGE was deleted 2026-08-01 (no line to cite: the stage is gone); the validator was not" },
306
307
  ];
307
308
 
@@ -415,33 +416,33 @@ export const TRIPWIRE_OUT_OF_SCOPE = [
415
416
  export const INNER_CODES = Object.freeze([
416
417
  // ── connotation-search.mjs — namespaced one-for-one by verify.mjs ────────────────────────────────
417
418
  //
418
- // `CONNOTATION_FORM_REASONS` (connotation-search.mjs:1599) is `CONNOTATION_REASONS` minus
419
+ // CONNOTATION_FORM_REASONS at connotation-search.mjs:1731 is `CONNOTATION_REASONS` minus
419
420
  // `no_recorded_queries`, and the `connotation_` family row (dynamic D1) is declared against exactly
420
421
  // that list. The four call codes are folded by a template — `connotation_${callFail.reason}` at
421
422
  // verify.mjs:1013 — so they are namespaced by construction rather than one branch at a time.
422
- { code: "call_never_made", mints: ["driver/connotation-search.mjs:1956"], rollsUpTo: ["connotation_call_never_made"],
423
+ { code: "call_never_made", mints: ["driver/connotation-search.mjs:2007"], rollsUpTo: ["connotation_call_never_made"],
423
424
  why: "CALL_AUDIT_ROWS. The typed transport's four call states, handed in by disposition-call-audit.mjs and namespaced at verify.mjs:1013." },
424
- { code: "call_truncated", mints: ["driver/connotation-search.mjs:1957"], rollsUpTo: ["connotation_call_truncated"],
425
+ { code: "call_truncated", mints: ["driver/connotation-search.mjs:2008"], rollsUpTo: ["connotation_call_truncated"],
425
426
  why: "As call_never_made — same table, same projection." },
426
- { code: "call_schema_violation", mints: ["driver/connotation-search.mjs:1958"], rollsUpTo: ["connotation_call_schema_violation"],
427
+ { code: "call_schema_violation", mints: ["driver/connotation-search.mjs:2009"], rollsUpTo: ["connotation_call_schema_violation"],
427
428
  why: "As call_never_made — same table, same projection." },
428
- { code: "call_partial", mints: ["driver/connotation-search.mjs:1959"], rollsUpTo: ["connotation_call_partial"],
429
+ { code: "call_partial", mints: ["driver/connotation-search.mjs:2010"], rollsUpTo: ["connotation_call_partial"],
429
430
  why: "As call_never_made — same table, same projection. This is the pair #1211 cites as its worked example: the composite is covered, the bare form reaches no stage." },
430
- { code: "quote_unbound", mints: ["driver/connotation-search.mjs:2090"], rollsUpTo: ["connotation_quote_unbound"],
431
- why: "The ruled-but-unbound row. Projected at verify.mjs:1057, and reported only once nothing is unruled." },
432
- { code: "token_absent", mints: ["driver/connotation-search.mjs:2117"], rollsUpTo: ["connotation_token_absent"],
431
+ { code: "quote_unbound", mints: ["driver/connotation-search.mjs:2141"], rollsUpTo: ["connotation_quote_unbound"],
432
+ why: "The ruled-but-unbound row. Projected at verify.mjs:1099, and reported only once nothing is unruled." },
433
+ { code: "token_absent", mints: ["driver/connotation-search.mjs:2168"], rollsUpTo: ["connotation_token_absent"],
433
434
  why: "#592 split this out of no_ruling. Row-level only — repairs.mjs:352 says so in as many words: `never a top-level token`." },
434
- { code: "cite_absent", mints: ["driver/connotation-search.mjs:2126"], rollsUpTo: ["connotation_cite_absent"],
435
+ { code: "cite_absent", mints: ["driver/connotation-search.mjs:2177"], rollsUpTo: ["connotation_cite_absent"],
435
436
  why: "#592, as token_absent. repairs.mjs:353: `row-level only — never a top-level token`." },
436
- { code: "no_ruling", mints: ["driver/connotation-search.mjs:2133"], rollsUpTo: ["connotation_no_ruling"],
437
+ { code: "no_ruling", mints: ["driver/connotation-search.mjs:2184"], rollsUpTo: ["connotation_no_ruling"],
437
438
  why: "The residual the other two split off from, and it has representatives — see the mint site. Declared as a corrective token at repairs.mjs:308 and in stages.mjs's E1 rows, always namespaced." },
438
439
  // THE ONE THAT IS RENAMED, NOT NAMESPACED — and it is the reason this table stores the composite as
439
440
  // data rather than deriving it. Every derivation anyone would write is `connotation_` + the code, and
440
441
  // for this row that produces `connotation_no_recorded_queries`, which nothing mints and nothing covers.
441
442
  // verify.mjs:912-913 states the ruling: a sweep that did not RUN is a canonical-only decision with its
442
443
  // own token and its own remedy, so the projector at verify.mjs:909 deliberately does not handle it.
443
- { code: "no_recorded_queries", mints: ["driver/connotation-search.mjs:1974"], rollsUpTo: ["connotation_search_missing"],
444
- why: "RENAMED, not namespaced: verify.mjs:398 emits `connotation_search_missing`. It is excluded from CONNOTATION_FORM_REASONS at connotation-search.mjs:1680 for exactly this reason." },
444
+ { code: "no_recorded_queries", mints: ["driver/connotation-search.mjs:2025"], rollsUpTo: ["connotation_search_missing"],
445
+ why: "RENAMED, not namespaced: verify.mjs emits `connotation_search_missing`. It is excluded from CONNOTATION_FORM_REASONS at connotation-search.mjs:1731 for exactly this reason." },
445
446
 
446
447
  // ── case-law-ledger.mjs — ONE token, the codes as census payload ─────────────────────────────────
447
448
  //
@@ -471,8 +472,8 @@ export const INNER_CODES = Object.freeze([
471
472
 
472
473
  // ── coverage-form.mjs ─────────────────────────────────────────────────────────────────────────────
473
474
  //
474
- // ONE CODE, TWO TOKENS, and the split is by `cause` rather than by code: verify.mjs:1169 peels the
475
- // `axis_invalid` cause into its own family before verify.mjs:1236 counts the rest. A ruling naming only
475
+ // ONE CODE, TWO TOKENS, and the split is by `cause` rather than by code: verify.mjs peels the
476
+ // `axis_invalid` cause into its own family before counting the rest. A ruling naming only
476
477
  // `coverage_no_status` would be true of most `no_status` records and false of the ones that matter most.
477
478
  { code: "no_status", mints: ["driver/coverage-form.mjs:836"], rollsUpTo: ["coverage_no_status", "coverage_form_axis_invalid"],
478
479
  why: "Two composites, split on the record's `cause`: verify.mjs:1172 for cause `axis_invalid`, verify.mjs:1263 for the rest. NOT cited at coverage-form.mjs:810 — that is the JSDoc @returns annotation, not the mint." },
@@ -487,7 +488,7 @@ export const INNER_CODES = Object.freeze([
487
488
  // the census output would have been half a ruling. verify.mjs:1075 already states the rule this row
488
489
  // records: `coverage_form_damaged`, never a bare `form_damaged`.
489
490
  { code: "form_damaged",
490
- mints: ["driver/connotation-search.mjs:1979", "driver/connotation-search.mjs:2055", "driver/coverage-form.mjs:818"],
491
+ mints: ["driver/connotation-search.mjs:2030", "driver/connotation-search.mjs:2106", "driver/coverage-form.mjs:818"],
491
492
  rollsUpTo: ["connotation_form_damaged", "coverage_form_damaged"],
492
493
  why: "Minted in two lanes and namespaced per lane: verify.mjs:1019 for the meaning sweep, verify.mjs:1119 for the register digest. The namespacing is what keeps them apart — see verify.mjs:1075." },
493
494
  ]);
@@ -89,7 +89,7 @@ export const REGISTER_SERVERS = {
89
89
  // mint is shared (mcp/supplemental.mjs); the only per-provider parts are doExecutePlan, which this
90
90
  // server already binds for the dictated plan, and the capability contract, which the mint reads to
91
91
  // decide what defers. Nothing new reaches the wire. While it was unwired the driver's composed prose
92
- // ordered the mint UNCONDITIONALLY — `stages.mjs:2095` and `:2131` — so a signa deploy was told to
92
+ // ordered the mint UNCONDITIONALLY — `stages.mjs` (the unconditional mint the register-units digest orders) — so a signa deploy was told to
93
93
  // make a call no server offered. Measured on over a real signa run: the order reached three
94
94
  // register-unit stages, no call was recorded against it, and the run still read `delivered`.
95
95
  //
@@ -2571,6 +2571,15 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2571
2571
  // — THE TOKEN THAT HAD NO HINT, and the absence is what made the failure
2572
2572
  // permanent rather than merely wrong.
2573
2573
  //
2574
+ // AND IT IS ONLY REACHABLE NOW WHEN A QUERY IS GENUINELY ABSENT. It used to be reachable when the
2575
+ // query was recorded in a different spelling, and then this hint asked for the one thing that could
2576
+ // not help: re-running a query that had already run and been recorded. Measured on a production
2577
+ // clearance, 2026-09-16 — one query of sixty-one carried a typographic apostrophe where the driver
2578
+ // wrote a straight one, the stage failed four times byte-identically, and the clearance stopped. The
2579
+ // join compares on `queryKey` now, so a re-run is the right remedy again; the closing sentence below
2580
+ // asks for the dictated spelling, which is what stops the ledger drifting from the list a second
2581
+ // time.
2582
+ //
2574
2583
  // Measured on the owner's own demo clearance: 61 dictated meaning queries, 60 recorded, ONE dropped
2575
2584
  // — the single query in the whole sweep that found nothing. Four attempts, the same refusal string
2576
2585
  // byte for byte, because a retry with no guidance can only repeat itself.
@@ -2585,13 +2594,31 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2585
2594
  // gate cannot tell "ran, found nothing" from "never ran", and it is not being asked to. What
2586
2595
  // changes is that the seat is now told how to say the first one.
2587
2596
  const dropped = (lastFail.match(/connotation_query_unrecorded:(.+)$/s) || [])[1] || "";
2588
- hint = `every dictated meaning query owes a row, including the ones that find nothing — these are ` +
2597
+ // ── TWO FAULTS, TWO REMEDIES, and sending the wrong one is what made this permanent ────────────
2598
+ //
2599
+ // The gate marks each dropped query `[absent from the ledger]` or `[unmatched; nearest recorded: …]`.
2600
+ // Absent means the search did not run and must. Unmatched means it DID run and the ledger's spelling
2601
+ // differs beyond punctuation — re-running it changes nothing, and telling a seat to re-run is how a
2602
+ // stage fails four times with the same string. Where both appear, both sentences are sent.
2603
+ const anyUnmatched = /\[unmatched; nearest recorded:/.test(dropped);
2604
+ const anyAbsent = /\[absent from the ledger\]/.test(dropped);
2605
+ hint = anyUnmatched && !anyAbsent
2606
+ ? `these dictated meaning queries ARE recorded in ${gridLedgerName} extras.pr_risk[] under a ` +
2607
+ `different wording, which is why the driver cannot match them: ${dropped}. Do NOT re-run them — ` +
2608
+ `the search already ran and its results are already in the ledger. EDIT each row's \`query\` ` +
2609
+ `field to the query text EXACTLY as the task message dictates it, character for character, and ` +
2610
+ `leave its results untouched. The driver matches your rows to its list by that text`
2611
+ : `every dictated meaning query owes a row, including the ones that find nothing — these are ` +
2589
2612
  `missing from ${gridLedgerName} extras.pr_risk[]: ${dropped}. A query that returned NO results is ` +
2590
2613
  `not an excuse to omit it: record it with an empty results array, which is the receipt that the ` +
2591
2614
  `search RAN and came back clean. That is the whole point of the sweep — on an "offensive meaning" ` +
2592
2615
  `query the empty answer IS the good news, and a missing row is indistinguishable from a search ` +
2593
2616
  `nobody performed. Re-run ONLY the listed queries, append a row per query to extras.pr_risk[] ` +
2594
- `whether or not it has hits, and leave every row already recorded exactly as it is`;
2617
+ `whether or not it has hits, and leave every row already recorded exactly as it is. Record each ` +
2618
+ `query's text EXACTLY as the task message dictates it — the driver matches your rows to its list by ` +
2619
+ `the query text, so a reworded or re-punctuated query reads as one you never ran. Where a query is ` +
2620
+ `marked \`[unmatched; nearest recorded: …]\` it is already in the ledger under that wording: edit ` +
2621
+ `that row's \`query\` to the dictated text rather than running the search again`;
2595
2622
  } else if (/connotation_search_missing/.test(lastFail)) {
2596
2623
  // — was "your PR / reputational section claims a clean meaning … but the ledger recorded ZERO
2597
2624
  // searches", which under the `ensure` prefix instructed the model to make the unbacked claim.
@@ -135,6 +135,12 @@ export function renderMatterFrame(model) {
135
135
  // to find extra classes, so finding none is the ordinary case and not an answer worth a row.
136
136
  for (const c of (model.identified_classes ?? []))
137
137
  out.push(`- **Class ${c.class} — identified by the frame:** ${c.reason}`);
138
+ // THE FORMS THE CLIENT RATIFIED, when there is more than one. One form is the ordinary case and
139
+ // renders nothing: a row saying the name is itself tells a reader nothing they did not have from the
140
+ // heading. Two or more is a fact about what was bought, and it is what the per-form read downstream
141
+ // is owed against — so the frame states it where a reader can see it rather than only in a field.
142
+ if ((model.ratified_forms ?? []).length > 1)
143
+ out.push(`- **Ratified forms:** ${model.ratified_forms.join(", ")}`);
138
144
  out.push("");
139
145
 
140
146
  // `Search channels:` — domains only; the grid site-restricts to them and the general web is always
@@ -166,7 +172,7 @@ export function renderMatterFrame(model) {
166
172
  */
167
173
  /** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
168
174
  const DECLARED = Object.freeze({
169
- "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes"],
175
+ "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms"],
170
176
  intake_asks: ["ask", "owner"],
171
177
  identified_classes: ["class", "reason"],
172
178
  });
@@ -194,6 +200,24 @@ export function frameIdentifiedClasses(runDir) {
194
200
  return (Array.isArray(rows) ? rows : []).map((r) => String(r?.class ?? "").trim()).filter(Boolean);
195
201
  }
196
202
 
203
+ /**
204
+ * The forms of the name this run's client ratified, as strings. IMPURE (reads the run's own accepted
205
+ * call). Empty or one form is the ordinary answer.
206
+ *
207
+ * WHAT READS IT IS A FLOOR, NOT A FEATURE. `validators.narrative` uses this to decide whether the
208
+ * narrative owes a per-form read, and it owes one only when the frame froze MORE THAN ONE. A run with
209
+ * no frame, a replayed or archived run whose accepted call predates the field, and every ordinary
210
+ * single-form run all return a list the floor ignores — so none of them changes behaviour and every one
211
+ * of them re-verifies byte-identically.
212
+ *
213
+ * THE TRIM IS NOT COSMETIC. A form is compared against the narrative's own text downstream, so a
214
+ * trailing space here would be a form the narrative can never be found to have named.
215
+ */
216
+ export function frameRatifiedForms(runDir) {
217
+ const rows = lastAcceptedMatterFrame(runDir)?.ratified_forms;
218
+ return (Array.isArray(rows) ? rows : []).map((r) => String(r ?? "").trim()).filter(Boolean);
219
+ }
220
+
197
221
  /** The last ACCEPTED call for this run, or null. */
198
222
  export function lastAcceptedMatterFrame(runDir) {
199
223
  return lastAccepted(matterFrameCallPaths(String(runDir ?? "")).accepted, readFileSync);
@@ -227,6 +251,7 @@ export function mergeMatterFrameCall(stored, received) {
227
251
  // NARROW THE SEARCH on the next compile, silently and in the direction that misses rights. An
228
252
  // omission here is a repair that did not mention them, never a decision to withdraw them.
229
253
  identified_classes: keepIfAbsent(received?.identified_classes, base.identified_classes),
254
+ ratified_forms: keepIfAbsent(received?.ratified_forms, base.ratified_forms),
230
255
  };
231
256
  }
232
257
 
@@ -294,6 +319,33 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
294
319
  identified_classes.push({ class: String(n), reason });
295
320
  }
296
321
 
322
+ // ── THE RATIFIED FORMS OF THE NAME, TYPED ─────────────────────────────────────────────────────────
323
+ //
324
+ // A client ratifies two forms of one candidate — the name and a slight twist on it — and both are
325
+ // searched. Until now the second form arrived only in the instruction PROSE, and nothing downstream
326
+ // could tell "two forms of one name" from "one name": `job.marks` with more than one entry means a
327
+ // BATCH, which is a different product. So the synthesis dictation asking for a per-form read had no
328
+ // deterministic floor under it: its arms matched the wording, which reds when the prose moves and
329
+ // cannot red when a model ignores the ask.
330
+ //
331
+ // TYPED RATHER THAN PARSED, for the reason the field above states and measures: a list derived from
332
+ // judgment prose dropped the PRIMARY entry in 19 of 21 runs. A field the frame fills in is a
333
+ // decision; a name scraped out of a sentence is a guess.
334
+ //
335
+ // ABSENT OR ONE FORM IS THE ORDINARY CASE and stays free: the floor that reads this is off unless
336
+ // there is more than one, so every archived run and every single-form run verifies exactly as before.
337
+ const ratified_forms = [];
338
+ for (const f of (Array.isArray(params?.ratified_forms) ? params.ratified_forms : [])) {
339
+ const form = str(f);
340
+ if (!form)
341
+ return { ok: false, reason: "matterframe_ratified_form_empty — a ratified form is the name as the client ratified it, never a blank row" };
342
+ // CASE-INSENSITIVE, because two rows differing only in case are one form written twice and the
343
+ // per-form read below would then be asked for a distinction the client never made.
344
+ if (ratified_forms.some((k) => k.toLowerCase() === form.toLowerCase()))
345
+ return { ok: false, reason: `matterframe_ratified_form_duplicate:${form} — one row per form` };
346
+ ratified_forms.push(form);
347
+ }
348
+
297
349
  const model = {
298
350
  schema_version: SCHEMA_VERSION,
299
351
  instructed_scope: instructedScope ?? null,
@@ -305,6 +357,7 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
305
357
  meaning_angles, meaning_angles_none,
306
358
  intake_asks,
307
359
  identified_classes,
360
+ ratified_forms,
308
361
  };
309
362
  return { ok: true, model, content: renderMatterFrame(model) };
310
363
  }
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.1-beta.2",
5
+ "version": "0.3.1-beta.4",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -43,6 +43,7 @@
43
43
 
44
44
  import { statSync, openSync, readSync, closeSync } from "node:fs";
45
45
  import { namedPoint } from "./portal-access.mjs";
46
+ import { recordedKeysFor } from "../shared/client-door.mjs"; // — how many issued keys a person holds, counted by the module that revokes them
46
47
 
47
48
  import { readFlagSnapshot, engineFor, providersFor, postureDisagreement } from "./flag-snapshot.mjs";
48
49
  // `isStale` is deliberately NOT imported any more: the age banner is retired (ruling,
@@ -282,7 +283,8 @@ export function authView({ mode = "", oidcIssuer = "", team = "", jwksUrl = "",
282
283
  * `companies` maps each company key the profile store holds to its name. `localSignIn` says this install
283
284
  * cannot hold a second person at all, which is what disables Add.
284
285
  */
285
- export function accessView({ grants, viewer = null, companies = {}, grantsFile = null, localSignIn = false }) {
286
+ export function accessView({ grants, viewer = null, companies = {}, grantsFile = null, localSignIn = false,
287
+ keysRevocable = false }) {
286
288
  const tenants = grants?.tenants ?? {};
287
289
  const everything = viewer?.everything === true;
288
290
  const viewerOrgs = viewer?.genericOrgs ?? [];
@@ -330,6 +332,23 @@ export function accessView({ grants, viewer = null, companies = {}, grantsFile =
330
332
  // whose permissions were considered and set to none.
331
333
  listed: key !== undefined,
332
334
  access: shown.map((p) => namedPoint(p, grants, companies)),
335
+ // WHETHER THIS ROW IS THE WHOLE OF THIS PERSON, from where the viewer stands. Every other field
336
+ // here is narrowed silently — a manager of one organisation sees that organisation's half of
337
+ // somebody and nothing says it is a half — and that was serviceable while the page could only
338
+ // ADD. It is not serviceable for a page that changes and removes: permissions belong to the
339
+ // person and not to a point, so a narrowed viewer must be told they cannot change them, and a
340
+ // removal they order must say it takes away their organisation rather than the install.
341
+ //
342
+ // The same test the write routes apply (`reachCovers`), computed here from the same `inside`
343
+ // that narrowed the list, so the sentence the page draws and the refusal the server would give
344
+ // cannot disagree.
345
+ covered: everything || all.every(inside),
346
+ // HOW MANY ISSUED KEYS THIS PERSON HOLDS — counted by the module that revokes them rather than by
347
+ // reading `connectKeys` here, so the count and the revocation cannot come to different answers
348
+ // about who a key belongs to. The removal confirmation needs it BEFORE the press: what happens to
349
+ // somebody's assistant is part of what the reader is agreeing to, and a page that found out
350
+ // afterwards would be telling them about it too late to matter.
351
+ keys: recordedKeysFor(grants, email).length,
333
352
  dangling: r.dangling,
334
353
  });
335
354
  }
@@ -339,6 +358,12 @@ export function accessView({ grants, viewer = null, companies = {}, grantsFile =
339
358
  // Add is offered to a manager, and never where local sign-in holds the install to one person.
340
359
  canAdd: viewer?.permissions?.manage === true && !localSignIn,
341
360
  localSignIn,
361
+ // Can an issued key be withdrawn on this installation at all? A connector started without a
362
+ // revocation list never loaded one, so a key minted through it cannot be called back and dies at its
363
+ // own expiry — `disablePlan` calls that state `lateArm` and refuses to strike the record for it.
364
+ // Stated here so the confirmation can say which of the two removals this would be before it happens,
365
+ // rather than correcting itself in the answer.
366
+ keysRevocable,
342
367
  // Companies named in grants that no profile matches — the other typo direction. Install-wide, so it
343
368
  // is shown to a person who sees everything and to nobody else.
344
369
  unknownAccounts: everything ? [...unknownAccounts].sort() : [],
@@ -799,14 +799,28 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
799
799
  let ratedUnderDropped = 0;
800
800
  out = out.replace(RATED_UNDER_RE, () => { ratedUnderDropped += 1; return ""; });
801
801
 
802
- // Staff keep their own connector block; a client must not see the staff host it points at.
802
+ // ── THE ASK-AI BAND COMES OUT FOR EVERY READER ──────────────────────────────────────────────────
803
+ //
804
+ // It used to come out for clients only, on the ground that the band names the STAFF host and staff may
805
+ // see it. That left two Ask-AI controls on one staff screen — this band with the staff address and its
806
+ // setup steps, and the shell's own header button — and the owner ruled on 2026-09-15 that there is one.
807
+ // The renderer stopped drawing it in that same change.
808
+ //
809
+ // SO THIS STRIP IS NOT DEAD CODE, AND THAT IS THE REASON IT RUNS UNCONDITIONALLY. Every report
810
+ // rendered before that commit is served from its baked bytes and still carries a band naming the staff
811
+ // host. Removing the strip on the argument that nothing emits one any more would put that host back in
812
+ // front of whoever opens an archived run, staff and client alike, and the archive is where most of the
813
+ // reports are.
803
814
  let mcpLeaks = 0;
804
815
  let internalTailsDropped = 0;
805
816
  let reviewerCodesDropped = 0;
817
+ out = stripBalanced(out, ASKAI_RE, "details", note).html;
806
818
  if (!staff) {
807
- out = stripBalanced(out, ASKAI_RE, "details", note).html;
808
819
  // Assert, do not assume. `.askband` markup is nested, so a non-greedy match could stop early and
809
820
  // leave the host behind in a sibling node. Anything surviving is redacted and counted.
821
+ //
822
+ // CLIENT-ONLY, and deliberately: this catches an MCP host anywhere in the document, not only inside
823
+ // the band, and the staff bytes elsewhere in a report are the designed rendering rather than a leak.
810
824
  out = out.replace(MCP_HOST_RE, () => {
811
825
  mcpLeaks += 1;
812
826
  return "";