clearotron 0.5.0-beta.2 → 0.5.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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/docs/architecture/05-config-governance.md +1 -1
  5. package/driver/CHANGELOG.md +11 -0
  6. package/driver/band-shape.mjs +4 -4
  7. package/driver/contract-vocabulary.mjs +3 -3
  8. package/driver/coverage-form-io.mjs +3 -1
  9. package/driver/coverage-form.mjs +21 -2
  10. package/driver/coverage-ledger.mjs +7 -1
  11. package/driver/declination-call.mjs +33 -16
  12. package/driver/degraded-parts.mjs +6 -0
  13. package/driver/engine/mcp/clarivate-server.mjs +2 -0
  14. package/driver/engine/mcp/corsearch-server.mjs +2 -0
  15. package/driver/engine/mcp/declination-server.mjs +4 -5
  16. package/driver/engine/mcp/euipo-server.mjs +2 -0
  17. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  18. package/driver/engine/mcp/gather-config.mjs +15 -0
  19. package/driver/engine/mcp/held-answers.mjs +11 -3
  20. package/driver/engine/mcp/signa-server.mjs +2 -0
  21. package/driver/engine/mcp/supplemental.mjs +5 -2
  22. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  23. package/driver/envelope-settle.mjs +13 -3
  24. package/driver/gateway.mjs +31 -10
  25. package/driver/package.json +1 -1
  26. package/driver/parts-not-completed.mjs +52 -11
  27. package/driver/pipeline-knockout.mjs +5 -4
  28. package/driver/pipeline.mjs +436 -72
  29. package/driver/placement-decisions.mjs +4 -4
  30. package/driver/placement-form-io.mjs +21 -1
  31. package/driver/placement-form.mjs +25 -15
  32. package/driver/placement-union.mjs +53 -8
  33. package/driver/publish/index.mjs +7 -3
  34. package/driver/runner.mjs +4 -4
  35. package/driver/skills/clearance-register/digest.md +1 -1
  36. package/driver/skills/clearance-search/phase2-execution.md +3 -3
  37. package/driver/skills/clearance-search/synthesis-rules.md +3 -3
  38. package/driver/skills/placement-inquiry/SKILL.md +3 -3
  39. package/driver/stages.mjs +5 -5
  40. package/driver/suite-census.json +84 -12
  41. package/driver/verify.mjs +6 -6
  42. package/mcp-server/CHANGELOG.md +4 -0
  43. package/mcp-server/package.json +1 -1
  44. package/package.json +1 -1
  45. package/portal-ui/package.json +1 -1
  46. package/providers/_shared/README.md +1 -0
  47. package/providers/_shared/enumerate.mjs +4 -3
  48. package/providers/_shared/execute-plan.mjs +24 -7
  49. package/providers/_shared/send-until.mjs +25 -0
  50. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  51. package/providers/oauth-mcp-bridge/package.json +1 -1
  52. package/scripts/band-shape-probe.mjs +1 -1
  53. package/scripts/e2e-first-time.mjs +2 -1
  54. package/shared/names-in-force.mjs +1 -0
  55. package/shared/slices-taken-out.mjs +65 -0
  56. package/skills/clearotron-account/SKILL.md +1 -1
  57. package/skills/clearotron-ops/SKILL.md +1 -1
@@ -13,7 +13,7 @@
13
13
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
14
  import { dirname } from "node:path";
15
15
  import { createHash } from "node:crypto";
16
- import { buildSelectionIndex, setAsideGrounds, isSetAsideGroupRow, lineKey, groupName, servedLines } from "./placement-form.mjs";
16
+ import { buildSelectionIndex, setAsideGrounds, isSetAsideGroupRow, lineKey, groupName, servedLines, FLOOR_GROUND_ON } from "./placement-form.mjs";
17
17
  import { unionPlacementForm } from "./placement-union.mjs";
18
18
  import { readPlacementForm, readPlacementFormInput } from "./placement-form-io.mjs";
19
19
  import { parsePlacementsJson } from "./placement-model.mjs";
@@ -78,7 +78,7 @@ export function placementSetAsideGrounds(P) { // @internal — driven by its t
78
78
 
79
79
  /**
80
80
  * THE FLOORS PLACEMENT SET ASIDE, AS THE REVIEW IS TOLD THEM: one line per standing decision on the shape now on disk,
81
- * with placement's ground once. A set-aside by name, on its status or its territory, covers one floor and names it. A
81
+ * with placement's ground once. A set-aside by name, on its status, territory, goods or mark, covers one floor and names it. A
82
82
  * crowd's row names its ground once and then every floor it covers, by record: the ground is the group's, so it was
83
83
  * never the floors' to repeat, and on a test run of a crowded matter repeating it on each of 1,177 floors made the
84
84
  * review's opening message 866 KB of its 899. Only decisions that stand on the current shape are listed
@@ -99,7 +99,7 @@ export function placementFloorDecisionLines(P) { // @internal — driven by it
99
99
  const key = `${g.group}\u0000${g.ground}`;
100
100
  if (!rows.has(key)) { rows.set(key, { group: g.group, ground: g.ground, ids: [] }); out.push(rows.get(key)); }
101
101
  rows.get(key).ids.push(f.record_id);
102
- } else if (g.on === "status" || g.on === "territory") {
102
+ } else if (FLOOR_GROUND_ON.includes(g.on)) {
103
103
  out.push(`- ${f.mark_text ?? "(unnamed)"}${f.owner_name ? ` — ${f.owner_name}` : ""} (${f.record_id}): set aside on its ${g.on}: ${g.ground}`);
104
104
  }
105
105
  }
@@ -130,7 +130,7 @@ export function standingPlacementDecisions(P) { // @internal — driven by its
130
130
  covered_lines_hash: createHash("sha256").update((e.covered_lines ?? []).join("\n")).digest("hex").slice(0, 16) }];
131
131
  const c = index.resolve(e.set_aside);
132
132
  return [{ kind: "name", record: e.set_aside, mark: c?.mark ?? null, owner: c?.owner ?? null, ground: e.ground,
133
- ...(e.on === "status" || e.on === "territory" ? { on: e.on } : {}) }];
133
+ ...(FLOOR_GROUND_ON.includes(e.on) ? { on: e.on } : {}) }];
134
134
  });
135
135
  } catch { return []; }
136
136
  }
@@ -25,6 +25,7 @@ import { join, dirname, basename } from "node:path";
25
25
  import { driverDir } from "../shared/driver-dir.mjs"; //
26
26
  import { PLACEMENT_FORM_NAME, placementFormSidecarName, parsePlacementForm, renderPlacementsJson } from "./placement-form.mjs";
27
27
  import { parsePlacementsJson } from "./placement-model.mjs";
28
+ import { normalizeRecordUri } from "./registry-fidelity.mjs";
28
29
  import { atomicWrite } from "./progress.mjs";
29
30
 
30
31
  export { PLACEMENT_FORM_NAME };
@@ -54,13 +55,32 @@ export function placementFormStamp(runDir) {
54
55
  export function readPlacementFormInput(runDir) {
55
56
  const shape = readJson(driverDir(runDir, "band-shape.json"));
56
57
  const pos = readJson(driverDir(runDir, "register-positions.json"));
57
- return {
58
+ const scope = readJson(driverDir(runDir, "instructed-scope.json"));
59
+ const input = {
58
60
  floors: Array.isArray(shape?.floors?.in_class_identical_or_near) ? shape.floors.in_class_identical_or_near : null,
59
61
  positions: Array.isArray(pos?.positions) ? pos.positions : null,
60
62
  // The shape's groups, which a group row names and is checked against; null on a shape that has none.
61
63
  groups: shape?.groups && typeof shape.groups === "object" ? shape.groups : null,
62
64
  served: readServed(runDir),
65
+ // The applied-for marks, written by the job before any model ran: a ground on a floor's mark is refused on the
66
+ // client's own mark. A scope that names one mark as a string names it all the same.
67
+ marks: Array.isArray(scope?.marks) ? scope.marks : (typeof scope?.marks === "string" && scope.marks ? [scope.marks] : []),
63
68
  };
69
+ // What each record's goods rest on (goods-evidence.mjs), which a ground on a floor's goods is checked against. Read
70
+ // only when a ground needs it: the record runs to megabytes and the form is judged on every pass. null where the
71
+ // run wrote none, which establishes no goods.
72
+ let evidence;
73
+ Object.defineProperty(input, "evidence", { enumerable: false, get: () => (evidence === undefined ? (evidence = readGoodsStates(runDir)) : evidence) });
74
+ return input;
75
+ }
76
+
77
+ /** canonical uri → the state goods-evidence.mjs recorded for that record's goods; null with no record. */
78
+ export function readGoodsStates(runDir) {
79
+ const doc = readJson(driverDir(runDir, "goods-evidence.json"));
80
+ if (!Array.isArray(doc?.records)) return null;
81
+ const out = new Map();
82
+ for (const r of doc.records) { const k = normalizeRecordUri(r?.uri); if (k && typeof r?.state === "string") out.set(k, r.state); }
83
+ return out;
64
84
  }
65
85
 
66
86
  /**
@@ -112,11 +112,15 @@ export const SELECT_ROW_CONTRACT = Object.freeze({
112
112
  // (ruled 2026-10-12). It is not a candidate, it renders into no placement, and nothing bounds how many there are.
113
113
  export const SET_ASIDE_ROW_FIELDS = ["set_aside", "ground", "on"];
114
114
  /**
115
- * What a ground setting a floor aside by name may be about (ruled 2026-10-12): its status or its territory.
116
- * Never its goods, which decide whether it sits in (a) or (b), and in a crowd (b)'s row is the goods ground;
117
- * never its owner's customers or field, whatever `on` says, so the form reads the ground's words too.
115
+ * What a ground setting a floor aside by name may be about: its status, its territory, its registered goods or a
116
+ * reasoned comparison of its mark with the client's (ruled 2026-10-27, superseding the status-or-territory rule of
117
+ * 2026-10-12). Never its owner's customers or field. The union checks each against the run's own evidence: a goods
118
+ * ground needs the line read and every floor's goods held complete, and a mark ground is refused on the client's
119
+ * own mark. The words of a ground on status or territory are read too, for customers, field and goods.
118
120
  */
119
- export const FLOOR_GROUND_ON = Object.freeze(["status", "territory"]);
121
+ export const FLOOR_GROUND_ON = Object.freeze(["status", "territory", "goods", "mark"]);
122
+ /** The grounds whose words the form reads for customers, field and goods: the two that are about neither. */
123
+ export const FLOOR_GROUND_WORDS_READ = Object.freeze(["status", "territory"]);
120
124
  export const FLOOR_GROUND_REFUSED = /\b(?:customers?|consumers?|clients|clientele|users?|audiences?|buyers?|purchasers?|patients?|professionals?|channels?|markets?|marketplaces?|sectors?|fields?|industr(?:y|ies|ial)|business(?:es)?|activit(?:y|ies)|commerce|commercial|trad(?:e|es|ing)(?!\s*-?\s*marks?)|distribution|wholesale|retail(?:ers?)?|sells?|sold|selling|b2b|b2c|off[- ]field|goods|products?|services?(?!\s*-?\s*marks?)|merchandise)\b/i;
121
125
  /** The lists a group row may name: the shape's groups, the rest, and the lines the goods test could not place. */
122
126
  export const ROW_GROUPS = Object.freeze([...BAND_GROUPS, "no_goods"]);
@@ -124,24 +128,30 @@ export const SET_ASIDE_ROW_CONTRACT = Object.freeze({
124
128
  when: "a line the shape lists, one mark of one owner, whose records you do not carry. One row gives that "
125
129
  + "line its ground.",
126
130
  fields: SET_ASIDE_ROW_FIELDS,
127
- set_aside: "one record id of that line, from the band. The driver finds the line on the register's own "
128
- + "projection; the ground covers every record of that mark of that owner the form does not place, and "
129
- + "never another mark of the owner, which needs its own fate. A record that names no owner stands for its "
130
- + "own position only.",
131
+ set_aside: "One record id of the line, from the band, or a list of record ids, one per line, that share one ground. "
132
+ + "The ground covers every unplaced record of that mark of that owner. Another mark of the same owner needs its "
133
+ + "own fate. A record that names no owner stands for its own position only.",
131
134
  ground: "why this line's records are not carried, in a lawyer's words.",
132
- on: "where the line holds a floor, a live identical or near-identical mark in an ordered class: what the "
133
- + "ground is about, status or territory. A floor is never set aside on its goods, which decide its group, "
134
- + "nor on its owner, its customers or its field; a ground in those terms is refused.",
135
+ on: "Required when the line holds a floor (a live identical or near-identical mark in an ordered class): one of "
136
+ + "status, territory, goods, mark. Use goods when the registered goods, read in full, do not meet the client's "
137
+ + "particular goods. Use mark when a reasoned comparison with the client's mark shows the two do not conflict. "
138
+ + "The owner, its customers and its field are never a ground.",
135
139
  not_a_candidate: "It places nothing. A record of the same line you do place stays placed.",
136
140
  latest: "A later row for the same line replaces the earlier ground. To remove one, hand back "
137
141
  + "{\"retract\":\"<its set_aside id>\"}.",
138
- unresolved: "A `set_aside` the band does not hold, one with no ground, or a floor's row whose `on` is not "
139
- + "status or territory or whose ground speaks of customers, channels, markets or field, is reported back "
140
- + "to you by id.",
142
+ unresolved: "The driver says a row back by id when: its record is not in the band; it has no ground; a floor's "
143
+ + "`on` is missing or not one of the four; a status or territory ground speaks of goods, customers, channels, "
144
+ + "markets or field; a goods ground rests on a line not read, or on goods the run does not hold in full; a mark "
145
+ + "ground names the client's own mark, or the run holds no applied-for mark to compare with. In a list, each "
146
+ + "line is judged on its own.",
141
147
  });
142
148
 
149
+ /** The record ids a set-aside row names: one, or the list of lines one ground covers. PURE. */
150
+ export const setAsideMembers = (row) => (Array.isArray(row?.set_aside) ? row.set_aside : [row?.set_aside])
151
+ .filter((id) => typeof id === "string" && id.trim() !== "").map((id) => id.trim());
152
+
143
153
  /** Is this submitted row an owner's set-aside? PURE. */
144
- export const isSetAsideRow = (row) => typeof row?.set_aside === "string" && row.set_aside.trim() !== "";
154
+ export const isSetAsideRow = (row) => setAsideMembers(row).length > 0;
145
155
 
146
156
  // ONE ROW PER GROUP. The band shape lists the band in the lawyer's order, one line per mark of an owner,
147
157
  // the rest last. A listed line gets a fate, not a row: it is placed, or set aside by name, or covered
@@ -27,9 +27,11 @@ import { buildSelectionIndex, selectionOf, rowIsSettled, seatFields, formRowKey,
27
27
  SEAT_ROW_CONTRACT, SELECT_ROW_CONTRACT, PLACEMENT_FORM_PROVENANCE,
28
28
  SET_ASIDE_ROW_CONTRACT, isSetAsideRow,
29
29
  SET_ASIDE_GROUP_CONTRACT, isSetAsideGroupRow, groupName, groupOfKeys, namedKey, lineKey, servedLines,
30
- groupRowCoversFloors, groupRowCoversFloorLine, FLOOR_GROUND_ON, FLOOR_GROUND_REFUSED, ROW_GROUPS, renderPlacementsJson, setAsideGrounds } from "./placement-form.mjs";
30
+ groupRowCoversFloors, groupRowCoversFloorLine, FLOOR_GROUND_ON, FLOOR_GROUND_WORDS_READ, FLOOR_GROUND_REFUSED, ROW_GROUPS, renderPlacementsJson,
31
+ setAsideGrounds, setAsideMembers } from "./placement-form.mjs";
31
32
  import { groupPageCall } from "./band-shape.mjs";
32
33
  import { normalizeRecordUri } from "./registry-fidelity.mjs";
34
+ import { formKey } from "../providers/_shared/script-form.mjs"; // — the one fold for "is this the same form of the mark"
33
35
  import { reconcileFloorDuty } from "./floor-duty.mjs";
34
36
 
35
37
  /** Why a `select` the fold does not hold is refused, said back with it. */
@@ -162,8 +164,8 @@ export function unionPlacementForm(prior, submitted, input, { covered = false }
162
164
  // placements.json. Prior first, so a later row for the same owner replaces the earlier ground, and a
163
165
  // retraction names the record id the row set aside. A row the band cannot resolve, or one with no
164
166
  // ground, is reported back by id, never dropped in silence.
165
- // A ground by name covers the named line only. On a line holding a floor it is about the mark, its goods,
166
- // its status or its territory, and says which in `on`; a ground about the owner never sets a floor aside.
167
+ // A ground by name covers the named line only. On a line holding a floor it is about its status, its territory,
168
+ // its registered goods or its mark, and says which in `on`; a ground about the owner never sets a floor aside.
167
169
  const groups = input?.groups ?? null;
168
170
  const floorUris = new Set((Array.isArray(input?.floors) ? input.floors : []).map((f) => normalizeRecordUri(f?.record_id) || String(f?.record_id ?? "")).filter(Boolean));
169
171
  const holdsFloor = (canonical) => (canonical?.records ?? []).some((u) => floorUris.has(normalizeRecordUri(u) || String(u ?? "")));
@@ -174,10 +176,52 @@ export function unionPlacementForm(prior, submitted, input, { covered = false }
174
176
  ...["a", "b", "c", "d", "no_goods"].flatMap((g) => (Array.isArray(groups?.[g]?.list) ? groups[g].list : []).filter((l) => l?.floors > 0).map((l) => l.key)),
175
177
  ]);
176
178
  const lineHoldsFloor = (canonical) => holdsFloor(canonical) || floorLines.has(namedKey(canonical));
179
+ // THE FLOORS OF EACH LINE, which a ground on a floor's goods or mark is checked against record by record.
180
+ const floorsOfLine = new Map();
181
+ for (const f of (Array.isArray(input?.floors) ? input.floors : [])) {
182
+ const u = normalizeRecordUri(f?.record_id) || String(f?.record_id ?? "");
183
+ const k = u ? namedKey(index.resolve(u)) : null;
184
+ if (k) floorsOfLine.set(k, [...(floorsOfLine.get(k) ?? []), { uri: u, mark: f?.mark_text ?? null }]);
185
+ }
186
+ // The records a ground on a line's goods or mark must hold for: the line's floors and the records of the position
187
+ // the row names, so a line the projection keys apart from its floors is still checked on the records it names.
188
+ const checkedRecords = (canonical) => {
189
+ const byUri = new Map((floorsOfLine.get(namedKey(canonical)) ?? []).map((f) => [f.uri, f]));
190
+ for (const u of canonical?.records ?? []) { const k = normalizeRecordUri(u) || String(u ?? ""); if (k && !byUri.has(k)) byUri.set(k, { uri: k, mark: canonical?.mark ?? null }); }
191
+ return [...byUri.values()];
192
+ };
193
+ const servedSet = servedLines(groups, input?.served, index);
194
+ const clientMarks = new Set((Array.isArray(input?.marks) ? input.marks : []).map((m) => formKey(String(m ?? ""))).filter(Boolean));
195
+ // WHY A GROUND ON A FLOOR'S GOODS OR MARK DOES NOT HOLD, or "" where it does (ruled 2026-10-27). Identical spelling
196
+ // and a shared class no longer bar the judgment; what bars it is the evidence. A goods ground stands on a line the
197
+ // step read and on goods the run holds complete for every floor of it: cut, unknown or missing goods establish
198
+ // nothing. A mark ground is a comparison with the client's mark, so it cannot set aside the client's mark itself.
199
+ const evidenceProblem = (on, canonical) => {
200
+ const records = checkedRecords(canonical);
201
+ if (on === "goods") {
202
+ if (!servedSet.has(namedKey(canonical)))
203
+ return "this line has not been read. Read it in band_shape or its group's page, or open its record with band_record, before you set it aside on its goods";
204
+ const states = input?.evidence instanceof Map ? input.evidence : null;
205
+ if (!states) return "the run has no record of this line's goods, so a goods ground cannot be checked. Place the line, or set it aside on its status, territory or mark";
206
+ const short = records.filter((r) => states.get(r.uri) !== "complete");
207
+ if (short.length)
208
+ return `the run does not hold the goods of ${short.slice(0, 3).map((r) => `${r.uri} (${states.get(r.uri) ?? "not recorded"})`).join(", ")}${short.length > 3 ? `, and ${short.length - 3} more` : ""} in full, so a goods ground cannot stand. Place the line, or set it aside on its status, territory or mark`;
209
+ }
210
+ if (on === "mark") {
211
+ if (!clientMarks.size) return "the run has no applied-for mark to compare with. Place the line, or set it aside on its status, territory or goods";
212
+ const own = records.find((r) => clientMarks.has(formKey(String(r.mark ?? ""))));
213
+ if (own) return `${own.uri} carries the client's own mark, so a mark ground cannot set it aside. Place the line, or set it aside on its status, territory or goods`;
214
+ }
215
+ return "";
216
+ };
177
217
  const setAside = new Map();
178
218
  const setAsideRefused = [];
179
219
  const takeSetAside = (row) => {
180
- const id = String(row.set_aside).trim();
220
+ // A LIST UNDER ONE GROUND is taken line by line: each member stands or is reported back on its own evidence, so a
221
+ // shared ground covers only the lines it holds for, and the exceptions are said back by id.
222
+ const members = setAsideMembers(row);
223
+ if (members.length > 1) { for (const m of members) takeSetAside({ ...row, set_aside: m }); return; }
224
+ const id = members[0];
181
225
  const ground = typeof row.ground === "string" ? row.ground.trim() : "";
182
226
  const canonical = index.resolve(id);
183
227
  const refuse = (why) => { if (!setAsideRefused.some((x) => x.set_aside === id && x.why === why)) setAsideRefused.push({ set_aside: id, why }); };
@@ -185,8 +229,10 @@ export function unionPlacementForm(prior, submitted, input, { covered = false }
185
229
  if (!ground) { refuse("no ground"); return; }
186
230
  const on = typeof row.on === "string" ? row.on.trim().toLowerCase() : "";
187
231
  if (lineHoldsFloor(canonical)) {
188
- if (!FLOOR_GROUND_ON.includes(on)) { refuse(`a floor is set aside by name only on its status or its territory: give "on" as one of ${FLOOR_GROUND_ON.join(", ")}`); return; }
189
- if (FLOOR_GROUND_REFUSED.test(ground)) { refuse("a floor is never set aside on its owner's customers, channels, markets or field: write a ground about its status or its territory"); return; }
232
+ if (!FLOOR_GROUND_ON.includes(on)) { refuse(`a floor's set-aside needs "on": one of ${FLOOR_GROUND_ON.join(", ")}`); return; }
233
+ if (FLOOR_GROUND_WORDS_READ.includes(on) && FLOOR_GROUND_REFUSED.test(ground)) { refuse(`a ${on} ground is about the ${on} only, and this one speaks of goods, customers, channels, markets or field. Rewrite it about the ${on}, or send a ground about the registered goods with "on": "goods"`); return; }
234
+ const problem = evidenceProblem(on, canonical);
235
+ if (problem) { refuse(problem); return; }
190
236
  }
191
237
  const key = namedKey(canonical);
192
238
  setAside.delete(key);
@@ -217,7 +263,6 @@ export function unionPlacementForm(prior, submitted, input, { covered = false }
217
263
  // does not list, a count that is not the shape's, or a row with no ground is reported back, never dropped.
218
264
  const setAsideGroups = new Map();
219
265
  const groupRefused = [];
220
- const servedSet = servedLines(groups, input?.served, index);
221
266
  const takeGroup = (row) => {
222
267
  const written = String(row.set_aside_group).trim();
223
268
  const refuse = (why) => { if (!groupRefused.some((x) => x.set_aside_group === written && x.why === why)) groupRefused.push({ set_aside_group: written, why }); };
@@ -289,7 +334,7 @@ export function unionPlacementForm(prior, submitted, input, { covered = false }
289
334
  // AN IDENTICAL MARK LIVE IN AN ORDERED OR FRAME-ADDED CLASS IS NEVER OUT-OF-SCOPE-FILTERED. The tier is
290
335
  // refused, said back, and the row left open, so the floor duty still owes it a fate.
291
336
  if (row.tier === "out-of-scope-filtered" && holdsFloor(row)) {
292
- tierRefused.push({ select: row.select ?? null, why: "an identical mark live in an ordered or frame-added class is never out-of-scope-filtered: place it, sheet-2 at the least, or set it aside by name on its status or its territory" });
337
+ tierRefused.push({ select: row.select ?? null, why: "a floor never takes out-of-scope-filtered. Place it at headline-candidate or sheet-2, or set its line aside by name" });
293
338
  row.tier = null;
294
339
  }
295
340
  // JUDGED ON THE MERGED ROW, not on either input — a tier written on one pass and its reason on the
@@ -1823,15 +1823,19 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
1823
1823
  // doc-52 — the cover banner speaks the SAME plain "Only you can close these" wording as the report, never
1824
1824
  // the engine clamp reasons (opts.conditions = verdict.json reasons). Machinery-only clamps (no client
1825
1825
  // item authored) degrade to one generic plain line — never raw engine jargon, never truncated mid-sentence.
1826
- const emailConditions = opts?.verdict === 'CONDITIONAL' ? actYouConditions(parseActionBuckets(secs['Actions']).you) : [];
1826
+ // A BLOCKING RESULT KEEPS THE REPORT'S ITEMS TOO (ruled 2026-10-27). The box showed only on a CONDITIONAL result, so
1827
+ // a blocking one lost the client's own actions the report lists. It shows them under the report's heading in the
1828
+ // report's words, and only where the report names any: no empty box, no fallback line.
1829
+ const emailConditions = opts?.verdict === 'CONDITIONAL' || opts?.verdict === 'BLOCKING' ? actYouConditions(parseActionBuckets(secs['Actions']).you) : [];
1827
1830
  // THE REPORT IS THE MASTER (ruled 2026-09-22). The banner used to print a second summary composed
1828
1831
  // beside the report — "On hold — …" on a run the report called conditional, although nothing is ever
1829
1832
  // held — and before that the delivery gate's word. It now quotes the report: the rating, then the
1830
1833
  // report's own conclusion (below), and where the report names what only the client can close, those
1831
1834
  // items under the report's own heading. Nothing here composes a sentence of its own.
1835
+ const closeBox = (items) => `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b>Only you can close these</b><br>${items.map((r) => `• ${cell(String(r))}`).join('<br>')}</p>`;
1832
1836
  const verdictBound = opts?.verdict === 'CONDITIONAL'
1833
- ? `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b>Only you can close these</b><br>${(emailConditions.length ? emailConditions : ['the open items set out in the report, before relying on a clean result']).map((r) => `• ${cell(String(r))}`).join('<br>')}</p>`
1834
- : '';
1837
+ ? closeBox(emailConditions.length ? emailConditions : ['the open items set out in the report, before relying on a clean result'])
1838
+ : opts?.verdict === 'BLOCKING' && emailConditions.length ? closeBox(emailConditions) : '';
1835
1839
 
1836
1840
  const reviewHeadline = `<div style="${FONT};font-size:11pt;color:#1a1a2e;margin:0 0 14px">`
1837
1841
  + flagBanner
package/driver/runner.mjs CHANGED
@@ -43,7 +43,7 @@ import { checkResolvedProduct } from "./scope-rules.mjs"; // every resolved-pr
43
43
  import { freeSlots } from "./slot-lock.mjs"; // admission arithmetic — see drainQueue
44
44
  import { orderedQueueFiles } from "./queue-order.mjs"; // the order jobs are admitted in (shared with the portal)
45
45
  import { isCancelled } from "./cancel.mjs"; // a stopped run must not come back through any resume path
46
- import { AGENT_WHATSAPP } from "./stages.mjs";
46
+ import { whatsappRouting } from "./stages.mjs";
47
47
  import { writeOutboxPacket } from "./outbox.mjs";
48
48
  import { writeRunStatus, terminalRunState } from "./progress.mjs"; // A5 — grace-exit park + reclaim-bound terminal go through the guarded status writer; terminalRunState is the run record's own terminal vocabulary (2155)
49
49
  import { identitySeed } from "./progress.mjs"; // — the same stamp a run seeds, for the process that runs them
@@ -702,7 +702,7 @@ function preRunFailNotify(agentId, base, job, reason) {
702
702
  jobId: job?.id ?? null, msgId: job?.msgId ?? null,
703
703
  forwarder: job?.forwarder ?? null, forwarderEmail: job?.forwarderEmail ?? null,
704
704
  markName: (job?.markName ?? job?.name ?? job?.marks?.[0]?.name) || null,
705
- whatsappTo: AGENT_WHATSAPP[agentId] ?? null,
705
+ ...whatsappRouting(job, agentId), // to whoever asked, as every other notice (the-notice-goes-to-whoever-asked)
706
706
  reason: terminalReasonFields(reason).reason, text,
707
707
  });
708
708
  note(`[runner] pre-run failure ${p ? `packet \u2192 ${p}` : "packet write FAILED"} for ${base}`);
@@ -739,7 +739,7 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
739
739
  // canonical runId form (charter P1 §3): `<slug>-<date>-<codename>` — owned.dir's leaf IS
740
740
  // "<date>-<codename>", so the dated id is derivable even here (matches status.runId + rescan).
741
741
  runId: `${slug}-${basename(owned.dir)}`, agent: agentId, job, failedStage: "pre-run",
742
- shortReason, reasonVerbatim: reason.slice(0, 1000), whatsappTo: AGENT_WHATSAPP[agentId] ?? null,
742
+ shortReason, reasonVerbatim: reason.slice(0, 1000),
743
743
  });
744
744
  // same per-send invariant as the pipeline's failure path: a fresh notice supersedes an older .sent
745
745
  // and its per-channel receipts (B4 — stale receipts would make clearotron-deliver skip this notice)
@@ -2089,7 +2089,7 @@ function reclaimOrphanedClaims(qdir, agentId) {
2089
2089
  // comment above calls out for NAMING THE ARTIFACT DIRECTORY at its end: the status field
2090
2090
  // was repaired by and the notice a human actually reads was left cutting it blind.
2091
2091
  shortReason: rt.reason, reasonVerbatim: reasonText, terminalKind: "reclaim-exhausted",
2092
- priorAttempts: priorReclaims, whatsappTo: AGENT_WHATSAPP[agentId] ?? null,
2092
+ priorAttempts: priorReclaims,
2093
2093
  });
2094
2094
  try { rmSync(join(owned.dir, ".sent"), { force: true }); } catch { /* none */ }
2095
2095
  try { rmSync(driverDir(owned.dir, "send-receipts.json"), { force: true }); } catch { /* none */ }
@@ -396,7 +396,7 @@ Every judgment below spans the band and/or needs data a single unit cannot have,
396
396
  **Consume `placement-recommendations.md` (from Touchpoint 2 `placement-inquiry`).** The per-candidate tiers ALSO arrive structured in `placements.json` beside the md — `{mark, owner, jurisdiction, records[], tier, reason}` per candidate (plus an optional `borderline: true` — see below), `reason` carrying placement's stated reasoning. When present, the JSON is the authoritative tier record: adopt or override each tier BY ENGAGING ITS `reason` (an override quotes the reason it contradicts — never a silent re-tier; a kept tier may tighten a label while reusing the reason). The md remains the home of the rulings tail (Disagreements, Coverage rulings, Open questions), which you carry forward exactly as below. On a corrective/repair pass do NOT re-read the whole md — the tiers are in the JSON, and the driver hands you the md's rulings tail verbatim in the dispatch (a `PLACEMENT RULINGS TAIL` block): adjudicate against it there, adopting or counter-reasoning each ruling. For each candidate `placement-inquiry` placed:
397
397
  - Find the matching entry in `placements.json` (mark text + owner + jurisdiction match; fall back to the md's tier sections on runs that predate the JSON)
398
398
  - Adopt the graduated outcome (`headline-candidate` / `sheet-2` / `watchlist-annex` / `out-of-scope-filtered`) as the placement
399
- - A sheet-2 placement is adopted as it stands — the driver writes its Sheet-2 row with placement's reason — unless you override it; you send rows for the headline positions and for the placements you override.
399
+ - Keep each placement decision that its evidence supports, with its reason. Before an entry goes to the writer, decide its relevance from the evidence you hold. Carry plausible conflicts and the questions the evidence leaves open. Drop every other entry once, with a substantive reason. The length of the report, thin use, a small owner and possible revocation are never substantive reasons. On its own, a sheet-2 tier, a class match or the words “completes the record” is no reason to carry an entry. Change a placement decision only by naming the evidence or the error that changes it.
400
400
  - **Between `headline-candidate` and `sheet-2`, what you are adopting is an ANSWER, not a label.** Those two tiers are placement-inquiry's written answer to one question — *does this conflict change the advice, or only complete the record?* (a headline entry names what it changes; a sheet-2 entry states why the advice reads the same either way). Adopting means adopting that answer. **Overriding means answering the SAME question the other way, in writing**, in your `### Disagreement resolutions` row — never re-labelling the tier and never substituting a risk-tier argument for the question. An override that does not say what the conflict does (or does not do) to the advice has not engaged the placement
401
401
  - **A `"borderline": true` entry in `placements.json` is placement declaring that its own answer could be argued either way on this record.** That is the expected place for your judgment to differ, and it is a correct declaration rather than a defect — but it must be **resolved either way, explicitly**, in a `### Disagreement resolutions` row (`ADOPTED placement-inquiry — <the reading you took, and why>` / `OVERRODE — <the other reading, and why>`), never silently carried and never left as a shrug. **The declaration is internal adjudication only**: it belongs in your audit trail beside the other open verification flags, and no borderline mark, no "arguably", no "either way" derived from it goes into the findings prose the client reads — you resolve it here so the report does not have to hedge it there
402
402
  - The "Disagreements / flags surfaced to downstream" section names cases where placement-inquiry deviated from class-match — read those reasonings carefully
@@ -155,9 +155,9 @@ is not a reason to skip it, and "the rule said so" is not an answer.
155
155
  headline.)
156
156
  - **Dominant-element spine / calibration:** is each finding ranked by the *dominant element* + whole-mark confusion
157
157
  test? Is a distinguished mark (house-mark-prefixed) **over-rated**, or a
158
- bare-dominant-element-in-field conflict **under-rated or dropped**? Did any **on-point identical /
159
- near-identical-in-class** hit fall out of the deliverable (it must not — even if revocable, and regardless
160
- of filer size)? Is any escalation of a distinguished mark resting on optics/PR/partner rather than a
158
+ bare-dominant-element-in-field conflict **under-rated or dropped**? Did an **on-point identical or
159
+ near-identical mark in the class** fall out of the report? It must not, even if revocable and whatever its
160
+ owner's size. Is any escalation of a distinguished mark resting on optics/PR/partner rather than a
161
161
  named confusion theory? Is the headline risk driven by the top on-point conflicts, not a distinguished
162
162
  mark or unrelated-field noise?
163
163
  - **Coverage ledger:** is any `deferred` / `coverage-limited` row being narrated (or about to be narrated) as a
@@ -164,9 +164,9 @@ Before rating individual findings, **rank and select them by the consumer-confus
164
164
  same/related G&S. Bare-dominant-element-in-field = top; dominant-element-plus-distinctive-matter
165
165
  (a third-party house mark in front of the shared element) = lower; unrelated-G&S = excluded as a legal
166
166
  finding (commercial awareness only).
167
- 2. **Never drop an on-point identical / near-identical mark in the relevant class** — a position placement put on
168
- sheet 2 is written up where it changes the advice, and otherwise set aside with its ground — even if it looks
169
- revocable or shelved. The staff lawyer: *"until it has actually been revoked it remains a live legal
167
+ 2. **Never drop an on-point identical or near-identical mark in the relevant class, even if it looks revocable or
168
+ shelved.** A position placement put on sheet 2 is written up when it changes the advice, and otherwise declined
169
+ with its ground. The staff lawyer: *"until it has actually been revoked it remains a live legal
170
170
  risk."* Surface it, rank it high, flag the non-use vulnerability as a Stage-2 mitigation. **Non-use is
171
171
  a revocation DEFENCE, never an injunction shield (spec-48 C5): a registered right can support an
172
172
  injunction without the owner's own use (country-dependent; grace periods run ~5 years). Never reason
@@ -28,7 +28,7 @@ Inherits the orchestrator's tier when invoked inline. No `sessions_spawn` — th
28
28
  ## Inputs you receive
29
29
 
30
30
  - `matter-context.md` from `matter-frame` at `studio/clearance-search/<slug>/<date>/matter-context.md` (the strategic foundation — read it once, reference it throughout)
31
- - **The named band** — read through the **band tools** (every call lands in the run's reading audit; never slice band files with shell). `band_shape` first: the deterministic shape of the merged band — totals, mechanical similarity tiers, **THE FLOORS** (every live in-class identical/near-identical record, each marked on its line in the band — **every floor has a fate on the form: placed by record id, or set aside by name on its status or its territory (the row's `on`), never on its goods or its owner's customers or field; only a crowd's row of a thousand records or more in (b) covers a floor.** Discussing a floor in your prose is NOT accounting for it: the form is what the driver can check, and a floor with no fate on it is recorded as unanswered whatever the prose says. This is the one duty with no cap — see **Length target**, which bounds the general band and not this), census, owner concentrations, crowd descriptors, blind spots. A large shape is served in parts, each naming the next call. Then `band_lookup` / `band_record` for the records your inquiry needs. This is the funnel's real output, lifted across the firewall:
31
+ - **The named band** — read through the **band tools** (every call lands in the run's reading audit; never slice band files with shell). `band_shape` first: the deterministic shape of the merged band — totals, mechanical similarity tiers, **THE FLOORS** (every live in-class identical/near-identical record, each marked on its line in the band — **every floor gets a fate on the form: placed by record id, or set aside by name with `on` set to status, territory, goods or mark; only a crowd's row of a thousand records or more in (b) covers a floor.** Discussing a floor in your prose is NOT accounting for it: the form is what the driver can check, and a floor with no fate on it is recorded as unanswered whatever the prose says. This is the one duty with no cap — see **Length target**, which bounds the general band and not this), census, owner concentrations, crowd descriptors, blind spots. A large shape is served in parts, each naming the next call. Then `band_lookup` / `band_record` for the records your inquiry needs. This is the funnel's real output, lifted across the firewall:
32
32
  - **enumerated records** — every named record the funnel paged to completion, each carrying `record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date, jurisdictions, screen_verdict` (+ `_query` provenance). Nothing was pre-sampled, so your gate is the *only* gate. Fifty? Read them all. A few hundred? Sort by the client's goods and markets, read the near ones first, list the rest. A thousand or more? The field is crowded: read the identical and live ones for the client's goods, tell the client it is crowded, and do not write up the rest. Count each group the shape lists on its own, and apply this to each by its own count. Where the shape shows (a) by its first page, page every line of it with `band_lookup`'s `group` filter before its row: reading a line is not writing it up. A crowd of a thousand or more outside (a) stands on its first page read and the count. The shape lists the band in this order: (a) identical or near-identical to the mark or one of its forms, for the client's goods; (b) the same, in the ordered classes; (c) the same family as the mark, for the client's goods; (d) every other live mark in the ordered classes, for the client's goods; then the rest. The ordered classes are the instructed ones and any class the frame added. Inside each group the lines run first by the client's goods on a live record in the ordered classes, then by live records in those classes, then by goods words, then by the client's markets. In a group of a few hundred, every line is read: the near ones get the full inquiry and a row; every other line is either carried or left to the group's row with the reason, except a floor, which is placed or set aside by name.
33
33
  - **`incomplete` crowd descriptors** — a query the funnel could NOT enumerate (genuinely too large, provider cap, budget): `query, total_hits, fetched, reason`. These are NOT findings and NOT clean negatives — they are signals you must rule on (cleared / material-gap; see "Crowd-descriptor materiality" below). They ride along on matching lookups too, so a counted-only zone can never read as a clean.
34
34
  - The unit's compact prose digest (`register-units/<axis>.md`) still exists for the audit trail. **The band shape, the band's summary, is the material you judge from; open a record only where the judgment needs it.** Before you set a line aside by name on its goods, read its goods in `band_record`, unless its goods words already show the ground on goods established as complete. Missing words in cut goods, or in goods whose completeness is unknown, do not establish that the goods are unrelated.
@@ -80,7 +80,7 @@ Two things the question deliberately is **not**. It is not "does this warrant th
80
80
 
81
81
  Judge an owner's records as a set. Write the position from the record in the client's market and class, quoting its goods. Carry forward only what a lawyer would raise with the client. Write down what you set aside and why.
82
82
 
83
- In the placement form each line the shape lists, one mark of one owner, gets a fate, not a row: you place it; or you set it aside by name, in one row naming one of its records (the form's `set_aside_row_contract`); or its group's one row covers it, saying why everything in that group you read and neither placed nor named is not raised with the client (the form's `set_aside_group_contract`). The rest is one such group. A group's row is refused until every line of the group was read, in the shape or by a lookup, except in a crowd of a thousand records or more outside (a), where it stands on the first page and the count; it covers a floor only in such a crowd in (b), and only once you have read the floor's line. A ground by name covers the named line only, never another mark of its owner, and on a floor it is about the mark's status or its territory, never its goods or its owner's customers or field. Neither kind is a candidate, and the length target below does not count them.
83
+ In the placement form each line the shape lists, one mark of one owner, gets a fate, not a row: you place it; or you set it aside by name, in one row naming one of its records (the form's `set_aside_row_contract`); or its group's one row covers it, saying why everything in that group you read and neither placed nor named is not raised with the client (the form's `set_aside_group_contract`). The rest is one such group. A group's row is refused until every line of the group was read, in the shape or by a lookup, except in a crowd of a thousand records or more outside (a), where it stands on the first page and the count; it covers a floor only in such a crowd in (b), and only once you have read the floor's line. A set-aside by name covers the line it names and no other mark of that owner. A floor's line may be set aside on one of four grounds: its status, its territory, its registered goods, or a reasoned comparison of its mark with the client's mark. Read the evidence for the ground before you set the line aside. A missing product word is not evidence. One ground may name a list of lines: name only the lines its evidence supports, and give every other line its own fate. Neither kind is a candidate, and the length target below does not count them.
84
84
 
85
85
  ### Headline candidates
86
86
 
@@ -169,7 +169,7 @@ The inquiry above is primary. The patterns below describe how the inquiry's answ
169
169
 
170
170
  - Class match + customer-base overlap + active owner + live registration → typically **headline-candidate**
171
171
  - Class match + customer-base overlap + non-use vulnerable or distinguishing prefix → typically **headline-candidate** with Stage-2 mitigant flagged
172
- - Class match + no customer overlap + matter-context off-field flag → typically **out-of-scope-filtered**. An identical mark live in an ordered or added class is placed, sheet 2 at the least, unless it is set aside by name on its status or territory, or sits in a crowd of a thousand or more identical marks and its goods, read on its line, are not the client's goods, where the crowd's row stands for it; it is never out-of-scope-filtered.
172
+ - Class match + no customer overlap + matter-context off-field flag → typically **out-of-scope-filtered**. A floor is a live identical or mechanically near-identical mark in an ordered or added class. Compare each floor's mark with the client's mark, and its registered goods with the client's particular goods. Place the floor when a conflict is plausible or the evidence leaves the question open. Otherwise set it aside, with the reason and the evidence. A shared class, or a match to a searched variant, is not on its own a reason to place a floor. Missing or incomplete goods, thin use, a small owner and possible revocation are never reasons to set a floor aside. A floor never takes out-of-scope-filtered.
173
173
  - Class match + no customer overlap + pending registration OR active enforcer history → typically **watchlist-annex**
174
174
  - Class match + partial customer overlap → typically **sheet-2**
175
175
  - Phonetic / visual variant + class match + on-field → **headline-candidate** or **sheet-2** as the promotion question answers it (does the variant change the advice, or only complete the record?), never on phonetic distance as such — **and** an *active same-field brand in the core classes* is not down-placed below the headline tier on phonetic distance alone (see Reasoning posture → "Active same-field brand"); phonetic distance is a rating-step input, not a placement demotion for a live in-class competitor
package/driver/stages.mjs CHANGED
@@ -2463,7 +2463,7 @@ export const STAGES = {
2463
2463
  `PLACEMENT FORM (MANDATORY): record every placement in ${P.placementForm} — {"rows":[…]}, and DO NOT WRITE placements.json (the driver renders it from this form).`,
2464
2464
  `· A REGISTER candidate: {"select":"<one record URI it holds>","tier":"…","reason":"…"} (+ optional "borderline":true). Naming ONE record selects the whole position — a mark registered in several territories is one candidate and you tier it once. The driver fills mark, owner, records, territories and classes from the register record itself; anything you write in those fields on a selected row is ignored, so do not re-type them.`,
2465
2465
  `· A COMMON-LAW candidate the register does not hold: write the row in full — {"kind":"seat","mark","owner","jurisdiction","records":[],"tier","reason"}. An empty records list is correct there, never a gap.`,
2466
- `· EACH LINE THE SHAPE LISTS, one mark of one owner, gets a fate, not a row: place it; or set it aside by name, in a row naming one of its records with the fields the form's set_aside_row_contract gives, which covers that line's records the form does not place and never another mark of its owner (a record that names no owner stands for its own position), and sets a floor aside only on its status or its territory, said in "on", never on its goods or its owner's customers or field; or leave it to its group's row.\n· ONE ROW PER GROUP: for each of (a) to (d), and for the rest, one row with the fields the form's set_aside_group_contract gives sets aside every line in it you read and neither place nor name, with the group's count and your reason. Below a thousand records, and in (a) at any size, a row waits until you have read every line, paging (a) with band_lookup's group filter where the shape shows its first page; a crowd of a thousand or more outside (a) stands on its first page read and the count, and the lines the goods test could not place take such a row only in a crowd. A floor is placed, sheet-2 at the least, or set aside by name on its status or its territory; only a crowd's row in (b) may leave it to the row. Carry forward only what a lawyer would raise with the client. Write down what you set aside and why.`,
2466
+ `· EACH LINE THE SHAPE LISTS (one mark of one owner) gets one fate: place it, set it aside by name, or leave it to its group's row. A set-aside by name is a row naming one of the line's records, with the fields in the form's set_aside_row_contract. It covers that line's unplaced records and no other mark of the owner. A record that names no owner stands for its own position. A floor's set-aside gives its ground in "on": status, territory, goods or mark.\n· ONE ROW PER GROUP: for each of (a) to (d), and for the rest, one row with the fields the form's set_aside_group_contract gives sets aside every line in it you read and neither place nor name, with the group's count and your reason. Below a thousand records, and in (a) at any size, a row waits until you have read every line, paging (a) with band_lookup's group filter where the shape shows its first page; a crowd of a thousand or more outside (a) stands on its first page read and the count, and the lines the goods test could not place take such a row only in a crowd. Place a floor at headline-candidate or sheet-2, or set it aside by name. Only a crowd's row in (b) may cover a floor. Carry forward only what a lawyer would raise with the client. Write down what you set aside and why.`,
2467
2467
  `· tier EXACTLY one of headline-candidate / sheet-2 / watchlist-annex / out-of-scope-filtered. reason = a SHORT PARAGRAPH carrying your STATED reasoning for the tier — the candidate characterisation (what the owner actually does, the customer/channel overlap read), the decisive placement ground, and any Stage-2 mitigant flag — substantial enough that a downstream stage can quote it and argue with it; NEVER a bare label, NEVER the full 7-point inquiry trace.`,
2468
2468
  `· The form ACCUMULATES across attempts: a row you complete stays complete even if a later pass never mentions it, so a pass that only fixes two rows writes only those two. To remove a row, hand back {"retract":"<its row_id>"} — silence never removes anything.`,
2469
2469
  `· The rulings tail of the md (Band reconciliation, Disagreements / flags surfaced to downstream, Coverage rulings & open questions, Open questions for the client / reviewer) is NOT mirrored into the form — it travels verbatim as prose.`,
@@ -2628,7 +2628,7 @@ export const STAGES = {
2628
2628
  // handed to it AS DATA by the driver (runDigest appends the PLACEMENT RULINGS TAIL block), never
2629
2629
  // left to the model to remember or to fetch back.
2630
2630
  `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.`,
2631
- `A sheet-2 placement is adopted as it stands — the driver writes its Sheet-2 row with placement's reason — unless you override it; you send rows for the headline positions and for the placements you override.`,
2631
+ `Keep each placement decision that its evidence supports, with its reason. Before an entry goes to the writer, decide its relevance from the evidence you hold. Carry plausible conflicts and the questions the evidence leaves open. Drop every other entry once, with a substantive reason. The length of the report, thin use, a small owner and possible revocation are never substantive reasons. On its own, a sheet-2 tier, a class match or the words “completes the record” is no reason to carry an entry. Change a placement decision only by naming the evidence or the error that changes it. Send no row for a sheet-2 placement you keep: the driver writes it with placement's reason. Send a row for each headline position and for each placement you change.`,
2632
2632
  // judgment-relocation (2026-06-23): the COMPLETE named band — run the relevance gate over the REAL
2633
2633
  // material. PR-8: read via the band tools (shape whole + logged lookups), never by slicing the raw file.
2634
2634
  placementSetAsideBlock(placementSetAsides),
@@ -3326,11 +3326,11 @@ export const STAGES = {
3326
3326
  // grant carried and no dictation mentioned — "granted-but-never-ordered", which is the silent
3327
3327
  // capability hole in the other direction, and the one that shipped before. A seat that is never
3328
3328
  // told a tool exists does not use it, and nothing records that it did not.
3329
- `DECLINATIONS: what you do not deliver, you decline BY NAME through the \`record_declination\` tool. A position or page on your findings surface leaves this stage as a finding in ${P.findings} naming any of its records, or as one declination with its ground, and there is no third way out.`,
3329
+ `DECLINATIONS: what you do not deliver, you decline BY NAME through the \`record_declination\` tool. End each position and page on your list with a finding, or with a declination whose reason is a fact about that position or page. Exclusions made before your step are complete: keep their evidence and reasons, and do not write them again. Reopen one only when you can name the evidence against it or the error in its reasoning. Never write a finding whose only purpose is to account for a position.`,
3330
3330
  ...(Array.isArray(findingsSurface) && findingsSurface.length ? [
3331
3331
  `DECLINATIONS (MANDATORY): the register digest carried ${findingsSurface.filter((r) => r?.kind !== "page").length} position(s) onto your findings surface${findingsSurface.some((r) => r?.kind === "page") ? `, and the web notes marked ${findingsSurface.filter((r) => r?.kind === "page").length} page(s) as candidates or conflicts` : ""}. Each one leaves this stage by one of two routes and there is no third: it becomes a finding in ${P.findings}, or you decline it BY NAME with a reason and one ground for the position. A position you simply do not mention is reported as a defect of this run, named individually in its trace and shipped as an open doubt, so silence costs you more than a declination ever will.${findingsSurface.some((r) => r?.kind === "page") ? " A page you do not mention is counted as a defect of this run." : ""}`,
3332
3332
  `Decline by calling the \`record_declination\` tool. Its schema names the fields it takes and what each is for — read them there, not here. The one thing to know before you call: you cite a record or page by its POSITION in the list below — there is no field for a mark name, a uri or a page address, so anything you were not handed cannot be expressed at all. The reason vocabulary is closed: ${DECLINATION_REASON_TOKENS.join(" / ")}, each an omission synthesis-rules.md already authorises, and if your ground is none of them the rules do not let you omit the record or page. ONE OF THEM IS NOT AVAILABLE ON A LIVE RECORD IN AN INSTRUCTED CLASS: \`not-worth-the-line\` is a statement about the report's budget rather than about the mark, and a live registration inside one of the matter's own classes is never omitted for want of room — carry it, or decline it on something true of THAT MARK. The call is refused if you try, so it costs you a turn. The grounds you write are one or two lines in your OWN words on why THIS record or page does not earn a line — never machine-parsed, and what the reviewing lawyer reads. Send them in one batch where you can; a refused row never voids its neighbours, and the answer names what is still undecided so you can finish in this turn.`,
3333
- `A REFUSAL FROM THAT TOOL IS ABOUT BOOKKEEPING, NEVER ABOUT YOUR LEGAL JUDGMENT. There is exactly ONE case: declining a mark IDENTICAL to the applied-for mark, live, in one of the matter's own filed classes, on a discretionary ground — because synthesis-rules.md orders that an on-point identical mark in the relevant class is never dropped, "regardless of filer profile", so such a declination contradicts the instruction you are already following. Nothing else is refused. If that record really is the applicant's own, or already delivered under another record, say exactly that with own-right or duplicate-of-delivered and it is accepted. Whether goods are related, whether a name is off-field, whether a conflict is worth the line — those are your calls and the tool does not have an opinion about any of them.`,
3333
+ `A REFUSAL FROM THAT TOOL IS ABOUT THE RECORD'S FACTS, NEVER YOUR LEGAL JUDGMENT. Besides the room refusal above, it refuses one case: a mark IDENTICAL to the applied-for mark, live in one of the matter's filed classes, declined on its field, for want of room, or on its sign. Decline such a mark as unrelated-goods when the run holds its goods in full and they do not meet the matter's goods; say how in the ground. Decline it as own-right or duplicate-of-delivered when that is true. Whether goods are related, and whether a name is off-field, is your call.`,
3334
3334
  `The records on your findings surface, by position:`,
3335
3335
  ...findingsSurface.flatMap((r, i) => r?.kind === "page" ? [] : [` ${i}. ${r.mark ?? "(unnamed)"}${r.owner ? ` — ${r.owner}` : ""}${r.tier ? ` [${r.tier}]` : ""}${r.uri ? ` ${r.uri}` : ""}${r.goods_from ? ` — goods: ${r.goods_from}` : ""}`]), ...(findingsSurface.some((r) => r?.kind === "page") ? [`The pages the web notes marked as candidates or conflicts, continuing the same positions:`, ...findingsSurface.flatMap((r, i) => r?.kind === "page" ? [` ${i}. ${r.url ?? r.page}`] : [])] : []),
3336
3336
  ] : []),
@@ -4815,7 +4815,7 @@ export const placementSetAsideBlock = (decisions) => (Array.isArray(decisions) &
4815
4815
 
4816
4816
  export const refutationStandingLines = ({ registerOnly = false, job = {}, floorDecisions = null } = {}) => [
4817
4817
  PLACEMENT_BORDERLINE_NOTE,
4818
- `REGISTER VERIFICATION TOOLS: you hold the read-only band tools — band_shape (the deterministic shape of the complete register band, including floors marked on their lines; crowded groups may show only their first page), band_lookup (pull any record the narrative relies on or omits) and band_record (the official registry record fetched this run). Check the narrative's register assertions against them. A floor row the narrative neither rates nor reasons away is a FLAGGED CORRECTION unless a validated placement decision already accounts for it under the placement rules. Those decisions are listed below with their grounds and the records they cover. Check those grounds against the evidence; flag an unsupported decision. They are read-only and logged; you hold no live register tools (by design, never an outage).`,
4818
+ `REGISTER VERIFICATION TOOLS: you hold the read-only band tools — band_shape (the deterministic shape of the complete register band, including floors marked on their lines; crowded groups may show only their first page), band_lookup (pull any record the narrative relies on or omits) and band_record (the official registry record fetched this run). Check the narrative's register assertions against them. Each floor must be rated in the narrative, reasoned away in it, or accounted for by a decision listed below. A floor that is none of these is a FLAGGED CORRECTION. Check each listed decision's ground against the evidence. Flag a decision only when you can name the evidence against its ground or the error in its reasoning. They are read-only and logged; you hold no live register tools (by design, never an outage).`,
4819
4819
  // The validated decisions themselves, written by the driver from the placement form (pipeline.mjs
4820
4820
  // placementFloorDecisionLines); none stands where the list is empty.
4821
4821
  ...(Array.isArray(floorDecisions) && floorDecisions.length ? floorDecisions : ["(none)"]),