clearotron 0.3.1 → 0.3.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CONTRIBUTING.md +30 -1
  2. package/README.md +1 -0
  3. package/build-info.json +2 -2
  4. package/docs/writing-rules.md +208 -0
  5. package/docs/writing-standard.md +85 -0
  6. package/driver/CHANGELOG.md +13 -0
  7. package/driver/contract-e3-backlog.mjs +4 -4
  8. package/driver/contract-vocabulary.mjs +15 -14
  9. package/driver/gateway.mjs +33 -17
  10. package/driver/package.json +1 -1
  11. package/driver/partial-payload-baseline.json +1 -1
  12. package/driver/pipeline.mjs +3 -3
  13. package/driver/portal-families.mjs +1 -1
  14. package/driver/predelivery-lint.mjs +23 -1
  15. package/driver/publish/index.mjs +24 -1
  16. package/driver/publish/render-knockout.mjs +6 -26
  17. package/driver/publish/render.mjs +25 -5
  18. package/driver/publish/report-data.mjs +2 -1
  19. package/driver/publish/search-depth.mjs +178 -0
  20. package/driver/suite-census.json +38 -2
  21. package/driver/terminal-clamp.mjs +41 -0
  22. package/driver/verify.mjs +22 -3
  23. package/mcp-server/CHANGELOG.md +8 -0
  24. package/mcp-server/package.json +1 -1
  25. package/package.json +1 -1
  26. package/portal-ui/dist/assets/{index-ChIQsMYp.js → index-BsbasHjM.js} +3350 -3288
  27. package/portal-ui/dist/assets/{index-DBIs21e4.css → index-DNQpLYZF.css} +17 -1
  28. package/portal-ui/dist/index.html +2 -2
  29. package/portal-ui/package.json +1 -1
  30. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  31. package/providers/oauth-mcp-bridge/package.json +1 -1
  32. package/scripts/freeze-example-run.mjs +27 -27
  33. package/scripts/mint-writing-standard-backlog.mjs +82 -0
  34. package/scripts/writing-standard-check.mjs +122 -0
  35. package/shared/says-something-new.mjs +62 -0
  36. package/shared/writing-standard-caveats.json +14 -0
  37. package/shared/writing-standard-classes.mjs +526 -0
package/CONTRIBUTING.md CHANGED
@@ -93,7 +93,7 @@ served to the model at run time — `synthesis-rules.md` is a 16,000-word progra
93
93
  brevity, tone or tidiness changes what a clearance concludes. Nothing in this section, and nothing in
94
94
  any writing pass over the documentation, applies to them.
95
95
 
96
- ## The three rules that fail CI
96
+ ## The four rules that fail CI
97
97
 
98
98
  **1. Build `portal-ui/dist` before you push.** The bundle is not committed; it is gitignored, and
99
99
  CI runs `npm run build:ui` from source. So there is nothing to add — run it locally when you touch
@@ -130,6 +130,35 @@ The reviewer is the check. Say what you checked in the PR rather than citing a p
130
130
  **3. Types are enforced, and `vite build` does not typecheck.** `npm run typecheck -w portal-ui`
131
131
  runs as its own CI step. Run it before you push.
132
132
 
133
+ **4. Every sentence a customer reads is written against the standard, and five classes of it are
134
+ checked.** The standard is [`docs/writing-standard.md`](docs/writing-standard.md); the prose rules
135
+ under it are [`docs/writing-rules.md`](docs/writing-rules.md), and they apply first. Together they
136
+ bind report HTML, portal screens, the README and the docs.
137
+
138
+ `node scripts/writing-standard-check.mjs` refuses what your change ADDS to one of those surfaces:
139
+ an engineering identifier in text a client reads, a reviewer-only marker in rendered output, a known
140
+ caveat sentence, a screen that writes its own page heading instead of using `PageHeader`, and a lede
141
+ whose words are all already in its title. It names the class and prints the line. It never rewrites —
142
+ a rewritten sentence is a sentence nobody reviewed.
143
+
144
+ It refuses what you add, not what is already here. The standing population is counted per file and
145
+ per class in `driver/test/fixtures/writing-standard-backlog.json`, and the floor beside it refuses
146
+ any file that grows, so the number can only fall. If you repair one, re-mint the floor with
147
+ `node scripts/mint-writing-standard-backlog.mjs --apply` so it drops with the tree.
148
+
149
+ ### The review step, for what no check can judge
150
+
151
+ Tone is not one of the five classes and no word list will ever catch it. A sentence can carry no
152
+ identifier, no caveat and no banned word and still be the thing the standard exists to stop: a
153
+ heading that restates its section, a paragraph explaining what the reader can already see, a sentence
154
+ that only works if you know how the engine is built.
155
+
156
+ So: **a pull request that changes text a customer reads is reviewed against the rendered page or
157
+ report, not against the diff.** One reviewer reads it as someone who has never seen this product and
158
+ asks one question of every new sentence — *what would a reader with zero context think this means?*
159
+ If the answer needs the codebase, the sentence is cut or rewritten. One reviewer, one question,
160
+ recorded in the pull request.
161
+
133
162
  ### If your change touches a Markdown file, two more will catch you
134
163
 
135
164
  Both are in `npm run test:full` rather than the fast tier, and neither is something a reviewer spots.
package/README.md CHANGED
@@ -139,6 +139,7 @@ credential.
139
139
  | Check it works before spending anything | [docs/E2E.md](docs/E2E.md) |
140
140
  | Understand the architecture | [docs/architecture/](docs/architecture/) · [decisions](docs/decisions/) |
141
141
  | Run it under your own name, or fork it | [docs/branding.md](docs/branding.md) · [TRADEMARKS.md](TRADEMARKS.md) |
142
+ | Write a sentence a customer will read | [docs/writing-standard.md](docs/writing-standard.md) · [docs/writing-rules.md](docs/writing-rules.md) |
142
143
 
143
144
  ## Development
144
145
 
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "6e95648a34fce4d8d0a73b9359a89925b0af3a36",
3
- "version": "0.3.1"
2
+ "commit": "4a4575724492736d2333d3bf2faf8027578427f2",
3
+ "version": "0.3.2-beta.1"
4
4
  }
@@ -0,0 +1,208 @@
1
+ # Writing Rules
2
+
3
+ From the On Style writing rules, https://github.com/gundersen/on-style, MIT licence; reproduced here so the standard is complete on its own. The licence text is at the end of this file.
4
+
5
+ *Apply these to all prose you produce.*
6
+
7
+ *These rules fight trained instincts. Pay special attention to: no hedging, no over-explaining, keep it short, no metadiscourse, finish strong, and don't fake contradiction. These are the rules most likely to be violated — enforce them first.*
8
+
9
+ ---
10
+
11
+ # Words — what to use and what to cut
12
+
13
+ ## Cut dead weight
14
+
15
+ Every word must earn its place. Kill:
16
+ - **Zombie nouns**: "make an appearance" → "appear"; "provide an explanation" → "explain"
17
+ - **Light verbs** propping up nominalizations: make, do, have, bring, put, take
18
+ - **Metaconcept filler**: matter, view, subject, process, basis, factor, level, model
19
+ - **Throat-clearing openers**: "It is worth noting that...", "It should be pointed out that...", "There is a tendency for..."
20
+
21
+ If a 40-word sentence says what 15 words can say, use 15.
22
+
23
+ ## Be concrete
24
+
25
+ Use nouns and verbs, not adjectives and adverbs. Prefer plain words over fancy ones. Replace abstractions with the tangible things they refer to — people, actions, objects. If the reader can't picture it, rewrite it. Be concrete about the argument; be vague about the scenery.
26
+
27
+ "The company experienced a significant deterioration in operational performance across multiple facilities during the quarter" — how many facilities? What happened? "Three factories missed production targets and the Chicago plant shut down for two weeks" — now the reader sees it.
28
+
29
+ ## No jargon
30
+
31
+ If a phrase requires a second phrase to explain it, use the explanation. "How the AI in our weapons makes decisions" — not "autonomy governance." "We raised $50M to build bigger rockets" — not "we secured a strategic capital infusion to scale our launch vehicle program." The test: could a smart outsider follow it without a glossary?
32
+
33
+ ## No hedging — make definite assertions
34
+
35
+ Don't reflexively pad with: almost, apparently, fairly, somewhat, sort of, to a certain degree, relatively, presumably. Cut throat-clearing qualifiers: actually, of course, surely, in fact, just, so, pretty, that said. Hedge only when genuine uncertainty exists and the reader needs to know about it. When you hedge, do it once and with precision — not as a verbal tic.
36
+
37
+ Beyond avoiding hedges, write with force. A sentence can be hedge-free and still limp. "The project went okay" has no qualifiers but says nothing. "The project shipped three weeks late and lost the client" says something.
38
+
39
+ ## No intensifiers
40
+
41
+ Drop: very, highly, extremely, incredibly, truly, really, quite. These words weaken rather than strengthen. An unmodified claim reads as categorical and confident. Adding "very" turns it into a score on a scale.
42
+
43
+ ## No GPT-isms
44
+
45
+ If it sounds like a language model wrote it, rewrite it. Ban list:
46
+ - **Compound modifiers that mean nothing**: "operationally relevant," "institutionally protected," "non-fragmentable," "identity-defining institutional constraint," "mission-critical," "best-in-class"
47
+ - **Faux-precision hedges**: "it's worth noting that," "it's important to understand," "this is particularly significant"
48
+ - **Breathless connectors**: "moreover," "furthermore," "indeed," "notably," "crucially," "importantly"
49
+ - **Vague authority words**: "robust," "comprehensive," "strategic," "leveraging," "ecosystem," "synergy," "paradigm," "holistic"
50
+ - **Performative structure**: "Let's break this down," "Here's the thing," "There are several key factors to consider"
51
+
52
+ The test: if the phrase exists only because an LLM stitched together impressive-sounding words, cut it.
53
+
54
+ ---
55
+
56
+ # Sentences — how they move
57
+
58
+ ## Active voice by default
59
+
60
+ Put the agent before the action. "The board approved the deal" — not "The deal was approved by the board." If you can't name who's doing it, the sentence is hiding. Use passive only when the doer is unknown, unimportant, or when you need to shift emphasis deliberately.
61
+
62
+ ## Light before heavy
63
+
64
+ Put short, simple phrases before long, complex ones. Topic before comment. Given information before new information. End sentences with the material that deserves the most emphasis.
65
+
66
+ "The executive team, after weeks of deliberation and consultation with outside counsel regarding the potential regulatory implications, approved the merger." The verb arrives too late. Flip it: "The executive team approved the merger after weeks of deliberation over regulatory risk."
67
+
68
+ ## One idea per sentence
69
+
70
+ If a sentence tries to do two things, split it. If the reader has to reread it, it's too long or too tangled. The difficulty of a sentence depends on its geometry, not just its word count — avoid deeply nested clauses.
71
+
72
+ ## Punctuation is breath
73
+
74
+ Punctuation controls how the reader hears you — the pacing, the pauses, the asides. A comma sounds different than a semicolon; parentheses make a different noise than dashes. Read sentences aloud. Where you pause naturally, punctuate. Where the punctuation forces an unnatural pause, remove it.
75
+
76
+ Two bright-line rules:
77
+ - **Always use the serial comma.** "Guards, dogs, and sensors" — keep the comma before "and." It eliminates ambiguity and costs nothing.
78
+ - **Never use exclamation points.** Let the words carry the emphasis. If the sentence needs a ! to land, the sentence isn't strong enough.
79
+
80
+ ## Don't fake contradiction
81
+
82
+ Don't toss in a "But" or "However" to create the illusion that a second thought contradicts a first when it doesn't. "However" is not a synonym for "and" or "also" — it signals a turn. If there's no turn, drop it. Use contrastive connectors only when there is an actual contrast.
83
+
84
+ ## Watch for dangling modifiers
85
+
86
+ "Walking through the data, the trend becomes clear" — the trend isn't walking. Make sure the modifier attaches to the subject of the sentence. "Walking through the data, we noticed a clear trend." If the sentence reads fine but the grammar is wrong, the reader will stumble even if they can't say why.
87
+
88
+ ## Name the thing
89
+
90
+ Don't open a sentence with "this," "that," "these," or "it" unless the referent is unmistakable from the previous sentence. Vague pronouns break coherence — the reader has to look back to figure out what you mean. When in doubt, name the thing. "This approach" instead of "This." "The deal" instead of "It."
91
+
92
+ ## Consistent terms, consistent vantage
93
+
94
+ If you use two different words, the reader assumes you mean two different things. Don't vary terminology for variety's sake. Stay in one tense, one voice, one point of view — don't flip without reason.
95
+
96
+ ---
97
+
98
+ # Paragraphs & Structure — shape and sequence
99
+
100
+ ## Show, don't announce
101
+
102
+ Lead with the thing itself. Never open with "In this section I will discuss..." or "It's important to note that..." — just say it. No metadiscourse. No narrating your own writing process.
103
+
104
+ ## Connect every sentence
105
+
106
+ Each sentence must follow from the one before it. Don't dump facts without showing how they relate. Use a logical sequence: general to specific, cause to effect, chronological, or big to small. If adding "therefore," "however," or "for example" between two sentences doesn't make sense, the transition is broken.
107
+
108
+ ## Every paragraph earns its place
109
+
110
+ Each paragraph should have a single job. Begin with a sentence that states or implies the point. Let the following sentences develop it. End with emphasis, not a digression. A paragraph is a complete thought — not a visual break you insert when the block of text looks too long. If you can't say what a paragraph is about in one sentence, it's doing too many things.
111
+
112
+ ## Finish strong
113
+
114
+ End paragraphs and sections with the sentence that carries the most weight. Don't trail off with qualifications or filler. The last thing the reader sees is what sticks.
115
+
116
+ "The deal, which was the largest acquisition in the company's history, closed Tuesday at $4.2 billion after six months of negotiation." The best fact is buried in the middle. Rearrange: "After six months of negotiation, the deal closed at $4.2 billion — the largest acquisition in the company's history."
117
+
118
+ ## Say it once
119
+
120
+ State a principle in one place. Reference it after that. Never re-derive the same idea from scratch in every section.
121
+
122
+ ## The rule of threes
123
+
124
+ People remember things better when grouped in threes. Three main points, three examples, three items in a list. It keeps structure clear and memorable. When you have five points, see if two of them are really the same point. When you have seven, you've lost the reader.
125
+
126
+ ## Keep it short
127
+
128
+ If an email could be 3 sentences, don't write 3 paragraphs. Say it in as few paragraphs as the argument requires. Cut entire paragraphs the same way you cut words — if removing it doesn't hurt the argument, it was never helping.
129
+
130
+ ---
131
+
132
+ # Calibration — proportions and matching claims
133
+
134
+ ## Respect the reader
135
+
136
+ Assume the reader is intelligent but doesn't share your specialized knowledge. Explain terms without being condescending. Never write in a way that makes the reader feel stupid — classic style makes the reader feel like a genius.
137
+
138
+ ## Sometimes less detail is more accurate
139
+
140
+ Don't fabricate specifics to sound authoritative. "The company was founded in the early 2000s" is more accurate than "the company was founded in 2003" if you don't actually know the year. Precision with nonessential details invites errors and adds noise. Be specific when the detail matters and you're confident in it.
141
+
142
+ ## Do not overstate
143
+
144
+ If you say "revolutionary" and the reader thinks "incremental," you've lost them — not just on that sentence but on everything that follows. Make claims proportional to the evidence. Overstatement erodes trust faster than understatement. Let the facts do the persuading.
145
+
146
+ ## Do not explain too much
147
+
148
+ State it, show it, move on. If you state a principle and then explain it and then explain the explanation, you're writing a textbook, not a memo. Let examples reveal the point. Trust the reader to follow.
149
+
150
+ ## Numbers over superlatives
151
+
152
+ If a number could replace a word or phrase, use the number. "Revenue growth significantly outpaced expectations in Q2" — by how much? "Revenue grew 34% in Q2, beating the Street estimate by 11 points" — now the reader can think with it. If a sentence still needs a qualitative descriptor after the number is included, use a calibrated and neutral term ("modest," "material" with a defined threshold) rather than an evocative one.
153
+
154
+ ## No emotionally charged framing
155
+
156
+ Phrases like "not your salvation," "you're in trouble," "this is the one to stare at" are persuasion techniques, not analysis. Present the data and let the conclusion follow naturally. Analysis should read like a term sheet, not an op-ed.
157
+
158
+ ---
159
+
160
+ # Voice & Framing — identity and stance
161
+
162
+ ## Say what you are, never what you're not
163
+
164
+ Define by positive assertion. Don't build identity through negation. "We build vertical rockets" — not "we are not a traditional aerospace company, not a components supplier, not a consultancy."
165
+
166
+ ## No hidden negatives
167
+
168
+ Some sentences look positive but structurally say "we don't do X." Flip them. "We design what we can build in volume" — not "We subordinate performance ambition to scalable manufacturability." "We ship weekly" — not "We don't let perfect block progress." If the sentence only makes sense by contrast with something you're rejecting, rewrite it as a direct claim.
169
+
170
+ ## Pass the "so what?" test
171
+
172
+ Every sentence must earn its place. If it sounds authoritative but wouldn't help settle an argument in a room, cut it. If you could swap in any name — any company, any person, any product — and the sentence still works, it's empty.
173
+
174
+ ## Guide decisions, don't prescribe process
175
+
176
+ A framework tells you what matters when you're making a choice. It doesn't create governance structures, escalation pathways, or approval chains. Those belong in an ops manual, not a strategy document or memo.
177
+
178
+ ---
179
+
180
+ ## Self-check
181
+
182
+ Rules decay over long outputs — the longer you write, the more likely you revert to trained habits. After writing any prose longer than two paragraphs, re-read it against the preamble list before presenting it. Check for: hedging, over-explaining, verbosity, metadiscourse, trailing caveats, fake contradiction, and vague pronoun references.
183
+
184
+ ---
185
+
186
+ ## Licence of these rules
187
+
188
+ MIT License
189
+
190
+ Copyright (c) 2026 gundy
191
+
192
+ Permission is hereby granted, free of charge, to any person obtaining a copy
193
+ of this software and associated documentation files (the "Software"), to deal
194
+ in the Software without restriction, including without limitation the rights
195
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
196
+ copies of the Software, and to permit persons to whom the Software is
197
+ furnished to do so, subject to the following conditions:
198
+
199
+ The above copyright notice and this permission notice shall be included in all
200
+ copies or substantial portions of the Software.
201
+
202
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
203
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
204
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
205
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
206
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
207
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
208
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ # Writing standard
2
+
3
+ Every surface a customer reads: report HTML, portal screens, README, docs. Two parts.
4
+
5
+ ## Part one: how to write
6
+
7
+ `writing-rules.md` beside this file is the prose standard, and it applies to every sentence in those
8
+ surfaces before anything below does. Read it in full once. The six rules it says to enforce first, because
9
+ they are the ones a trained writer and a language model both break by instinct: no hedging, no
10
+ over-explaining, keep it short, no metadiscourse, finish strong, and do not fake contradiction. If a
11
+ sentence sounds like a language model wrote it, rewrite it.
12
+
13
+ ## Part two: the product's own rules
14
+
15
+ Eight rules, each with the sentence that caused it.
16
+
17
+ ## One header per page, and nothing restating it
18
+
19
+ A page names itself once. A line under the title earns its place only by saying something the title
20
+ does not.
21
+
22
+ - Before: `Home` over `Now`.
23
+ - After: `Home`.
24
+
25
+ ## Example text goes inside the field
26
+
27
+ A field's example belongs in its placeholder, where the reader is typing. A sentence above the field is
28
+ read before the reader knows what the field is for.
29
+
30
+ - Before, above the field: "A sentence is enough, or paste the whole thread."
31
+ - After, in the field: "Need a quick check on AQUAPLUS for energy drinks in the US before Friday."
32
+
33
+ ## No caveat, no disclaimer, no "what this is not"
34
+
35
+ Say what the search did and what happens next. Never define the product by negation.
36
+
37
+ - Before: "What it is not. A clearance search. We drew no register conclusions and give no filing advice."
38
+ - After: "A name that passes here goes on to clearance."
39
+
40
+ ## No engineering word in anything a client reads
41
+
42
+ Error codes, connector names, routing tables and internal identifiers are not the reader's vocabulary.
43
+ State the limit and its consequence.
44
+
45
+ - Before: "the Japan adapter was unavailable this session (CONNECTION_CLOSED)".
46
+ - After: "Case-law research could not be completed for Japan."
47
+
48
+ ## A count only when it changes what the reader does
49
+
50
+ Register hit counts tell a lawyer how crowded a field is. Keep those. Activity counts measure the
51
+ engine, not the answer.
52
+
53
+ - Before: "Fifty-two meaning and connotation queries were run across the Latin, katakana and hiragana forms."
54
+ - After: "Meaning was checked in three scripts; nothing loaded attaches to the name."
55
+
56
+ ## The product's own words for its own states
57
+
58
+ A state has one name, used on the screen, in the report, in help and in the export.
59
+
60
+ - Before: "No permissions".
61
+ - After: "View reports".
62
+
63
+ ## The audit lives in the workbook and the MCP server, never on the page
64
+
65
+ Every search run, every empty result and the working notes go to the audit workbook, and the MCP server
66
+ answers questions about any of them. The page carries the finding.
67
+
68
+ - Before: "Source routing attempted: Per the source-selection table, Japan is a non-US, non-EU
69
+ jurisdiction, so the applicable adapter is…"
70
+ - After: nothing on the page. One row in the workbook.
71
+
72
+ ## A fault is fixed, or shown to the person who can act on it
73
+
74
+ A customer cannot repair a missing coverage record. Narrating the failure to them turns our defect into
75
+ their problem.
76
+
77
+ - Before: "No coverage record was produced for this run. This section normally lists what each search
78
+ covered and what is still open… Ask us before relying on it."
79
+ - After: the operator is told; the report carries the coverage that exists.
80
+
81
+ ## What holds these
82
+
83
+ `enforcement.md` lists the classes a CI check refuses in a diff. Tone is not one of them, and no word
84
+ list will ever catch it: a reviewer reads the rendered page as someone who has never seen this product,
85
+ against `writing-rules.md`, and asks what they would think each sentence means.
@@ -1,5 +1,18 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.2-beta.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2a81abc: New: A report can now show how much was searched to reach its answer. It records the names read and cleared, the records read in each country, and the checks made. Countries where nothing was found are included.
8
+ - 2a81abc: Fixed: The conditions listed on a report are now written in plain legal English, matching the summary line above them. One condition could previously appear as an internal engine note with counts and identifiers in it.
9
+
10
+ ## 0.3.2-beta.0
11
+
12
+ ### Patch Changes
13
+
14
+ - 7cc0d29: Fixed: A report now lists every part of the search that was left open. One with a short name could be hidden by another line that happened to mention the same word. The overall result was never affected, only the list of what remained open.
15
+
3
16
  ## 0.3.1
4
17
 
5
18
  ### Patch Changes
@@ -488,7 +488,7 @@ export const E3_BACKLOG = [
488
488
  where: "driver/stages.mjs:3231",
489
489
  surface: "stage-message",
490
490
  evidence: "- meters: {\"mark_similarity\":{...},\"goods_proximity\":{...},\"use\":{...},\"enforcer\":{...}} — all four present, each {\"token\",\"basis\",\"source\"}. … mark_similarity = high | medium | low. goods_proximity = high | medium | low. enforcer = high | medium | low | unknown. use = confirmed | not-confirmed | un",
491
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; driver/verify.mjs:1108 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
491
+ reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; driver/verify.mjs:1127 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
492
492
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
493
493
  },
494
494
  {
@@ -638,7 +638,7 @@ export const E3_BACKLOG = [
638
638
  where: "driver/stages.mjs:3470",
639
639
  surface: "stage-message",
640
640
  evidence: "EVERY \"Grounded profile\" section MUST start its body with the line \"- ord: <N>\" naming which finding it grounds (use the ordinal from this list; a profile that grounds no listed finding omits the line)",
641
- reparsedBy: "driver/publish/parse.mjs:339 parseCaseLawProfiles (\"the optional '- ord: <N>' first body line … gives an EXACT join\"); driver/findings-model.mjs:273 /^-\\s*ord:\\s*(\\d+)\\s*$/m; driver/publish/index.mjs:776",
641
+ reparsedBy: "driver/publish/parse.mjs:339 parseCaseLawProfiles (\"the optional '- ord: <N>' first body line … gives an EXACT join\"); driver/findings-model.mjs:273 /^-\\s*ord:\\s*(\\d+)\\s*$/m; driver/publish/index.mjs:778 runOrigins",
642
642
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
643
643
  },
644
644
  {
@@ -684,7 +684,7 @@ export const E3_BACKLOG = [
684
684
  // because an un-anchorable row lands in the NOT-CHECKED slice, and that is coverage lost rather than
685
685
  // a pass.
686
686
  evidence: "is ONE of coverage-disposition | fact | rating | narrative — pick the one your own legal read says the correction IS: coverage-disposition (a coverage row / disposition placement is wrong or dishonest)",
687
- reparsedBy: "driver/verify.mjs:784 CORRECTION_KIND_RE = /\\[kind:\\s*([a-z][a-z-]*)\\s*\\]/i → parseCorrectionKinds (verify.mjs:1007), consumed in pipelineInner() in pipeline.mjs for the run.jsonl `correction-kinds` histogram, which since #1558 also carries `kindChannelOk` — the counts are DERIVED from the parsed rows, and that key states whether the reviewer's kind channel produced anything at all rather than leaving a reader to infer it by comparing untyped against total. Telemetry only today",
687
+ reparsedBy: "driver/verify.mjs:803 CORRECTION_KIND_RE = /\\[kind:\\s*([a-z][a-z-]*)\\s*\\]/i → parseCorrectionKinds (verify.mjs:1026), consumed in pipelineInner() in pipeline.mjs for the run.jsonl `correction-kinds` histogram, which since #1558 also carries `kindChannelOk` — the counts are DERIVED from the parsed rows, and that key states whether the reviewer's kind channel produced anything at all rather than leaving a reader to infer it by comparing untyped against total. Telemetry only today",
688
688
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
689
689
  },
690
690
  {
@@ -697,7 +697,7 @@ export const E3_BACKLOG = [
697
697
  // `[on: -]` case went from a value to an ABSENCE — you omit the field — which is the one part a
698
698
  // reader could get wrong from the old wording, since there is no value meaning "no finding".
699
699
  evidence: "**AND EVERY FLAG CARRIES WHICH FINDING IT IS ABOUT** — the `on` field, an array of ordinals. Same rule as `kind`: you send the values, the driver renders the token.",
700
- reparsedBy: "driver/verify.mjs:801 CORRECTION_ON_RE = /\\[on:\\s*([0-9,\\s-]*?)\\s*\\]/i. SKILL-FILE ONLY — the stage message at stages.mjs:1842-1877 never mentions `[on:]`. This is #850's \"the element shape is in the skill file, not the stage message\" in its purest form: an E3 lint reading stages.mjs alone sees the [kind:] token and misses its twin",
700
+ reparsedBy: "driver/verify.mjs:820 CORRECTION_ON_RE = /\\[on:\\s*([0-9,\\s-]*?)\\s*\\]/i. SKILL-FILE ONLY — the stage message at stages.mjs:1842-1877 never mentions `[on:]`. This is #850's \"the element shape is in the skill file, not the stage message\" in its purest form: an E3 lint reading stages.mjs alone sees the [kind:] token and misses its twin",
701
701
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
702
702
  },
703
703
  {
@@ -16,9 +16,9 @@
16
16
  // extractor never sees:
17
17
  //
18
18
  // D1 verify.mjs:940 fail(`connotation_${reason}:…`) — reason iterates the table at line 894
19
- // D2 verify.mjs:704 fail(String(e.message)) — parseFindingsJson throws token-first
20
- // D3 verify.mjs:1165 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
21
- // D4 verify.mjs:1603 parseCoverageLedgerJson, same shape
19
+ // D2 verify.mjs fail(String(e.message)) — parseFindingsJson throws token-first
20
+ // D3 verify.mjs:1184 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
21
+ // D4 verify.mjs 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
24
24
  // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2018 disclosureTextByAxis
@@ -99,7 +99,7 @@ export const VOCABULARY = [
99
99
  { token: "ratified_form_unread", stages: ["synthesis"], site: "driver/verify.mjs" },
100
100
  { token: "coverage_recommendation", stages: ["synthesis"], site: "driver/verify.mjs" },
101
101
  { token: "coverage_gap_unexplained", stages: ["synthesis"], site: "driver/verify.mjs" },
102
- { token: "finding", stages: ["synthesis"], site: "driver/verify.mjs:704", family: "driver/findings-model.mjs (token-first throws; `finding_*` and `findings_*`)", dynamic: "D2" },
102
+ { token: "finding", stages: ["synthesis"], site: "driver/verify.mjs", family: "driver/findings-model.mjs (token-first throws; `finding_*` and `findings_*`)", dynamic: "D2" },
103
103
 
104
104
  // ── register-digest ────────────────────────────────────────────────────────────────────────────────
105
105
  { token: "coverage_form_damaged", stages: ["register-digest"], site: "driver/verify.mjs" },
@@ -109,16 +109,16 @@ export const VOCABULARY = [
109
109
  { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
110
110
  { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
111
111
  { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
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
+ { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1510 coverageFormFail", family: "driver/register-plan.mjs:1642 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
113
113
  { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1417 validatePlanFeasibility", dynamic: "D6" },
114
114
  { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1766 searchedJurisdictionsFromPlan", dynamic: "D6" },
115
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" },
116
116
  { token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs" },
117
- { token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
118
- { token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
119
- { token: "coverage_axis_", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
120
- { token: "coverage_status_invalid", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
121
- { token: "coverage_classes_invalid", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
117
+ { token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
118
+ { token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
119
+ { token: "coverage_axis_", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
120
+ { token: "coverage_status_invalid", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
121
+ { token: "coverage_classes_invalid", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
122
122
  { token: "plan_execution_unreadable", stages: ["register-digest", "narrative-refutation"], site: "driver/verify.mjs:1496, 1712" },
123
123
 
124
124
  // ── matter-frame / prelim-variants / blind-frame / frame-diff ──────────────────────────────────────
@@ -130,7 +130,7 @@ export const VOCABULARY = [
130
130
  { token: "variantmodel_term_markup", stages: ["prelim-variants"], site: "driver/verify.mjs" },
131
131
  { token: "variantmodel_missing", stages: ["prelim-variants"], site: "driver/verify.mjs:1153, 1212" },
132
132
  // Recovered during E2 authoring, absent from the draft census: variant-manifest.json is strict-parsed
133
- // through checkSiblingJson (verify.mjs:1178) → checkJson (:742), so the WHOLE variantmodel_* family
133
+ // through checkSiblingJson (verify.mjs:1197) → checkJson (:742), so the WHOLE variantmodel_* family
134
134
  // reaches prelim-variants, not just the four literal tokens above.
135
135
  // CONVERSION 3 widened this family's SOURCE without widening its prefix. `acceptPrelimVariants` raises
136
136
  // `variantmodel_scope_layer_invalid`, `_scope_status_invalid`, `_scope_item_missing` and `_scope_pipe`
@@ -319,7 +319,8 @@ export const STAGE_UNREACHABLE_VALIDATORS = [
319
319
  *
320
320
  * The drift is CLUSTERED, not random: 34, 35, 36, 52 recur. That is one insertion above a block moving
321
321
  * every citation below it, all at once, silently, in a PR that was about something else entirely. A
322
- * reader following `coverage_ledger_ → verify.mjs:1603` lands on a comment 219 lines from the dispatcher.
322
+ * reader following `coverage_ledger_ → verify.mjs` to line 1603 landed on a comment 219 lines from the
323
+ * dispatcher. (That number is the 2026 measurement's own datum, not a pointer into today's file.)
323
324
  *
324
325
  * `symbol:` names a thing instead — a function, a constant, an exported name. It survives every move.
325
326
  * `site:` stays as the hint it always was, and `contract-audit.test.mjs` asserts that any `symbol:` a row
@@ -419,9 +420,9 @@ export const INNER_CODES = Object.freeze([
419
420
  // CONNOTATION_FORM_REASONS at connotation-search.mjs:1731 is `CONNOTATION_REASONS` minus
420
421
  // `no_recorded_queries`, and the `connotation_` family row (dynamic D1) is declared against exactly
421
422
  // that list. The four call codes are folded by a template — `connotation_${callFail.reason}` at
422
- // verify.mjs:1013 — so they are namespaced by construction rather than one branch at a time.
423
+ // verify.mjs — so they are namespaced by construction rather than one branch at a time.
423
424
  { code: "call_never_made", mints: ["driver/connotation-search.mjs:2007"], rollsUpTo: ["connotation_call_never_made"],
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." },
425
+ why: "CALL_AUDIT_ROWS. The typed transport's four call states, handed in by disposition-call-audit.mjs and namespaced at verify.mjs." },
425
426
  { code: "call_truncated", mints: ["driver/connotation-search.mjs:2008"], rollsUpTo: ["connotation_call_truncated"],
426
427
  why: "As call_never_made — same table, same projection." },
427
428
  { code: "call_schema_violation", mints: ["driver/connotation-search.mjs:2009"], rollsUpTo: ["connotation_call_schema_violation"],
@@ -2596,29 +2596,45 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2596
2596
  const dropped = (lastFail.match(/connotation_query_unrecorded:(.+)$/s) || [])[1] || "";
2597
2597
  // ── TWO FAULTS, TWO REMEDIES, and sending the wrong one is what made this permanent ────────────
2598
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.
2599
+ // The gate marks each dropped query `[unmatched; nearest recorded: …]` or `[no recorded query
2600
+ // resembles this one]`, and the difference is what the gate can SEE, not what happened.
2601
+ //
2602
+ // UNMATCHED is knowable: something close is recorded, so the search ran and the wording differs.
2603
+ // Re-running changes nothing, and telling a seat to re-run is how a stage fails four times with the
2604
+ // same string.
2605
+ //
2606
+ // NO RESEMBLANCE IS NOT KNOWABLE, and this hint used to pretend otherwise. It said the query was
2607
+ // missing and to go and run it — but a query recorded under a translation, a transliteration or the
2608
+ // seat's own rewording resembles nothing and has already run, and that seat was then sent round the
2609
+ // same loop the unmatched branch exists to break. The gate cannot tell the two apart; no threshold
2610
+ // can, and a threshold that could would be one that hides a query nobody ran.
2611
+ //
2612
+ // So this branch stops asserting and hands over both repairs. Both are cheap, a seat can tell which
2613
+ // applies by looking at its own ledger, and neither wastes an attempt: if the search did run, fix
2614
+ // the row's wording; if it did not, run it and append the row. Where both labels appear, both
2615
+ // sentences are sent, as before.
2603
2616
  const anyUnmatched = /\[unmatched; nearest recorded:/.test(dropped);
2604
- const anyAbsent = /\[absent from the ledger\]/.test(dropped);
2605
- hint = anyUnmatched && !anyAbsent
2617
+ const anyUnresembled = /\[no recorded query resembles this one\]/.test(dropped);
2618
+ hint = anyUnmatched && !anyUnresembled
2606
2619
  ? `these dictated meaning queries ARE recorded in ${gridLedgerName} extras.pr_risk[] under a ` +
2607
2620
  `different wording, which is why the driver cannot match them: ${dropped}. Do NOT re-run them — ` +
2608
2621
  `the search already ran and its results are already in the ledger. EDIT each row's \`query\` ` +
2609
2622
  `field to the query text EXACTLY as the task message dictates it, character for character, and ` +
2610
2623
  `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 ` +
2612
- `missing from ${gridLedgerName} extras.pr_risk[]: ${dropped}. A query that returned NO results is ` +
2613
- `not an excuse to omit it: record it with an empty results array, which is the receipt that the ` +
2614
- `search RAN and came back clean. That is the whole point of the sweep — on an "offensive meaning" ` +
2615
- `query the empty answer IS the good news, and a missing row is indistinguishable from a search ` +
2616
- `nobody performed. Re-run ONLY the listed queries, append a row per query to extras.pr_risk[] ` +
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`;
2624
+ : `the driver cannot match these dictated meaning queries to any row in ${gridLedgerName} ` +
2625
+ `extras.pr_risk[]: ${dropped}. That means one of two things and the driver cannot tell which, so ` +
2626
+ `check your own ledger and do whichever applies — both are cheap. IF THE SEARCH ALREADY RAN and ` +
2627
+ `you recorded it under different wording — a translation, a reordering, your own phrasing — do ` +
2628
+ `NOT run it again. EDIT that row's \`query\` field to the query text EXACTLY as the task message ` +
2629
+ `dictates it, character for character, and leave its results untouched. IF IT NEVER RAN, run it ` +
2630
+ `now and append a row to extras.pr_risk[]. A query that returned NO results still owes its row: ` +
2631
+ `record it with an empty results array, which is the receipt that the search RAN and came back ` +
2632
+ `clean. On an "offensive meaning" query the empty answer IS the good news, and a missing row is ` +
2633
+ `indistinguishable from a search nobody performed. Either way, record each query's text EXACTLY ` +
2634
+ `as the task message dictates it — the driver matches your rows to its list by that text, so a ` +
2635
+ `reworded query reads as one you never ran. Touch ONLY the listed queries and leave every other ` +
2636
+ `recorded row exactly as it is. Where a query is marked \`[unmatched; nearest recorded: …]\` the ` +
2637
+ `driver has already found its row for you: edit that row's \`query\` and do not search again`;
2622
2638
  } else if (/connotation_search_missing/.test(lastFail)) {
2623
2639
  // — was "your PR / reputational section claims a clean meaning … but the ledger recorded ZERO
2624
2640
  // searches", which under the `ensure` prefix instructed the model to make the unbacked claim.
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.1",
5
+ "version": "0.3.2-beta.1",
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": {
@@ -56,7 +56,7 @@
56
56
  "methodology",
57
57
  "handling_note"
58
58
  ],
59
- "why": "a caption-only call renders 149B, clears the 120-char floor, and ships a SECTION OF THE CLIENT'S ONE REPORT without the 'Checks we ran' bullets, the methodology or the handling note. Traced: report-overview.md is the head of report.md (assembleReportMd() in pipeline.mjs) which renders to the HTML (publish/index.mjs:648). The 'Only you can close these' register is NOT lost — it is code-built from findings.json under spec 64's 'code wins' ruling."
59
+ "why": "a caption-only call renders 149B, clears the 120-char floor, and ships a SECTION OF THE CLIENT'S ONE REPORT without the 'Checks we ran' bullets, the methodology or the handling note. Traced: report-overview.md is the head of report.md (assembleReportMd() in pipeline.mjs) which renders to the HTML (publish/index.mjs:649 publishReport). The 'Only you can close these' register is NOT lost — it is code-built from findings.json under spec 64's 'code wins' ruling."
60
60
  },
61
61
  {
62
62
  "tool": "record_prelim_variants",
@@ -10,7 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
10
10
  import { createHash } from "node:crypto";
11
11
  import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
12
12
  import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
13
- import { terminalClampDecision, orderClausesForLede } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
13
+ import { terminalClampDecision, orderClausesForLede, clientConditions } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
14
14
  import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { runStage, correctionHint, gridLedgerNameFor, draftCarryEligible, toolWrittenArtifact, selectEngine } from "./gateway.mjs";
@@ -13006,7 +13006,7 @@ async function pipelineInner(job, opts = {}) {
13006
13006
  const statement = riskStatement({ tier: derived.tier, verdict, reasons: reasonsOut, clauses: orderedClauses,
13007
13007
  basis: isRegisterOnly(ctx.searchPolicy) ? "register-only" : null });
13008
13008
  const tmp = driverDir(run.runDir, "verdict.json.tmp");
13009
- writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, kinds: kindsOut,
13009
+ writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses: orderedClauses, kinds: kindsOut,
13010
13010
  tier: derived.tier, badge: derived.badge, gaugeIndex: derived.gaugeIndex, maxComposite: derived.maxComposite,
13011
13011
  band: derived.band ?? null, statement, stance: verdictStance(verdict) }, null, 2));
13012
13012
  renameSync(tmp, driverDir(run.runDir, "verdict.json"));
@@ -14859,7 +14859,7 @@ async function pipelineInner(job, opts = {}) {
14859
14859
  let emailVerdictOpts = { productName: emailProductName ?? undefined };
14860
14860
  // SPREAD, never reassign: this used to replace the whole object, which would now drop productName
14861
14861
  // above on every run that has a verdict sidecar — i.e. on every healthy run, and on no test.
14862
- try { const v = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); emailVerdictOpts = { ...emailVerdictOpts, verdict: v.verdict, conditions: v.reasons, tier: v.tier, statement: v.statement ?? null }; } catch { /* legacy path — no row */ }
14862
+ try { const v = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); emailVerdictOpts = { ...emailVerdictOpts, verdict: v.verdict, conditions: clientConditions(v), tier: v.tier, statement: v.statement ?? null }; } catch { /* legacy path — no row */ }
14863
14863
  // wp50 — thread the findings too: the table overlay's rating cells are code-bound to the canonical
14864
14864
  // ratings (joinFindingToBlock), never the summary's own words.
14865
14865
  try { emailVerdictOpts.findings = parseFindingsJsonLenient(readFileSync(P.findings, "utf8"))?.findings ?? undefined; } catch { /* no findings — table falls back to the summary words */ }