clearotron 0.5.0-beta.1 → 0.5.0-beta.3

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 (128) hide show
  1. package/.env.example +7 -0
  2. package/INSTALL.md +1 -1
  3. package/THIRD-PARTY-NOTICES.md +38 -7
  4. package/build-info.json +2 -2
  5. package/docs/DELIVERY.md +4 -5
  6. package/docs/architecture/03-run-lifecycle.md +1 -2
  7. package/docs/architecture/04-configuration-reference.md +1 -1
  8. package/docs/architecture/05-config-governance.md +2 -2
  9. package/docs/architecture/06-operations-runbook.md +35 -40
  10. package/docs/architecture/07-quality-and-audit.md +1 -5
  11. package/driver/CHANGELOG.md +18 -0
  12. package/driver/band-shape.mjs +32 -11
  13. package/driver/close-verify.mjs +3 -2
  14. package/driver/contract-e3-backlog.mjs +17 -17
  15. package/driver/contract-vocabulary.mjs +6 -6
  16. package/driver/coverage-form-io.mjs +3 -1
  17. package/driver/coverage-form.mjs +42 -6
  18. package/driver/coverage-ledger.mjs +37 -7
  19. package/driver/declination-call.mjs +44 -17
  20. package/driver/declination-duty.mjs +21 -10
  21. package/driver/degraded-parts.mjs +43 -4
  22. package/driver/deliver-trigger.sh +3 -2
  23. package/driver/driver.config.mjs +26 -28
  24. package/driver/engine/CONTRACT.md +3 -1
  25. package/driver/engine/mcp/README.md +1 -1
  26. package/driver/engine/mcp/clarivate-server.mjs +4 -1
  27. package/driver/engine/mcp/corsearch-server.mjs +4 -1
  28. package/driver/engine/mcp/crowd-note.mjs +34 -0
  29. package/driver/engine/mcp/declination-server.mjs +6 -7
  30. package/driver/engine/mcp/euipo-server.mjs +4 -1
  31. package/driver/engine/mcp/free-tier-server.mjs +4 -1
  32. package/driver/engine/mcp/gather-config.mjs +15 -0
  33. package/driver/engine/mcp/held-answers.mjs +100 -12
  34. package/driver/engine/mcp/perplexity-server.mjs +4 -1
  35. package/driver/engine/mcp/signa-server.mjs +4 -1
  36. package/driver/engine/mcp/stdio-server.mjs +8 -3
  37. package/driver/engine/mcp/supplemental.mjs +66 -8
  38. package/driver/engine/mcp/uspto-local-server.mjs +4 -1
  39. package/driver/engine/openai-agent.mjs +61 -8
  40. package/driver/engine/session-record.mjs +21 -3
  41. package/driver/envelope-settle.mjs +13 -3
  42. package/driver/findings-model.mjs +16 -0
  43. package/driver/frame-diff-model.mjs +53 -1
  44. package/driver/gateway.mjs +31 -10
  45. package/driver/outbox.mjs +2 -1
  46. package/driver/package.json +1 -1
  47. package/driver/parts-not-completed.mjs +128 -0
  48. package/driver/pipeline-knockout.mjs +5 -4
  49. package/driver/pipeline.mjs +1470 -787
  50. package/driver/placement-decisions.mjs +21 -13
  51. package/driver/placement-form-io.mjs +21 -1
  52. package/driver/placement-form.mjs +40 -21
  53. package/driver/placement-union.mjs +56 -11
  54. package/driver/publish/audit-from-spine.mjs +6 -3
  55. package/driver/publish/index.mjs +45 -14
  56. package/driver/publish/publish-inputs.mjs +8 -0
  57. package/driver/publish/render.mjs +48 -11
  58. package/driver/publish/report-data.mjs +4 -0
  59. package/driver/publish/search-depth.mjs +10 -3
  60. package/driver/publish/xlsx.mjs +30 -16
  61. package/driver/record-fold.mjs +4 -1
  62. package/driver/register-digest-record.mjs +57 -5
  63. package/driver/register-plan.mjs +87 -20
  64. package/driver/registration-scripts.mjs +56 -0
  65. package/driver/remedy-accounting.mjs +1 -1
  66. package/driver/repair-composers.mjs +35 -49
  67. package/driver/report-overview-record.mjs +1 -1
  68. package/driver/research-answers.mjs +108 -0
  69. package/driver/run-economics.mjs +3 -2
  70. package/driver/runner.mjs +4 -4
  71. package/driver/scope-ledger.mjs +13 -6
  72. package/driver/skeptic-search.mjs +19 -3
  73. package/driver/skills/blind-frame/SKILL.md +1 -1
  74. package/driver/skills/case-law-citation/SKILL.md +5 -5
  75. package/driver/skills/case-law-citation/sources/courtlistener.md +2 -2
  76. package/driver/skills/case-law-citation/sources/eurlex.md +2 -2
  77. package/driver/skills/case-law-citation/sources/legaldatahunter.md +1 -1
  78. package/driver/skills/clearance-common-law/SKILL.md +26 -34
  79. package/driver/skills/clearance-common-law/perplexity-prompts.md +4 -6
  80. package/driver/skills/clearance-register/SKILL.md +18 -26
  81. package/driver/skills/clearance-register/digest.md +14 -10
  82. package/driver/skills/clearance-register/providers/clarivate.md +10 -8
  83. package/driver/skills/clearance-register/providers/corsearch.md +4 -5
  84. package/driver/skills/clearance-register/providers/euipo.md +1 -2
  85. package/driver/skills/clearance-register/providers/signa.md +5 -6
  86. package/driver/skills/clearance-register/providers/uspto-local.md +3 -3
  87. package/driver/skills/clearance-register/register-recipes.md +33 -42
  88. package/driver/skills/clearance-register/unit.md +18 -26
  89. package/driver/skills/clearance-search/SKILL.md +14 -30
  90. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -3
  91. package/driver/skills/clearance-search/phase2-execution.md +23 -37
  92. package/driver/skills/clearance-search/synthesis-rules.md +4 -3
  93. package/driver/skills/clearance-search/worked-examples.md +62 -62
  94. package/driver/skills/clearance-variants/SKILL.md +20 -20
  95. package/driver/skills/clearance-variants/transliteration-scripts.md +3 -3
  96. package/driver/skills/knockout-assess/SKILL.md +3 -2
  97. package/driver/skills/knockout-frame/SKILL.md +3 -3
  98. package/driver/skills/matter-frame/SKILL.md +1 -1
  99. package/driver/skills/narrative-refutation/SKILL.md +6 -6
  100. package/driver/skills/placement-inquiry/SKILL.md +9 -9
  101. package/driver/stages.mjs +65 -31
  102. package/driver/suite-census.json +506 -44
  103. package/driver/systemd/README.md +54 -150
  104. package/driver/verify.mjs +19 -10
  105. package/mcp-server/CHANGELOG.md +8 -0
  106. package/mcp-server/package.json +1 -1
  107. package/mcp-server/server.mjs +1 -1
  108. package/package.json +1 -1
  109. package/portal-ui/package.json +1 -1
  110. package/providers/_shared/README.md +1 -0
  111. package/providers/_shared/enumerate.mjs +11 -8
  112. package/providers/_shared/execute-plan.mjs +122 -11
  113. package/providers/_shared/follow-up-searches.mjs +59 -8
  114. package/providers/_shared/send-until.mjs +25 -0
  115. package/providers/clarivate/src/core.js +63 -4
  116. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  117. package/providers/oauth-mcp-bridge/package.json +1 -1
  118. package/providers/perplexity/src/core.js +42 -6
  119. package/scripts/band-shape-probe.mjs +1 -1
  120. package/scripts/e2e-first-time.mjs +7 -1
  121. package/scripts/e2e-scenario-ops.mjs +17 -2
  122. package/scripts/freeze-example-run.mjs +8 -8
  123. package/shared/client-failure-note.mjs +13 -0
  124. package/shared/names-in-force.mjs +1 -1
  125. package/shared/slices-taken-out.mjs +65 -0
  126. package/skills/clearotron-account/SKILL.md +1 -1
  127. package/skills/clearotron-ops/COURIER.md +11 -3
  128. package/skills/clearotron-ops/SKILL.md +4 -1
package/.env.example CHANGED
@@ -184,6 +184,13 @@ CLEAROTRON_DATABASE=clarivate
184
184
  # CLEAROTRON_GATHER_SESSION_KEY, CLEAROTRON_GATHER_SESSION_ID, CLEAROTRON_GATHER_AGENT,
185
185
  # CLEAROTRON_REGISTER_CALL_LOG, CLEAROTRON_REGISTER_RECORD_LOG
186
186
  #
187
+ # The time, in epoch milliseconds, at which a turn's register tools stop sending new questions, so the
188
+ # unit can write what it holds inside its own time. The driver writes it per turn into each register
189
+ # tool server's env, a quarter of the turn (at most five minutes) ahead of the turn's deadline. Leave it
190
+ # empty: a value set here would be read by the driver's own code-side executor as a fixed time.
191
+ # effect: tuning
192
+ CLEAROTRON_GATHER_SEND_UNTIL_MS=
193
+ #
187
194
  # THERE IS NO RETENTION SETTING AND NO CLEANUP JOB. CLEAROTRON_REGISTER_LEDGER_MAX_BYTES,
188
195
  # CLEAROTRON_REGISTER_LEDGER_KEEP and `npm run prune:ledger` are gone with the rotation tool, because the
189
196
  # thing they bounded no longer grows: RECORD BODIES ARE WRITTEN INTO THE RUN'S OWN DIRECTORY
package/INSTALL.md CHANGED
@@ -59,7 +59,7 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
59
59
  it first. VS Code's Remote-SSH port forwarding is the common case. If the page that opens is not this
60
60
  install's sign-in, run the same command again with `--port 28802`, or any free number.
61
61
 
62
- A *hosted* deployment needs Linux for one further thing, the systemd outbox trigger —
62
+ A *hosted* deployment runs the product as systemd user units, so it needs Linux —
63
63
  [driver/systemd/README.md](driver/systemd/README.md).
64
64
  - **Setup installs the reasoning program.** Clearotron runs each step of a clearance as a short,
65
65
  unattended session of Claude Code or the Codex CLI. Setup installs the one your engine uses, for this
@@ -9,7 +9,7 @@ file rather than a courtesy.
9
9
  production tree; `--check` fails when it no longer matches. Development dependencies are excluded:
10
10
  they are not distributed, so they carry no obligation for anyone who installs this.
11
11
 
12
- 177 packages.
12
+ 178 packages.
13
13
 
14
14
  > **Declared but NOT INSTALLED, so not distributed and not attributed below:** `@cfworker/json-schema`.
15
15
  > npm lists these with no version and no path — an unmet or optional peer. Recorded because a name
@@ -534,7 +534,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
534
534
  THE SOFTWARE.
535
535
  ```
536
536
 
537
- ## body-parser@2.3.0
537
+ ## body-parser@2.4.0
538
538
 
539
539
  - **Licence declared:** `MIT`
540
540
  - **Repository:** expressjs/body-parser
@@ -932,6 +932,37 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
932
932
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
933
933
  ```
934
934
 
935
+ ## content-type@2.1.0
936
+
937
+ - **Licence declared:** `MIT`
938
+ - **Repository:** jshttp/content-type
939
+ - **Licence file:** `LICENSE`
940
+
941
+ ```
942
+ (The MIT License)
943
+
944
+ Copyright (c) 2015 Douglas Christopher Wilson
945
+
946
+ Permission is hereby granted, free of charge, to any person obtaining
947
+ a copy of this software and associated documentation files (the
948
+ 'Software'), to deal in the Software without restriction, including
949
+ without limitation the rights to use, copy, modify, merge, publish,
950
+ distribute, sublicense, and/or sell copies of the Software, and to
951
+ permit persons to whom the Software is furnished to do so, subject to
952
+ the following conditions:
953
+
954
+ The above copyright notice and this permission notice shall be
955
+ included in all copies or substantial portions of the Software.
956
+
957
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
958
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
959
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
960
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
961
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
962
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
963
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
964
+ ```
965
+
935
966
  ## content-type@2.0.0
936
967
 
937
968
  - **Licence declared:** `MIT`
@@ -1968,7 +1999,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1968
1999
  SOFTWARE.
1969
2000
  ```
1970
2001
 
1971
- ## fast-uri@3.1.8
2002
+ ## fast-uri@3.1.9
1972
2003
 
1973
2004
  - **Licence declared:** `BSD-3-Clause`
1974
2005
  - **Repository:** https://github.com/fastify/fast-uri
@@ -2626,7 +2657,7 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
2626
2657
  PERFORMANCE OF THIS SOFTWARE.
2627
2658
  ```
2628
2659
 
2629
- ## ip-address@10.7.2
2660
+ ## ip-address@10.7.5
2630
2661
 
2631
2662
  - **Licence declared:** `MIT`
2632
2663
  - **Repository:** https://github.com/beaugunderson/ip-address
@@ -4927,7 +4958,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
4927
4958
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4928
4959
  ```
4929
4960
 
4930
- ## qs@6.16.0
4961
+ ## qs@6.16.1
4931
4962
 
4932
4963
  - **Licence declared:** `BSD-3-Clause`
4933
4964
  - **Repository:** https://github.com/ljharb/qs
@@ -6193,7 +6224,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
6193
6224
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6194
6225
  ```
6195
6226
 
6196
- ## undici@7.29.1
6227
+ ## undici@7.30.1
6197
6228
 
6198
6229
  - **Licence declared:** `MIT`
6199
6230
  - **Repository:** https://github.com/nodejs/undici
@@ -6476,7 +6507,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
6476
6507
  OTHER DEALINGS IN THE SOFTWARE.
6477
6508
  ```
6478
6509
 
6479
- ## zod@4.4.3
6510
+ ## zod@4.6.7
6480
6511
 
6481
6512
  - **Licence declared:** `MIT`
6482
6513
  - **Repository:** https://github.com/colinhacks/zod
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "39f3bdc47543630a4f206ac667aa0fd923a2b32b",
3
- "version": "0.5.0-beta.1"
2
+ "commit": "9a5ba90756f37ddefaee2a21ab79e9f7a374667a",
3
+ "version": "0.5.0-beta.3"
4
4
  }
package/docs/DELIVERY.md CHANGED
@@ -142,7 +142,7 @@ drives the whole delivery side over the ops MCP face — mint its token verb-sco
142
142
  `sendPending` until a send is confirmed, whichever lane wrote the packet, and `mark_sent` is the only
143
143
  thing that clears it. Acknowledging the event instead removes the marker and leaves the run owed, so
144
144
  the backstop scan re-arms it and the requester is told again on the next sweep.
145
- 4. For the remaining kinds — `intake-rejected`, `duplicate-skipped`, `late-bind-ack` — route the
145
+ 4. For the remaining kinds — `intake-rejected`, `duplicate-skipped`, `pre-run-failed`, `late-bind-ack` — route the
146
146
  packet's `text` → **`ack_event(file)`** (idempotent; validated as a bare `*.pending` name). These
147
147
  describe something that did not become a run, so there is no run to settle.
148
148
 
@@ -188,10 +188,9 @@ double-sends (`.sent` guards it).
188
188
  ## Legacy notes
189
189
 
190
190
  - The `delivered` outbox marker's plain-text body (agent id) predates the JSON packets and is
191
- kept for compatibility with the existing watcher (`driver/deliver-trigger.sh` + the
192
- `driver/systemd/` reference units, which wake an agent). Those are **one deployment's reference
193
- integration**, not part of the headless product contract; the full packet is always in
194
- `_driver/delivery.json`.
191
+ kept for compatibility with `driver/deliver-trigger.sh`, **one deployment's reference wake**. Its units no
192
+ longer ship, and the packaged install runs no outbox watcher: the integrator wakes itself. The script is not
193
+ part of the headless product contract; the full packet is always in `_driver/delivery.json`.
195
194
 
196
195
  ## One report per run
197
196
 
@@ -244,8 +244,7 @@ Reading order for the phases, with what code decides at each:
244
244
  by receipt; survivors become a front-matter coverage note, not a halt.
245
245
  6. **Frame-diff + bounded reopen** — the blind frame is diffed against the run's own framing;
246
246
  directives (including deterministic mechanical form-gap directives) can reopen register and
247
- source arms once, under a fetch ceiling (`CLEAROTRON_REOPEN_MAX_FETCH`, default 150), with
248
- per-directive closure verification. The whole block is non-fatal; unclosed directives demote to
247
+ source arms once, with per-directive closure verification. The whole block is non-fatal; unclosed directives demote to
249
248
  disclosed deferrals that later clamp the verdict.
250
249
  7. **Placement → register-digest** — both fatal. Every digest pass (fresh, escalation, envelope,
251
250
  late-bind, stale-repair) goes through the single `runDigest` chokepoint, which drops stale ledgers,
@@ -282,7 +282,6 @@ unrecognised policy value that leaves the default behaviour standing.
282
282
  | `CLEAROTRON_PLAN_DISPATCH` (`0` or `off` disables) | Pure-code provider `executePlan` repairs at fan-in and reopen. **Never silently inert:** every entry in `PROVIDERS` ships an `executePlan` adapter, and `preflightCredentials` refuses the run before any spend under one that does not — a credential is not a capability. §5.4 of [05-config-governance.md](05-config-governance.md) still lists `signa` as the exception to that and has not been updated since its two missing tools were mounted. |
283
283
  | `CLEAROTRON_FRAME_REOPEN` (+ `CLEAROTRON_FRAME_REOPEN_MAX`, default 1) | The bounded frame-diff reopen. |
284
284
  | `CLEAROTRON_REGISTER_GAP_CLAMP` | The registerGap verdict clamp arm. |
285
- | `CLEAROTRON_REOPEN_MAX_FETCH` (default 150) | Detail-fetch ceiling inside the reopen closure pass. |
286
285
  | `CLEAROTRON_UNREACHABLE_SENIOR` (`open-item` \| `clamp`, default `open-item`) | Policy when a verdict-driving senior right can't be retrieved. |
287
286
 
288
287
  ### Payload ceilings
@@ -374,6 +373,7 @@ cannot be read as one list.
374
373
  | `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
375
374
  | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
376
375
  | `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
376
+ | `CLEAROTRON_GATHER_SEND_UNTIL_MS` | set per turn | When a turn's register tools stop sending new questions: a quarter of the turn, at most five minutes, ahead of its deadline (set by the gather config; not operator-set). |
377
377
  | `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
378
378
  | `PORTAL_READ_MODEL` | `sonnet` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
379
379
  | `CLEAROTRON_ORDER_PROBE_SEED` | unset | Seed for `scripts/band-shape-probe.mjs`, so an ordering probe can be replayed. A diagnostic script's knob, not a run's. |
@@ -205,7 +205,7 @@ move once the same window has been read across more runs.
205
205
 
206
206
  `CLEAROTRON_PLAN_DISPATCH`, `CLEAROTRON_SATPROBE_CODESIDE`,
207
207
  `CLEAROTRON_BAND_TRUTH_GATE` (**never disable in prod — restores the fabrication**),
208
- `CLEAROTRON_FRAME_REOPEN` (+`CLEAROTRON_FRAME_REOPEN_MAX`=1, `CLEAROTRON_REOPEN_MAX_FETCH`=150),
208
+ `CLEAROTRON_FRAME_REOPEN` (+`CLEAROTRON_FRAME_REOPEN_MAX`=1),
209
209
  `CLEAROTRON_REGISTER_GAP_CLAMP`, `CLEAROTRON_WARM_RETRY`,
210
210
  `CLEAROTRON_MODEL_WIRE_CHECK` (fails a turn whose
211
211
  provider reports a different model FAMILY than the driver asked for; disarming it silences the refusal
@@ -282,7 +282,7 @@ edited — that box runs pre-rebuild code and genuinely still sets it.
282
282
  `CLEAROTRON_OUTBOX_BACKOFF_MAX_RETRIES` (5) / `CLEAROTRON_OUTBOX_GIVEUP_COOLDOWN_SEC` (3600 — note this one
283
283
  drops BACKOFF from the prefix, which the elided spelling this row used to carry actively misled about),
284
284
  `CLEAROTRON_OUTBOX_NOPROGRESS_MAX`, `TRADEMARK_MSGID_DOMAIN` (enqueue.local).
285
- Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`.
285
+ Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`; per turn: `CLEAROTRON_GATHER_SEND_UNTIL_MS`.
286
286
 
287
287
  **Who the completion notice reaches** — `CLEAROTRON_REQUESTER_WHATSAPP` (empty; effect: deployment) is a
288
288
  JSON map of requester email or handle → number, and `CLEAROTRON_WHATSAPP_OPERATOR_COPY` (on unless `0`,
@@ -20,41 +20,37 @@ directives); `%h` is that account's home. The load-bearing operational fact: the
20
20
  ```mermaid
21
21
  flowchart TB
22
22
  subgraph VM["Single VM — systemd --user (service account)"]
23
- DP[".path: watches 3 agent queue globs<br/>(inotify, sub-second)"]
24
- DT[".timer: 90s fallback re-drain<br/>(covers unwatched queues)"]
25
- DS["prelim-driver.service<br/>oneshot: node runner.mjs<br/>TimeoutStartSec=6h"]
26
- OP["outbox .path: *.pending markers"]
27
- OT["outbox .timer: 50min rescan<br/>(fixed-cadence backstop)"]
28
- OS["prelim-outbox.service<br/>oneshot: deliver-trigger.sh<br/>TimeoutStartSec=1500"]
29
- PS["profile-service.service<br/>long-running, loopback :18794"]
30
- GW["integrator agent-platform gateway<br/>(reference integration; comms plane)"]
31
- DP --> DS
23
+ WK["clearotron-worker.service<br/>simple: node runner.mjs --watch<br/>drains the queue, resumes parked runs"]
24
+ PT["clearotron-portal.service<br/>simple: node portal-service.mjs"]
25
+ MF["clearotron-mcp-face.service<br/>simple: node mcp-server/http-server.mjs"]
26
+ CM["clearotron-client-mcp.service<br/>simple: node mcp-server/http-server-client.mjs"]
27
+ DT["clearotron-deploy.timer<br/>boot+5min, every 1h (optional)"]
28
+ DS["clearotron-deploy.service<br/>oneshot: clearotron update"]
32
29
  DT --> DS
33
- OP --> OS
34
- OT --> OS
30
+ PT --> MF
35
31
  end
36
- Q[("per-agent queue dirs")] -.watched.-> DP
37
- OB[("outbox: runId.pending")] -.watched.-> OP
32
+ Q[("per-agent queue dirs")] -.polled.-> WK
38
33
  POOL[("publish pool — CLEAROTRON_REPORTS_DIR<br/>(web-served, group-readable)")]
39
- CF["auth proxy + tunnel<br/>(this deployment: Cloudflare)"] --> CADDY["reverse proxy<br/>(this deployment: Caddy)"] --> PS
34
+ WK --> POOL
35
+ CF["auth proxy + tunnel<br/>(this deployment: Cloudflare)"] --> CADDY["reverse proxy<br/>(this deployment: Caddy)"] --> PT
40
36
  CADDY --> POOL
41
- DS --> POOL
42
- OS --> GW
43
37
  ```
44
38
 
45
39
  | Unit | Trigger | Execs | Key settings (why) |
46
40
  |---|---|---|---|
47
- | `prelim-driver.path` | new `*.json` in the **three explicitly listed** agent queues | `prelim-driver.service` | A new agent workspace is only covered by the timer until this unit gains a glob line + deploy |
48
- | `prelim-driver.timer` | boot+2min, every 90s | same | Self-healing backstop; atomic claims mean a re-drain can never double-run |
49
- | `prelim-driver.service` | (path/timer only — not `[Install]`ed) | `node runner.mjs` | `Type=oneshot` (single instance guaranteed); `EnvironmentFile=%h/.env`; `StartLimitIntervalSec=0` (a gateway outage must not lock the unit out); **`TimeoutStartSec=6h`** — a ceiling for a *wedged* runner only; honest drains are bounded by the runner's 2h admission budget (`CLEAROTRON_ADMISSION_BUDGET_MS`) + one run's wall. Change the two together: a 6h unit ceiling under a longer admission budget kills honest drains |
50
- | `prelim-outbox.path` | `*.pending` in the outbox | `prelim-outbox.service` | Level-triggered: re-fires while any marker exists — the script's pacing sleep is load-bearing |
51
- | `prelim-outbox.service` | (path/timer) | `driver/deliver-trigger.sh` | **`TimeoutStartSec=1500`**: one hung wake once wedged the lane 19h (the CLI finished its turn but never exited); with a real ceiling the unit self-heals. The 1500 s covers the worst legitimate wake end to end — the script's own `timeout 840 --kill-after=30` wall, plus its drain wait (180 s) and its pacing sleep (≤300 s). Change them together |
52
- | `prelim-outbox.timer` | boot+10min, every 50min | same | Rescan for owed sends (`sendPending` true, no `.sent`); self-defers while the instant lane is busy |
53
- | `profile-service.service` | long-running | `node profile-service.mjs` | Loopback :18794 behind the reverse proxy and auth gate (this deployment: Caddy + Cloudflare Access); deliberately **no** `EnvironmentFile` (least-privilege; pins its own non-secret audience identifiers) |
54
-
55
- **"Activating" for hours is normal** on `prelim-driver.service` during a multi-job drain — oneshot
56
- semantics put the whole drain inside the activating state. Do not add `Restart=` or a start limit;
57
- the unit comments and tests pin why.
41
+ | `clearotron-worker.service` | long-running (`[Install]`ed) | `node driver/runner.mjs --watch` | `Type=simple`; polls for queued jobs and for parked runs whose window has passed; `Restart=on-failure`, `RestartSec=10`. The one unit that spends |
42
+ | `clearotron-portal.service` | long-running | `node driver/portal-service.mjs` | The UI, the API, the sign-in and the configuration surface; ordered after the MCP face, which it calls |
43
+ | `clearotron-mcp-face.service` | long-running | `node mcp-server/http-server.mjs` | The engine door the portal calls over MCP |
44
+ | `clearotron-client-mcp.service` | long-running | `node mcp-server/http-server-client.mjs` | The door for a company's signed-in people and their access keys |
45
+ | `clearotron-deploy.timer` | boot+5min, every 1h | `clearotron-deploy.service` | Optional: keeps the checkout on `origin/main` |
46
+ | `clearotron-deploy.service` | (timer only) | `clearotron update` | `Type=oneshot`; refuses while a run is queued or in flight and exits 4, which `SuccessExitStatus=4` counts as success, so a busy box skips that firing; `TimeoutStartSec=30min`, so a hung `npm ci` cannot block later firings |
47
+
48
+ Every unit loads `EnvironmentFile=%h/.env`, expands `${CLEAROTRON_CHECKOUT_DIR}` in `ExecStart=` and sets
49
+ `CLEAROTRON_NO_ENV_FILE=1`. `clearotron start --background` installs the four services; the deploy pair is copied by
50
+ hand ([`driver/systemd/README.md`](../../driver/systemd/README.md)). The worker's normal state is `active (running)`.
51
+
52
+ The packaged install runs no outbox watcher: the integrator wakes itself to courier what the outbox holds
53
+ ([`docs/DELIVERY.md`](../DELIVERY.md)), and `driver/deliver-trigger.sh` is one agent platform's reference wake.
58
54
 
59
55
  Delivery truth lives in the run dir, not the marker: `status.sendPending` + the `.sent` sentinel
60
56
  make wakes idempotent — a lost marker can delay a send (≤50 min), never lose or double it.
@@ -64,7 +60,7 @@ settle it).
64
60
 
65
61
  ## Deploy
66
62
 
67
- This repository ships the unit files (`driver/systemd/`, seven of them) and the delivery trigger
63
+ This repository ships the unit files (`driver/systemd/`, six of them) and a reference delivery wake
68
64
  (`driver/deliver-trigger.sh`); it does not ship the script that installs them. Whatever plays that
69
65
  role on your host — a dispatcher, a config-management run, a hand-written script — owes the
70
66
  properties below, each of which was learned the expensive way:
@@ -75,21 +71,20 @@ properties below, each of which was learned the expensive way:
75
71
  deployment would drain plus the run-slot locks, and refuse on any of them. `--override
76
72
  "<reason>"` is the operator's deliberate exception; the reason is printed into the deploy output, and a
77
73
  blank one exits 2 rather than passing. This step used to be a line of prose asking a human to
78
- check, which is not a guard on the night it matters. Then **stop the four trigger units** for
79
- the deploy window and install an **EXIT trap that restarts them on every exit path**, success or
80
- abort — trap *after* stop, or an early abort leaves the machine silent.
74
+ check, which is not a guard on the night it matters. Then **stop the worker** for the deploy
75
+ window and install an **EXIT trap that restarts it on every exit path**, success or abort — trap
76
+ *after* stop, or an early abort leaves the machine silent.
81
77
  2. **Skill scanner gate** over `driver/skills/` — the tree the driver actually reads — and over any
82
78
  skills tree your integrator carries. Abort on high/critical findings, and if the scanner is
83
79
  absent say so loudly rather than passing: a gate that cannot run is not a gate that passed.
84
80
  3. **Drift detection on live agent workspaces** — abort on local skill edits rather than promoting
85
81
  them; silent auto-promotion twice resurrected intentionally deleted files. A skill relocated into
86
82
  the driver tree is relocation, not drift.
87
- 4. **Driver block** — install dependencies, copy the seven unit files + `daemon-reload`,
88
- install/enable/restart the sibling services (`profile-service`, `client-access`, `client-mcp`), and
89
- **pre-create the queue dirs and the outbox**: an inotify `.path` watch on a directory that does
90
- not exist never fires.
91
- 5. **Restart the gateway last**, with no live run and the triggers stopped, then re-enable the four
92
- trigger units.
83
+ 4. **Driver block** — install dependencies, copy the unit files + `daemon-reload`,
84
+ install/enable/restart the deployment's sibling services (`client-access`, `client-mcp`), and
85
+ **pre-create the queue dirs and the outbox**.
86
+ 5. **Restart the gateway last**, with no live run and the worker stopped, then start the worker
87
+ again.
93
88
 
94
89
  ### Never hand-edit (a deploy overwrites these silently)
95
90
 
@@ -117,8 +112,8 @@ and the environment file holding the secrets.
117
112
 
118
113
  | Surface | What it tells you |
119
114
  |---|---|
120
- | `systemctl --user status prelim-driver.{path,timer,service} clearance-outbox.{path,timer,service} profile-service` | Trigger health; remember "activating = draining" |
121
- | `journalctl --user -u prelim-driver.service -f` | Runner notes (stderr): claims, dedup parks, preflight failures, orphan reclaims |
115
+ | `systemctl --user status clearotron-worker clearotron-portal clearotron-mcp-face clearotron-client-mcp clearotron-deploy.timer` | Unit health; the four services read `active (running)` |
116
+ | `journalctl --user -u clearotron-worker.service -f` | Runner notes (stderr): claims, dedup parks, preflight failures, orphan reclaims |
122
117
  | Run dir `status.json` / `run.jsonl` | Per-run state + the append-only decision trace; grep keys: `axes`, `profile`, `verdict`, `escalation`, `postponed`, `delivered`, `profile-mismatch` |
123
118
  | `_driver/<stage>.jsonl` | Per-attempt telemetry: status, fail token, kill signals, wall, tokens |
124
119
  | Queue dirs | Marker states at a glance ([03 §2](03-run-lifecycle.md#2--claim-identity-and-crash-safety)): `.json`, `.processing` (+`.skips` — a growing skip tally is the wedge signature), `.postponed`, `.duplicate`, `.done`, `.failed` |
@@ -161,7 +156,7 @@ survive until terminal state.
161
156
  | Run parked with `*.tainted-N` artifacts | Timeout-taint convergence loop ([07 §3](07-quality-and-audit.md#3--completed-coverage-honesty-in-code)) | Let it converge; repeated signature goes terminal honestly |
162
157
  | Chat failure ping never arrived for a failed run | By design: nothing here sends. The failure packet IS the notice, and an integrator consumes it | Check `_driver/failure.json` + the outbox lane (the guaranteed notice) |
163
158
  | Deploy refused because a run is in flight | The in-flight guard above | Wait for the run. A force-restart of the gateway is not the answer — that is for a wedged gateway |
164
- | Everything quiet after a deploy abort | Should not happen (EXIT trap restarts triggers) — if it does: `systemctl --user start prelim-driver.{path,timer} clearance-outbox.{path,timer}` and file it |
159
+ | Everything quiet after a deploy abort | Should not happen (EXIT trap restarts triggers) — if it does: `systemctl --user start clearotron-worker.service` and file it |
165
160
 
166
161
  ## Selftest — retired
167
162
 
@@ -151,11 +151,7 @@ What makes the coverage statement trustworthy:
151
151
 
152
152
  Register budgets deserve one honest sentence: the per-worker call budgets in the skill prose
153
153
  (enumerates, phoneme ≤ 5, image ≤ 10) are **observed, prompt-level budgets, not code-enforced
154
- quotas** — and so is the reopen detail-fetch ceiling (`CLEAROTRON_REOPEN_MAX_FETCH`, default 150).
155
- `reopenFetchCeiling` resolves the figure in code, then the reopen prompt builders
156
- (`buildFrameReopenFollowup`, `buildFrameReopenRetryMessage`) interpolate it into the instruction the
157
- model reads; nothing counts a run's detail-fetches and nothing refuses the next one. The bound that
158
- *is* code is the executor's enumerate resource guard (`CLEAROTRON_ENUMERATE_CEILING`): it owns the page
154
+ quotas**. The bound that *is* code is the executor's enumerate resource guard (`CLEAROTRON_ENUMERATE_CEILING`): it owns the page
159
155
  loop and returns an honest `incomplete` descriptor for an over-ceiling band rather than a truncated
160
156
  one. Calls are metered per run (billing-grade ledger), never hard-capped.
161
157
 
@@ -1,5 +1,23 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.5.0-beta.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: The delivery email for a result the review could not sign off now lists the report's "Only you can close these" items.
8
+ - Fixed: When a clearance fails, its chat notice now goes to the person who asked for it, with a copy to the operator.
9
+ - Fixed: A clearance no longer repeats a finished step because a web lookup inside it timed out.
10
+ - Fixed: A clearance whose register search cannot finish, such as one that keeps timing out, now delivers its report instead of ending with none.
11
+ - Fixed: A search that cannot be shown to have run is now listed as not searched, and the report is still delivered.
12
+ - Fixed: Reports can set aside an identical mark registered for unrelated goods, recording the reason in the audit workbook instead of listing it as a finding.
13
+
14
+ ## 0.5.0-beta.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Fixed: Reports no longer show "Nothing found" in "Where it stands" for a country the register could not search; that country is left out.
19
+ - Fixed: A finding registered in several countries now shows under each of them in "Where it stands". Before, only its first country showed it, and the others could read "Nothing found".
20
+
3
21
  ## 0.5.0-beta.1
4
22
 
5
23
  ### Patch Changes
@@ -664,11 +664,12 @@ export function recordGroup({ tier, live, inScope, hits, dominant = false }) {
664
664
  * EACH GROUP RUNS IN THE ORDER THE OWNER RULED (2026-10-12): a line with the client's goods on a live record
665
665
  * in the ordered classes first; then more live records in those classes; then more goods words; then a
666
666
  * client market; then more records. Code sorts on fields the records carry and judges nothing. A line
667
- * carries the goods words it matched, never the goods text, which is one lookup away.
667
+ * carries the goods words it matched, and not the goods text, which is one lookup away, except a floor in (b):
668
+ * it matched none of the client's words, so it shows the goods the test read, cut short.
668
669
  *
669
670
  * A line sits in the best group its records in scope reach. `keysOf` gives a record's line and its owner's
670
671
  * set-aside key, both read through the positions fold, so a line covers exactly what a row naming one of
671
- * its records reaches. A line carries the words matched, so nothing on it is cut; the one line that cannot
672
+ * its records reaches. A line carries the words matched, so nothing on it is cut but a (b) floor's goods; the one line that cannot
672
673
  * be judged from itself is one the goods test placed while its records carry no goods on file.
673
674
  *
674
675
  * @param records the band's enumerated records
@@ -708,6 +709,19 @@ function bindingOffices(territories, code) {
708
709
  return out;
709
710
  }
710
711
 
712
+ // The goods text the test read for a record: the reader hands it as text, or as one statement per class, each with its
713
+ // text. Then that text cut short for a line.
714
+ const goodsReadText = (g) => (Array.isArray(g) ? g.map((x) => (typeof x === "string" ? x : x?.text)) : [typeof g === "string" ? g : g?.text])
715
+ .filter((t) => typeof t === "string" && t.trim()).join("; ").replace(/\s+/g, " ").trim() || null;
716
+ export const FLOOR_GOODS_SHOWN = 140;
717
+ function cutGoods(text) {
718
+ const t = String(text ?? "");
719
+ if (t.length <= FLOOR_GOODS_SHOWN) return t;
720
+ const head = t.slice(0, FLOOR_GOODS_SHOWN);
721
+ const space = head.lastIndexOf(" ");
722
+ return `${(space > FLOOR_GOODS_SHOWN / 2 ? head.slice(0, space) : head).replace(/[\s,;:.]+$/, "")}…`;
723
+ }
724
+
711
725
  export function bandGroups(records, { classify, inScope, goods = null, dominantTerm = "", keysOf = null, markets = [], territories = [], evidenceOf = null } = {}) {
712
726
  const fold = (x) => foldDiacritics(String(x ?? "")).toLowerCase();
713
727
  const dom = fold(dominantTerm).trim();
@@ -746,7 +760,8 @@ export function bandGroups(records, { classify, inScope, goods = null, dominantT
746
760
  const group = outside && !floor ? "rest" : recordGroup({ tier, live, inScope: s, hits, dominant });
747
761
  // "On file" is a body `band_record` can open: goods read off the band row decide the group, never that.
748
762
  const onFile = goods?.held ? goods.held(r) : Boolean(g);
749
- const rec = { r, tier, hits, words, live, inScope: s, group, outside, floor, onFile, territories: ts, inMarket: ts.some((t) => market.has(t)) };
763
+ const rec = { r, tier, hits, words, live, inScope: s, group, outside, floor, onFile, territories: ts, inMarket: ts.some((t) => market.has(t)),
764
+ ...(floor ? { read: goodsReadText(g) } : {}) };
750
765
  const keys = typeof keysOf === "function" ? keysOf(r) : null;
751
766
  const ownerKey = keys?.owner ?? (normKey(r?.owner_name) ? `owner:${normKey(r?.owner_name)}` : `record:${String(r?.record_id ?? "").trim().toLowerCase()}`);
752
767
  const key = keys?.line ?? `line:${ownerKey}|${normMark(r?.mark_text) || String(r?.record_id ?? "").trim().toLowerCase()}`;
@@ -788,6 +803,9 @@ export function bandGroups(records, { classify, inScope, goods = null, dominantT
788
803
  return { goods_evidence: ev?.state ?? null, ...(ev?.state === "unknown" && ev?.readable == null ? { goods_text: false } : {}) };
789
804
  })() : {}),
790
805
  floors: l.recs.filter((x) => x.floor).length,
806
+ // A FLOOR IN (b) SHOWS THE GOODS THE TEST READ, cut short: a (b) line matched none of the client's words, so
807
+ // without them a page of floors could not be judged without opening each record.
808
+ ...(group === "b" && known ? (() => { const read = l.recs.find((x) => x.floor && x.read)?.read; return read ? { floor_goods: cutGoods(read) } : {}; })() : {}),
791
809
  ...(known ? {} : { goods_on_file: inGroup.some((x) => x.onFile) }),
792
810
  ...(offices.size && l.recs.every((x) => x.outside) ? { outside: true } : {}),
793
811
  };
@@ -1109,13 +1127,14 @@ export const PLACEMENT_GROUP_ORDERS = Object.freeze([SORT_THE_NEAR_ONES, ONE_SET
1109
1127
  // group's identity, members and count stay, and so does the fact that a crowded group prints only its first page.
1110
1128
  export const FLOORS_BY_NAME_HERE = "Every floor here is placed or set aside by name; this group's row covers only its other lines.";
1111
1129
  export const A_CROWDS_ONE_ROW = "Read the first page, carry forward any line a lawyer would raise, and set the rest aside in this group's one row with the count: the client is told the field is crowded.";
1130
+ export const A_CROWDS_ROW_COVERS_A_READ_FLOOR = "This row covers a floor only once you have read its line and its goods are not the client's goods.";
1112
1131
  export const AN_UNREAD_CROWDS_ONE_ROW = "Carry forward any line a lawyer would raise, and set the rest aside in this list's one row with the count, on the ground that their goods are unreadable and the field is crowded.";
1113
- export const AN_UNREAD_CROWDS_FLOOR = "A floor here is placed or set aside by name on its status or its territory; this row never covers it.";
1114
- export const AN_UNREAD_LISTS_FLOOR = "A floor here is placed, or set aside by name on its status or its territory; any other line may be set aside on the ground that its goods are unknown.";
1132
+ export const AN_UNREAD_CROWDS_FLOOR = "This row never covers a floor. Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full.";
1133
+ export const AN_UNREAD_LISTS_FLOOR = "Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full. Any other line here may be set aside because its goods are unknown.";
1115
1134
  // What placement does with a line of that list before its fate, by its marker (approved wording, 2026-10-19). Placement's
1116
1135
  // alone, as its other orders are: every other stage keeps the markers as evidence and is not told of a fate.
1117
1136
  export const AN_UNREAD_LINE_IS_CHECKED = 'A line marked "on file, not readable", "cut by the register, no goods word found", "goods held; completeness not established" or "no goods text held; completeness not established" must be checked with band_record before its fate. Read the evidence held for it, keeping its source and goods completeness clear. A line marked "no goods on file", "fetch failed, no goods on file" or "the register\'s record lists no goods" has no goods text on file. Missing goods do not establish that the goods are unrelated.';
1118
- export const PLACEMENT_LINE_ORDERS = Object.freeze([FLOORS_BY_NAME_HERE, A_CROWDS_ONE_ROW, AN_UNREAD_CROWDS_ONE_ROW, AN_UNREAD_CROWDS_FLOOR, AN_UNREAD_LISTS_FLOOR, AN_UNREAD_LINE_IS_CHECKED]);
1137
+ export const PLACEMENT_LINE_ORDERS = Object.freeze([FLOORS_BY_NAME_HERE, A_CROWDS_ONE_ROW, A_CROWDS_ROW_COVERS_A_READ_FLOOR, AN_UNREAD_CROWDS_ONE_ROW, AN_UNREAD_CROWDS_FLOOR, AN_UNREAD_LISTS_FLOOR, AN_UNREAD_LINE_IS_CHECKED]);
1119
1138
  const SHAPE_LISTS = ["a", "b", "c", "d", "rest", "no_goods"];
1120
1139
  const pageEveryOtherLine = (g) => `; page every other line with ${groupPageCall(g)}`;
1121
1140
  const pageWhereALineNeedsIt = (g) => `; page with ${groupPageCall(g)} only where a line needs reading`;
@@ -1166,13 +1185,14 @@ function renderGroupsMd(md, groups, rows = []) {
1166
1185
  if (l.goods_evidence === "complete") return "on file, not readable";
1167
1186
  return l.goods_on_file ? "on file, not readable" : "no goods on file";
1168
1187
  };
1169
- const words = (l) => (l.goods_words == null ? unread(l) : `${fmtN(l.goods_words.length)} of ${fmtN(total)}${l.goods_words.length ? `: ${l.goods_words.join(", ")}` : ""}`);
1188
+ const words = (l) => (l.goods_words == null ? unread(l)
1189
+ : `${fmtN(l.goods_words.length)} of ${fmtN(total)}${l.goods_words.length ? `: ${l.goods_words.join(", ")}` : ""}${l.floor_goods ? ` «${l.floor_goods}»` : ""}`);
1170
1190
  const count = (grp) => `${fmtN(grp.records)} records in ${fmtN(grp.lines)} lines from ${fmtN(grp.owners)} owners`;
1171
1191
  const where = (l) => `${l.territories.join(",")}${l.outside ? " (outside the order)" : ""}`;
1172
1192
  const row = (l) => `| ${cell(l.mark)} | ${cell(l.owner_name)} | ${cell(l.classes.join(","))} | ${fmtN(l.live)} of ${fmtN(l.records)} | ${cell(where(l))} | ${cell(words(l))} | ${l.floors > 0 ? "yes" : "no"} | ${cell(l.record_id)} |`;
1173
1193
  const head = ["| mark | owner | classes | live | territories | goods words | floor | record |", "|---|---|---|---|---|---|---|---|"];
1174
1194
  // A crowded group's own words for what it holds, in the crowded-field method's sentence (ruled 2026-10-12).
1175
- const crowd = { b: "identical marks in the ordered classes for other goods than the client's",
1195
+ const crowd = { b: "identical marks in the ordered classes whose goods name none of the client's product words",
1176
1196
  c: "marks of the same family as the mark, or carrying the searched word, for the client's goods", d: "other marks in the ordered classes for the client's goods" };
1177
1197
  const firstPage = `The first ${fmtN(groups.first_page)} lines are below`;
1178
1198
  for (const g of ["a", "b", "c", "d"]) {
@@ -1186,7 +1206,8 @@ function renderGroupsMd(md, groups, rows = []) {
1186
1206
  ? `${count(grp)}. ${readA} ${firstPage}${pageEveryOtherLine(g)}. ${FLOORS_BY_NAME_HERE}`
1187
1207
  : grp.lines <= READ_ALL_LINES ? `${count(grp)}. Fifty? Read them all.` : `${count(grp)}. ${readA}`)
1188
1208
  : crowded
1189
- ? `${count(grp)}. A thousand or more? The field is crowded. These are ${crowd[g]}. ${A_CROWDS_ONE_ROW} ${firstPage}${pageWhereALineNeedsIt(g)}.`
1209
+ ? `${count(grp)}. A thousand or more? The field is crowded. These are ${crowd[g]}. ${A_CROWDS_ONE_ROW} ${firstPage}${pageWhereALineNeedsIt(g)}.${
1210
+ g === "b" ? ` ${A_CROWDS_ROW_COVERS_A_READ_FLOOR} ${pageTheFloorsLeft(g)}` : ""}`
1190
1211
  : grp.lines <= READ_ALL_LINES
1191
1212
  ? `${count(grp)}. Fifty? Read them all.`
1192
1213
  : `${count(grp)}. ${SORT_THE_NEAR_ONES}`, "");
@@ -1210,8 +1231,8 @@ function renderGroupsMd(md, groups, rows = []) {
1210
1231
  if (groups.no_goods.lines) {
1211
1232
  // The lines the goods test could not place, read by the same size rule (ruled 2026-10-12): under a thousand
1212
1233
  // records, a fate per line by name, opened where goods are on file; at a thousand or more, the first page,
1213
- // and one row on the rest whose goods are unreadable in a crowded field. A floor is never set aside on
1214
- // its goods, so a floor here is placed or set aside on its status or territory.
1234
+ // and one row on the rest whose goods are unreadable in a crowded field. A floor is set aside on its goods only
1235
+ // once they are read in full, which unknown goods never are, so a floor here is placed or set aside by name.
1215
1236
  const ng = groups.no_goods;
1216
1237
  const crowded = ng.records >= groups.crowded_at;
1217
1238
  const lead = tested.source === "none"
@@ -62,13 +62,14 @@ export function blockCollapsed(block) {
62
62
  /**
63
63
  * The plan row that answers a minted slice: the slice itself, or, where the fold refused it as already-asked, the
64
64
  * row it duplicates. Such a slice is never frozen into the plan and never runs; its question was asked by the twin.
65
- * ONLY THE DUPLICATE KIND RESOLVES: an identity collision or a malformed term asked a question nothing else asked.
65
+ * So does a spelling the re-sweep left on a list the reading turn read (`counted-on-list`): the list's row holds its
66
+ * count. ONLY THOSE TWO KINDS RESOLVE: an identity collision or a malformed term asked a question nothing else asked.
66
67
  * `foldRefusals` maps a refused qid to `{kind, twin}`, as `foldSupplementalEntries` records it. One rule for the
67
68
  * directive below and the remedy term ledger (remedy-accounting.mjs), so the two cannot read a slice apart. PURE.
68
69
  */
69
70
  export function answeredAs(qid, foldRefusals) {
70
71
  const r = foldRefusals?.get?.(qid);
71
- return r && r.kind === "duplicate-question" && r.twin ? String(r.twin) : qid;
72
+ return r && (r.kind === "duplicate-question" || r.kind === "counted-on-list") && r.twin ? String(r.twin) : qid;
72
73
  }
73
74
 
74
75
  /**