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.
- package/.env.example +7 -0
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +1 -0
- package/docs/architecture/05-config-governance.md +1 -1
- package/driver/CHANGELOG.md +11 -0
- package/driver/band-shape.mjs +4 -4
- package/driver/contract-vocabulary.mjs +3 -3
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +21 -2
- package/driver/coverage-ledger.mjs +7 -1
- package/driver/declination-call.mjs +33 -16
- package/driver/degraded-parts.mjs +6 -0
- package/driver/engine/mcp/clarivate-server.mjs +2 -0
- package/driver/engine/mcp/corsearch-server.mjs +2 -0
- package/driver/engine/mcp/declination-server.mjs +4 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +15 -0
- package/driver/engine/mcp/held-answers.mjs +11 -3
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +5 -2
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/envelope-settle.mjs +13 -3
- package/driver/gateway.mjs +31 -10
- package/driver/package.json +1 -1
- package/driver/parts-not-completed.mjs +52 -11
- package/driver/pipeline-knockout.mjs +5 -4
- package/driver/pipeline.mjs +436 -72
- package/driver/placement-decisions.mjs +4 -4
- package/driver/placement-form-io.mjs +21 -1
- package/driver/placement-form.mjs +25 -15
- package/driver/placement-union.mjs +53 -8
- package/driver/publish/index.mjs +7 -3
- package/driver/runner.mjs +4 -4
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-search/phase2-execution.md +3 -3
- package/driver/skills/clearance-search/synthesis-rules.md +3 -3
- package/driver/skills/placement-inquiry/SKILL.md +3 -3
- package/driver/stages.mjs +5 -5
- package/driver/suite-census.json +84 -12
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -0
- package/providers/_shared/enumerate.mjs +4 -3
- package/providers/_shared/execute-plan.mjs +24 -7
- package/providers/_shared/send-until.mjs +25 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/band-shape-probe.mjs +1 -1
- package/scripts/e2e-first-time.mjs +2 -1
- package/shared/names-in-force.mjs +1 -0
- package/shared/slices-taken-out.mjs +65 -0
- package/skills/clearotron-account/SKILL.md +1 -1
- 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
|
|
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 (
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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: "
|
|
128
|
-
+ "
|
|
129
|
-
+ "
|
|
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: "
|
|
133
|
-
+ "
|
|
134
|
-
+ "
|
|
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: "
|
|
139
|
-
+ "
|
|
140
|
-
+ "
|
|
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) =>
|
|
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,
|
|
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
|
|
166
|
-
// its
|
|
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
|
-
|
|
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
|
|
189
|
-
if (FLOOR_GROUND_REFUSED.test(ground)) { refuse(
|
|
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: "
|
|
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
|
package/driver/publish/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
?
|
|
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 {
|
|
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
|
-
|
|
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),
|
|
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,
|
|
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
|
-
-
|
|
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
|
|
159
|
-
near-identical
|
|
160
|
-
|
|
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
|
|
168
|
-
sheet 2 is written up
|
|
169
|
-
|
|
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
|
|
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
|
|
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**.
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
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
|
|
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.
|
|
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)"]),
|