clearotron 0.3.2 → 0.3.3-beta.0

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 (105) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +29 -7
  4. package/bin/start.mjs +1 -1
  5. package/build-info.json +2 -2
  6. package/demo/MANIFEST.json +27 -0
  7. package/docs/INTAKE.md +8 -0
  8. package/docs/architecture/04-configuration-reference.md +1 -1
  9. package/driver/CHANGELOG.md +37 -0
  10. package/driver/clearance-variants-record.mjs +12 -1
  11. package/driver/common-law-coverage-status.mjs +113 -0
  12. package/driver/contract-audit.mjs +1 -1
  13. package/driver/contract-e3-backlog.mjs +37 -37
  14. package/driver/contract-vocabulary.mjs +8 -8
  15. package/driver/coverage-form-io.mjs +3 -1
  16. package/driver/coverage-form.mjs +38 -11
  17. package/driver/coverage-ledger.mjs +37 -7
  18. package/driver/coverage-union.mjs +2 -2
  19. package/driver/crowd-context.mjs +19 -6
  20. package/driver/drainer-identity.mjs +1 -1
  21. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  22. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  23. package/driver/engine/mcp/coverage-server.mjs +1 -1
  24. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  25. package/driver/engine/mcp/euipo-server.mjs +2 -0
  26. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  27. package/driver/engine/mcp/gather-config.mjs +8 -2
  28. package/driver/engine/mcp/probe-server.mjs +28 -0
  29. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  30. package/driver/engine/mcp/recording-server.mjs +30 -0
  31. package/driver/engine/mcp/signa-server.mjs +2 -0
  32. package/driver/engine/mcp/supplemental.mjs +89 -12
  33. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  34. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  35. package/driver/engine/openai-agent.mjs +7 -0
  36. package/driver/engine/probe.mjs +67 -14
  37. package/driver/engine/tool-refusal.mjs +16 -0
  38. package/driver/enqueue-schema.mjs +2 -2
  39. package/driver/envelope-settle.mjs +82 -13
  40. package/driver/findings-model.mjs +4 -4
  41. package/driver/gateway.mjs +18 -2
  42. package/driver/manager-groups-verdict.mjs +1 -1
  43. package/driver/matter-frame-record.mjs +24 -7
  44. package/driver/named-band.mjs +1 -1
  45. package/driver/package.json +1 -1
  46. package/driver/partial-payload-baseline.json +12 -3
  47. package/driver/pipeline.mjs +141 -37
  48. package/driver/publish/index.mjs +40 -25
  49. package/driver/publish/xlsx.mjs +26 -4
  50. package/driver/queue-markers.mjs +44 -0
  51. package/driver/queue-watch-verdict.mjs +2 -2
  52. package/driver/register-availability.mjs +2 -2
  53. package/driver/register-plan.mjs +313 -21
  54. package/driver/roster-verdict.mjs +1 -1
  55. package/driver/runner.mjs +26 -2
  56. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  57. package/driver/skills/clearance-register/SKILL.md +44 -3
  58. package/driver/skills/clearance-register/digest.md +5 -5
  59. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  60. package/driver/skills/clearance-register/unit.md +39 -0
  61. package/driver/skills/clearance-variants/SKILL.md +1 -1
  62. package/driver/skills/matter-frame/SKILL.md +4 -2
  63. package/driver/stages.mjs +12 -5
  64. package/driver/status-snapshot.mjs +1 -1
  65. package/driver/suite-census.json +236 -14
  66. package/driver/synthesis-record.mjs +80 -2
  67. package/driver/unit-file-drift.mjs +3 -3
  68. package/driver/unit-inventory.mjs +2 -2
  69. package/driver/unit-state-verdict.mjs +1 -1
  70. package/driver/updater-identity.mjs +2 -3
  71. package/driver/variant-manifest-model.mjs +11 -1
  72. package/driver/verify.mjs +5 -5
  73. package/driver/withheld-families.mjs +104 -0
  74. package/mcp-server/CHANGELOG.md +4 -0
  75. package/mcp-server/lib/brief.mjs +5 -7
  76. package/mcp-server/lib/runs.mjs +1 -1
  77. package/mcp-server/package.json +1 -1
  78. package/mcp-server/server.mjs +3 -2
  79. package/package.json +1 -1
  80. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  81. package/portal-ui/dist/index.html +1 -1
  82. package/portal-ui/package.json +1 -1
  83. package/providers/_shared/count.mjs +2 -2
  84. package/providers/_shared/enumerate.mjs +15 -2
  85. package/providers/_shared/execute-plan.mjs +19 -1
  86. package/providers/_shared/plan-guards.mjs +40 -0
  87. package/providers/clarivate/src/capabilities.js +15 -5
  88. package/providers/clarivate/src/core.js +41 -5
  89. package/providers/corsearch/src/capabilities.js +4 -0
  90. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  91. package/providers/oauth-mcp-bridge/package.json +1 -1
  92. package/providers/signa/src/capabilities.js +22 -8
  93. package/providers/signa/src/core.js +12 -1
  94. package/scripts/demo-evidence.mjs +114 -0
  95. package/scripts/engine-probe.mjs +6 -5
  96. package/scripts/env-audit.mjs +1 -1
  97. package/scripts/freeze-example-run.mjs +3 -3
  98. package/scripts/live-surface-check.mjs +26 -19
  99. package/scripts/mint-suite-census.mjs +66 -0
  100. package/scripts/package-size-budget.mjs +117 -0
  101. package/scripts/register-plan-shape.mjs +259 -0
  102. package/scripts/release-note-required.mjs +38 -1
  103. package/scripts/settings-render-check.mjs +36 -0
  104. package/scripts/travelling-predicates.mjs +1 -1
  105. package/shared/identifier-scan.mjs +22 -5
@@ -33,6 +33,12 @@
33
33
  // token) carry `when: { runs_if_enumerated: <parent qid> }` — they run ONLY if the parent
34
34
  // contains-slice proved tractable. A crowd parent is TERMINAL for its children, encoded, not
35
35
  // remembered.
36
+ // - AND A SECOND, DIFFERENT WAIT (ruled 2026-09-21, on every matter): the decision-10 families carry
37
+ // `when: { awaits_reading_turn: true }`. No result releases it. They run when the reading turn
38
+ // ASKS for them after reading the identical mark's own list, and the ask arrives as a
39
+ // supplemental entry. The two tokens are not interchangeable: one is "your parent was a crowd",
40
+ // which is a fact about a result, and this one is "nobody has decided you are worth asking",
41
+ // which is a fact about judgment not yet made.
36
42
  // - JUDGMENT EXTRAS ARE BAND BLOCKS, NOT PLAN ENTRIES: skeptic/frame-reopen/escalation-driven
37
43
  // additions land as qid-less blocks in the band (the executor's merge preserves them); the
38
44
  // compiled plan is never hand-edited.
@@ -51,6 +57,7 @@ import { canonicalJurisdictionCode, isKnownJurisdictionCode } from "./jurisdicti
51
57
  import { normalizeTerritory } from "../providers/_shared/territory-codes.mjs";
52
58
  import { bindingLayersFor, layerCoverageFor } from "./binding-layers.mjs";
53
59
  import { entryTermIssues, goodsTermsList, stripGoodsReservedWords, hasAnchoredWildcard, termAnnotationIssue, termMarkupIssue, termShapeIssue, termSubstanceIssue } from "../providers/_shared/term-shape.mjs";
60
+ import { AWAITS_READING_TURN, awaitsReadingTurn, guardParentQid } from "../providers/_shared/plan-guards.mjs";
54
61
  import { formKey, romanizationSpellings, isNonLatinTerm } from "../providers/_shared/script-form.mjs";
55
62
 
56
63
  export const PLAN_SCHEMA_VERSION = 1;
@@ -107,6 +114,11 @@ export function romanizedTermsFromPlan(plan, term) {
107
114
 
108
115
  export const PLAN_PROVENANCE = ["floor", "model", "mark"];
109
116
 
117
+ // The two waits an entry can carry, and what each means, are defined once beside the executor that
118
+ // must agree with this compiler about them — providers/_shared/plan-guards.mjs says why there.
119
+ // Re-exported so a reader of the plan's own module finds them where the plan shape is described.
120
+ export { AWAITS_READING_TURN, awaitsReadingTurn, guardParentQid };
121
+
110
122
  // ── PROVIDER CAPABILITIES AT COMPILE TIME (phase 3) ──────────────────────────────────────────────
111
123
  //
112
124
  // The frozen plan must be executable BY CONSTRUCTION by whichever provider is active. Three knobs,
@@ -114,7 +126,7 @@ export const PLAN_PROVENANCE = ["floor", "model", "mark"];
114
126
  // module stays PURE and never imports a vendor):
115
127
  //
116
128
  // 1. OR-WIDTH — PLAN_MAX_OR_WIDTH is the corsearch-shaped DEFAULT; the effective width is
117
- // capabilities.maxOrWidth (clarivate 500 JSON-nesting, signa 1 — no OR surface at all).
129
+ // capabilities.maxOrWidth (clarivate 496 JSON-nesting, signa 1 — no OR surface at all).
118
130
  // 2. PREDICATES — a predicate with NO mapping on the active provider does NOT compile into a wrong
119
131
  // query. The entry is emitted with `unsupported:true` + a plain-English `unsupported_reason`, and
120
132
  // the executor turns that into an error:true block (→ joins MISSING) instead of a silently weaker
@@ -219,6 +231,42 @@ export function goodsTextGap(entry, capabilities) {
219
231
  // The reader is `goodsTermsList`, imported from the shared term vocabulary — the compiler, the
220
232
  // executor and the connectors all ask the question with the same function.
221
233
 
234
+ // ── A TERM TOO SHORT FOR THE REGISTER'S CONTAINS FORM IS ASKED ON THE EXACT FORM ───────────────────
235
+ //
236
+ // Measured on a two-letter mark, 2026-09-22: every always-on goods-narrowed question and the saturation
237
+ // probe went out on the contains form, and a register that documents a three-character floor for that
238
+ // form refused all of them with a 400. The identical question on the same mark had crowded, so the one
239
+ // narrowing the matter needed was the one the register would not run.
240
+ //
241
+ // So each register declares the shortest term its contains form accepts (`containsMinLength`), and a
242
+ // shorter term is asked on the exact form with everything else unchanged: the same classes, the same
243
+ // goods words, the same one question in place of one question. Nothing is added and nothing is
244
+ // dropped. The entry carries `contains_substituted`, so the plan says which question was asked in
245
+ // another form and why, and nobody reading it takes an exact count for a containing one.
246
+ //
247
+ // FOLDED LENGTH, because that is what the floor is measured in: case and accents folded, and spaces
248
+ // not counted. Counting a space could let a term the register would still refuse through on the
249
+ // contains form; not counting it at worst asks exactly where contains would have answered.
250
+ export function foldedTermLength(term) {
251
+ return [...String(term ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, "")].length;
252
+ }
253
+
254
+ /**
255
+ * The substitution a contains-form question needs on this register, or null when it needs none. PURE.
256
+ * A register that declares no floor (`containsMinLength` absent or null) is sent the contains form at
257
+ * every length, as it always has been.
258
+ */
259
+ export function containsFormSubstitution(term, capabilities) {
260
+ const min = capabilities?.containsMinLength;
261
+ if (!Number.isInteger(min) || min < 2) return null;
262
+ const length = foldedTermLength(term);
263
+ if (length >= min) return null;
264
+ return { from: "default", to: "exact", min_length: min, term_length: length,
265
+ reason: `the register (${capabilities.id ?? "unknown"}) answers its contains form only for a term of `
266
+ + `${min} or more characters, and this term has ${length}, so it was asked on the exact form with the `
267
+ + `same classes and goods words` };
268
+ }
269
+
222
270
  /**
223
271
  * 2026-07-29 hardening — is a VARIANTS-MODEL value un-searchable as a mark term? Returns the
224
272
  * plain-English reason, or null. Two classes shipped as silent nil "cleans" on the 2026-07-28 run
@@ -359,7 +407,27 @@ export function resolveRegions(jurisdictions, capabilities) {
359
407
  // coverage-ledger's TOOL_ABSENCE_RE keys on it to relabel the row `deferred`.
360
408
  const code = normalizeTerritory(j);
361
409
  if (code === "") { worldwide = true; continue; } // Worldwide: no region restriction, not a gap
362
- if (code === null) {
410
+ // A CODE THE ENGINE DOES NOT KNOW IS A GAP, NOT A DESTINATION — and `null` is not the only way to
411
+ // get one. `normalizeTerritory` derives its long tail of display names from the runtime's own region
412
+ // data, and that data carries entries which are not jurisdictions: the Canary Islands, the Eurozone,
413
+ // the United Nations, Outlying Oceania, four dependencies with no register of their own, and
414
+ // `XB` "Pseudo-Bidi", which is a pseudolocale rather than a place. Ten names resolve that way. Each
415
+ // answers with a CODE, so each skipped the branch above and was carried to the wire as a real
416
+ // territory.
417
+ //
418
+ // A provider that publishes `offices.covered` caught them at the membership test below — five of the
419
+ // six do. Corsearch does not publish one, and its translate is an ISO passthrough, so for that
420
+ // provider alone all ten became `region:` clauses. That is the copper-bastion failure this loop was
421
+ // written to prevent, reached by a different door: a region value no register can answer, which that
422
+ // provider returns as a server error, which auto-recovery reads as transient and retries until the
423
+ // park budget is gone. The loop translated a name it should never have accepted, so the guard that
424
+ // was supposed to stop it never saw a bad value.
425
+ //
426
+ // Membership in the known universe is the test, not a roster of the ten: it is the same question
427
+ // `foldJurisdictionCodes` asks before reporting a code as unknown, and it stays true for whatever
428
+ // the next runtime's region data adds. Deferring is the loud answer — the gap is disclosed on the
429
+ // report and can be escalated — where wiring it is the silent one.
430
+ if (code === null || !isKnownJurisdictionCode(code)) {
363
431
  deferred.push({ jurisdiction: String(j).toUpperCase(), reason: uncoveredJurisdictionReason([String(j).toUpperCase()], capabilities?.id ?? "unknown") });
364
432
  continue;
365
433
  }
@@ -843,7 +911,7 @@ export function withoutHouseElementTerms(entry, house) {
843
911
  return isHouseElementTerm(entry.term, house) ? null : entry;
844
912
  }
845
913
 
846
- export function compileRegisterPlan({ manifest, job, form = null, skillVersion = "", capabilities = null, unavailableOffices = [], houseElement = null }) {
914
+ export function compileRegisterPlan({ manifest, job, form = null, skillVersion = "", capabilities = null, unavailableOffices = [], houseElement = null, addedClasses = [] }) {
847
915
  // ── AN EXCLUDED ELEMENT'S FORM BAND MUST BE UNREACHABLE, NOT MERELY UNASKED-FOR ─────────────────
848
916
  //
849
917
  // THE DEFECT THIS CLOSES, found by following the seam rather than by a failing arm. `bandFor` falls
@@ -863,6 +931,33 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
863
931
  }
864
932
  const classes = (job?.classes ?? []).map(String).filter(Boolean);
865
933
  if (!classes.length) throw new Error("register_plan_classes_missing: a plan is always class-scoped — compile with the matter's in-scope Nice classes");
934
+ // ── DECISION 18's BOUND, APPLIED WHERE THE CLASSES COME IN ──────────────────────────────────────
935
+ //
936
+ // Stated as rejects rather than as a judgment, because the judgment is the frame's and belongs in the
937
+ // frame's own turn. What belongs here is the floor nothing downstream can talk its way past:
938
+ //
939
+ // · A CLASS WITH NO REASON IS DROPPED. The owner's text requires one sentence saying why, tied to
940
+ // the client's own goods. A class added with none cannot be read back by anyone checking the
941
+ // bound held, and an unjustified widening is the whole failure mode this bound exists for.
942
+ // · AN INSTRUCTED CLASS IS NEVER "ADDED". It is already in scope and already on every entry; minting
943
+ // a second identical-mark entry for it would ask the same question twice and read, to anyone
944
+ // counting, as a widening that never happened.
945
+ // · NOTHING IS EVER REMOVED. This list only ever appends entries; `classes` above is untouched by
946
+ // it, so no path here can narrow what the order instructed.
947
+ //
948
+ // A malformed class number is dropped rather than coerced: a plan is class-scoped, and a scope built
949
+ // from a value nobody can read is the one thing worse than a missing one.
950
+ const instructed = new Set(classes);
951
+ const addedClassRows = [];
952
+ for (const raw of (Array.isArray(addedClasses) ? addedClasses : [])) {
953
+ const cls = String(raw?.class ?? "").trim();
954
+ const reason = String(raw?.reason ?? "").trim();
955
+ if (!/^([1-9]|[1-3][0-9]|4[0-5])$/.test(cls)) continue;
956
+ if (!reason) continue;
957
+ if (instructed.has(cls)) continue;
958
+ if (addedClassRows.some((r) => r.class === cls)) continue;
959
+ addedClassRows.push({ class: cls, reason });
960
+ }
866
961
  const caps = capabilities ?? null;
867
962
  const maxOrWidth = planMaxOrWidth(caps);
868
963
  const resolved = resolveRegions(job?.jurisdictions, caps);
@@ -1103,16 +1198,33 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1103
1198
 
1104
1199
  // saturation-probe — count-only crowd descriptors for every common/saturated element. These
1105
1200
  // enumerate NOTHING (limit:1 probes); they describe the crowd for judgment. Never the anchor.
1201
+ // A term shorter than the register's contains floor is probed on the exact form instead, and the
1202
+ // entry says so (containsFormSubstitution, above): a refused probe describes no crowd at all.
1106
1203
  for (const el of manifest.elements) {
1107
- if (el.kind === "common" || el.kind === "saturated-common")
1108
- push({ axis: "saturation-probe", predicate: "default", term: el.value, expected_kind: "count", provenance: "model" });
1204
+ if (el.kind === "common" || el.kind === "saturated-common") {
1205
+ const sub = containsFormSubstitution(el.value, caps);
1206
+ push({ axis: "saturation-probe", predicate: sub ? "exact" : "default", term: el.value, expected_kind: "count",
1207
+ provenance: "model", ...(sub ? { contains_substituted: sub } : {}) });
1208
+ }
1109
1209
  }
1110
1210
 
1111
1211
  // primary-sweep — the dangerous NAMED band, all enumerates:
1112
1212
  // exact mark, each non-transliteration variant (core/phonetic/visual/composite/other),
1113
1213
  // the dominant-element contains slice (the crowd-gate PARENT), the machine FORM band
1114
1214
  // (exact OR-stack + when-guarded wildcard fringe), and the cross-class merch check.
1115
- push({ axis: "primary-sweep", predicate: markPredicate(manifest.mark), term: manifest.mark, expected_kind: "enumerate", provenance: "mark", ...literalStamp(manifest.mark) });
1215
+ // ── THE IDENTICAL QUESTION, AND WHAT WAITS BEHIND IT ────────────────────────────────────────────
1216
+ //
1217
+ // This is the question the whole crowded-field design turns on: is the client's own mark already
1218
+ // registered? On a dense matter it came back a COUNT rather than a list — 1,289 live records against
1219
+ // a 600 fetch ceiling, nine forms of the question, no records released — and the run then read 4,805
1220
+ // records from 139 OTHER questions while the one that mattered went unread. Seventeen identical-mark
1221
+ // records reached the band, every one of them through a side door.
1222
+ //
1223
+ // So the wider families now WAIT on it — and since 2026-09-21 they wait for the READING TURN, not for
1224
+ // the identical question's own result. A question that came back as a comfortable list is still a
1225
+ // question nobody has read yet, and releasing the widenings on it spends the run's reading on
1226
+ // scripts, neighbours and compounds before anyone has looked at the mark itself.
1227
+ const identicalQid = push({ axis: "primary-sweep", predicate: markPredicate(manifest.mark), term: manifest.mark, expected_kind: "enumerate", provenance: "mark", ...literalStamp(manifest.mark) });
1116
1228
  // — the floor's `spacing-punctuation` family is deliberately NOT pushed here, and the reason is
1117
1229
  // this compiler's own equivalence: `norm` and `formKey` both strip separators, so "BIO VELTRIS",
1118
1230
  // "BIOVELTRIS", "BIO-VELTRIS" and "BIO.VELTRIS" are ONE key. The variant loop below already drops a
@@ -1185,7 +1297,18 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1185
1297
  // has no honest form — joining it would intersect the words instead of offering them as
1186
1298
  // alternatives, which asks for filings covering ALL of them and answers 200 with a population that
1187
1299
  // shrinks as the list grows. So the entry is not compiled; the broad sweep still runs.
1188
- const goodsWords = Array.isArray(manifest.goods_words) ? manifest.goods_words : [];
1300
+ // ── ASKED AND UNANSWERED IS NOT THE SAME AS ANSWERED "NONE" ─────────────────────────────────────
1301
+ //
1302
+ // `null` is the stage never answering; `[]` is it answering that no word is worth narrowing by. Both
1303
+ // compile no narrowed entry — there is nothing to narrow BY either way — but only one of them is a
1304
+ // fact about the RUN rather than about the matter, and the plan says which.
1305
+ //
1306
+ // This is the distinction that let a narrowing ship inert for a week: the key could not be sent at
1307
+ // all, the manifest carried an empty list, and the run was indistinguishable from a matter that
1308
+ // genuinely had no goods words. A reader of that plan could not tell "nobody asked" from "nothing
1309
+ // applied", and neither could anyone reading the run after it.
1310
+ const goodsAnswered = Array.isArray(manifest.goods_words);
1311
+ const goodsWords = goodsAnswered ? manifest.goods_words : [];
1189
1312
  // A FOURTH REASON, and it is the one the parser deliberately does NOT decide: the list may carry a
1190
1313
  // short phrase, and only some registers match a phrase as a phrase. Where this one does not, the
1191
1314
  // connector would refuse the entry at the door — so the entry must not be compiled in the first
@@ -1246,6 +1369,11 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1246
1369
  ? { goods_text_omitted: goodsOmitted, goods_text_omitted_reason: goodsOmittedReason }
1247
1370
  : {};
1248
1371
 
1372
+ // The same floor for the goods-narrowed questions: on a mark shorter than the register's contains
1373
+ // form accepts, each is asked on the exact form with the same classes and goods words, one for one.
1374
+ const goodsSub = containsFormSubstitution(manifest.dominant_element, caps);
1375
+ const goodsPredicate = goodsSub ? "exact" : "default";
1376
+ const goodsSubStamp = goodsSub ? { contains_substituted: goodsSub } : {};
1249
1377
  if (goodsSendable.length && caps?.goodsTextSearch === true) {
1250
1378
  if (caps.goodsTextListOr === true) {
1251
1379
  // The register offers the list as alternatives in one clause: one question, one count — phrases
@@ -1253,9 +1381,9 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1253
1381
  // `a ADJ b OR c`, and that precedence is MEASURED rather than assumed: the mixed clause answers
1254
1382
  // the union of its alternatives, not the distributed reading `a ADJ (b OR c)`. Were it the
1255
1383
  // other way the clause would ask a different question and still answer 200.
1256
- push({ axis: "primary-sweep", predicate: "default", term: manifest.dominant_element,
1384
+ push({ axis: "primary-sweep", predicate: goodsPredicate, term: manifest.dominant_element,
1257
1385
  expected_kind: "enumerate", provenance: "mark", goods_text: goodsSendable,
1258
- goods_text_gaps: gapsFor(goodsSendable), qidSuffix: "+goods", ...omittedStamp });
1386
+ goods_text_gaps: gapsFor(goodsSendable), qidSuffix: "+goods", ...omittedStamp, ...goodsSubStamp });
1259
1387
  } else {
1260
1388
  // ── ONE ENTRY PER WORD, where the register has no OR on this field ──────────────────────────
1261
1389
  //
@@ -1274,10 +1402,10 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1274
1402
  // is N questions on such a register, where a register with an OR asks one. The manifest's
1275
1403
  // 24-word ceiling is what bounds it.
1276
1404
  goodsSendable.forEach((word, i) => {
1277
- push({ axis: "primary-sweep", predicate: "default", term: manifest.dominant_element,
1405
+ push({ axis: "primary-sweep", predicate: goodsPredicate, term: manifest.dominant_element,
1278
1406
  expected_kind: "enumerate", provenance: "mark", goods_text: [word],
1279
1407
  goods_text_gaps: gapsFor([word]),
1280
- qidSuffix: `+goods-${termIdentity(word)}`, ...(i === 0 ? omittedStamp : {}) });
1408
+ qidSuffix: `+goods-${termIdentity(word)}`, ...(i === 0 ? omittedStamp : {}), ...goodsSubStamp });
1281
1409
  });
1282
1410
  }
1283
1411
  }
@@ -1292,7 +1420,7 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1292
1420
  // byte-identical (resolvePlanAgainstStore), so existing matters keep their unsplit shape; only
1293
1421
  // fresh compiles get the split entries. Chunk qids differ naturally (slug of each chunk's first
1294
1422
  // term); the #n dedup above covers collisions.
1295
- // The split width is PROVIDER-DERIVED (capabilities.maxOrWidth): 80 on corsearch's URI budget, 500
1423
+ // The split width is PROVIDER-DERIVED (capabilities.maxOrWidth): 80 on corsearch's URI budget, 496
1296
1424
  // on clarivate's JSON nesting cap, 1 on signa (no OR surface at all — one term per call).
1297
1425
  // Post-merge audit 2 (e): the partition is BY SCRIPT before it is by width. An OR-stack never
1298
1426
  // carries romanizedTerms (one member's Latin form must never substitute a whole chunk's names —
@@ -1321,6 +1449,35 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1321
1449
  push({ axis: "primary-sweep", predicate: markPredicate(manifest.mark), term: manifest.mark, expected_kind: "enumerate",
1322
1450
  nice_classes: ["25"], qidSuffix: "+merch", provenance: "mark", ...literalStamp(manifest.mark) });
1323
1451
 
1452
+ // ── DECISION 18: THE CLASSES THE FRAME ADDED, ONE QUESTION EACH ─────────────────────────────────
1453
+ //
1454
+ // The matter frame starts from the classes the order names and adds one the client's own goods
1455
+ // plainly reach — as the reviewing lawyer added class 28 for video game accessories on an order that
1456
+ // named 9, 38, 41 and 42. The frame has always been able to say so, and its rows have always carried
1457
+ // a reason. What was missing is the BOUND.
1458
+ //
1459
+ // WHAT AN ADDED CLASS USED TO COST. The classes were unioned into the plan's own class scope, so
1460
+ // every entry in the plan became class-scoped to the wider set: measured on a four-class order, one
1461
+ // added class landed on five of six entries — every variant, every family, every script. The
1462
+ // dictation told the model so in as many words ("searched for every variant, not only the exact
1463
+ // name") and used the cost as the reason to be sparing, which is a bound made of reluctance.
1464
+ //
1465
+ // WHAT IT COSTS NOW: one question, the identical mark in that class, pushed here exactly the way the
1466
+ // merchandise cross-class probe above is pushed — the recipes' one sanctioned exception to
1467
+ // class-scoping, and the same shape for the same reason. The added class is then searched under
1468
+ // decision 14 like every other: the identical mark first, the count looked at before anything is
1469
+ // read. It is an identical-mark entry, so it is one of the three ungated kinds and runs without an
1470
+ // ask — and being ungated releases NOTHING: the waiting families still wait for the reading turn.
1471
+ //
1472
+ // THE REASON RIDES WITH THE CLASS, on the entry and on the plan. A class added with no reason tied to
1473
+ // the client's own goods is the bound's first reject, and a reason recorded nowhere cannot be read
1474
+ // back by anyone checking that the bound held.
1475
+ for (const row of addedClassRows) {
1476
+ push({ axis: "primary-sweep", predicate: markPredicate(manifest.mark), term: manifest.mark,
1477
+ expected_kind: "enumerate", nice_classes: [row.class], qidSuffix: `+class${row.class}`,
1478
+ provenance: "mark", added_class_reason: row.reason, ...literalStamp(manifest.mark) });
1479
+ }
1480
+
1324
1481
  // ✕ THIS AXIS IS NOT WIDENED BY, AND THAT IS THE KNOWN REMAINDER. The doctrine's
1325
1482
  // strip rule covers the transliteration branch, so a transliteration variant's Value is a root too —
1326
1483
  // but it compiles HERE, where `exact enumerates` is the axis's stated design and the provider indexes
@@ -1367,6 +1524,57 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1367
1524
  // The axis is NOT gone: the incumbent-class anchor above still compiles, so the coverage skeleton
1368
1525
  // still carries the axis and no clean is ever claimed over an axis that vanished.
1369
1526
 
1527
+ // ── DECISION 10, AS AMENDED 2026-09-21: THE WIDER FAMILIES WAIT FOR THE READING TURN ────────────
1528
+ //
1529
+ // THE WAIT IS NOT ON A RESULT, AND THAT IS THE WHOLE OF THE AMENDMENT. These families used to wait
1530
+ // on the identical question ENUMERATING — so a matter whose identical question came back as a
1531
+ // comfortable list released every one of them in the same breath, before a single record had been
1532
+ // read. Measured on the production run of 21 September: the identical question answered with a list
1533
+ // of 100, every wider family then ran, 1,921 records were read, and the reading stages took the
1534
+ // hours. A design that is careful only when the mark is crowded and opens everything otherwise is
1535
+ // backwards, which is the owner's ruling in his own words.
1536
+ //
1537
+ // So the release is an ASK, not a state. These entries run when the reading turn asks for them under
1538
+ // step 6 of the manual, having read the identical list and found it thin — and never otherwise,
1539
+ // whatever the identical question returned. `awaits_reading_turn` says exactly that and cannot be
1540
+ // satisfied by any result, which is why it is a different token from `runs_if_enumerated` rather
1541
+ // than a parent qid chosen to never enumerate.
1542
+ //
1543
+ // Applied here, in ONE place, rather than at each push site: which families wait is a property of
1544
+ // the plan as a whole, and spreading it across a dozen call sites is how the list and the rule drift
1545
+ // apart. The exceptions are the whole of it:
1546
+ //
1547
+ // · the identical-mark entries — the question everything else waits on cannot wait on itself
1548
+ // · the saturation probe — a cheap count that tells judgment how crowded the field is at all, and
1549
+ // is the other half of "look at the count before you read anything"
1550
+ // · the goods-narrowed contains entry — on every matter (ruled 2026-09-20), and it is the one entry that
1551
+ // makes a crowded identical question answerable rather than merely deferred
1552
+ // · anything already waiting on something else, which keeps its own parent
1553
+ //
1554
+ // Everything else is a widening: scripts and transliterations, neighbour lists, compounds, the
1555
+ // wildcard and phonetic fringes. On the measured dense matter every one of the 4,805 records read
1556
+ // came from these, while the identical question went unread — which is the defect, stated as
1557
+ // arithmetic.
1558
+ const identicalTermKey = formKey(manifest.mark);
1559
+ const isIdenticalQuestion = (e) =>
1560
+ e.provenance === "mark" && !goodsTermsList(e).length
1561
+ && String(e.predicate) !== "default"
1562
+ && formKey(e.term ?? "") === identicalTermKey;
1563
+ for (const e of entries) {
1564
+ if (e.axis === "saturation-probe") continue;
1565
+ if (e.when) continue; // already waiting on its own parent
1566
+ if (goodsTermsList(e).length) continue; // compiled on every matter (2026-09-20)
1567
+ if (isIdenticalQuestion(e)) continue;
1568
+ // AN UNSUPPORTED ENTRY IS A DISCLOSURE, NOT A SEARCH. It was stamped at compile because this
1569
+ // provider cannot express it, so it costs no reading and answers nothing — gating it would hold
1570
+ // back a coverage gap the run already knows about, and turn a `deferred` row (this was not
1571
+ // searchable) into a `skipped` one (nothing on the axis ran), which says something different
1572
+ // about why a territory went unread. The gate exists to stop the run SPENDING its reading on
1573
+ // widenings before the mark itself is read; it has no business delaying a fact.
1574
+ if (e.unsupported === true) continue;
1575
+ e.when = { ...AWAITS_READING_TURN };
1576
+ }
1577
+
1370
1578
  // stable ordering: axis (REGISTER_AXES order) then insertion order within the axis
1371
1579
  const axisRank = new Map(REGISTER_AXES.map((a, i) => [a, i]));
1372
1580
  const ordered = entries.map((e, i) => [e, i]).sort((a, b) =>
@@ -1401,6 +1609,12 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1401
1609
  // DEFERRED coverage row for the ledger, never a dropped filter. Absent on a fully-covered plan, so
1402
1610
  // corsearch plans stay byte-identical to the pre-phase-3 compiler.
1403
1611
  ...(deferredJurisdictions.length ? { deferred_coverage: deferredJurisdictions } : {}),
1612
+ // THE CLASSES THE FRAME ADDED, BESIDE THE INSTRUCTED ONES, EACH WITH ITS REASON (decision 18). A
1613
+ // widening nobody can read back is one nobody can check the bound on: this is where a reviewer sees
1614
+ // which classes the order did not name, why each was judged to be reached by the client's own
1615
+ // goods, and — by their absence from `classes` — that no instructed class was traded for one.
1616
+ // Absent where the frame added none, which is the ordinary answer, so those plans stay identical.
1617
+ ...(addedClassRows.length ? { added_classes: addedClassRows } : {}),
1404
1618
  // A GOODS TERM THE COMPILER DROPPED IS A DISCLOSURE, NOT A DETAIL. The words in this list are
1405
1619
  // OR-ed, so removing one makes the clause NARROWER: the search returns fewer records and a
1406
1620
  // conflict that term would have surfaced is simply never found. The model was asked for these
@@ -1418,6 +1632,11 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1418
1632
  // comparing the word list to what was searched would otherwise find a term that appears to have
1419
1633
  // been asked and was not, exactly.
1420
1634
  ...(goodsRewritten.length ? { goods_text_rewritten: goodsRewritten } : {}),
1635
+ // THE STAGE WAS ASKED FOR GOODS WORDS AND DID NOT ANSWER. Recorded on the plan because it is a
1636
+ // fact about the run, not about the matter: a matter with no goods words to give answers with an
1637
+ // empty list, and that is a different thing from a stage that never answered at all. Absent when
1638
+ // the stage answered, so every plan whose stage did its job stays byte-identical.
1639
+ ...(goodsAnswered ? {} : { goods_words_unanswered: true }),
1421
1640
  ...(caps ? { provider: caps.id } : {}),
1422
1641
  entries: ordered,
1423
1642
  };
@@ -1699,6 +1918,13 @@ export function entryQuestionKey(entry, plan) {
1699
1918
  predicate: String(e.predicate ?? ""),
1700
1919
  terms: [...terms].sort(),
1701
1920
  owner: String(e.owner ?? ""),
1921
+ // THE GOODS NARROWING IS PART OF THE QUESTION, and leaving it out makes the crowd-narrow path
1922
+ // impossible rather than merely imprecise. The first move against a crowded identical mark is to
1923
+ // re-ask THAT question limited to the client's goods words: same axis, same predicate, same terms,
1924
+ // same classes, same regions. Without the goods in this key the fold reads it as the question the
1925
+ // plan already holds — the very crowd it is narrowing — and refuses it as a duplicate. The lever
1926
+ // would be in the schema, in the manual and in the model's proposal, and nothing would ever run.
1927
+ goods: [...goodsTermsList(e)].sort(),
1702
1928
  nice_classes: sorted(e.nice_classes),
1703
1929
  regions: sorted(own),
1704
1930
  term_literal: e.term_literal === true,
@@ -1745,6 +1971,10 @@ export function foldSupplementalEntries(plan, entries) {
1745
1971
  const asked = new Map();
1746
1972
  for (const e of plan.entries) {
1747
1973
  if (e?.unsupported === true) continue;
1974
+ // Nor does a family WAITING for the reading turn. It stands in the plan as the record of a question
1975
+ // not yet asked; the reading turn's ask of that same question is the answer it waits for, and refusing
1976
+ // the ask as its duplicate left the family recorded as never asked while its question had run.
1977
+ if (awaitsReadingTurn(e?.when)) continue;
1748
1978
  const k = entryQuestionKey(e, plan);
1749
1979
  if (k && !asked.has(k)) asked.set(k, e.qid);
1750
1980
  }
@@ -1865,7 +2095,7 @@ export function validatePlanFeasibility(plan, { capabilities = null, maxOrWidth
1865
2095
  if (Array.isArray(e?.terms) && e.terms.length > maxOrWidth) add("repairable", `OR-stack of ${e.terms.length} names exceeds the executor bound (${maxOrWidth}) — the executor runs it chunked`);
1866
2096
  for (const t of names) if (String(t).length > maxNameLength) add("repairable", `name exceeds ${maxNameLength} chars ("${String(t).slice(0, 40)}…") — provider-side truncation risk only`);
1867
2097
  if ((e?.nice_classes ?? []).some((c) => !Number.isFinite(Number(c)))) add("unexecutable", "non-numeric nice_class");
1868
- if (e?.when && !qids.has(e.when.runs_if_enumerated)) add("unexecutable", `when-guard targets unknown qid "${e.when?.runs_if_enumerated}"`);
2098
+ if (e?.when && guardParentQid(e.when) != null && !qids.has(guardParentQid(e.when))) add("unexecutable", `when-guard targets unknown qid "${guardParentQid(e.when)}"`);
1869
2099
  // An entry the compiler already stamped `unsupported` (variantTermIssue / capability gap) is a
1870
2100
  // DISCLOSED deferred row, never dispatched — the shape lint must not re-flag it as unexecutable
1871
2101
  // (the ownerGap check below models the same exemption).
@@ -1944,7 +2174,11 @@ export function parseRegisterPlan(raw) {
1944
2174
  const hasTerm = typeof e.term === "string" && e.term.trim();
1945
2175
  const hasTerms = Array.isArray(e.terms) && e.terms.length && e.terms.every((t) => typeof t === "string" && t.trim());
1946
2176
  if (!hasTerm && !hasTerms) throw new Error(`register_plan_term_missing:${short(e.qid)}`);
1947
- if (e.when != null && (typeof e.when !== "object" || typeof e.when.runs_if_enumerated !== "string"))
2177
+ // CLOSED TO THE TWO SHAPES, because a `when` the readers do not recognise is a guard that silently
2178
+ // does nothing — the entry would run as if ungated, which is the exact failure this gate exists to
2179
+ // prevent. A ruling-204 wait is the literal `{ awaits_reading_turn: true }` and carries no qid.
2180
+ if (e.when != null && !awaitsReadingTurn(e.when)
2181
+ && (typeof e.when !== "object" || typeof e.when.runs_if_enumerated !== "string"))
1948
2182
  throw new Error(`register_plan_when_invalid:${short(e.qid)}`);
1949
2183
  // F1: `owner` is an optional SCOPE FIELD on a mark-text entry (the owner×term intersection slice);
1950
2184
  // a bare owner sweep stays predicate:"owner" with the owner name as its term. A present-but-empty
@@ -1968,7 +2202,9 @@ export function parseRegisterPlan(raw) {
1968
2202
  throw new Error(`register_plan_covered_by_invalid:${short(e.qid)} (covered_by is a non-empty array of qid strings when present)`);
1969
2203
  }
1970
2204
  for (const e of p.entries) {
1971
- if (e.when && !seen.has(e.when.runs_if_enumerated))
2205
+ // A ruling-204 wait names no qid, so there is nothing to orphan — it is checked for SHAPE above
2206
+ // and skipped here. Only a parent-qid guard can point at a question the plan does not carry.
2207
+ if (e.when && guardParentQid(e.when) != null && !seen.has(guardParentQid(e.when)))
1972
2208
  throw new Error(`register_plan_when_orphan:${short(e.qid)} (guard names a qid the plan does not carry)`);
1973
2209
  if (Array.isArray(e.covered_by)) for (const q of e.covered_by) {
1974
2210
  if (!seen.has(q)) throw new Error(`register_plan_covered_by_orphan:${short(e.qid)} (covered_by names qid "${q.slice(0, 60)}" which the plan does not carry)`);
@@ -2000,11 +2236,39 @@ export function joinPlanToBands(plan, bandBlocksByAxis) {
2000
2236
  const byQid = new Map();
2001
2237
  for (const b of blocks) if (typeof b.qid === "string" && b.qid) byQid.set(b.qid, b);
2002
2238
 
2003
- const executed = [], missing = [], skipped = [], deferred = [];
2239
+ const executed = [], missing = [], skipped = [], deferred = [], awaiting = [], asked = [];
2240
+ // A WAITING FAMILY THE READING TURN ASKED IS NOT WAITING. Its ask arrives as an ordinary entry — a
2241
+ // supplemental with the same question — and the family's own row stays guarded forever, so without this
2242
+ // link every family asked read as never asked. Matched on the question (entryQuestionKey), against
2243
+ // every entry that is not itself waiting or unsupported; the asker's own state says what it returned.
2244
+ const askedBy = new Map();
2245
+ for (const e of plan.entries) {
2246
+ if (e?.unsupported === true || awaitsReadingTurn(e?.when)) continue;
2247
+ const k = entryQuestionKey(e, plan);
2248
+ if (k && !askedBy.has(k)) askedBy.set(k, e.qid);
2249
+ }
2004
2250
  for (const e of plan.entries) {
2005
2251
  if (e.when) {
2252
+ // A RULING-204 WAIT IS ITS OWN BUCKET, NOT A SKIP, and the difference is what the reader is
2253
+ // told. `skipped` means a parent question proved intractable and took its children down with it
2254
+ // — the audit classes call that sanctioned, with the parent's crowd standing as dilution
2255
+ // context. A family awaiting the reading turn has no such parent and no such crowd: it is a
2256
+ // question nobody decided to ask, on a matter where the identical mark may have answered
2257
+ // perfectly. Folding the two together would file "not asked, by judgment" under "held back by a
2258
+ // crowd", which is a different sentence about a different thing, on every matter.
2259
+ if (awaitsReadingTurn(e.when)) {
2260
+ const by = askedBy.get(entryQuestionKey(e, plan));
2261
+ if (by) asked.push({ qid: e.qid, axis: e.axis, asked_by: by });
2262
+ else awaiting.push({ qid: e.qid, axis: e.axis });
2263
+ continue;
2264
+ }
2006
2265
  const parent = byQid.get(e.when.runs_if_enumerated);
2007
2266
  const parentState = String(parent?.state ?? "").toLowerCase();
2267
+ // A CROWD IS WHAT HOLDS A CHILD BACK, and only a crowd. `enumerated` covers a question answered
2268
+ // with no records as well as one answered with many — `verified-zero` is a per-term disposition,
2269
+ // never a band state (named-band.mjs BAND_STATES), so it cannot appear here. The case where a
2270
+ // clean zero wrongly held its children was the per-term and per-class rescue in enumerate.mjs,
2271
+ // and it is corrected there: a fully resolved stack is a complete band whose answer is zero.
2008
2272
  if (parentState !== "enumerated") { skipped.push({ qid: e.qid, guard: e.when.runs_if_enumerated }); continue; }
2009
2273
  }
2010
2274
  const b = byQid.get(e.qid);
@@ -2054,7 +2318,7 @@ export function joinPlanToBands(plan, bandBlocksByAxis) {
2054
2318
  const planQids = new Set(plan.entries.map((e) => e.qid));
2055
2319
  const unplanned = blocks.filter((b) => typeof b.qid === "string" && b.qid && !planQids.has(b.qid))
2056
2320
  .map((b) => ({ qid: b.qid, query: String(b.query ?? "").slice(0, 80) }));
2057
- return { executed, missing, skipped, deferred, unplanned };
2321
+ return { executed, missing, skipped, deferred, awaiting, asked, unplanned };
2058
2322
  }
2059
2323
 
2060
2324
  // ──: the slice the provider ACCEPTED and then hard-errored at RUN time ──────────────────────────
@@ -2182,7 +2446,9 @@ export function deferExhaustedProviderErrors(joinRes, bandBlocksByAxis, exhauste
2182
2446
  * Code-derived per-axis coverage truth from the plan + join result. An axis is:
2183
2447
  * "executed" — every guard-active entry has a band block, none missing;
2184
2448
  * "incomplete" — executed but ≥1 block came back a crowd (incomplete state);
2185
- * "unexecuted" — ≥1 guard-active entry has NO band block (the F3 hole).
2449
+ * "unexecuted" — ≥1 guard-active entry has NO band block (the F3 hole);
2450
+ * "awaiting-judgment" — nothing ran because every entry is a ruling-204 family still waiting for the
2451
+ * reading turn to ask for it. Nothing is wrong; nothing was searched either.
2186
2452
  * The skeleton is the floor the digest's ledger claims are checked against — it never makes a
2187
2453
  * sufficiency judgment (that stays Layer B), it only states what RAN.
2188
2454
  */
@@ -2247,18 +2513,28 @@ export const PLAN_AUDIT_HEAD =
2247
2513
  // Graded: only a slice that NEVER RAN blocks; a sanctioned skip and a crowd descriptor are JUDGMENT
2248
2514
  // inputs — the seat reasons over them, it never manufactures a verdict from a label.
2249
2515
  export const PLAN_AUDIT_CLASSES =
2250
- `THE THREE CLASSES, GRADED: (1) a slice listed MISSING NEVER RAN — nothing resting on it may be stated as searched-clean, and nothing may describe what such a search would have shown; (2) a crowd-gated SKIPPED fringe is SANCTIONED (#361 — its parent proved intractable): the parent crowd is dilution context, never a searched-clean slice; (3) a CROWD/INCOMPLETE descriptor is a signal FOR JUDGMENT and never a verdict input — the lawyer's materiality reasoning over it STANDS (off-field, dilution evidence), and a state label never manufactures a CONDITIONAL by itself.`;
2516
+ `THE FOUR CLASSES, GRADED: (1) a slice listed MISSING NEVER RAN — nothing resting on it may be stated as searched-clean, and nothing may describe what such a search would have shown; (2) a crowd-gated SKIPPED fringe is SANCTIONED (#361 — its parent proved intractable): the parent crowd is dilution context, never a searched-clean slice; (3) a CROWD/INCOMPLETE descriptor is a signal FOR JUDGMENT and never a verdict input — the lawyer's materiality reasoning over it STANDS (off-field, dilution evidence), and a state label never manufactures a CONDITIONAL by itself; (4) an AWAITING-JUDGMENT family was not asked because you have not asked for it (ruling 204): it has no parent crowd and says nothing about the field — it is a question still open to you under step 6, and until you ask it nothing resting on it may be stated as searched-clean.`;
2251
2517
 
2252
2518
  export function deriveCoverageSkeleton(plan, join) {
2253
2519
  const missing = new Set(join.missing);
2254
2520
  const stateByQid = new Map(join.executed.map((x) => [x.qid, x.state]));
2255
2521
  const skippedQids = new Set(join.skipped.map((x) => x.qid));
2256
2522
  const deferredQids = new Set((join.deferred ?? []).map((x) => x.qid));
2523
+ const awaitingQids = new Set((join.awaiting ?? []).map((x) => x.qid));
2524
+ const askedQids = new Set((join.asked ?? []).map((x) => x.qid));
2257
2525
  const axes = new Map();
2258
2526
  for (const e of plan.entries) {
2259
- if (!axes.has(e.axis)) axes.set(e.axis, { axis: e.axis, entries: 0, executed: 0, crowds: 0, missing: [], skipped: 0, deferred: [] });
2527
+ if (!axes.has(e.axis)) axes.set(e.axis, { axis: e.axis, entries: 0, executed: 0, crowds: 0, missing: [], skipped: 0, deferred: [], awaiting: 0, asked: 0 });
2260
2528
  const a = axes.get(e.axis);
2261
2529
  a.entries++;
2530
+ // COUNTED BEFORE `skipped`, and separately from it. With the families waiting on every matter this is the ordinary state of
2531
+ // most axes on most matters, so folding it into `skipped` would make "nothing on this axis ran
2532
+ // because a parent crowded" the routine reading of a healthy run — and would say it to the model,
2533
+ // which reads these states as judgment input.
2534
+ if (awaitingQids.has(e.qid)) { a.awaiting++; continue; }
2535
+ // A waiting family the reading turn asked is answered by the entry that asked it, which is counted
2536
+ // where it ran; counting the family as executed too would count one question twice.
2537
+ if (askedQids.has(e.qid)) { a.asked++; continue; }
2262
2538
  if (skippedQids.has(e.qid)) { a.skipped++; continue; }
2263
2539
  if (missing.has(e.qid)) { a.missing.push(e.qid); continue; }
2264
2540
  // a capability gap is NOT executed — it is a disclosed hole in the axis
@@ -2275,11 +2551,19 @@ export function deriveCoverageSkeleton(plan, join) {
2275
2551
  // so nothing on the axis was ever dispatched) is its OWN state. It used to fall through to "executed"
2276
2552
  // — the arithmetic reads "0 missing, 0 crowds" and the honest reading of that is "nothing ran", not
2277
2553
  // "everything ran clean". Held to the same standard as `deferred` below: a clean cannot rest on it.
2554
+ // THE READING-TURN WAIT'S STATE, and it is held to the same standard as `skipped` and `deferred`: nothing on
2555
+ // the axis ran, so no clean may rest on it. It ranks BELOW those two in urgency because nothing is
2556
+ // wrong — the questions were not asked because judgment did not ask for them, which is the design.
2557
+ // It is last in the chain so that a real failure on the same axis still wins the label: an axis
2558
+ // that is part-awaiting and part-missing reads `unexecuted`, which is the louder truth.
2278
2559
  state: a.missing.length ? "unexecuted"
2279
2560
  : (a.deferred.length ? "deferred"
2280
2561
  : (a.crowds ? "incomplete"
2281
- : (a.executed === 0 && a.skipped > 0 ? "skipped" : "executed"))),
2562
+ : (a.executed === 0 && a.skipped > 0 ? "skipped"
2563
+ : (a.executed === 0 && a.awaiting > 0 ? "awaiting-judgment" : "executed")))),
2282
2564
  entries: a.entries, executed: a.executed, crowds: a.crowds, skipped: a.skipped, missing: a.missing,
2565
+ ...(a.awaiting ? { awaiting: a.awaiting } : {}),
2566
+ ...(a.asked ? { asked: a.asked } : {}),
2283
2567
  ...(a.deferred.length ? { deferred: a.deferred } : {}),
2284
2568
  }));
2285
2569
  }
@@ -2321,6 +2605,14 @@ export function findUnexecutedCleanClaims(claimedRows, skeleton) {
2321
2605
  // never ran. Same standard as the two above — the slice was not searched, so a clean cannot rest on it.
2322
2606
  else if (s && s.state === "skipped")
2323
2607
  out.push({ axis: s.axis, token: `coverage_clean_skipped:${s.axis}`, missing: [] });
2608
+ // THE READING-TURN WAIT: an axis whose families are still waiting for the reading turn's ask did not run
2609
+ // either, so a clean over it has the same absent foundation. The token is its OWN, and that is the
2610
+ // point: `skipped` would send the reader to look for a crowd that never happened, where this says
2611
+ // the questions were not asked because judgment did not ask for them. Same strictness, true
2612
+ // sentence. Not a rejection of the run — a clean claim here is the model asserting something it
2613
+ // has no basis for, and it is told which.
2614
+ else if (s && s.state === "awaiting-judgment")
2615
+ out.push({ axis: s.axis, token: `coverage_clean_awaiting_judgment:${s.axis}`, missing: [] });
2324
2616
  }
2325
2617
  return out;
2326
2618
  }
@@ -55,7 +55,7 @@ export function rosterVerdict({ keys, onDisk, bundledDemos, expectDemos, caller
55
55
  if (!sameSet(keys, expected) && unreadable)
56
56
  // A KEY WHOSE CLAIMS CANNOT BE READ may be narrowing the answer, and a narrowing looks exactly like a
57
57
  // door that disagrees with its store. Neither passed nor failed: not compared, and said so.
58
- return { state: "skip", message: `the door sees ${keys.length} customer(s) (${keys.join(", ")}), `
58
+ return { state: "skip", blocked: true, message: `the door sees ${keys.length} customer(s) (${keys.join(", ")}), `
59
59
  + `the configured store holds ${onDisk.length} (${onDisk.join(", ")}), and the claims of the key this `
60
60
  + "check asked with could not be read — so a narrowing by that key cannot be told from a door that "
61
61
  + "disagrees with its store. NOT compared" };