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.
- package/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +29 -7
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/driver/CHANGELOG.md +37 -0
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/drainer-identity.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +28 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline.mjs +141 -37
- package/driver/publish/index.mjs +40 -25
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +1 -1
- package/driver/suite-census.json +236 -14
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/brief.mjs +5 -7
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +26 -19
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
package/driver/register-plan.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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" };
|