clearotron 0.3.2-beta.7 → 0.3.2-beta.8
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 +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +76 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +26 -26
- package/driver/contract-vocabulary.mjs +44 -10
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +24 -18
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +200 -23
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +18 -1
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +142 -64
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +27 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +15 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
|
@@ -141,6 +141,16 @@ export function renderMatterFrame(model) {
|
|
|
141
141
|
// is owed against — so the frame states it where a reader can see it rather than only in a field.
|
|
142
142
|
if ((model.ratified_forms ?? []).length > 1)
|
|
143
143
|
out.push(`- **Ratified forms:** ${model.ratified_forms.join(", ")}`);
|
|
144
|
+
// THE EXCLUSION IS EVIDENCED ON THE DOCUMENT A READER SEES, and it is stated as a PROPOSAL because
|
|
145
|
+
// that is what it is at this point in the run. A reader meeting "excluded" here would believe the
|
|
146
|
+
// search was narrowed on the frame's authority; the driver's verification has not run yet, and if it
|
|
147
|
+
// cannot confirm the client's ownership the element is searched in full and this line is the only
|
|
148
|
+
// place the question was ever raised.
|
|
149
|
+
if (model.house_element_candidate)
|
|
150
|
+
out.push(`- **Client's own element, proposed for exclusion:** ${model.house_element_candidate.element}`
|
|
151
|
+
+ ` — the analysis would be limited to ${model.house_element_candidate.remainder}.`
|
|
152
|
+
+ ` Basis: ${model.house_element_candidate.owner_basis}.`
|
|
153
|
+
+ " Excluded only if the driver confirms the client's own registrations on the register; otherwise searched in full.");
|
|
144
154
|
out.push("");
|
|
145
155
|
|
|
146
156
|
// `Search channels:` — domains only; the grid site-restricts to them and the general web is always
|
|
@@ -172,9 +182,10 @@ export function renderMatterFrame(model) {
|
|
|
172
182
|
*/
|
|
173
183
|
/** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
|
|
174
184
|
const DECLARED = Object.freeze({
|
|
175
|
-
"": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms"],
|
|
185
|
+
"": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms", "house_element_candidate"],
|
|
176
186
|
intake_asks: ["ask", "owner"],
|
|
177
187
|
identified_classes: ["class", "reason"],
|
|
188
|
+
house_element_candidate: ["element", "remainder", "owner_basis"],
|
|
178
189
|
});
|
|
179
190
|
|
|
180
191
|
/** Refuse an undeclared key by path, at depth. Shared walk; the table above is what is this tool's. */
|
|
@@ -218,6 +229,26 @@ export function frameRatifiedForms(runDir) {
|
|
|
218
229
|
return (Array.isArray(rows) ? rows : []).map((r) => String(r ?? "").trim()).filter(Boolean);
|
|
219
230
|
}
|
|
220
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The house element this run's frame PROPOSED, or null. IMPURE (reads the run's own accepted call).
|
|
234
|
+
*
|
|
235
|
+
* A PROPOSAL, AND THE CALLER MUST TREAT IT AS ONE. Nothing here has been checked against the register:
|
|
236
|
+
* the frame runs before the plan, holds no band tool, and is reporting how it reads the matter. The
|
|
237
|
+
* caller verifies ownership by owner-scoped lookup and writes its own receipt; the plan excludes on
|
|
238
|
+
* that receipt. A caller that excluded on this return would be dropping an element from the search on a
|
|
239
|
+
* model's say-so, which is the one direction that reaches a client as a clean answer over unswept
|
|
240
|
+
* ground rather than as a visible failure.
|
|
241
|
+
*
|
|
242
|
+
* Null on every archived and replayed run whose accepted call predates the field, so none of them moves.
|
|
243
|
+
*/
|
|
244
|
+
export function frameHouseElementCandidate(runDir) {
|
|
245
|
+
const h = lastAcceptedMatterFrame(runDir)?.house_element_candidate;
|
|
246
|
+
const element = String(h?.element ?? "").trim();
|
|
247
|
+
const remainder = String(h?.remainder ?? "").trim();
|
|
248
|
+
if (!element || !remainder) return null;
|
|
249
|
+
return { element, remainder, owner_basis: String(h?.owner_basis ?? "").trim() };
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
/** The last ACCEPTED call for this run, or null. */
|
|
222
253
|
export function lastAcceptedMatterFrame(runDir) {
|
|
223
254
|
return lastAccepted(matterFrameCallPaths(String(runDir ?? "")).accepted, readFileSync);
|
|
@@ -252,6 +283,15 @@ export function mergeMatterFrameCall(stored, received) {
|
|
|
252
283
|
// omission here is a repair that did not mention them, never a decision to withdraw them.
|
|
253
284
|
identified_classes: keepIfAbsent(received?.identified_classes, base.identified_classes),
|
|
254
285
|
ratified_forms: keepIfAbsent(received?.ratified_forms, base.ratified_forms),
|
|
286
|
+
// KEEP-IF-ABSENT, and the direction of its failure is the OPPOSITE of the two above — which is
|
|
287
|
+
// worth saying, because the reasoning that protects them does not transfer and a reader who assumed
|
|
288
|
+
// it did would mis-rank this key. Dropping the identified classes NARROWS the next compile, towards
|
|
289
|
+
// missing rights. Dropping this one WIDENS it: the house element goes back to being searched as a
|
|
290
|
+
// conflict axis, which is the band this field exists to shrink, so a partial call that lost it costs
|
|
291
|
+
// a slower run and the report's one sentence explaining what was excluded and why — never coverage.
|
|
292
|
+
// It is kept because a repair turn that did not mention the element is not a withdrawal of it, which
|
|
293
|
+
// is the same rule, reached by a different road.
|
|
294
|
+
house_element_candidate: keepIfAbsent(received?.house_element_candidate, base.house_element_candidate),
|
|
255
295
|
};
|
|
256
296
|
}
|
|
257
297
|
|
|
@@ -346,6 +386,54 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
|
|
|
346
386
|
ratified_forms.push(form);
|
|
347
387
|
}
|
|
348
388
|
|
|
389
|
+
// ── THE CLIENT'S OWN HOUSE ELEMENT — A CANDIDATE, NEVER A DECISION ───────────────────────────────
|
|
390
|
+
//
|
|
391
|
+
// THE DEFECT (production run, 2026-09-16). The mark was the client's own famous house mark followed by
|
|
392
|
+
// a tagline, and the plan treated the house element as a conflict axis: exact, variants, one-letter
|
|
393
|
+
// mutations, transliterations, incumbent checks. Over half the band came from that element. The
|
|
394
|
+
// reviewing lawyer's method for the same matter was three queries — the whole phrase, the shorter
|
|
395
|
+
// phrase, the last word alone — because an element the client already owns outright is not what the
|
|
396
|
+
// analysis is about. The engine planned thirty-six.
|
|
397
|
+
//
|
|
398
|
+
// WHY THE FIELD IS NAMED `candidate`, AND WHY THE NAME IS LOAD-BEARING. This frame CANNOT verify
|
|
399
|
+
// ownership: `BAND_READING_STAGES` is placement-inquiry, register-digest and synthesis, and the band
|
|
400
|
+
// does not exist yet when the frame runs. So everything here is the seat's reading of the matter, and
|
|
401
|
+
// an exclusion taken on a seat's say-so is an unsearched element justified by an assertion — a clean
|
|
402
|
+
// report over ground nobody swept, which is the one failure that reaches a client as a wrong answer
|
|
403
|
+
// rather than as no answer. The driver verifies against the register by owner and writes the receipt;
|
|
404
|
+
// the plan excludes on the RECEIPT and never on this field. Requirement 4 ("when the frame cannot
|
|
405
|
+
// verify, it does not exclude, and says so") is then the write order rather than a branch someone has
|
|
406
|
+
// to remember: no receipt, no exclusion.
|
|
407
|
+
//
|
|
408
|
+
// TYPED RATHER THAN PARSED, for the reason `identified_classes` gives and measures: a list derived
|
|
409
|
+
// from judgment prose dropped the primary entry in 19 of 21 runs.
|
|
410
|
+
let house_element_candidate = null;
|
|
411
|
+
if (params?.house_element_candidate !== undefined && params?.house_element_candidate !== null) {
|
|
412
|
+
const h = params.house_element_candidate;
|
|
413
|
+
const element = str(h?.element), remainder = str(h?.remainder), owner_basis = str(h?.owner_basis);
|
|
414
|
+
if (!element)
|
|
415
|
+
return { ok: false, reason: "matterframe_house_element_empty: name the element of the mark the client already owns, or omit the field entirely — a blank row is not an answer" };
|
|
416
|
+
if (!owner_basis)
|
|
417
|
+
return { ok: false, reason: `matterframe_house_element_basis_missing:${element} — say why you read this as the client's own registered element. It is not taken on your word (the driver verifies it against the register by owner), but the reader of the report is owed the ground, and an unverifiable basis is how a wrong exclusion would be argued for` };
|
|
418
|
+
// ── THE FLOOR, AND IT IS ON THE POPULATION RATHER THAN ON THE RULE ────────────────────────────
|
|
419
|
+
//
|
|
420
|
+
// The catastrophic direction here is naming too MUCH as the house element: mark "ACME WIDGETS",
|
|
421
|
+
// element "ACME WIDGETS", remainder nothing — and the plan becomes three queries that do not exist.
|
|
422
|
+
// Ownership can verify perfectly in that case, so requirement 4 does not catch it and no refusal
|
|
423
|
+
// downstream would either: "queries on the house element: 0" is satisfied by a plan with no queries
|
|
424
|
+
// at all. So the remainder is checked for being something a search can be built on, here, where the
|
|
425
|
+
// claim is made.
|
|
426
|
+
if (!remainder)
|
|
427
|
+
return { ok: false, reason: `matterframe_house_element_no_remainder:${element} — excluding it would leave nothing to search. The remainder is what the analysis is about; if the mark IS the client's own element with nothing distinctive after it, there is no exclusion to make and the field is omitted` };
|
|
428
|
+
if (remainder.toLowerCase() === element.toLowerCase())
|
|
429
|
+
return { ok: false, reason: `matterframe_house_element_remainder_same:${element} — the remainder must be the part of the mark that is NOT the house element` };
|
|
430
|
+
// The whole mark cannot be the house element by another spelling: an element that swallows the
|
|
431
|
+
// remainder leaves the same empty plan, arriving as two fields that merely look different.
|
|
432
|
+
if (element.toLowerCase().includes(remainder.toLowerCase()))
|
|
433
|
+
return { ok: false, reason: `matterframe_house_element_swallows_remainder:${element} — the element you named contains the remainder, so excluding it excludes the whole mark` };
|
|
434
|
+
house_element_candidate = { element, remainder, owner_basis };
|
|
435
|
+
}
|
|
436
|
+
|
|
349
437
|
const model = {
|
|
350
438
|
schema_version: SCHEMA_VERSION,
|
|
351
439
|
instructed_scope: instructedScope ?? null,
|
|
@@ -358,6 +446,7 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
|
|
|
358
446
|
intake_asks,
|
|
359
447
|
identified_classes,
|
|
360
448
|
ratified_forms,
|
|
449
|
+
house_element_candidate,
|
|
361
450
|
};
|
|
362
451
|
return { ok: true, model, content: renderMatterFrame(model) };
|
|
363
452
|
}
|
package/driver/named-band.mjs
CHANGED
|
@@ -29,6 +29,23 @@ import { abbrev } from "./repair-contract.mjs";
|
|
|
29
29
|
|
|
30
30
|
export const BAND_STATES = ["enumerated", "incomplete"];
|
|
31
31
|
|
|
32
|
+
// ── WHAT ONE QUERY MAY PUT INTO THE BAND ─────────────────────────────────────────────────────────
|
|
33
|
+
//
|
|
34
|
+
// THE DEFECT (production run, 2026-09-16). A 2,146-record band, of which 1,154 — 54% — were reachable
|
|
35
|
+
// from two queries and nothing else. Both were machine-built forms of an ordinary short word, so they
|
|
36
|
+
// matched every mark containing that word across four registers. Measured on the preserved band: the
|
|
37
|
+
// three biggest queries returned 589, 583 and 271 records; the fourth returned 185. A ceiling at 200
|
|
38
|
+
// therefore bites exactly those three and leaves the rest of that plan untouched, which is why it is
|
|
39
|
+
// the number rather than a rounder one.
|
|
40
|
+
//
|
|
41
|
+
// AN OVER-CAP QUERY IS NOT TRUNCATED, IT IS RECLASSIFIED. The band already has a word for "this query
|
|
42
|
+
// matched more than we carried": `incomplete`, which produces a crowd descriptor carrying the full
|
|
43
|
+
// count. So the excess is DISCLOSED with its number rather than dropped — judgment reads the crowd and
|
|
44
|
+
// can say the ground was too broad to enumerate, which is a true statement about the search. Silently
|
|
45
|
+
// keeping the first two hundred would be the one outcome worse than the flood: a narrower band that
|
|
46
|
+
// reads as complete.
|
|
47
|
+
export const BAND_QUERY_CAP = 200;
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Parse + lightly validate the named-band artifact. Returns { enumerated:[…records], crowds:[…descriptors] }.
|
|
34
51
|
* Throws `named_band_*` tokens (token FIRST) so the stage validator + corrective-retry can key on the defect,
|
|
@@ -95,7 +112,22 @@ export function parseNamedBand(raw) {
|
|
|
95
112
|
// at band-shape.mjs's `record_id` filter — the same loss, one step further from anything that
|
|
96
113
|
// could name it.
|
|
97
114
|
const recs = Array.isArray(b.records) ? b.records : [];
|
|
98
|
-
|
|
115
|
+
const kept = [];
|
|
116
|
+
for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) kept.push({ ...r, ...prov }); }
|
|
117
|
+
if (kept.length > BAND_QUERY_CAP) {
|
|
118
|
+
// The count the provider reported is the truth about the ground; `kept.length` is only what this
|
|
119
|
+
// block carried. Prefer the reported total and fall back to what we hold, so the descriptor never
|
|
120
|
+
// claims a smaller crowd than it can prove.
|
|
121
|
+
const total = countOrNull(b.total_hits) ?? kept.length;
|
|
122
|
+
for (const r of kept.slice(0, BAND_QUERY_CAP)) enumerated.push(r);
|
|
123
|
+
crowds.push({
|
|
124
|
+
query, total_hits: total, fetched: BAND_QUERY_CAP, sample: [],
|
|
125
|
+
reason: `one query returned ${kept.length} record(s), over the ${BAND_QUERY_CAP}-record ceiling any single query may add to this band; the first ${BAND_QUERY_CAP} are carried and the rest are disclosed here as a crowd rather than enumerated`,
|
|
126
|
+
...(typeof b.qid === "string" && b.qid ? { qid: b.qid } : {}),
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
for (const r of kept) enumerated.push(r);
|
|
130
|
+
}
|
|
99
131
|
} else {
|
|
100
132
|
// count-first rescue (2026-07-10, copper-lattice): a crowd descriptor may carry per-term truth —
|
|
101
133
|
// `term_counts` (each term's tool-derived count + disposition) and the fully-enumerated tractable
|
|
@@ -137,7 +169,7 @@ export function parseNamedBand(raw) {
|
|
|
137
169
|
// byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
|
|
138
170
|
// run: four capability-gap blocks carried `error:true, deferred:true` into this function and
|
|
139
171
|
// reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
|
|
140
|
-
// COUNT for this slice". register-plan.mjs:
|
|
172
|
+
// COUNT for this slice". register-plan.mjs:1594 validatePlanFeasibility already enforces the same rule one layer up
|
|
141
173
|
// ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
|
|
142
174
|
// RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
|
|
143
175
|
// Conditional like the four keys above, so old bands carry neither key and nothing shifts.
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.8",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
package/driver/pipeline.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
|
|
12
12
|
import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
|
|
13
|
-
import { terminalClampDecision, orderClausesForLede, clientConditions } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
13
|
+
import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
14
14
|
import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { runStage, correctionHint, gridLedgerNameFor, draftCarryEligible, toolWrittenArtifact, selectEngine } from "./gateway.mjs";
|
|
@@ -31,7 +31,7 @@ import { buildRunContext, deriveSlug, kebab } from "./phase0.mjs";
|
|
|
31
31
|
import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveTier, lines, AGENT_WHATSAPP, whatsappRouting,
|
|
32
32
|
chainEntries, stageOrdinal, stageInputs, stageOutputs, dependencyOrder, REGISTER_AXES, REGISTER_ENUMERATE_TOOL,
|
|
33
33
|
buildEscalationFollowup, buildEnvelopeCloseFollowup, buildFrameReopenFollowup,
|
|
34
|
-
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage } from "./band-size.mjs";
|
|
34
|
+
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage, derivedLimitSec, limitExceedsCeiling, ceilingRefusal } from "./band-size.mjs";
|
|
35
35
|
import { IDENTITY_FILE as REPORT_IDENTITY_FILE } from "./report-overview-record.mjs";
|
|
36
36
|
import { dispatchRows, clearedSignatures } from "./seat-attempts.mjs";
|
|
37
37
|
import { CONTEXT_DERIVATIONS, DISPATCH_EXTRAS, INLINE_CONTEXT, sandboxManifest, sandboxGaps, derivationsFor } from "./stage-context.mjs"; // — what a stage is actually handed
|
|
@@ -45,7 +45,8 @@ import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
|
|
|
45
45
|
import { parseNamedBand, mergeNamedBands, findCollapsedBands, quarantineUnknownStates, taintQuarantineCleanBlocks, bandRecords } from "./named-band.mjs";
|
|
46
46
|
import { recordOriginsFor } from "./record-origins.mjs";
|
|
47
47
|
import { REGISTER_PROVIDER } from "./driver.config.mjs";
|
|
48
|
-
import { FACTS_FILE as DIGEST_FACTS_FILE, ACCOUNTING_STAMP as DIGEST_ACCOUNTING_STAMP, recordedFindingUris
|
|
48
|
+
import { FACTS_FILE as DIGEST_FACTS_FILE, ACCOUNTING_STAMP as DIGEST_ACCOUNTING_STAMP, recordedFindingUris,
|
|
49
|
+
digestAccountingGap, digestBatchBrief, batchesOf } from "./register-digest-record.mjs"; // conversion 11 — the render's facts sidecar and the accounting era stamp
|
|
49
50
|
import { buildBandShape, dominantElementComposites, deriveRegisterPositions, floorTierByMark, floorMarkKey } from "./band-shape.mjs"; // PR-8 — the deterministic reading layer; P2-A — candidates + positions
|
|
50
51
|
import { deriveOwnerScreen, ownerScreenNegative } from "./owner-screen.mjs"; // P2-B — the owner×element screen's own receipt
|
|
51
52
|
import { reconcileRecall, parseFindingsEndings, parseCrowdRulings, readOkRecordUris,
|
|
@@ -96,8 +97,10 @@ import { publishReport, composeEmailHtml, deliverySubject } from "./publish/inde
|
|
|
96
97
|
import { parseCaseLawProfiles, joinCaseLawProfiles } from "./publish/parse.mjs";
|
|
97
98
|
import { buildAuditMd, parseSpineFindingBlocks } from "./publish/audit-from-spine.mjs";
|
|
98
99
|
import { deriveRegisterPresence } from "./publish/register-presence.mjs"; // — the audit stores every live in-scope record
|
|
99
|
-
import { lastAcceptedMatterFrame, frameIdentifiedClasses } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, which the plan compile unions in
|
|
100
|
-
import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs";
|
|
100
|
+
import { lastAcceptedMatterFrame, frameIdentifiedClasses, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, which the plan compile unions in
|
|
101
|
+
import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs";
|
|
102
|
+
import { excludeHouseElement, verifyHouseElementOwnership, resolveRegions as resolvePlanRegions, HOUSE_ELEMENT_RECEIPT } from "./register-plan.mjs"; // 647 — the client's own element leaves the conflict analysis only on a verified receipt
|
|
103
|
+
import { resolveRecordExecutor } from "./register-records.mjs"; // — the stamp the late lanes never met
|
|
101
104
|
import { slimLine, crowdLine } from "./hit-list.mjs"; // — the list the run works from; crowds ride it as a sibling array
|
|
102
105
|
import { mintCrossCheckDoubts, mintContradictionDoubts, stitchDoubts, applyClosure } from "./doubt-ledger.mjs"; // doubt-stitch + doubt-closure (2026-07-22)
|
|
103
106
|
// Conversion 6: the two line-form parsers are no longer on the live path — the seat sends typed
|
|
@@ -2258,6 +2261,69 @@ function logFullyDeferredAxes(plan, P, source) {
|
|
|
2258
2261
|
// `frozenOnly` (item): read the FROZEN plan or nothing. reconstructCtx is an introspection path —
|
|
2259
2262
|
// it must never compile and freeze a plan into an existing run as a side effect of being asked what the
|
|
2260
2263
|
// run already holds. (attachProfile / attachFramework take the same posture via `write:false`.)
|
|
2264
|
+
/**
|
|
2265
|
+
* Ask the register whether the client owns the element the frame proposed, and record the answer.
|
|
2266
|
+
*
|
|
2267
|
+
* ASYNC AND SEPARATE FROM THE COMPILE, which is why it is its own step rather than a branch inside
|
|
2268
|
+
* `attachRegisterPlan`: that function is synchronous and is called on the resume path too. The shape is
|
|
2269
|
+
* the digest's — an async driver step writes a receipt, a synchronous consumer reads it — and it buys
|
|
2270
|
+
* the property that matters here: the compile cannot accidentally exclude anything by reaching for a
|
|
2271
|
+
* promise it does not await.
|
|
2272
|
+
*
|
|
2273
|
+
* NOT A REGISTERED CONTEXT DERIVATION, and the reason is worth stating because the obvious reading is
|
|
2274
|
+
* that it should be. `stage-context.mjs` declares the derivations a sandbox can REPLAY, and its runners
|
|
2275
|
+
* are synchronous. This one asks a live register a question; there is no offline re-derivation of that,
|
|
2276
|
+
* and declaring it would promise the `--experiment` rig something no runner can deliver — which the
|
|
2277
|
+
* guard catches by name, loudly, as it should. A sandbox instead inherits the receipt with the rest of
|
|
2278
|
+
* `_driver/`, and a sandbox that has none excludes nothing, which is the same answer every other
|
|
2279
|
+
* unverified run gets.
|
|
2280
|
+
*
|
|
2281
|
+
* NEVER-KILL. Every failure writes an unverified receipt and the run plans exactly as it does today.
|
|
2282
|
+
* The element being searched in full is the safe direction and the status quo; the only thing that can
|
|
2283
|
+
* narrow a client's search is a register answer naming their own live registration.
|
|
2284
|
+
*/
|
|
2285
|
+
async function verifyAndRecordHouseElement(ctx, opts = {}) {
|
|
2286
|
+
const P = ctx.paths;
|
|
2287
|
+
try {
|
|
2288
|
+
const proposed = frameHouseElementCandidate(P.runDir);
|
|
2289
|
+
if (!proposed) return; // the ordinary case: the frame proposed nothing and there is no question
|
|
2290
|
+
|
|
2291
|
+
const classes = [...new Set([...inScopeClassList(ctx.job, ctx.profile).map(String), ...frameIdentifiedClasses(P.runDir)])];
|
|
2292
|
+
// THE CLIENT'S OWN NAMES. The profile's trading names are the curated list; the matter's customer is
|
|
2293
|
+
// the free-text one. Both, because a profile may be absent on a one-off matter and a customer string
|
|
2294
|
+
// may be a person where the filings are held by a company.
|
|
2295
|
+
const owners = [...new Set([...(ctx.profile?.selfExclusionOwners ?? []), ctx.job?.customer].map((o) => String(o ?? "").trim()).filter(Boolean))];
|
|
2296
|
+
const caps = registerCapabilities();
|
|
2297
|
+
const { regions } = resolvePlanRegions(registerJurisdictions(ctx.job, ctx.profile), caps);
|
|
2298
|
+
const rec = resolveRecordExecutor({
|
|
2299
|
+
lister: opts?.recordLister ?? null, adapter: activeProvider(),
|
|
2300
|
+
agentId: ctx.agentId ?? null, sessionKey: `prelim-${ctx.run.slug}-${ctx.run.codename}`,
|
|
2301
|
+
recordLog: runRecordLogPath(P.runDir),
|
|
2302
|
+
fixtureDir: ctx.job?.registerFixtures?.records ?? null,
|
|
2303
|
+
});
|
|
2304
|
+
// AN EXACT-NAME LISTING OF THE ELEMENT, filtered to the client's own live in-class registrations by
|
|
2305
|
+
// the verifier. The owner join is done on REAL RECORDS the register returned, which is what
|
|
2306
|
+
// "verified by owner, not asserted" means — there is no owner-predicate listing on this interface
|
|
2307
|
+
// and inventing one would be a second way to ask the same question.
|
|
2308
|
+
const lookup = typeof rec.list !== "function" ? null : async ({ element, classes: cls }) => {
|
|
2309
|
+
const r = await rec.list(element, { classes: cls, regions, limit: 50 });
|
|
2310
|
+
if (!r || r.ok === false) return { ok: false, reason: String(r?.reason ?? "the register listing did not answer") };
|
|
2311
|
+
return { ok: true, records: Array.isArray(r.records) ? r.records : [] };
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
const receipt = await verifyHouseElementOwnership({ element: proposed.element, classes, owners, lookup });
|
|
2315
|
+
receipt.remainder = proposed.remainder; // carried so the compile needs only this one file
|
|
2316
|
+
ensureDriverDir(P.runDir);
|
|
2317
|
+
atomicWrite(driverDir(P.runDir, HOUSE_ELEMENT_RECEIPT), JSON.stringify(receipt, null, 2) + "\n");
|
|
2318
|
+
runLog(P.runDir, { event: "house-element-ownership", verified: receipt.verified, reason: receipt.reason,
|
|
2319
|
+
records: receipt.records.length, owners: owners.length, executor: rec.source });
|
|
2320
|
+
if (!receipt.verified) note(`house element: ${proposed.element} stays in the search — ${receipt.reason}`);
|
|
2321
|
+
} catch (e) {
|
|
2322
|
+
// A THROW HERE MUST NOT COST A REPORT. No receipt means no exclusion, which is today's plan.
|
|
2323
|
+
runLog(P.runDir, { event: "house-element-ownership-failed", reason: String(e?.message ?? e).slice(0, 160) });
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2261
2327
|
function attachRegisterPlan(ctx, { frozenOnly = false } = {}) {
|
|
2262
2328
|
const P = ctx.paths;
|
|
2263
2329
|
if (frozenOnly) {
|
|
@@ -2281,7 +2347,30 @@ function attachRegisterPlan(ctx, { frozenOnly = false } = {}) {
|
|
|
2281
2347
|
ctx.registerPlan = null;
|
|
2282
2348
|
let compiled;
|
|
2283
2349
|
try {
|
|
2284
|
-
|
|
2350
|
+
let manifest = parseVariantManifestModel(readFileSync(P.variantManifestModel, "utf8"));
|
|
2351
|
+
// ── 647 — THE CLIENT'S OWN ELEMENT LEAVES THE CONFLICT ANALYSIS, ON EVIDENCE OR NOT AT ALL ─────
|
|
2352
|
+
//
|
|
2353
|
+
// The receipt is the ONLY thing that arms this. An absent, unreadable or unverified receipt plans
|
|
2354
|
+
// exactly as this compile did before the rule existed, which is the element searched in full — the
|
|
2355
|
+
// safe direction, and the one a client is never harmed by.
|
|
2356
|
+
let houseConfirmation = null;
|
|
2357
|
+
let houseReceipt = null;
|
|
2358
|
+
try { houseReceipt = JSON.parse(readFileSync(driverDir(P.runDir, HOUSE_ELEMENT_RECEIPT), "utf8")); }
|
|
2359
|
+
catch { houseReceipt = null; }
|
|
2360
|
+
if (houseReceipt?.verified === true) {
|
|
2361
|
+
const r = excludeHouseElement(manifest, { element: houseReceipt.element, remainder: houseReceipt.remainder });
|
|
2362
|
+
if (r.refused) {
|
|
2363
|
+
// VERIFIED OWNERSHIP AND STILL NOT APPLIED. The receipt answers who owns the element; the
|
|
2364
|
+
// transform answers whether this manifest's mark can survive the cut. Both must hold.
|
|
2365
|
+
runLog(P.runDir, { event: "house-element-not-applied", reason: r.refused, element: houseReceipt.element });
|
|
2366
|
+
} else {
|
|
2367
|
+
manifest = r.manifest;
|
|
2368
|
+
houseConfirmation = r.confirmation;
|
|
2369
|
+
runLog(P.runDir, { event: "house-element-excluded", element: houseReceipt.element,
|
|
2370
|
+
remainder: houseReceipt.remainder, dominant_element: manifest.dominant_element,
|
|
2371
|
+
evidence: (houseReceipt.records ?? []).length });
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2285
2374
|
let form = null;
|
|
2286
2375
|
try { form = JSON.parse(readFileSync(P.formNeighbourhood, "utf8")); } catch { /* form band optional */ }
|
|
2287
2376
|
compiled = compileRegisterPlan({
|
|
@@ -2308,6 +2397,19 @@ function attachRegisterPlan(ctx, { frozenOnly = false } = {}) {
|
|
|
2308
2397
|
// rides the plan as a disclosed deferral, never as a silently narrower search.
|
|
2309
2398
|
unavailableOffices: registerUnavailableOffices(),
|
|
2310
2399
|
});
|
|
2400
|
+
// REQUIREMENT 3 — the element is still checked ONCE, as a confirmation of the client's own live
|
|
2401
|
+
// registrations rather than as a conflict sweep, so the exclusion is evidenced on the report by a
|
|
2402
|
+
// query that ran. Appended after the compile because it is not derived from the manifest: it is the
|
|
2403
|
+
// one question about the element the plan still owes.
|
|
2404
|
+
if (houseConfirmation && compiled?.entries) {
|
|
2405
|
+
const used = new Set(compiled.entries.map((e) => e.qid));
|
|
2406
|
+
compiled.entries.push({
|
|
2407
|
+
...houseConfirmation,
|
|
2408
|
+
qid: mintSupplementalQid({ prefix: "house", term: houseConfirmation.term, used }),
|
|
2409
|
+
nice_classes: compiled.nice_classes ?? [],
|
|
2410
|
+
regions: compiled.regions ?? [],
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2311
2413
|
} catch (e) {
|
|
2312
2414
|
// NEVER-KILL at mint time: no/malformed sibling or a class-less matter degrades to the legacy
|
|
2313
2415
|
// path with a logged reason — the plan improves the run, it never turns delivery off.
|
|
@@ -3648,6 +3750,27 @@ export function digestDispatchExtra(ctx, { trigger = "fresh", willRun = true, ex
|
|
|
3648
3750
|
}
|
|
3649
3751
|
}
|
|
3650
3752
|
} catch (e) { note(`coverage-form brief skipped (non-fatal — the form is in _driver/ and the validator refuses an unsettled row): ${String(e.message).slice(0, 80)}`); }
|
|
3753
|
+
// — THE BATCH BLOCK: how the driver split this run's band, and which batches are still outstanding.
|
|
3754
|
+
//
|
|
3755
|
+
// The seat is told the split HERE rather than in the stage's dictation because the numbers are the
|
|
3756
|
+
// run's, not the contract's: a band of 40 records is one batch and a band of 1,161 is twelve, and on a
|
|
3757
|
+
// resume the outstanding set is what the killed attempt did not reach. The dictation says the rule;
|
|
3758
|
+
// this says the arithmetic.
|
|
3759
|
+
//
|
|
3760
|
+
// BEST-EFFORT, AND SAFE BY CONSTRUCTION rather than by argument — the coverage brief's precedent above,
|
|
3761
|
+
// for its reason. Nothing here enforces anything: a call naming a batch is judged against the driver's
|
|
3762
|
+
// own split whether or not this block composed, the tool's refusal re-states what is outstanding, and
|
|
3763
|
+
// the stage's exit gate refuses a document that ends fewer records than the run carried in. A brief
|
|
3764
|
+
// that fails to compose costs a corrective round; it cannot cost a record.
|
|
3765
|
+
try {
|
|
3766
|
+
const gap = digestAccountingGap(P.runDir);
|
|
3767
|
+
const block = digestBatchBrief(gap);
|
|
3768
|
+
if (block) {
|
|
3769
|
+
out = out ? `${out}\n\n${block}` : block;
|
|
3770
|
+
runLog(P.runDir, { event: "digest-batch-brief", trigger, owed: gap.owed.length,
|
|
3771
|
+
batches: batchesOf(gap.owed).length, unaccounted: gap.unaccounted.length });
|
|
3772
|
+
}
|
|
3773
|
+
} catch (e) { note(`digest batch brief skipped (non-fatal — the tool judges every batch against the driver's own split regardless): ${String(e.message).slice(0, 80)}`); }
|
|
3651
3774
|
// AD-2 A9 (E2E-R2) + the P5 review (2026-07-31): a corrective/repair pass is told NOT to re-read the
|
|
3652
3775
|
// whole placement file — the per-candidate tiers are in placements.json — but the RULINGS TAIL (band
|
|
3653
3776
|
// reconciliation, disagreements, coverage rulings, open questions) lives ONLY in the md. Leaving it to
|
|
@@ -4342,6 +4465,25 @@ async function stageOnce(name, ctx, opts = {}) {
|
|
|
4342
4465
|
// Evaluated on `effThinking`, not `thinking`: the anthropic gate above nulls the tier for a
|
|
4343
4466
|
// cross-provider model, and a guard that reads the nulled value would go blind on the same overrides.
|
|
4344
4467
|
assertEffectiveTier(label, { model, thinking: effThinking });
|
|
4468
|
+
// ── THE TIME LIMIT IS DERIVED FROM THE BAND THIS DISPATCH IS HANDED ─────────────────────────────
|
|
4469
|
+
//
|
|
4470
|
+
// `def.timeoutSec` was a constant chosen once against a band nobody recorded beside it, and on a
|
|
4471
|
+
// dense matter the two band-reading stages died AT their walls having written nothing. The base is
|
|
4472
|
+
// still the stage's own number; what the band decides is how far above it this dispatch may go. A
|
|
4473
|
+
// band at or under the reference returns the base untouched, so nothing already verified moves.
|
|
4474
|
+
const dispatchBand = bandSizeForStage(name, P);
|
|
4475
|
+
const derivedLimit = derivedLimitSec(def.timeoutSec, dispatchBand);
|
|
4476
|
+
// REFUSED BEFORE A SEAT IS DISPATCHED, not after the wall. A limit past the ceiling is a prediction
|
|
4477
|
+
// that this stage will be killed; starting it spends the whole prediction to arrive where the refusal
|
|
4478
|
+
// already is, and the refusal NAMES the band size, which is the finding a killed attempt never gives.
|
|
4479
|
+
if (limitExceedsCeiling(derivedLimit.sec)) {
|
|
4480
|
+
const reason = ceilingRefusal(name, derivedLimit);
|
|
4481
|
+
runLog(P.runDir, { event: "stage-input-over-ceiling", stage: name,
|
|
4482
|
+
inputBytes: derivedLimit.inputBytes, derivedLimitSec: derivedLimit.sec });
|
|
4483
|
+
throw new StageFailure(name, reason, undefined, { failClass: "deterministic" });
|
|
4484
|
+
}
|
|
4485
|
+
runLog(P.runDir, { event: "stage-limit-derived", stage: name, base: def.timeoutSec,
|
|
4486
|
+
inputBytes: derivedLimit.inputBytes, derivedLimitSec: derivedLimit.sec, basis: derivedLimit.basis });
|
|
4345
4487
|
const r = await runStage(label, {
|
|
4346
4488
|
agent: execAgent,
|
|
4347
4489
|
message,
|
|
@@ -4350,11 +4492,12 @@ async function stageOnce(name, ctx, opts = {}) {
|
|
|
4350
4492
|
// opts.sessionKey lets a followup RESUME the exact key a prior run won on (winning-key hardening); else
|
|
4351
4493
|
// the canonical base key. (A followup with a stale base key would resume a failed attempt — see runStage.)
|
|
4352
4494
|
sessionKey: opts.sessionKey ?? `prelim-${ctx.run.slug}-${ctx.run.codename}-${name}${keyAxis}`,
|
|
4353
|
-
timeoutSec: def.timeoutSec,
|
|
4495
|
+
timeoutSec: derivedLimit.sec ?? def.timeoutSec,
|
|
4496
|
+
derivedLimit, // recorded on every attempt row: the size it was derived from, and the number
|
|
4354
4497
|
stallSec: def.stallSec, // per-stage stall override (heavy stages); undefined → global CLEAROTRON_STALL_MS
|
|
4355
4498
|
expectFile: out,
|
|
4356
4499
|
validate: def.validate,
|
|
4357
|
-
runDir: P.runDir, bandSize:
|
|
4500
|
+
runDir: P.runDir, bandSize: dispatchBand,
|
|
4358
4501
|
// ── — THE PRESENTING SIDE, RECOMPOSED PER ATTEMPT ────────────────────────────────────────
|
|
4359
4502
|
//
|
|
4360
4503
|
// A stage that declares `refreshCtx` is saying its dispatch text depends on state the run CHANGES
|
|
@@ -7329,7 +7472,7 @@ export function fullProseOrdinals(findings) {
|
|
|
7329
7472
|
.filter((o) => o != null);
|
|
7330
7473
|
}
|
|
7331
7474
|
// ASSEMBLE report.md = the overview shell (front-matter + Actions/Coverage/Methodology) + `# Marks` + the
|
|
7332
|
-
// per-card files in render order (composite desc, ordinal asc — mirrors render.mjs
|
|
7475
|
+
// per-card files in render order (composite desc, ordinal asc — mirrors render.mjs `sortedAll`), `open: true` on the
|
|
7333
7476
|
// single top card. Findings with no card file (a failed report-card, or a secondary finding) are NOT emitted —
|
|
7334
7477
|
// render.mjs synthesizes them structured-only from findings.json, so nothing is silently dropped. Pure file IO.
|
|
7335
7478
|
// spec 64 — "### Only you can close these" is CODE-BUILT from the typed actions register (the report-
|
|
@@ -8849,6 +8992,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
8849
8992
|
must(await stage("prelim-variants", ctx), "prelim-variants");
|
|
8850
8993
|
deriveScopeLedgerJson(ctx); // frame-omission design: code-derive scope-ledger.json from the validated prose (never-kill)
|
|
8851
8994
|
deriveFormNeighbourhood(ctx); // mechanical FORM band: code-derive form-neighbourhood.json from the manifest's distinctive element(s) — the model-free form floor the register funnel searches (never-kill)
|
|
8995
|
+
await verifyAndRecordHouseElement(ctx, opts); // ask the register who owns the frame's proposed house element, BEFORE the plan is compiled from it
|
|
8852
8996
|
attachRegisterPlan(ctx); // WS2 (B3): compile/freeze/reuse the deterministic register plan (flag-gated; frozen plan wins on resume; never-kill on mint)
|
|
8853
8997
|
|
|
8854
8998
|
// spec 64 (B2) — proactive recall probes: prior-confirmed conflicts for THIS mark (the workspace
|
|
@@ -12872,7 +13016,11 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12872
13016
|
reason: `synthesis_unaccounted_delivered:${duty.unaccounted.length} of ${duty.totals.owed} record(s) reached the findings surface and the delivered document accounts for none of them — neither a finding that names them nor a declination with a ground: ${sample}${duty.unaccounted.length > 4 ? " …" : ""}. The report ships with this named rather than being withheld; these records are open points a reader must weigh.`,
|
|
12873
13017
|
// — the READER's sentence: what is open, in a lawyer's nouns. Counts survive;
|
|
12874
13018
|
// the token, the record ids and the engine's nouns stay in `reason` and the run record.
|
|
12875
|
-
|
|
13019
|
+
// COMPOSED BY THE AUTHORITY, NOT SPELLED HERE. The same sentence has to be reachable from a
|
|
13020
|
+
// run that stored no clause, where these two objects are long gone and only the reason's own
|
|
13021
|
+
// counts survive; `terminal-clamp.mjs` composes it from the counts either way, so the fresh
|
|
13022
|
+
// run and the republished archive cannot drift apart.
|
|
13023
|
+
clause: clauseForDefect("synthesis_unaccounted_delivered", duty.unaccounted.length, duty.totals.owed),
|
|
12876
13024
|
});
|
|
12877
13025
|
}
|
|
12878
13026
|
}
|
|
@@ -12898,7 +13046,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12898
13046
|
// — the READER's sentence. "Floor row" is an engine noun; what the fact IS for a
|
|
12899
13047
|
// lawyer: live registrations identical or near-identical to the mark that the report does not
|
|
12900
13048
|
// individually address. Counts survive; token, ids and engine nouns stay in `reason`.
|
|
12901
|
-
|
|
13049
|
+
// Composed by the authority in `terminal-clamp.mjs` for the reason the sibling site above
|
|
13050
|
+
// gives: a republished pre-split run has to reach the same sentence from the counts alone.
|
|
13051
|
+
clause: clauseForDefect("floor_duty_undischarged", block.undischarged, block.floors),
|
|
12902
13052
|
});
|
|
12903
13053
|
}
|
|
12904
13054
|
}
|
|
@@ -12940,7 +13090,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12940
13090
|
for (const c of fresh) clampClauses.push(conditionClauses[conditions.indexOf(c)] ?? c);
|
|
12941
13091
|
clampReasons.push(...fresh);
|
|
12942
13092
|
if (verdict === "CLEAR") {
|
|
12943
|
-
runLog(run.runDir, { event: "coverage-floor-clamp", from: "CLEAR", to: "CONDITIONAL", legalActions: conditions.length });
|
|
13093
|
+
runLog(run.runDir, { event: "coverage-floor-clamp", cause: "forward-actions", from: "CLEAR", to: "CONDITIONAL", legalActions: conditions.length });
|
|
12944
13094
|
note(`deliver-conditional floor: the opinion names ${conditions.length} forward legal action(s) a human must take (${conditions[0].slice(0, 140)}${conditions.length > 1 ? `; +${conditions.length - 1} more` : ""}) — clamping CLEAR→CONDITIONAL (spec 64: the disposition is derived from the findings' named actions).`);
|
|
12945
13095
|
verdict = "CONDITIONAL";
|
|
12946
13096
|
writeRunStatus(ctx, { verdict });
|
|
@@ -12979,7 +13129,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12979
13129
|
// The CLAMP is outside that guard on purpose: a re-ask can flip a clamped verdict back to CLEAR,
|
|
12980
13130
|
// and it must meet this floor again with the reason already recorded.
|
|
12981
13131
|
if (droppedConditions.length && verdict === "CLEAR") {
|
|
12982
|
-
runLog(run.runDir, { event: "coverage-floor-clamp", from: "CLEAR", to: "CONDITIONAL", actionsDropped: droppedConditions.length });
|
|
13132
|
+
runLog(run.runDir, { event: "coverage-floor-clamp", cause: "dropped-actions", from: "CLEAR", to: "CONDITIONAL", actionsDropped: droppedConditions.length });
|
|
12983
13133
|
verdict = "CONDITIONAL";
|
|
12984
13134
|
writeRunStatus(ctx, { verdict });
|
|
12985
13135
|
}
|
|
@@ -13070,22 +13220,49 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13070
13220
|
// could-not-examine record, and an unfinished register slice — CONDITIONAL carries
|
|
13071
13221
|
// lawyer-judged/disclosed residue only.
|
|
13072
13222
|
if (coverageInsufficient || frameResidual || screenGateGap || seniorGap || registerGap || deadlineGap) {
|
|
13223
|
+
// THESE SENTENCES REACH A CLIENT AND THEY ARE NOT OURS TO WRITE (owner, 2026-09-17). One of
|
|
13224
|
+
// them — the screen-gate line, which says a mark "could not be record_fetched" — carries an
|
|
13225
|
+
// engine identifier into the list a client reads as the conditions on their result, by a route
|
|
13226
|
+
// `terminalClampDecision` guards and this one does not. A reader's sentence for it was written
|
|
13227
|
+
// here and refused: the objection was the class, not the wording. It is on the owner's design
|
|
13228
|
+
// table as audit item 25, and until he rules, this stays exactly as it was rather than carrying
|
|
13229
|
+
// a caveat a developer composed.
|
|
13073
13230
|
const reasons = [];
|
|
13074
|
-
|
|
13075
|
-
if (
|
|
13076
|
-
|
|
13077
|
-
if (
|
|
13078
|
-
if (
|
|
13231
|
+
const machinery = (reason) => reasons.push(reason);
|
|
13232
|
+
if (coverageInsufficient) machinery(`the lawyer judged a material slice not fully cleared: ${coverageJudgment.reason || "register coverage gap"}`);
|
|
13233
|
+
if (frameGap) machinery("the blind frame-diff flagged a dominant-element omission the reopen pass did not close");
|
|
13234
|
+
else if (frameDeferrals.length) machinery(`follow-ups left open this run: ${frameDeferrals.map((d) => plainDirective(d.directive)).slice(0, 3).join(", ")}`);
|
|
13235
|
+
if (screenGateGap) machinery(`${sgUnresolved.length} in-scope mark(s) dropped on goods could not be record_fetched (unverified): ${sgUnresolved.map((g) => g.mark).join(", ")}`);
|
|
13236
|
+
if (seniorGap) machinery(`the oldest registration in a verdict-driving family could not be retrieved (policy: clamp): ${(ctx.seniorRights?.rows ?? []).filter((r) => r.applicable && !r.verified).map((r) => r.mark).join(", ")}`);
|
|
13079
13237
|
if (registerGap) {
|
|
13080
|
-
if (regGap.deferred.length)
|
|
13081
|
-
if (regGap.taintAxes.length)
|
|
13238
|
+
if (regGap.deferred.length) machinery(`register coverage deferred on ${[...new Set(regGap.deferred.map((g) => g.axis))].join(", ")} — the search did not finish and must be re-run before this can be relied on`);
|
|
13239
|
+
if (regGap.taintAxes.length) machinery(`the ${regGap.taintAxes.join(", ")} register pass was cut down at the timeout wall and its self-reported coverage is unverified`);
|
|
13082
13240
|
// Named regressions (2026-07-22): `<MARK> (<owner> — <canonical uri>)` — a bare mark name
|
|
13083
13241
|
// shipped "ION, ION, ION" (three indistinguishable strings); the identity is front-loaded
|
|
13084
13242
|
// because the delivered statement truncates from the tail.
|
|
13085
|
-
if (regGap.recallRegressions.length)
|
|
13243
|
+
if (regGap.recallRegressions.length) machinery(`a prior-confirmed live conflict was neither carried nor justified this run: ${regGap.recallRegressions.slice(0, 3).map(formatRecallRegression).join(", ")}`);
|
|
13086
13244
|
}
|
|
13087
|
-
if (deadlineGap)
|
|
13088
|
-
|
|
13245
|
+
if (deadlineGap) machinery(`a recorded opposition deadline was delivered without its date: ${ctx.deadlineCarryMaterial.map((v) => `${v.mark_text ?? v.uri} (window closes ${v.opposition_end})`).slice(0, 3).join(", ")}`);
|
|
13246
|
+
// ── THE THIRD CLAMP SITE NAMES ITSELF, AND NAMES WHICH OF ITS SEVEN INPUTS FIRED ────────────────
|
|
13247
|
+
//
|
|
13248
|
+
// All three clamp sites emitted this event under one name with the same from/to, distinguishable
|
|
13249
|
+
// only by which optional payload key happened to be present. Two of them fired three milliseconds
|
|
13250
|
+
// apart on a production run — the first without `frameDeferred`, the second with it — which reads
|
|
13251
|
+
// as one decision logged twice. It was two different decisions wearing one name, and the test lane
|
|
13252
|
+
// reasonably discounted one of them. `cause` makes the event self-describing.
|
|
13253
|
+
//
|
|
13254
|
+
// THE EVENT NAME IS DELIBERATELY NOT SPLIT. It is a true statement about the effect — the verdict
|
|
13255
|
+
// was clamped — and something downstream may already count clamps in aggregate. A discriminator is
|
|
13256
|
+
// additive; three names would not be.
|
|
13257
|
+
//
|
|
13258
|
+
// This site is itself seven causes under one name, so it also lists WHICH fired rather than
|
|
13259
|
+
// leaving a reader to key on field presence and guess.
|
|
13260
|
+
const clampInputs = Object.entries({
|
|
13261
|
+
coverageInsufficient, frameGap, frameDeferred: frameDeferrals.length,
|
|
13262
|
+
screenGate: screenGateGap ? sgUnresolved.length : 0, seniorRight: seniorGap,
|
|
13263
|
+
registerGap, deadlineCarry: deadlineGap ? ctx.deadlineCarryMaterial.length : 0,
|
|
13264
|
+
}).filter(([, v]) => Boolean(v)).map(([k]) => k);
|
|
13265
|
+
runLog(run.runDir, { event: "coverage-floor-clamp", cause: "coverage", causes: clampInputs, from: "CLEAR", to: "CONDITIONAL", coverageInsufficient: coverageInsufficient || undefined, frameGap: frameGap || undefined, frameDeferred: frameDeferrals.length || undefined, screenGate: screenGateGap ? sgUnresolved.length : undefined, seniorRight: seniorGap || undefined, registerGap: registerGap ? { deferred: regGap.deferred.length, taint: regGap.taintAxes.length, recall: regGap.recallRegressions.length } : undefined, deadlineCarry: deadlineGap ? ctx.deadlineCarryMaterial.length : undefined });
|
|
13089
13266
|
note(`deliver-conditional floor: ${reasons.join("; ")} — clamping CLEAR→CONDITIONAL so the delivered status carries the gap (never withheld, never halted).`);
|
|
13090
13267
|
verdict = "CONDITIONAL";
|
|
13091
13268
|
// APPEND (dedup by exact text) — the legalActions arm may already have recorded conditions,
|
|
@@ -51,11 +51,15 @@ import { readFlagSnapshot, engineFor, providersFor, postureDisagreement } from "
|
|
|
51
51
|
// reading is, because the question it was standing in for — does this still describe the box — now has
|
|
52
52
|
// a direct answer in `lastRun.disagrees`.
|
|
53
53
|
import { engineMode } from "./config-inventory.mjs"; // — the mode is DERIVED at read time, never stored
|
|
54
|
+
import { BILLING_MODES } from "./engine/auth.mjs"; // — the three words a capture's billing mode may be shown as
|
|
54
55
|
// THE ENGINE TABLE, READ FOR TWO WORDS. A row saying an engine cannot run has to name the program it
|
|
55
56
|
// could not find and the command that installs it, or the reader is told they have a problem and not
|
|
56
57
|
// what to do about it — and this table is already where the wizard and the run-door preflight read
|
|
57
58
|
// both of those, so naming them here adds no second description of an engine.
|
|
58
59
|
import { ENGINE_BINARIES } from "./driver.config.mjs";
|
|
60
|
+
// WHICH SETUP COMMAND THIS READER CAN TYPE, as a word and never a path: the same answer `/me` sends the
|
|
61
|
+
// search screen, so the two pages name the same command for the same install.
|
|
62
|
+
import { installRoute } from "../shared/invocation.mjs";
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
65
|
* The flag view.
|
|
@@ -88,6 +92,12 @@ function withProgram(engine) {
|
|
|
88
92
|
// layout and is deliberately kept out of anything a browser renders.
|
|
89
93
|
program: spec?.fallback ?? null,
|
|
90
94
|
install: spec?.install ?? null,
|
|
95
|
+
// The setting that names the program's full path, which is what an administrator sets when the
|
|
96
|
+
// services cannot find a program this machine has. A name, never its value.
|
|
97
|
+
programSetting: spec?.env ?? null,
|
|
98
|
+
// SETUP INSTALLS THE PROGRAM NOW, so a row saying it cannot be found names setup, the way this reader
|
|
99
|
+
// can run it: `packaged` or `checkout`.
|
|
100
|
+
setupRoute: installRoute(),
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -135,8 +145,27 @@ function postureView(snap) {
|
|
|
135
145
|
* The capture does not go away; it stops being the answer. It becomes "what the last run saw", and its
|
|
136
146
|
* job is to name any field on which it disagrees with the live reading.
|
|
137
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* A CAPTURE'S BILLING WORD, SHOWN ONLY WHEN IT IS ONE OF THE MODES.
|
|
150
|
+
*
|
|
151
|
+
* The inventory records a billing word that is not a mode as `unknown`, because whatever was typed into
|
|
152
|
+
* the setting that decides who pays can be a key pasted there by mistake. A capture written before it
|
|
153
|
+
* did holds the typed word itself, as its mode and inside its refusal's sentence, and this page would
|
|
154
|
+
* print it twice: in the last-run comparison, whose live side now reads `unknown`, and under a capture
|
|
155
|
+
* shown in place of a live reading. So such a capture is read as `unknown`, with no refusal sentence,
|
|
156
|
+
* which the page does not read anyway (it words the refusal from `reason`). Until the engine service
|
|
157
|
+
* restarts and writes its capture again, that is the only copy of the word the page could reach.
|
|
158
|
+
*/
|
|
159
|
+
function withKnownBillingWord(snap) {
|
|
160
|
+
const billing = snap?.engine?.billing;
|
|
161
|
+
if (!billing || typeof billing !== "object") return snap;
|
|
162
|
+
if (billing.mode == null || billing.mode === "unknown" || BILLING_MODES.includes(billing.mode)) return snap;
|
|
163
|
+
const { refusal: _typed, ...rest } = billing;
|
|
164
|
+
return { ...snap, engine: { ...snap.engine, billing: { ...rest, mode: "unknown" } } };
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
export function flagView(poolRoot, { live = null } = {}) {
|
|
139
|
-
const snap = readFlagSnapshot(poolRoot);
|
|
168
|
+
const snap = withKnownBillingWord(readFlagSnapshot(poolRoot));
|
|
140
169
|
|
|
141
170
|
// NO LIVE POSTURE IS A DIFFERENT PAGE, NOT A DEGRADED ONE. A caller that supplied none cannot be
|
|
142
171
|
// answered "live" at all, so this says which reading it is showing rather than presenting a capture
|