clearotron 0.4.1-beta.5 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.env.example +7 -0
  2. package/build-info.json +2 -2
  3. package/docs/architecture/04-configuration-reference.md +1 -0
  4. package/driver/CHANGELOG.md +42 -0
  5. package/driver/ask-ledger.mjs +6 -1
  6. package/driver/band-shape.mjs +12 -4
  7. package/driver/close-verify.mjs +27 -8
  8. package/driver/engine/anthropic-agent.mjs +7 -3
  9. package/driver/engine/common.mjs +10 -3
  10. package/driver/engine/mcp/band-server.mjs +55 -12
  11. package/driver/engine/mcp/codex-config.mjs +24 -4
  12. package/driver/engine/mcp/gather-config.mjs +4 -3
  13. package/driver/gateway.mjs +13 -5
  14. package/driver/package.json +1 -1
  15. package/driver/pipeline.mjs +87 -31
  16. package/driver/predelivery-lint.mjs +1 -1
  17. package/driver/record-goods.mjs +30 -0
  18. package/driver/remedy-accounting.mjs +2 -6
  19. package/driver/skills/knockout-assess/SKILL.md +2 -2
  20. package/driver/stages-knockout.mjs +0 -13
  21. package/driver/stages.mjs +11 -2
  22. package/driver/suite-census.json +81 -3
  23. package/driver/verify-knockout.mjs +27 -10
  24. package/mcp-server/CHANGELOG.md +8 -0
  25. package/mcp-server/http-smoke.mjs +2 -0
  26. package/mcp-server/lib/audit-view.mjs +10 -3
  27. package/mcp-server/lib/events.mjs +7 -0
  28. package/mcp-server/lib/knockout.mjs +7 -5
  29. package/mcp-server/package.json +1 -1
  30. package/package.json +1 -1
  31. package/portal-ui/package.json +1 -1
  32. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  33. package/providers/oauth-mcp-bridge/package.json +1 -1
  34. package/scripts/ask-ai-render-check.mjs +1 -0
  35. package/scripts/authority-boundary-probe.mjs +2 -0
  36. package/scripts/report-header-render-check.mjs +1 -0
  37. package/scripts/report-offline-render-check.mjs +1 -0
  38. package/scripts/report-screenshot.mjs +1 -0
  39. package/scripts/report-sections-render-check.mjs +1 -0
  40. package/scripts/report-theme-render-check.mjs +1 -0
  41. package/scripts/revisit-render-check.mjs +1 -0
  42. package/scripts/settings-render-check.mjs +1 -0
  43. package/scripts/undeclared-key-census.mjs +1 -0
  44. package/scripts/verify-publishable.mjs +2 -0
  45. package/shared/names-in-force.mjs +1 -0
@@ -78,7 +78,7 @@ import { armCoverageForm, coverageFormInput, coverageFormPaths, coverageFormStam
78
78
  import { releasedFamilyQids, readWithheldFamilies } from "./withheld-families.mjs"; // the waiting families the reading turn released join as dictated entries
79
79
  import { unionPlacementForm } from "./placement-union.mjs";
80
80
  import { readPlacementForm, readPlacementFormInput, writePlacementForm } from "./placement-form-io.mjs";
81
- import { buildSelectionIndex, setAsideGrounds, setAsideKeysOf, ownerFates } from "./placement-form.mjs";
81
+ import { buildSelectionIndex, setAsideGrounds, setAsideKeysOf, ownerFates, isSetAsideGroupRow } from "./placement-form.mjs";
82
82
  import { dictatedPaths, findStrayArtifacts, treeSnapshot, findStrayInTree, matterSiblings, findStrayMatterSiblings } from "./stray-artifacts.mjs"; // — a run dir holds no document no stage dictated; — nor does the doctrine tree
83
83
  import { resolveProfile, resolveEffectiveProfile, derivedFloor, derivedBatchSize, gridBatchFor, applicantMatchesProfile, NEUTRAL_DELIVERY, deliveryForRun, recipeProseGuard, withRunPlatforms, profileStoreResolution } from "./profiles.mjs"; // adds profileStoreResolution — the CONFIG store's identity, beside the doctrine tree's
84
84
  import { resolveSearchPolicy, gateResolvedPolicy, loadRecipes, policyFor, isRegisterOnly, reportIdentityFor, depthFor } from "./search-policy.mjs";
@@ -2847,22 +2847,12 @@ export function deriveFloorDuty(ctx, r, trigger = null) { // @internal — dri
2847
2847
  return null;
2848
2848
  };
2849
2849
  try {
2850
- if (!existsSync(P.bandShape)) return notComputable("no band-shape.json — the floors are not on disk to check against");
2851
- if (!existsSync(P.placementModel)) return notComputable("no placements.json (a run predating the structured placement mirror)");
2852
- let floors, placements;
2853
- try {
2854
- const shape = JSON.parse(readFileSync(P.bandShape, "utf8"));
2855
- // ABSENT vs EMPTY, and they are not the same answer. A shape with no floors slice cannot be
2856
- // reconciled and says so; a shape whose floors slice IS empty is a real answer — this band held no
2857
- // live in-class identical record — and reconciles trivially at 0/0.
2858
- const slice = shape?.floors?.in_class_identical_or_near;
2859
- if (!Array.isArray(slice)) return notComputable("band-shape.json carries no floors.in_class_identical_or_near array");
2860
- floors = slice;
2861
- } catch (e) { return notComputable(`band-shape.json unreadable: ${String(e?.message ?? e).slice(0, 80)}`); }
2862
- try { placements = parsePlacementsJson(readFileSync(P.placementModel, "utf8")).placements; }
2863
- catch (e) { return notComputable(`placements.json unparseable: ${String(e?.message ?? e).slice(0, 80)}`); }
2864
- const artifact = reconcileFloorDuty({ floors, placements, setAsideGroundOf: placementSetAsideGrounds(P) });
2865
- artifact.owners = placementOwnerFates(P, placements);
2850
+ const now = floorDutyNow(P);
2851
+ if (now.reason) return notComputable(now.reason);
2852
+ const { artifact, placements } = now;
2853
+ const fates = placementOwnerFates(P, placements);
2854
+ artifact.owners = fates.counted ?? null;
2855
+ if (!fates.counted) artifact.owners_absent = fates.absent; // no count, and why: no groups, or an input that would not read
2866
2856
  // The pass's own outcome, beside the rows. That lesson: a stage that failed or skipped makes every
2867
2857
  // record it did not reach an UPSTREAM ABSENCE, not a judgment, and a reader blaming the seat for a
2868
2858
  // crash is reading the artifact wrong.
@@ -2885,6 +2875,42 @@ export function deriveFloorDuty(ctx, r, trigger = null) { // @internal — dri
2885
2875
  }
2886
2876
  }
2887
2877
 
2878
+ /**
2879
+ * The floors reconciled against the band shape and the placements now on disk: `{artifact, placements}`, or
2880
+ * `{reason}` when they cannot be. Read, never written: `deriveFloorDuty` writes a pass's account from it, and a
2881
+ * resumed run's skip reads it (`floorDutyOwedNow`).
2882
+ */
2883
+ function floorDutyNow(P) {
2884
+ const notComputable = (reason) => ({ reason });
2885
+ if (!existsSync(P.bandShape)) return notComputable("no band-shape.json — the floors are not on disk to check against");
2886
+ if (!existsSync(P.placementModel)) return notComputable("no placements.json (a run predating the structured placement mirror)");
2887
+ let floors, placements;
2888
+ try {
2889
+ const shape = JSON.parse(readFileSync(P.bandShape, "utf8"));
2890
+ // ABSENT vs EMPTY, and they are not the same answer. A shape with no floors slice cannot be
2891
+ // reconciled and says so; a shape whose floors slice IS empty is a real answer — this band held no
2892
+ // live in-class identical record — and reconciles trivially at 0/0.
2893
+ const slice = shape?.floors?.in_class_identical_or_near;
2894
+ if (!Array.isArray(slice)) return notComputable("band-shape.json carries no floors.in_class_identical_or_near array");
2895
+ floors = slice;
2896
+ } catch (e) { return notComputable(`band-shape.json unreadable: ${String(e?.message ?? e).slice(0, 80)}`); }
2897
+ try { placements = parsePlacementsJson(readFileSync(P.placementModel, "utf8")).placements; }
2898
+ catch (e) { return notComputable(`placements.json unparseable: ${String(e?.message ?? e).slice(0, 80)}`); }
2899
+ return { artifact: reconcileFloorDuty({ floors, placements, setAsideGroundOf: placementSetAsideGrounds(P) }), placements };
2900
+ }
2901
+
2902
+ /**
2903
+ * DOES PLACEMENT OWE FLOORS ON THE SHAPE NOW ON DISK? A resumed run derives the band shape afresh, and one derived
2904
+ * since the last placement pass can owe more than that pass's floor-duty artifact says: a floor the new shape adds,
2905
+ * or a crowded (b) whose row no longer stands. Read from the artifact alone, placement skipped, the duty was derived
2906
+ * again straight after it, and the report shipped clamped with the step never asked. Read here as well, placement
2907
+ * runs again, and its dispatch-time union tells the step what changed. False where nothing can be reconciled.
2908
+ */
2909
+ export function floorDutyOwedNow(P, name) { // @internal — driven by its test with a run's own files
2910
+ if (name !== "placement-inquiry" || !floorDutyArmed(P.runDir)) return false;
2911
+ try { return Boolean(floorDutyBlock(floorDutyNow(P).artifact, { armed: true })); } catch { return false; }
2912
+ }
2913
+
2888
2914
  function derivePlacementCarry(ctx, trigger) {
2889
2915
  const P = ctx.paths;
2890
2916
  const write = (artifact) => {
@@ -3008,24 +3034,44 @@ function recordPlacementSeam(ctx, r, trigger = null) {
3008
3034
  });
3009
3035
  }
3010
3036
 
3037
+ /**
3038
+ * The form's set-aside entries as they stand on the shape now on disk. A ground by name stands as stored. A
3039
+ * group's row is checked again exactly as the union took it, on its count and on every line read, because the
3040
+ * shape may have been derived again since: a resumed run derives it afresh, and a record body fetched after
3041
+ * placement can move a line out of the list the goods test could not place and into a group. A row taken on
3042
+ * the earlier shape then covers no line the later one added. Nothing is written back. PURE.
3043
+ */
3044
+ function standingSetAside(entries, input) {
3045
+ const all = Array.isArray(entries) ? entries : [];
3046
+ const grouped = all.filter(isSetAsideGroupRow);
3047
+ if (!grouped.length) return all;
3048
+ const { form } = unionPlacementForm({ rows: [], set_aside: grouped }, null, input);
3049
+ return [...all.filter((e) => !isSetAsideGroupRow(e)), ...form.set_aside_groups];
3050
+ }
3051
+
3011
3052
  /** The set-aside grounds the placement form holds, by name and by group, as uri → entry, through the form's own selection index. */
3012
- function placementSetAsideGrounds(P) {
3053
+ export function placementSetAsideGrounds(P) { // @internal — driven by its test with a run's own files
3013
3054
  try {
3014
3055
  const form = readPlacementForm(P.runDir);
3015
3056
  if (!form.set_aside?.length) return () => null;
3016
3057
  const input = readPlacementFormInput(P.runDir);
3017
- return setAsideGrounds(form.set_aside, buildSelectionIndex(input), input.groups, input.floors);
3058
+ return setAsideGrounds(standingSetAside(form.set_aside, input), buildSelectionIndex(input), input.groups, input.floors);
3018
3059
  } catch { return () => null; }
3019
3060
  }
3020
3061
 
3021
- /** Every owner's fate at placement, by the shape's lists (placement-form.mjs `ownerFates`); null when the shape has no groups. */
3022
- function placementOwnerFates(P, placements) {
3062
+ /**
3063
+ * Every owner's fate at placement, by the shape's lists (placement-form.mjs `ownerFates`): `{counted}`, or `{absent}`
3064
+ * saying why there is no count. A shape with no groups has nothing to count by; an input that would not read is a
3065
+ * gap in the record. The two are not one fact, and the floor duty's artifact keeps them apart.
3066
+ */
3067
+ export function placementOwnerFates(P, placements) { // @internal — driven by its test with a run's own files
3023
3068
  try {
3024
3069
  const input = readPlacementFormInput(P.runDir);
3025
- if (!input.groups) return null;
3026
- return ownerFates({ groups: input.groups, entries: readPlacementForm(P.runDir).set_aside, index: buildSelectionIndex(input),
3070
+ if (!input.groups) return { absent: "the band shape lists no groups" };
3071
+ const counted = ownerFates({ groups: input.groups, entries: standingSetAside(readPlacementForm(P.runDir).set_aside, input), index: buildSelectionIndex(input),
3027
3072
  placedUris: (Array.isArray(placements) ? placements : []).flatMap((p) => (Array.isArray(p?.records) ? p.records : [])) });
3028
- } catch { return null; }
3073
+ return counted ? { counted } : { absent: "the band shape's groups could not be counted" };
3074
+ } catch (e) { return { absent: `an input could not be read: ${String(e?.message ?? e).slice(0, 120)}` }; }
3029
3075
  }
3030
3076
 
3031
3077
  /**
@@ -4428,7 +4474,7 @@ async function stageOnce(name, ctx, opts = {}) {
4428
4474
  // undischargeable and the run spent every remaining stage travelling toward a failure it was already
4429
4475
  // doomed to. Reconciled toward the stricter side — the predicate lives in floor-duty.mjs and is CALLED,
4430
4476
  // never copied, so the two judgements cannot drift apart again.
4431
- const dutyBlocksSkip = !forced && floorDutyBlocksSkip(P.runDir, name);
4477
+ const dutyBlocksSkip = !forced && (floorDutyBlocksSkip(P.runDir, name) || floorDutyOwedNow(P, name));
4432
4478
  if (dutyBlocksSkip) {
4433
4479
  note(`[${label}] NOT skipped — its floor duty is undischarged, so its output is not valid to reuse; `
4434
4480
  + "re-running so the seat can place the outstanding floor(s) or name them with a ground "
@@ -10876,12 +10922,14 @@ async function pipelineInner(job, opts = {}) {
10876
10922
  // (`x:0` vs `x:null` must differ by value, never by presence). The demote paths carry the SAME
10877
10923
  // rows forward unchanged, deliberately — a failed re-digest demotes CLOSURE, not searchedness:
10878
10924
  // a query that ran still ran.
10879
- const writeReopenReceipt = (swept, deferrals, dom, partials = [], accounting = null) => {
10925
+ const writeReopenReceipt = (swept, deferrals, dom, partials = [], accounting = null, answered = []) => {
10880
10926
  try {
10881
10927
  writeFileSync(reopenReceiptPath, JSON.stringify({
10882
10928
  ts: new Date().toISOString(), requested: firing.map(reopenKey),
10883
10929
  swept, deferrals, remaining: deferrals.map((x) => x.directive),
10884
10930
  ...(partials.length ? { partials } : {}),
10931
+ // a closed directive whose slice the fold refused as already-asked, with the plan row that answered it
10932
+ ...(answered.length ? { answered_elsewhere: answered } : {}),
10885
10933
  dominant_element_gap, domClosed: dom,
10886
10934
  directive_qids: reopenQids, born: reopenBorn,
10887
10935
  remedy_terms: accounting?.computable === true ? accounting.rows : null,
@@ -10927,6 +10975,7 @@ async function pipelineInner(job, opts = {}) {
10927
10975
  // minted subset — the refusal is disclosed per directive (receipt `partials` row on a close,
10928
10976
  // folded into the defer reason otherwise), never dropped into the aggregate count alone.
10929
10977
  const regPartialMint = new Map();
10978
+ const regAnsweredBy = new Map(); // reopenKey → the plan rows that answered its slices the fold refused as already-asked
10930
10979
  // — the per-TERM rows the remedy ledger accounts. One row per remedy term per directive,
10931
10980
  // recorded at the mint seam (the only place the term→qid mapping exists) so the receipt can
10932
10981
  // later say what each term asked and what came back. reopenKey → [{term, predicates, qids,
@@ -11086,10 +11135,16 @@ async function pipelineInner(job, opts = {}) {
11086
11135
  const blocksByQid = new Map((bands[a] ?? []).filter((b) => b && b.qid).map((b) => [b.qid, b]));
11087
11136
  return { outcome, executedQids, blocksByQid };
11088
11137
  };
11089
- const verifyOne = (d, ev) => verifyRegisterDirectiveClose({
11090
- qids: directiveQids.get(reopenKey(d)) ?? [], intendedClasses: directiveIntended.get(reopenKey(d)) ?? [],
11091
- executedQids: ev.executedQids, blocksByQid: ev.blocksByQid,
11092
- });
11138
+ // A slice the fold refused as already-asked is checked on the row that answered it, as the remedy
11139
+ // term ledger reads it; the directive it closes names that row in the receipt.
11140
+ const verifyOne = (d, ev) => {
11141
+ const v = verifyRegisterDirectiveClose({
11142
+ qids: directiveQids.get(reopenKey(d)) ?? [], intendedClasses: directiveIntended.get(reopenKey(d)) ?? [],
11143
+ executedQids: ev.executedQids, blocksByQid: ev.blocksByQid, foldRefusals: regFoldRefusals,
11144
+ });
11145
+ if (v.closed && v.answered_by) regAnsweredBy.set(reopenKey(d), v.answered_by);
11146
+ return v;
11147
+ };
11093
11148
  const mintedDirectives = regDirectives.filter((d) => (directiveQids.get(reopenKey(d)) ?? []).length);
11094
11149
  let ev = await dispatchAndVerify(added);
11095
11150
  regLastJoin = ev;
@@ -11430,7 +11485,8 @@ async function pipelineInner(job, opts = {}) {
11430
11485
  // ending states "closed on N of M remedy proposals; K refused: <reason>", never a bare close.
11431
11486
  const partials = swept.filter((k) => regPartialMint.has(k))
11432
11487
  .map((k) => ({ directive: k, note: `verified-closed on the minted subset — ${regPartialMint.get(k)}` }));
11433
- writeReopenReceipt(swept, deferrals, domClosed, partials, remedyAccounting);
11488
+ const answered = swept.filter((k) => regAnsweredBy.has(k)).map((k) => ({ directive: k, slices: regAnsweredBy.get(k) }));
11489
+ writeReopenReceipt(swept, deferrals, domClosed, partials, remedyAccounting, answered);
11434
11490
  runLog(run.runDir, { event: "frame-reopen", requested: firing.length, swept: swept.length, deferred: deferrals.length, remaining: deferrals.length, dominant_element_gap, domClosed, reopenFetchCap: regDirectives.some((d) => d.severity === "dominant-element") ? reopenFetchCap : undefined });
11435
11491
  // C2 (review fix, 2026-07-29) — the redigest-fail demotion invariant, LOCKED path. The
11436
11492
  // unlocked path demotes verified-closed directives inline when its pre-synthesis flush
@@ -2358,7 +2358,7 @@ export function runLint({ depth, commonLawGrid, matterContext, clientPartyName,
2358
2358
  // prescription-prose
2359
2359
  // Everything else is ABSENT BY DESIGN and says so in the receipt (KNOCKOUT_ABSENT_BY_DESIGN below) —
2360
2360
  // never an entry that silently passes, which is the failure mode this tranche exists to kill. Measured
2361
- // against two real archived knockout runs (2026-07-28 BRIMSTONE, 2026-07-29 TASTICLES): zero flags on the
2361
+ // against two archived knockout runs, from 2026-07-28 and 2026-07-29: zero flags on the
2362
2362
  // applicable five, and templateChecks false-FAILED names-cell-populated on both (there is no NAME(S)
2363
2363
  // row on this lane) — which is why it is excluded rather than run.
2364
2364
  //
@@ -96,6 +96,36 @@ export function recordGoods(body, { english = true } = {}) {
96
96
  return out;
97
97
  }
98
98
 
99
+ // THE RAW GOODS LIST SAYS NOTHING THE NORMALISED ONE DOES NOT. A Clarivate record holds its goods twice: as the
100
+ // register wrote them (`_raw.goodsServices.intClassDescriptions`) and as the provider maps them, item for item,
101
+ // into `goodsServices` beside it (providers/clarivate/src/core.js `normalizeGoodsServices`). On a crowded test
102
+ // run, 2026-10-13, the raw goods were 22.7 of the 53.5 MB of record text, and all 1,348 records held both
103
+ // lists. Only the five fields that mapping reads are known here: a raw item carrying any other field, classes
104
+ // written other than as numbers between commas (one statement may cover several classes: "9,35,42"), or any
105
+ // value the normalised item does not repeat exactly means the raw list says something of its own. The rest of
106
+ // the raw goods block (a national class list, on six of those records) is not the normalised list's to carry.
107
+ const RAW_GOODS_FIELDS = new Set(["intClassNumber", "intGoodsLanguageCode", "intGoodsServicesDescription",
108
+ "cmComputerTranslationIntGoodsLanguageCode", "cmComputerTranslationIntGoodsServicesDescription"]);
109
+
110
+ /** Does the record's `goodsServices` carry its raw international goods list item for item, so that list adds nothing? PURE. */
111
+ export function rawGoodsCarried(body) {
112
+ const items = body?._raw?.goodsServices?.intClassDescriptions, norm = body?.goodsServices;
113
+ if (!Array.isArray(items) || !items.length || !Array.isArray(norm) || norm.length !== items.length) return false;
114
+ return items.every((d, i) => {
115
+ const n = norm[i];
116
+ if (!d || typeof d !== "object" || Array.isArray(d) || !n || typeof n !== "object") return false;
117
+ if (Object.keys(d).some((k) => !RAW_GOODS_FIELDS.has(k))) return false;
118
+ const cls = String(d.intClassNumber ?? "").trim();
119
+ if (cls !== "" && !/^\d+(\s*,\s*\d+)*$/.test(cls)) return false;
120
+ const classes = cls === "" ? [] : cls.split(",").map((c) => Number(c.trim()));
121
+ const same = (a, b) => (a ?? null) === (b ?? null);
122
+ return Array.isArray(n.classes) && n.classes.length === classes.length && n.classes.every((c, j) => c === classes[j])
123
+ && same(d.intGoodsLanguageCode, n.language) && same(d.intGoodsServicesDescription, n.description)
124
+ && same(d.cmComputerTranslationIntGoodsLanguageCode, n.translation_language)
125
+ && same(d.cmComputerTranslationIntGoodsServicesDescription, n.translation);
126
+ });
127
+ }
128
+
99
129
  /**
100
130
  * Every record's goods this run holds, by canonical record uri: Map(uri → [{classes, text}]). Read from the
101
131
  * record ledger, then from `_records/`, the later copy of a record winning, as `band_record` reads them. A
@@ -42,7 +42,7 @@
42
42
  // re-stated: verifyRegisterDirectiveClose asks the identical question of the same blocks, and two
43
43
  // copies of a matcher is how two matchers drift apart.
44
44
 
45
- import { blockCollapsed } from "./close-verify.mjs";
45
+ import { blockCollapsed, answeredAs } from "./close-verify.mjs";
46
46
  import { reopenKey } from "./frame-diff-model.mjs";
47
47
 
48
48
  export const REMEDY_ACCOUNTING_SCHEMA_VERSION = 1;
@@ -158,11 +158,7 @@ export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids
158
158
  //
159
159
  // ONLY THE DUPLICATE KIND RESOLVES. An identity collision or a malformed term is a genuine fault and
160
160
  // stays `dispatch-failed`: nothing else asked those questions, so nothing else answers them.
161
- const resolve = (q) => {
162
- const r = foldRefusals.get?.(q);
163
- return r && r.kind === "duplicate-question" && r.twin ? String(r.twin) : q;
164
- };
165
- const landsAs = new Map(qids.map((q) => [q, resolve(q)]));
161
+ const landsAs = new Map(qids.map((q) => [q, answeredAs(q, foldRefusals)])); // one rule with the directive check
166
162
  const blockFor = (q) => blocksByQid.get(landsAs.get(q) ?? q);
167
163
  const ranAs = (q) => executedQids.has(landsAs.get(q) ?? q);
168
164
  const slices = qids.map((q) => {
@@ -59,8 +59,8 @@ there is no other value: a band that needs sharpening is the band above it, stat
59
59
  known-to-enforce rights holders" — is RETIRED. That is owner size and fame driving a band, which the
60
60
  framework decks forbid in terms: "Optics — owner size, fame, partnership — never move the legal
61
61
  read." It was the only instruction in this seat's context licensing that move, and the readiest
62
- explanation for a Disney hit rating Very High and an EA hit rating High against a lawyer's Medium and
63
- Manageable. The rest of the rule stands and is doctrine.)*
62
+ explanation for a famous owner's hit rated well above the lawyer's grade. The rest of the rule stands
63
+ and is doctrine.)*
64
64
  3. **RETIRED — how ordinary the words are is a note for the reviewing lawyer, never a band.**
65
65
  This forbade the lowest band for a common English phrase and put a floor a band above it. On a
66
66
  scale that ends in Low it kept an everyday name off "no issues found"; on a client scale with no
@@ -467,19 +467,6 @@ export const KO_STAGES = {
467
467
  // the same run had taken. The turn was not lying: it is never shown the count lane's output (rule 1
468
468
  // of register-count.mjs) and it filled the gap with the only thing it had. Nobody who cannot see
469
469
  // the machinery describes it — the renderer owns that sentence and writes it from the sidecars.
470
- // ── THE GAG SPLITS: THE LANE STAYS GAGGED, THE FILINGS DO NOT ─────────────────────────────────
471
- //
472
- // The rule below is about COVERAGE — whether the register lane ran, how much it found, how far it
473
- // reached. That stays, and its reason is unchanged: this seat cannot see that lane, the report
474
- // states coverage in code from the run's own measurements, and a summary saying the registers were
475
- // not run above a table of counts that were is the contradiction it closes.
476
- //
477
- // What it must NOT be read as, and was: a bar on weighing an individual filing you were handed.
478
- // The validator arm behind it (`REGISTER_CLAIM_RE`) is KEPT and is already scoped correctly —
479
- // driven on seven sentences, it matches all three coverage narrations and none of four weighing
480
- // sentences ("Disney holds a Hong Kong registration…", "the German registration is vulnerable to
481
- // non-use revocation…"). The issue asked for the arm to be deleted; deleting it would remove
482
- // exactly the protection the issue's own next sentence says to keep. Measured, raised, kept.
483
470
  `SAY NOTHING ABOUT WHETHER THE REGISTER LANE RAN — whether registers were searched, counted,`,
484
471
  `overlaid or checked, how many filings exist, or how far the search reached. Not in any field, not`,
485
472
  `in the chunk summary. You cannot see that lane; the report states its coverage in code from the`,
package/driver/stages.mjs CHANGED
@@ -2610,7 +2610,7 @@ export const STAGES = {
2610
2610
  `Adopt or override each placement BY ENGAGING ITS reason — an override quotes the reason it contradicts (never silently re-tiers), and a kept tier may still tighten a label while reusing the reason. On a corrective/repair pass, do NOT re-read the whole placement file: the tiers are in ${P.placementModel}, and the md's rulings tail is handed to you verbatim in this dispatch (the PLACEMENT RULINGS TAIL block) — adjudicate against it there.`,
2611
2611
  // judgment-relocation (2026-06-23): the COMPLETE named band — run the relevance gate over the REAL
2612
2612
  // material. PR-8: read via the band tools (shape whole + logged lookups), never by slicing the raw file.
2613
- BAND_READING_CONTRACT,
2613
+ BAND_READING_CONTRACT_AFTER_PLACEMENT,
2614
2614
  `Reconcile your Coverage ledger against the shape's crowd descriptors: an `+"`incomplete`"+` slice is an open crowd the lawyer must weigh (a count-only / unenumerable slice), never a clean.`,
2615
2615
  // P2-A (charter P2d): identity collapse is CODE-derived before you read — the shape's Positions
2616
2616
  // section (and _driver/register-positions.json) is the exact-identity projection. "Owner
@@ -3198,7 +3198,7 @@ export const STAGES = {
3198
3198
  // dangerous named band or reported HONEST INCOMPLETENESS; you read the complete material and decide
3199
3199
  // relevance, risk, sufficiency and prioritisation. PR-8: the reading goes through the band tools
3200
3200
  // (shape whole + logged lookups) — nothing was sampled away upstream, and nothing is sliced by hand.
3201
- BAND_READING_CONTRACT,
3201
+ BAND_READING_CONTRACT_AFTER_PLACEMENT,
3202
3202
  // PR-8: synthesis holds NO live register tools — by design, never an outage. The funnel owns
3203
3203
  // enumeration; a register check this stage still wants is either answerable from the frozen
3204
3204
  // material (the band tools) or it is NEW SEARCH WORK, which enters only through the supplemental
@@ -4749,6 +4749,15 @@ export function dispatchBlockWhy(ctx, stage, id) {
4749
4749
  // onto a different real line passes while describing the wrong code. Adding below the last cited line
4750
4750
  // is the one placement that cannot do that. See band-size.mjs for what reads this.
4751
4751
  export const BAND_READING_STAGES = new Set(["placement-inquiry", "register-digest", "synthesis"]);
4752
+ // THE STAGE THAT OWES THE SHAPE'S GROUPS THEIR FATES, and so the only one offered `band_lookup`'s group pages.
4753
+ // Every line a group lists gets a fate at placement; no other stage owes the band a read, and a synthesis that
4754
+ // was handed the same offer paged an exhausted group 2,498 times (measured on a test run, 2026-10-13).
4755
+ export const GROUP_READING_STAGES = new Set(["placement-inquiry"]);
4756
+ // The contract as the stages after placement read it: without "Every floor has a fate on the form.", which is an
4757
+ // order to placement, whose form it names. The digest and synthesis owe no floor a fate (Krzys's ruling,
4758
+ // 2026-10-14). Removed from the one contract, never restated, so the two cannot drift apart.
4759
+ export const FLOOR_FATE_ORDER = "Every floor has a fate on the form.";
4760
+ export const BAND_READING_CONTRACT_AFTER_PLACEMENT = BAND_READING_CONTRACT.replace(` ${FLOOR_FATE_ORDER}`, "");
4752
4761
 
4753
4762
  // The register reading step's order on the waiting families. One constant, because the cross-check
4754
4763
  // follow-up reads it word for word (repair-composers.mjs); declared last so no line above it moves.
@@ -185,7 +185,7 @@
185
185
  },
186
186
  "a-childs-output-is-decoded-as-a-stream.test.mjs": {
187
187
  "tests": 3,
188
- "asserts": 10,
188
+ "asserts": 14,
189
189
  "skips": 1,
190
190
  "todos": 0
191
191
  },
@@ -249,6 +249,12 @@
249
249
  "skips": 1,
250
250
  "todos": 0
251
251
  },
252
+ "a-codex-stage-runs-without-the-sign-ins-apps.test.mjs": {
253
+ "tests": 1,
254
+ "asserts": 5,
255
+ "skips": 0,
256
+ "todos": 0
257
+ },
252
258
  "a-command-printed-from-npx-outlives-the-cache.test.mjs": {
253
259
  "tests": 3,
254
260
  "asserts": 14,
@@ -657,6 +663,12 @@
657
663
  "skips": 0,
658
664
  "todos": 0
659
665
  },
666
+ "a-group-row-covers-no-line-a-later-shape-added.test.mjs": {
667
+ "tests": 4,
668
+ "asserts": 16,
669
+ "skips": 0,
670
+ "todos": 0
671
+ },
660
672
  "a-group-row-covers-only-the-lines-it-was-shown.test.mjs": {
661
673
  "tests": 13,
662
674
  "asserts": 41,
@@ -777,6 +789,12 @@
777
789
  "skips": 0,
778
790
  "todos": 0
779
791
  },
792
+ "a-knockout-refusal-is-routed-as-the-knockout-records.test.mjs": {
793
+ "tests": 2,
794
+ "asserts": 6,
795
+ "skips": 0,
796
+ "todos": 0
797
+ },
780
798
  "a-knockout-run-with-no-method-is-not-offered-inputs.test.mjs": {
781
799
  "tests": 2,
782
800
  "asserts": 5,
@@ -909,6 +927,12 @@
909
927
  "skips": 0,
910
928
  "todos": 0
911
929
  },
930
+ "a-missing-fate-count-says-why.test.mjs": {
931
+ "tests": 3,
932
+ "asserts": 7,
933
+ "skips": 0,
934
+ "todos": 0
935
+ },
912
936
  "a-mixed-alphabet-look-alike-is-not-sent-where-the-register-drops-its-letters.test.mjs": {
913
937
  "tests": 10,
914
938
  "asserts": 46,
@@ -921,6 +945,12 @@
921
945
  "skips": 0,
922
946
  "todos": 0
923
947
  },
948
+ "a-name-above-its-cards-is-refused-where-the-model-can-fix-it.test.mjs": {
949
+ "tests": 3,
950
+ "asserts": 6,
951
+ "skips": 0,
952
+ "todos": 0
953
+ },
924
954
  "a-name-documented-in-any-example-file-is-documented.test.mjs": {
925
955
  "tests": 7,
926
956
  "asserts": 16,
@@ -1113,6 +1143,12 @@
1113
1143
  "skips": 0,
1114
1144
  "todos": 0
1115
1145
  },
1146
+ "a-question-the-plan-already-asked-closes-its-directive.test.mjs": {
1147
+ "tests": 6,
1148
+ "asserts": 13,
1149
+ "skips": 0,
1150
+ "todos": 0
1151
+ },
1116
1152
  "a-queued-job-is-visible-before-a-worker-claims-it.test.mjs": {
1117
1153
  "tests": 6,
1118
1154
  "asserts": 16,
@@ -1179,6 +1215,12 @@
1179
1215
  "skips": 0,
1180
1216
  "todos": 0
1181
1217
  },
1218
+ "a-records-goods-are-served-once.test.mjs": {
1219
+ "tests": 6,
1220
+ "asserts": 19,
1221
+ "skips": 0,
1222
+ "todos": 0
1223
+ },
1182
1224
  "a-refusal-is-not-a-pass.test.mjs": {
1183
1225
  "tests": 6,
1184
1226
  "asserts": 10,
@@ -1335,6 +1377,12 @@
1335
1377
  "skips": 0,
1336
1378
  "todos": 0
1337
1379
  },
1380
+ "a-resumed-run-asks-the-step-when-the-shape-moved.test.mjs": {
1381
+ "tests": 3,
1382
+ "asserts": 13,
1383
+ "skips": 0,
1384
+ "todos": 0
1385
+ },
1338
1386
  "a-retrieval-group-does-not-vote-on-seat-writes.test.mjs": {
1339
1387
  "tests": 5,
1340
1388
  "asserts": 15,
@@ -1923,6 +1971,12 @@
1923
1971
  "skips": 0,
1924
1972
  "todos": 0
1925
1973
  },
1974
+ "an-engines-output-is-decoded-as-a-stream.test.mjs": {
1975
+ "tests": 2,
1976
+ "asserts": 3,
1977
+ "skips": 0,
1978
+ "todos": 0
1979
+ },
1926
1980
  "an-environment-read-through-optional-chaining-is-a-read.test.mjs": {
1927
1981
  "tests": 6,
1928
1982
  "asserts": 19,
@@ -4197,6 +4251,12 @@
4197
4251
  "skips": 0,
4198
4252
  "todos": 0
4199
4253
  },
4254
+ "placements-orders-reach-placement-alone.test.mjs": {
4255
+ "tests": 2,
4256
+ "asserts": 11,
4257
+ "skips": 0,
4258
+ "todos": 0
4259
+ },
4200
4260
  "plain-words-on-what-the-reader-sees-first.test.mjs": {
4201
4261
  "tests": 20,
4202
4262
  "asserts": 50,
@@ -6009,6 +6069,12 @@
6009
6069
  "skips": 0,
6010
6070
  "todos": 0
6011
6071
  },
6072
+ "the-group-filter-is-offered-to-placement-alone.test.mjs": {
6073
+ "tests": 2,
6074
+ "asserts": 14,
6075
+ "skips": 0,
6076
+ "todos": 0
6077
+ },
6012
6078
  "the-heartbeat-stays-fresh-while-a-run-is-in-flight.test.mjs": {
6013
6079
  "tests": 4,
6014
6080
  "asserts": 8,
@@ -6099,6 +6165,12 @@
6099
6165
  "skips": 0,
6100
6166
  "todos": 0
6101
6167
  },
6168
+ "the-knockout-review-meets-the-tone-and-figure-checks.test.mjs": {
6169
+ "tests": 2,
6170
+ "asserts": 5,
6171
+ "skips": 0,
6172
+ "todos": 0
6173
+ },
6102
6174
  "the-knockout-reviewing-pass-rewrites-what-a-reader-sees-first.test.mjs": {
6103
6175
  "tests": 21,
6104
6176
  "asserts": 80,
@@ -7179,8 +7251,8 @@
7179
7251
  "todos": 0
7180
7252
  },
7181
7253
  "events.test.mjs": {
7182
- "tests": 7,
7183
- "asserts": 26,
7254
+ "tests": 9,
7255
+ "asserts": 31,
7184
7256
  "skips": 0,
7185
7257
  "todos": 0
7186
7258
  },
@@ -7346,6 +7418,12 @@
7346
7418
  "skips": 0,
7347
7419
  "todos": 0
7348
7420
  },
7421
+ "the-knockout-stage-notes-match-the-pipeline.test.mjs": {
7422
+ "tests": 2,
7423
+ "asserts": 5,
7424
+ "skips": 0,
7425
+ "todos": 0
7426
+ },
7349
7427
  "the-package-starts-the-mcp-server.test.mjs": {
7350
7428
  "tests": 6,
7351
7429
  "asserts": 22,
@@ -626,18 +626,15 @@ export const validators = {
626
626
  // fields are the sentences a reader now sees FIRST, so a banned tone or a fabricated quantity in
627
627
  // `basis` reaches the page ahead of anything in `bullets`. A check whose span stops short of the
628
628
  // most prominent prose on the card is a check that stopped working the day the card changed.
629
- const prose = [m.contextFraming, m.basis, ...(m.factors ?? []), ...(m.counterFactors ?? []), m.mitigation,
630
- ...(m.bullets ?? []), ...(m.purpleNotes ?? []), m.registerEstimate].filter(Boolean).join("\n");
629
+ const prose = markProse(m);
631
630
  // — the model may not speak for the register lane. Checked over the mark's whole prose and
632
631
  // over this chunk's summary: the live defect was in the summary, and the fields are new surface
633
632
  // for exactly the same invention.
634
633
  const claim = [prose, c.chunkSummary].filter(Boolean).join("\n").match(REGISTER_CLAIM_RE);
635
634
  if (claim)
636
635
  return { ok: false, reason: `knockout_register_claim:${m.name}: "${claim[0].trim()}" — this turn cannot see the register lane and must not describe it. The report states register coverage in code, from the run's own count sidecar; a summary that says the registers were not run, above a table of counts that were, is the contradiction this rule closes. Delete the clause` };
637
- const banned = prose.match(BANNED_TONE_RE);
638
- if (banned) return { ok: false, reason: `mark "${m.name}": banned tone "${banned[0]}" — measured tone only (the band colour carries urgency)` };
639
- const quant = prose.match(QUANT_CLAIM_RE);
640
- if (quant) return { ok: false, reason: `mark "${m.name}": quantitative claim "${quant[0].trim()}" — describe nature and reach, never counts/figures` };
636
+ const toneOrFigure = toneOrFigureRefusal(m, prose);
637
+ if (toneOrFigure) return { ok: false, reason: toneOrFigure };
641
638
  // PR-5 — a "tool was blocked / lacked permission" explanation for missing coverage is a false
642
639
  // excuse on this lane too (register work is a PENDING framing here, never an outage story). Fires
643
640
  // in the chunk validator so the corrective ladder re-asks THIS chunk with the quoted lines.
@@ -656,6 +653,12 @@ export const validators = {
656
653
  // artifact (validateMergedFindings below), which is the object every surface renders from.
657
654
  try { validateKnockoutFindings(m.findings, method === undefined ? { manifest: fw } : { manifest: fw, method }); }
658
655
  catch (e) { return { ok: false, reason: `mark "${m.name}": ${e.message}` }; }
656
+ // A NAME IS NEVER RATED ABOVE THE WORST CARD ON ITS PAGE, refused here as well as on the merged artifact.
657
+ // The merged gate's refusal fails the stage outside the corrective ladder, so the turn that could rate the
658
+ // name again never heard why, and the run went back to the register listing. Refused at the chunk, the
659
+ // same rule's own words reach this chunk's corrective turn. After the findings check, which settles bands.
660
+ const above = markRatedAboveItsCards(fw, m);
661
+ if (above) return { ok: false, reason: above };
659
662
  // URL-receipts gate: every cited URL ∈ that mark's raw payload (one function, three doors)
660
663
  const rec = knockoutReceipts(runDir, [m]);
661
664
  if (!rec.ok) return { ok: false, reason: rec.failures[0] };
@@ -724,6 +727,19 @@ export function raterCaveats(list) {
724
727
  .filter((c) => String(c ?? "").trim() && !isEngineAppendedCaveat(c));
725
728
  }
726
729
 
730
+ /** A mark's prose as a reader meets it: every field the tone, figure and permission checks sweep. PURE. */
731
+ const markProse = (m) => [m.contextFraming, m.basis, ...(m.factors ?? []), ...(m.counterFactors ?? []), m.mitigation,
732
+ ...(m.bullets ?? []), ...(m.purpleNotes ?? []), m.registerEstimate].filter(Boolean).join("\n");
733
+
734
+ /** The banned-tone or quantity refusal for a mark's prose, in the chunk gate's own words; null when clean. PURE. */
735
+ function toneOrFigureRefusal(m, prose = markProse(m)) {
736
+ const banned = prose.match(BANNED_TONE_RE);
737
+ if (banned) return `mark "${m.name}": banned tone "${banned[0]}" — measured tone only (the band colour carries urgency)`;
738
+ const quant = prose.match(QUANT_CLAIM_RE);
739
+ if (quant) return `mark "${m.name}": quantitative claim "${quant[0].trim()}" — describe nature and reach, never counts/figures`;
740
+ return null;
741
+ }
742
+
727
743
  export function validateMergedFindings(runDir, merged, plan) {
728
744
  const failures = [];
729
745
  if (merged?.schema_version !== 1) failures.push("schema_version must be 1");
@@ -774,12 +790,13 @@ export function validateMergedFindings(runDir, merged, plan) {
774
790
  // each mark's contextFraming/bullets/purpleNotes/registerEstimate. It does NOT cover findings[].name /
775
791
  // .description, which the report renders onto the page; that span is picked up (as a FLAG, never a
776
792
  // refusal) by the publish-path lint added 2026-07-31.
777
- const mergedProse = [String(merged?.batch?.executiveSummary ?? ""),
778
- ...marks.map((m) => [m.contextFraming, m.basis, ...(m.factors ?? []), ...(m.counterFactors ?? []), m.mitigation,
779
- ...(m.bullets ?? []), ...(m.purpleNotes ?? []), m.registerEstimate].filter(Boolean).join("\n")),
780
- ].join("\n");
793
+ const mergedProse = [String(merged?.batch?.executiveSummary ?? ""), ...marks.map(markProse)].join("\n");
781
794
  for (const c of permissionProseChecks({ text: mergedProse, surface: "findings", idSuffix: ":knockout", structural: true, cards: false }).filter((x) => !x.pass))
782
795
  failures.push(c.detail);
796
+ // THE CHUNK GATE'S TONE AND FIGURE CHECKS, AGAIN ON WHAT SHIPS. The review's rewrites reach the record through
797
+ // this gate alone (pipeline-knockout.mjs), so a phrase or a figure the first pass would have refused is
798
+ // refused here too, in the same words, and the rated record stands.
799
+ for (const m of marks) { const toneOrFigure = toneOrFigureRefusal(m); if (toneOrFigure) failures.push(toneOrFigure); }
783
800
  // backstop, on the MERGED artifact — the same reason the permission check has one: a resume can
784
801
  // compose from chunk files written before this rule existed, and the register sentence is the one the
785
802
  // reader meets in the executive summary. The renderer owns register coverage; nothing else states it.
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.4.1
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.4.1-beta.6
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.4.1-beta.5
4
12
 
5
13
  No changes in this release.
@@ -40,6 +40,7 @@ const child = spawn("node", [join(here, "http-server.mjs")], {
40
40
  env: { ...process.env, TRADEMARK_MCP_AUTH_DISABLED: "1", TRADEMARK_MCP_DEV: "1", TRADEMARK_MCP_HTTP_PORT: String(PORT), TRADEMARK_MCP_HTTP_HOST: "127.0.0.1", CLEAROTRON_ACCESS_FILE: grantsFile },
41
41
  stdio: ["ignore", "ignore", "pipe"],
42
42
  });
43
+ child.stderr.setEncoding("utf8");
43
44
  child.stderr.on("data", (d) => process.stderr.write(`[server] ${d}`));
44
45
 
45
46
  let failed = false;
@@ -47,6 +48,7 @@ const ok = (cond, msg) => { console.log(`${cond ? "ok " : "FAIL"} ${msg}`); if
47
48
 
48
49
  await new Promise((resolve, reject) => {
49
50
  const to = setTimeout(() => reject(new Error("http-server did not become ready in 8s")), 8000);
51
+ child.stderr.setEncoding("utf8");
50
52
  child.stderr.on("data", (d) => { if (String(d).includes("listening")) { clearTimeout(to); resolve(); } });
51
53
  child.on("exit", (c) => { clearTimeout(to); reject(new Error(`http-server exited early (${c})`)); });
52
54
  });