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
@@ -35,7 +35,8 @@ mark) and the assessment turn; nothing you write here searches anything.
35
35
  integers only. Scope expansion is NEVER silent.
36
36
  4. **Context framing** per mark (MANDATORY): what does this name read as in THIS context — coined,
37
37
  compound, common phrase, cultural echo? ("Coop" = chicken coop for a poultry brand, not a
38
- cooperative.) The assessor's rating hangs off this framing.
38
+ cooperative.) It tells the assessor what the name reads as. It does not set the band — a framing
39
+ never raises or lowers a rating, and a risk belonging to the name is a finding of its own.
39
40
  5. **Prior knowledge** from the request/instructions: anything the forwarder said about a mark
40
41
  ("client loves this one", known history) — folded in verbatim, never invented.
41
42
  6. **Execution order**: common-word / known-problem marks first (they surface the batch's hard calls
@@ -23,17 +23,17 @@
23
23
 
24
24
  ### Manageable
25
25
 
26
- - **Triage read.** Minor findings, no strong claims. Flag major competitors/brand owners if relevant. The floor for any common English phrase.
26
+ - **Triage read.** Minor findings, no strong claims. Flag major competitors/brand owners if relevant.
27
27
 
28
28
  ### Low
29
29
 
30
- - **Triage read.** No issues found. Available only for coined/fanciful terms (calibration rule 3).
30
+ - **Triage read.** No issues found.
31
31
 
32
32
  ## Calibration rules (they override gut instinct — reviewer-derived)
33
33
 
34
34
  1. **Class-specific ratings are mandatory at Medium and above** — "Medium (Classes 9, 25, 28, 41)", never a bare band.
35
35
  2. **Crowded field = diluted risk, not amplified** — many small non-enforcing players typically caps risk at Medium.
36
- 3. **Common English phrases never rate Low** (Manageable floor + a purple register-pending note); **coined/fanciful terms CAN be Low**. The test: could a reasonable person use this word in everyday speech without reference to the proposed mark? No → coined → Low available. Yes → common phrase → Manageable floor.
36
+ 3. **RETIRED — an everyday name with nothing serious found can reach the bottom band** (owner, 2026-09-15). This read "common English phrases never rate Low"; it decided a band from how ordinary the words were, which no evidence supports and which the clearance lane's own instruction forbids in terms — the applicant's own-mark read is advisory and never moves a band. How ordinary or ownable the words are is a note for the reviewing lawyer. Anything that should lift a name is a finding of its own, rated as one.
37
37
  4. **Every rating is pending register overlay** — the standing caveat rides every summary.
38
38
  5. **Enforcer profiling matters** at Medium+ — portfolio brand? opposition history? institutional filer? "No assertive enforcers" can cap a Medium at "Medium (low)".
39
39
  6. **Client's prior use mitigates, never overrides** — rate the full external landscape first; note the mitigation in a purple bullet.
package/driver/stages.mjs CHANGED
@@ -1363,6 +1363,16 @@ export const STAGES = {
1363
1363
  // The driver STAMPS the instructed-scope section from _driver/instructed-scope.json, so the seat is
1364
1364
  // not asked to quote back values the driver wrote at intake. That retyping was the stage's
1365
1365
  // `frame_scope_missing` loop and it is gone; see matter-frame-record.mjs.
1366
+ // RATIFIED FORMS CARRY THEIR OWN IMPERATIVE, for the reason stated above this block: a field
1367
+ // phrased outside one was written 0 of 9 times against 74 of 74 when imperative-carried. Without
1368
+ // this sentence the typed field is never filled, and the floor under the per-form read in
1369
+ // validators.narrative is green because it is inert — which is the exact defect that floor exists
1370
+ // to end, arriving one layer up.
1371
+ //
1372
+ // ONLY WHEN THE CLIENT RATIFIED MORE THAN ONE. The ordinary matter is one name and must stay
1373
+ // free: a seat asked to list forms for a single-form matter will invent a second one, and the
1374
+ // floor would then demand a read of a form nobody bought.
1375
+ `Send \`ratified_forms\` ONLY if the instruction ratifies more than one form of the same name — the name and a variant of it, both to be searched as one matter. Send each form exactly as the client wrote it. Omit the field entirely for an ordinary single-name matter; do not invent a second form.`,
1366
1376
  `You are NOT asked to quote the request's marks, classes, territories or goods back — the driver stamps that section from its own intake record, so it cannot drift from what was asked.`,
1367
1377
  `Do NOT write or edit any file. There is no path for you to write to: the driver renders the frame from what you send, and nothing you hand-write is read.`,
1368
1378
  `When the tool accepts your call, return ONLY a 2-3 line summary of the frame.`,
@@ -2089,7 +2099,7 @@ export const STAGES = {
2089
2099
  },
2090
2100
  "enumerated block records carried verbatim — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date, jurisdictions, screen_verdict": {
2091
2101
  class: "mechanical:tool-written", tokens: ["named_band_collapsed"],
2092
- why: "register_enumerate already returns each record batch-screened; carrying it is transcription. `named_band_collapsed` (verify.mjs:1283, findCollapsedBands) is exactly the recall loss that transcription produces — a slice claiming total_hits with zero records reaching the band. [citation unverified]",
2102
+ why: "register_enumerate already returns each record batch-screened; carrying it is transcription. `named_band_collapsed` (verify.mjs:1325, findCollapsedBands) is exactly the recall loss that transcription produces — a slice claiming total_hits with zero records reaching the band. [citation unverified]",
2093
2103
  },
2094
2104
  "per-axis prose digest — the SHORT AUDIT NOTE at register-units/<axis>.md": {
2095
2105
  class: "judgment", tokens: ["too_short"],
@@ -2655,7 +2665,7 @@ export const STAGES = {
2655
2665
  },
2656
2666
  "the escalation decision — which register axes carry a material, unresolved, genuinely closeable gap, and the one-line reason for each": {
2657
2667
  class: "judgment", tokens: [],
2658
- why: "Whether a documented coverage-limited row, a capability-gap deferral or a fresh concern on a confirmed-clean row warrants spending a re-run. The driver hands the coverage/execution truth in as a computed table (stages.mjs:1466; skepticDeferralExtra) precisely so this is a call over data rather than a re-derivation — but the call itself is nobody else's. [citation unverified]",
2668
+ why: "Whether a documented coverage-limited row, a capability-gap deferral or a fresh concern on a confirmed-clean row warrants spending a re-run. The driver hands the coverage/execution truth in as a computed table (stages.mjs:1476; skepticDeferralExtra) precisely so this is a call over data rather than a re-derivation — but the call itself is nobody else's. [citation unverified]",
2659
2669
  },
2660
2670
  "escalation decisions — one {axis, reason} per axis that must be re-run, sent through record_skeptic": {
2661
2671
  class: "mechanical:code-rendered", tokens: [],
@@ -2860,6 +2870,10 @@ export const STAGES = {
2860
2870
  class: "mechanical:code-rendered", tokens: ["intake_ask_unanswered"],
2861
2871
  why: "the driver already code-builds the report's section from the ask_answers register (assembleReportMd · buildAskAnswersSection, named in stages.mjs) — the narrative section is a second authoring of a register the driver renders anyway",
2862
2872
  },
2873
+ "narrative per-form read — each ratified form reasoned through the framework, or the reads stated alike": {
2874
+ class: "judgment", tokens: ["ratified_form_unread"],
2875
+ why: "a client who ratified two forms is choosing between them; which conflicts move between the forms is the read they bought, and the token is a floor on whether the question was answered at all rather than on the reasoning",
2876
+ },
2863
2877
  "narrative coverage prose — how a coverage gap is stated": {
2864
2878
  class: "judgment", tokens: ["coverage_recommendation", "coverage_gap_unexplained"],
2865
2879
  why: "what the run could and could not clear is the lawyer's honest statement; the token polices only the forbidden 'commission a re-run' form",
@@ -2886,7 +2900,7 @@ export const STAGES = {
2886
2900
  },
2887
2901
  "findings[].meters.use.token / meters.enforcer.token — the two strengths with no quadrant axis": {
2888
2902
  class: "judgment", tokens: ["finding_meter_missing", "finding_meter_token_invalid", "finding_meter_unknown"],
2889
- why: "Judgment, and S1 does not reach them: quadrant plots goods proximity and mark similarity only (stages.mjs:1733), so nothing derives use (confirmed/not-confirmed/unknown) or enforcer strength. Deriving these from the quadrant would invent a reading the position does not carry. [citation unverified]",
2903
+ why: "Judgment, and S1 does not reach them: quadrant plots goods proximity and mark similarity only (stages.mjs, the quadrant element), so nothing derives use (confirmed/not-confirmed/unknown) or enforcer strength. Deriving these from the quadrant would invent a reading the position does not carry.",
2890
2904
  },
2891
2905
  "findings[].meters.mark_similarity.token / meters.goods_proximity.token — the coarse 3-pip restatement of the quadrant position": {
2892
2906
  class: "mechanical:code-assigned", tokens: ["finding_meter_missing", "finding_meter_token_invalid", "finding_meter_unknown"],
@@ -3042,7 +3056,7 @@ export const STAGES = {
3042
3056
  // therapeutic-goods discipline, no-use ≠ safety (pipelines run 5-10y), practitioner/pharmacist
3043
3057
  // confusion. Conditional read (frameworkFor pattern); doctrine TABLES live elsewhere, untouched.
3044
3058
  reads(synthesisSkillReads({ profile, job })),
3045
- `Produce the joint synthesis narrative (dominant-element spine, cross-pollination Option-D cap N=10, actual-use check, owner workup on high-risk findings, rate each conflict net of merits defences UNDER THE FRAMEWORK YOU JUST READ — state the likelihood of confusion in plain words, then give the band those words require under the framework's own definitions (the prose and the band are one judgment and may not disagree), coverage honesty).`,
3059
+ `Produce the joint synthesis narrative (dominant-element spine, cross-pollination Option-D cap N=10, actual-use check, owner workup on high-risk findings, rate each conflict net of merits defences UNDER THE FRAMEWORK YOU JUST READ — state the likelihood of confusion in plain words, then give the band those words require under the framework's own definitions (the prose and the band are one judgment and may not disagree), coverage honesty). WHERE THE MATTER RATIFIED MORE THAN ONE FORM OF THE NAME — a second spelling the instruction told you to read as a variant of the same candidate — REASON EACH FORM THROUGH THE FRAMEWORK ON ITS OWN, then say in the narrative which conflicts move between them and why, naming the form each read belongs to. A conflict that turns on one form exactly is not the same exposure for the other, and a reader choosing between the forms cannot act on a single undifferentiated read. Where every conflict lands the same on both, say so in one line — "the reads are the same for both forms" — as an asserted finding rather than a silence: a reader must be able to tell a matter where the forms do not differ from one where nobody looked. This changes the NARRATIVE and nothing about the report's shape: one overall band as today, and never a second one.`,
3046
3060
  // — the prose rung. Empty on a one-country run, and `lines` drops it.
3047
3061
  proseRungDirective(depth, framework?.bands ?? null),
3048
3062
  // doc 50 — THE RATING AUTHORITY: the framework read above (the customer's own, or the Generic default)
@@ -3702,7 +3716,7 @@ export const STAGES = {
3702
3716
  `Hand the shell back by calling the \`record_report_overview\` tool. There is no file to write and no front-matter to type: the driver renders the shell — every front-matter key it already holds, the \`# Actions\` section and the optional \`# Methodology\` note — from the values you send, and assembles the cards around it. Do NOT author any \`## <owner> — <MARK>\` card and do not think about a \`# Marks\` heading; each card is rendered separately. Risk scoring is unchanged.`,
3703
3717
  // TRANSFORM, NOT FREE-GENERATION (Fix B1 — carried from main's report-synthesis): the shell RESHAPES the
3704
3718
  // settled synthesis, it never re-derives. Applied here to overall_caption + # Actions.
3705
- `TRANSFORM, NOT FREE-GENERATION: overall_caption and # Actions draw ONLY on the settled synthesis narrative ${P.narrative} (and findings.json ${P.findings}) — introduce NO finding, owner, or claim that is not already there, NEVER re-compute a band, and never let a claim outrun its evidence (a verified-from-record fact stays fact; an inferred-from-signal claim reads AS an inference — "on the available signals…" — never flattened to bald fact).`,
3719
+ `TRANSFORM, NOT FREE-GENERATION: overall_caption and # Actions draw ONLY on the settled synthesis narrative ${P.narrative} (and findings.json ${P.findings}) — introduce NO finding, owner, or claim that is not already there, NEVER re-compute a band, and never let a claim outrun its evidence (a verified-from-record fact stays fact; an inferred-from-signal claim reads AS an inference — "on the available signals…" — never flattened to bald fact). WHERE THE SETTLED NARRATIVE REASONED MORE THAN ONE RATIFIED FORM OF THE NAME, CARRY ITS PER-FORM READ THROUGH — the overall_caption says which form the decisive conflicts turn on, or that the reads are the same for both, in the narrative's own words. Do not flatten two reads into one undifferentiated statement: that loses the distinction the client ratified both forms to get. This is a transform of what the narrative already settled, not a second rating, and the band is unchanged — one overall band as today.`,
3706
3720
  // — the D2 case-law line is RETIRED HERE (it stays on `report-card`, which is where
3707
3721
  // enforcement-history and opposition-likelihood are actually written). Two reasons, and the second
3708
3722
  // is the general one:
@@ -471,6 +471,12 @@
471
471
  "skips": 1,
472
472
  "todos": 0
473
473
  },
474
+ "a-name-is-never-rated-above-its-worst-card.test.mjs": {
475
+ "tests": 4,
476
+ "asserts": 15,
477
+ "skips": 0,
478
+ "todos": 0
479
+ },
474
480
  "a-name-the-wizard-returns-is-a-setup-name.test.mjs": {
475
481
  "tests": 5,
476
482
  "asserts": 9,
@@ -843,6 +849,12 @@
843
849
  "skips": 0,
844
850
  "todos": 0
845
851
  },
852
+ "a-two-form-matter-owes-a-read-per-form.test.mjs": {
853
+ "tests": 8,
854
+ "asserts": 25,
855
+ "skips": 0,
856
+ "todos": 0
857
+ },
846
858
  "a-unit-configuration-cannot-reach-is-resolved-at-install.test.mjs": {
847
859
  "tests": 5,
848
860
  "asserts": 14,
@@ -1378,8 +1390,8 @@
1378
1390
  "todos": 0
1379
1391
  },
1380
1392
  "common-law-receipts.test.mjs": {
1381
- "tests": 36,
1382
- "asserts": 129,
1393
+ "tests": 40,
1394
+ "asserts": 141,
1383
1395
  "skips": 0,
1384
1396
  "todos": 0
1385
1397
  },
@@ -1439,7 +1451,7 @@
1439
1451
  },
1440
1452
  "connect-clients-are-data.test.mjs": {
1441
1453
  "tests": 17,
1442
- "asserts": 87,
1454
+ "asserts": 91,
1443
1455
  "skips": 0,
1444
1456
  "todos": 0
1445
1457
  },
@@ -1505,7 +1517,7 @@
1505
1517
  },
1506
1518
  "contract-audit.test.mjs": {
1507
1519
  "tests": 54,
1508
- "asserts": 167,
1520
+ "asserts": 168,
1509
1521
  "skips": 0,
1510
1522
  "todos": 0
1511
1523
  },
@@ -1725,6 +1737,12 @@
1725
1737
  "skips": 0,
1726
1738
  "todos": 0
1727
1739
  },
1740
+ "deprecating-below-a-stable-touches-nothing-above-it.test.mjs": {
1741
+ "tests": 4,
1742
+ "asserts": 18,
1743
+ "skips": 0,
1744
+ "todos": 0
1745
+ },
1728
1746
  "depth-ladder-loop-bounds.test.mjs": {
1729
1747
  "tests": 6,
1730
1748
  "asserts": 10,
@@ -2205,6 +2223,12 @@
2205
2223
  "skips": 0,
2206
2224
  "todos": 0
2207
2225
  },
2226
+ "every-json-response-says-how-it-may-be-cached.test.mjs": {
2227
+ "tests": 4,
2228
+ "asserts": 14,
2229
+ "skips": 0,
2230
+ "todos": 0
2231
+ },
2208
2232
  "every-lane-declares-its-transport-position.test.mjs": {
2209
2233
  "tests": 7,
2210
2234
  "asserts": 12,
@@ -2439,9 +2463,15 @@
2439
2463
  "skips": 0,
2440
2464
  "todos": 0
2441
2465
  },
2466
+ "giving-access-to-everything-is-a-switch-not-a-point.test.mjs": {
2467
+ "tests": 1,
2468
+ "asserts": 4,
2469
+ "skips": 0,
2470
+ "todos": 0
2471
+ },
2442
2472
  "grant-command.test.mjs": {
2443
2473
  "tests": 19,
2444
- "asserts": 75,
2474
+ "asserts": 76,
2445
2475
  "skips": 0,
2446
2476
  "todos": 0
2447
2477
  },
@@ -2817,6 +2847,12 @@
2817
2847
  "skips": 0,
2818
2848
  "todos": 0
2819
2849
  },
2850
+ "no-two-dictated-queries-fold-to-one-key.test.mjs": {
2851
+ "tests": 5,
2852
+ "asserts": 10,
2853
+ "skips": 0,
2854
+ "todos": 0
2855
+ },
2820
2856
  "no-vendor-provenance-in-the-public-cut.test.mjs": {
2821
2857
  "tests": 4,
2822
2858
  "asserts": 11,
@@ -3142,8 +3178,8 @@
3142
3178
  "todos": 0
3143
3179
  },
3144
3180
  "portal-service.test.mjs": {
3145
- "tests": 124,
3146
- "asserts": 673,
3181
+ "tests": 127,
3182
+ "asserts": 683,
3147
3183
  "skips": 1,
3148
3184
  "todos": 0
3149
3185
  },
@@ -3665,7 +3701,7 @@
3665
3701
  },
3666
3702
  "render.test.mjs": {
3667
3703
  "tests": 154,
3668
- "asserts": 630,
3704
+ "asserts": 632,
3669
3705
  "skips": 0,
3670
3706
  "todos": 0
3671
3707
  },
@@ -4377,6 +4413,12 @@
4377
4413
  "skips": 0,
4378
4414
  "todos": 0
4379
4415
  },
4416
+ "taking-access-away-is-the-editor-that-gave-it.test.mjs": {
4417
+ "tests": 9,
4418
+ "asserts": 30,
4419
+ "skips": 0,
4420
+ "todos": 0
4421
+ },
4380
4422
  "teardown-leaves-nothing-claimable.test.mjs": {
4381
4423
  "tests": 4,
4382
4424
  "asserts": 19,
@@ -4527,6 +4569,12 @@
4527
4569
  "skips": 0,
4528
4570
  "todos": 0
4529
4571
  },
4572
+ "the-containment-rule-does-not-depend-on-the-string-tmp.test.mjs": {
4573
+ "tests": 2,
4574
+ "asserts": 3,
4575
+ "skips": 0,
4576
+ "todos": 0
4577
+ },
4530
4578
  "the-corrections-section-is-an-allowlist.test.mjs": {
4531
4579
  "tests": 6,
4532
4580
  "asserts": 18,
@@ -4815,6 +4863,12 @@
4815
4863
  "skips": 0,
4816
4864
  "todos": 0
4817
4865
  },
4866
+ "the-people-page-can-change-and-remove-a-person.test.mjs": {
4867
+ "tests": 16,
4868
+ "asserts": 80,
4869
+ "skips": 0,
4870
+ "todos": 0
4871
+ },
4818
4872
  "the-person-who-installs-is-a-person-not-a-domain.test.mjs": {
4819
4873
  "tests": 18,
4820
4874
  "asserts": 86,
@@ -5151,6 +5205,12 @@
5151
5205
  "skips": 0,
5152
5206
  "todos": 0
5153
5207
  },
5208
+ "two-ratified-forms-get-reasoned-apart.test.mjs": {
5209
+ "tests": 7,
5210
+ "asserts": 17,
5211
+ "skips": 0,
5212
+ "todos": 0
5213
+ },
5154
5214
  "two-word-annotated-variant-is-refused.test.mjs": {
5155
5215
  "tests": 6,
5156
5216
  "asserts": 21,
@@ -5450,6 +5510,18 @@
5450
5510
  "skips": 0,
5451
5511
  "todos": 0
5452
5512
  },
5513
+ "a-removed-person-loses-their-assistant-too.test.mjs": {
5514
+ "tests": 4,
5515
+ "asserts": 12,
5516
+ "skips": 0,
5517
+ "todos": 0
5518
+ },
5519
+ "a-run-list-names-its-client.test.mjs": {
5520
+ "tests": 4,
5521
+ "asserts": 29,
5522
+ "skips": 0,
5523
+ "todos": 0
5524
+ },
5453
5525
  "account-audit-chain.test.mjs": {
5454
5526
  "tests": 10,
5455
5527
  "asserts": 46,
@@ -5720,6 +5792,12 @@
5720
5792
  "skips": 0,
5721
5793
  "todos": 0
5722
5794
  },
5795
+ "the-portal-can-tell-who-has-connected-an-assistant.test.mjs": {
5796
+ "tests": 10,
5797
+ "asserts": 28,
5798
+ "skips": 0,
5799
+ "todos": 0
5800
+ },
5723
5801
  "token-door-boot.test.mjs": {
5724
5802
  "tests": 7,
5725
5803
  "asserts": 8,
@@ -5738,14 +5816,14 @@
5738
5816
  "ext": "ts",
5739
5817
  "perFile": {
5740
5818
  "api.test.ts": {
5741
- "tests": 44,
5742
- "asserts": 176,
5819
+ "tests": 45,
5820
+ "asserts": 183,
5743
5821
  "skips": 0,
5744
5822
  "todos": 0
5745
5823
  },
5746
5824
  "askAi.test.ts": {
5747
- "tests": 5,
5748
- "asserts": 12,
5825
+ "tests": 11,
5826
+ "asserts": 34,
5749
5827
  "skips": 0,
5750
5828
  "todos": 0
5751
5829
  },
@@ -5815,6 +5893,12 @@
5815
5893
  "skips": 0,
5816
5894
  "todos": 0
5817
5895
  },
5896
+ "decodePersonCarriesEveryField.test.ts": {
5897
+ "tests": 4,
5898
+ "asserts": 17,
5899
+ "skips": 0,
5900
+ "todos": 0
5901
+ },
5818
5902
  "effortModelParity.test.ts": {
5819
5903
  "tests": 7,
5820
5904
  "asserts": 31,
@@ -5864,8 +5948,8 @@
5864
5948
  "todos": 0
5865
5949
  },
5866
5950
  "home.test.ts": {
5867
- "tests": 51,
5868
- "asserts": 169,
5951
+ "tests": 56,
5952
+ "asserts": 196,
5869
5953
  "skips": 0,
5870
5954
  "todos": 0
5871
5955
  },
@@ -5941,6 +6025,12 @@
5941
6025
  "skips": 0,
5942
6026
  "todos": 0
5943
6027
  },
6028
+ "oneHeaderPerPage.test.ts": {
6029
+ "tests": 4,
6030
+ "asserts": 15,
6031
+ "skips": 0,
6032
+ "todos": 0
6033
+ },
5944
6034
  "ownerNames.test.ts": {
5945
6035
  "tests": 9,
5946
6036
  "asserts": 23,
@@ -5977,6 +6067,12 @@
5977
6067
  "skips": 0,
5978
6068
  "todos": 0
5979
6069
  },
6070
+ "proseIsStrippedOnce.test.ts": {
6071
+ "tests": 3,
6072
+ "asserts": 12,
6073
+ "skips": 0,
6074
+ "todos": 0
6075
+ },
5980
6076
  "readiness.test.ts": {
5981
6077
  "tests": 6,
5982
6078
  "asserts": 19,
@@ -6062,8 +6158,8 @@
6062
6158
  "todos": 0
6063
6159
  },
6064
6160
  "useYourAI.test.ts": {
6065
- "tests": 13,
6066
- "asserts": 41,
6161
+ "tests": 12,
6162
+ "asserts": 38,
6067
6163
  "skips": 0,
6068
6164
  "todos": 0
6069
6165
  },
@@ -668,12 +668,70 @@ export function validateMergedFindings(runDir, merged, plan) {
668
668
  // would put a field in the artifact the stage never emitted.
669
669
  try { const ranked = validateKnockoutFindings(m.findings, { manifest }); if (Array.isArray(m.findings)) m.findings = ranked; }
670
670
  catch (e) { failures.push(`mark "${m.name}": ${e.message}`); }
671
+ const above = markRatedAboveItsCards(manifest, m);
672
+ if (above) failures.push(above);
671
673
  }
672
674
  const receipts = knockoutReceipts(runDir, marks);
673
675
  failures.push(...receipts.failures);
674
676
  return { ok: failures.length === 0, failures, receipts: receipts.checked };
675
677
  }
676
678
 
679
+ // ── A NAME IS NEVER RATED ABOVE THE WORST CARD ON ITS PAGE ───────────────────────────────────────────
680
+ //
681
+ // Three names went out rated a band above every conflict card on their own pages. Twelve cards across
682
+ // the three, all at the client's bottom band; three names a band above them; nothing on any page
683
+ // explaining the gap. The reviewing lawyer read the cards as right and the names as wrong.
684
+ //
685
+ // Nothing tied the two together. The name's band is the model's own `rating` and no check compared it
686
+ // with the bands underneath it — a gap the full clearance does not have, where the headline is computed
687
+ // from the worst live finding. The doctrine that produced the lift (the everyday-words floor) is retired
688
+ // in this change; this is the floor under it, so the same shape cannot arrive by another route.
689
+ //
690
+ // IT REFUSES AND ASKS AGAIN. It never lowers the band itself, and that restraint is the point: a page
691
+ // whose badge a script moved while its basis line still argues for the old band gives a reader a
692
+ // Manageable chip over a Medium argument, which is worse than the defect. The corrective ladder this
693
+ // failure list feeds re-dispatches the mark with the reason, and the model reasons the band again.
694
+ //
695
+ // A CARD IS A RATED ITEM ON THIS NAME'S PAGE — a `findings[]` record, or a `registerReads[]` row that
696
+ // carries a band. A register row's band is optional by design (a read with no band prints no chip), so
697
+ // a row without one is not a card and does not count; including it would invent a floor out of silence.
698
+ // There is no withdrawn state on a knockout finding — that is the clearance lane's concept and it does
699
+ // not exist here, so nothing is filtered for it. Whoever brings it over adds the filter with it.
700
+ //
701
+ // RATING BELOW THE WORST CARD STAYS ALLOWED. It has happened, and the one that was checked was reasoned
702
+ // — the worst card's holder touched only the edge of the request. This refuses one direction only.
703
+ //
704
+ // INDEX 0 IS THE WORST BAND, as `worstBand` below says in its own comment, so "above" is a SMALLER
705
+ // index. Written the other way round this check would pass on the exact production run it was written
706
+ // for and fire on correct ones; the arms assert the direction against a real four-band ladder rather
707
+ // than trusting this sentence.
708
+ const cardBands = (m) => [
709
+ ...(Array.isArray(m?.findings) ? m.findings : []).map((f) => f?.band),
710
+ ...(Array.isArray(m?.registerReads) ? m.registerReads : []).map((r) => r?.band),
711
+ ].filter((b) => b != null && String(b).trim());
712
+
713
+ export function markRatedAboveItsCards(framework, m) {
714
+ const ladder = ladderOf(framework);
715
+ if (!ladder.length) return null; // no frozen ladder — the band checks above own that
716
+ const mine = bandIdx(ladder, m?.rating);
717
+ if (mine < 0) return null; // an unknown band is knockout_band_unknown's to name
718
+ const idxs = cardBands(m).map((b) => bandIdx(ladder, b)).filter((i) => i >= 0);
719
+ // NO CARDS, NOTHING TO BE ABOVE — and the code stops here deliberately.
720
+ //
721
+ // The doctrine says a name with nothing found against it takes the bottom band, and that belongs in
722
+ // the reasoning where it is written (calibration rule 8). Enforcing it HERE would be a second rule in
723
+ // code: a floor that forces a band down, which is what the ruling forbids in terms — "do not replace
724
+ // the removed rule with another floor, cap or word list. The two rulings are the whole requirement."
725
+ //
726
+ // The first draft of this function did enforce it, and the e2e batch is what said so: a mark with no
727
+ // cards, correctly rated Manageable on a five-band ladder, was refused because the bottom of that
728
+ // ladder is Low. That is the retired rule's own shape — a band decided by something other than the
729
+ // evidence on the page — arriving from the opposite direction.
730
+ if (!idxs.length) return null;
731
+ const worst = Math.min(...idxs);
732
+ return mine < worst ? `knockout_rating_above_cards:${m?.name}: rated "${m?.rating}", above every card on its page — the worst is "${ladder[worst]}". Rate the name from its cards, or write the risk that lifts it as a finding of its own and rate it there. Rating BELOW the worst card is allowed and wants a sentence saying which card does not carry the name.` : null;
733
+ }
734
+
677
735
  // worst band across the batch, in the frozen ladder's own order (index 0 = worst)
678
736
  export function worstBand(framework, marks) {
679
737
  const ladder = ladderOf(framework);
package/driver/verify.mjs CHANGED
@@ -12,9 +12,9 @@ import { dirname, join, basename } from "node:path";
12
12
  import { driverDir } from "../shared/driver-dir.mjs"; //
13
13
  import { findReceiptViolations, findGridLedgerViolations, findPlatformIdentityViolations, parsePrRiskQueries, MEANING_SEAT } from "./common-law-receipts.mjs";
14
14
  // Conversion 2 — the discriminator the two rulings above key on. PURE-ish: one existsSync-shaped read.
15
- import { matterFrameWasRecorded } from "./matter-frame-record.mjs";
15
+ import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-record.mjs";
16
16
  import { findConnotationViolations, parsePrRiskResults, MEANING_ANGLES_RE,
17
- parseDispositionForm, CONNOTATION_UNRULED_REASONS } from "./connotation-search.mjs";
17
+ parseDispositionForm, CONNOTATION_UNRULED_REASONS, queryKey } from "./connotation-search.mjs";
18
18
  import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
19
19
  // B — the transport's own four failure states. The audit reads the run's records; this file locates them.
20
20
  import { auditDispositionCalls, CALL_FAILURE_REASONS } from "./disposition-call-audit.mjs";
@@ -354,12 +354,54 @@ function commonLawMeaningSeat(p, c) {
354
354
  try { ledgerRaw = readFileSync(join(dirname(p), `common-law-grid.half-${MEANING_SEAT}.json`), "utf8"); } catch { /* missing → fail-closed below */ }
355
355
  if (ledgerRaw == null) return fail(`grid_ledger_missing:common-law-grid.half-${MEANING_SEAT}.json absent while _driver/grid-spec.half-${MEANING_SEAT}.json dictates the meaning sweep`);
356
356
  const dictated = Array.isArray(spec?.connotation?.queries) ? spec.connotation.queries : [];
357
- let recordedQ;
358
- try { recordedQ = new Set(parsePrRiskResults(ledgerRaw).map((e) => String(e?.query ?? "").trim())); }
357
+ // COMPARED ON A KEY, NOT ON THE RAW TEXT — `queryKey` in connotation-search.mjs, beside the parser,
358
+ // so there is one author of what "the same query" means. The dictated spelling is the driver's; the
359
+ // recorded one comes back through a provider that returns typographic punctuation. Measured on a
360
+ // production clearance, 2026-09-16: one query of sixty-one differed by a single right single quotation
361
+ // mark where the driver wrote an apostrophe, and the grid had run and recorded every one of them.
362
+ //
363
+ // THE ORIGINAL TEXT IS WHAT THE REFUSAL QUOTES, below. Somebody chasing a query that really is missing
364
+ // needs the spelling the driver asked for, not a flattened key.
365
+ let recordedRaw;
366
+ try { recordedRaw = parsePrRiskResults(ledgerRaw).map((e) => String(e?.query ?? "")); }
359
367
  catch (e) { return fail(`grid_ledger_unparseable:${String(e.message).slice(0, 80)}`); }
360
- const dropped = dictated.filter((q) => !recordedQ.has(String(q).trim()));
361
- if (dropped.length)
362
- return fail(`connotation_query_unrecorded:${dropped.slice(0, 3).map((q) => abbrev(q, 40)).join(",")}${dropped.length > 3 ? ` (+${dropped.length - 3} more)` : ""}`);
368
+ const recordedQ = new Set(recordedRaw.map(queryKey));
369
+ const dropped = dictated.filter((q) => !recordedQ.has(queryKey(q)));
370
+ if (dropped.length) {
371
+ // ── THE REFUSAL SAYS WHICH OF TWO FAULTS THIS IS, because they need opposite remedies ──────────
372
+ //
373
+ // ABSENT: no recorded query resembles it, so the search was not run and the seat must run it.
374
+ // UNMATCHED: something close IS recorded, so the search ran and the two spellings disagree beyond
375
+ // what the key folds — a re-ordering, a translation, a truncation, a query the provider chose for
376
+ // itself. Telling the seat to "re-run the missing query" in that case asks for the one thing that
377
+ // cannot help, and that is what turned one attempt into four on a production clearance.
378
+ //
379
+ // NO THRESHOLD DECIDES ANYTHING (owner's ruling). The nearest recorded query is shown so a person or
380
+ // a seat can SEE the difference in one attempt; it never makes the gate pass. A similarity score
381
+ // that could pass this gate would be a score that can hide a skipped query, which is what the gate
382
+ // is for.
383
+ const nearest = (q) => {
384
+ const words = new Set(queryKey(q).split(" ").filter(Boolean));
385
+ if (!words.size) return null;
386
+ let best = null, bestScore = 0;
387
+ for (const r of recordedRaw) {
388
+ const rw = queryKey(r).split(" ").filter(Boolean);
389
+ if (!rw.length) continue;
390
+ const shared = rw.filter((w) => words.has(w)).length / Math.max(words.size, rw.length);
391
+ if (shared > bestScore) { bestScore = shared; best = r; }
392
+ }
393
+ // A single shared word is a coincidence in a sweep about one mark, where the mark's own name is in
394
+ // every query. Below half the words in common, say nothing rather than point at a red herring.
395
+ return bestScore >= 0.5 ? best : null;
396
+ };
397
+ const parts = dropped.slice(0, 3).map((q) => {
398
+ const n = nearest(q);
399
+ return n
400
+ ? `${abbrev(q, 40)} [unmatched; nearest recorded: ${abbrev(n, 40)}]`
401
+ : `${abbrev(q, 40)} [absent from the ledger]`;
402
+ });
403
+ return fail(`connotation_query_unrecorded:${parts.join(",")}${dropped.length > 3 ? ` (+${dropped.length - 3} more)` : ""}`);
404
+ }
363
405
  if (spec?.connotation?.disposition_required === true) {
364
406
  const recorded = parsePrRiskResults(ledgerRaw);
365
407
  const form = dispositionForm(dirname(p), spec?.connotation?.dispositions_path);
@@ -2249,6 +2291,40 @@ export const validators = {
2249
2291
  return fail(`intake_ask_unanswered:${asks.length - answered}:of:${asks.length}`);
2250
2292
  }
2251
2293
  } catch { /* no sidecar / unreadable — legacy run, gate off */ }
2294
+
2295
+ // ── A TWO-FORM MATTER OWES A READ PER FORM ────────────────────────────────────────────────────
2296
+ //
2297
+ // Synthesis is told to reason each ratified form through the framework and to say which conflicts
2298
+ // move between them — or to state in one line that the reads are the same for both. Until this,
2299
+ // nothing enforced it: the arms on that dictation match the wording, so they red when the prose
2300
+ // moves and cannot red when a model ignores the ask. This is the deterministic floor under it.
2301
+ //
2302
+ // GATED ON THE FRAME HAVING FROZEN MORE THAN ONE FORM, which is why it can exist at all. A run with
2303
+ // no frame, an archived or replayed run whose accepted call predates the field, and every ordinary
2304
+ // single-form run reach `forms.length > 1` as false and leave here untouched — so every one of them
2305
+ // re-verifies byte-identically. Same shape as the intake-asks gate above, and the same reason.
2306
+ //
2307
+ // WHAT IT ASKS FOR IS THE CHEAPEST HONEST THING: each form NAMED in the narrative, or the
2308
+ // stated-alike line. It does not judge the reasoning — a floor that tried would be a worse version
2309
+ // of the refutation pass, which stays the semantic backstop. It judges that the question was
2310
+ // answered at all, which is exactly what was missing: a narrative that never mentions the second
2311
+ // form is indistinguishable from one that examined it and found nothing, and a client who ratified
2312
+ // two forms is choosing between them.
2313
+ try {
2314
+ const forms = frameRatifiedForms(dirname(p));
2315
+ if (forms.length > 1) {
2316
+ const text = String(c ?? "");
2317
+ // The alternative the dictation offers, matched on its own terms rather than by a fixed
2318
+ // sentence: a narrative may state the reads are alike instead of splitting them.
2319
+ const alike = /\bthe reads are the same for both forms\b/i.test(text);
2320
+ if (!alike) {
2321
+ const unnamed = forms.filter((f) => !text.toLowerCase().includes(f.toLowerCase()));
2322
+ if (unnamed.length)
2323
+ return fail(`ratified_form_unread:${unnamed.length}:of:${forms.length}:${abbrev(unnamed[0], 60)}`);
2324
+ }
2325
+ }
2326
+ } catch { /* no frame / unreadable — gate off, exactly as above */ }
2327
+
2252
2328
  // V4-4 item 6 — narrative coverage contract (code-checked, conditional on the closure receipt):
2253
2329
  // a marketplace coverage gap may be stated ONLY as an attempted-and-unreachable fact, never as
2254
2330
  // "commission a re-run" work for a human — the driver already closed or proved-unclosable every
@@ -1,5 +1,16 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.1-beta.4
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.3.1-beta.3
8
+
9
+ ### Patch Changes
10
+
11
+ - 56a760f: Fixed: Removing somebody's access now ends their assistant's connection on its next request. Until now an assistant that had already connected kept the access it started with for up to half an hour.
12
+ - c05e0a7: Fixed: Ask your assistant for a client's recent searches by the client's name and it finds them. Every search in the list now says which client it was for, and which project. A search whose own record cannot be read says so, instead of appearing to belong to nobody.
13
+
3
14
  ## 0.3.1-beta.2
4
15
 
5
16
  ### Patch Changes