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.
Files changed (85) hide show
  1. package/.env.example +24 -23
  2. package/INSTALL.md +142 -75
  3. package/README.md +3 -3
  4. package/bin/onboard.mjs +637 -216
  5. package/bin/start.mjs +133 -23
  6. package/bin/update.mjs +58 -11
  7. package/build-info.json +2 -2
  8. package/docs/architecture/04-configuration-reference.md +26 -11
  9. package/docs/architecture/05-config-governance.md +17 -7
  10. package/driver/CHANGELOG.md +76 -0
  11. package/driver/band-size.mjs +59 -0
  12. package/driver/config-inventory.mjs +112 -9
  13. package/driver/contract-arm2-baseline.json +1 -3
  14. package/driver/contract-e3-backlog.mjs +26 -26
  15. package/driver/contract-vocabulary.mjs +44 -10
  16. package/driver/door-gates.mjs +41 -7
  17. package/driver/driver.config.mjs +272 -59
  18. package/driver/engine/CONTRACT.md +10 -3
  19. package/driver/engine/README.md +2 -2
  20. package/driver/engine/anthropic-agent.mjs +77 -21
  21. package/driver/engine/auth.mjs +129 -10
  22. package/driver/engine/jx-turn.mjs +7 -6
  23. package/driver/engine/mcp/recording-server.mjs +13 -0
  24. package/driver/engine/openai-agent.mjs +4 -2
  25. package/driver/engine/probe.mjs +110 -23
  26. package/driver/findings-model.mjs +1 -1
  27. package/driver/flag-snapshot.mjs +28 -5
  28. package/driver/gateway.mjs +24 -18
  29. package/driver/jx-lanes.mjs +21 -2
  30. package/driver/jx-units.mjs +6 -3
  31. package/driver/jx.mjs +4 -2
  32. package/driver/matter-frame-record.mjs +90 -1
  33. package/driver/named-band.mjs +34 -2
  34. package/driver/package.json +1 -1
  35. package/driver/pipeline.mjs +200 -23
  36. package/driver/portal-config-view.mjs +30 -1
  37. package/driver/portal-report.mjs +15 -1
  38. package/driver/portal-service.mjs +46 -6
  39. package/driver/predelivery-lint.mjs +12 -2
  40. package/driver/publish/index.mjs +46 -5
  41. package/driver/publish/knockout.mjs +10 -1
  42. package/driver/publish/render-knockout.mjs +69 -7
  43. package/driver/publish/render.mjs +170 -59
  44. package/driver/publish/report-data.mjs +4 -1
  45. package/driver/publish/report-topbar.mjs +58 -0
  46. package/driver/publish/templates/report.css +18 -1
  47. package/driver/publish/xlsx.mjs +13 -1
  48. package/driver/register-availability.mjs +2 -2
  49. package/driver/register-coverage.mjs +94 -1
  50. package/driver/register-digest-record.mjs +236 -11
  51. package/driver/register-plan.mjs +170 -0
  52. package/driver/result-noun-fields.mjs +2 -2
  53. package/driver/run-economics.mjs +41 -10
  54. package/driver/run-requirements.mjs +173 -9
  55. package/driver/runner.mjs +3 -3
  56. package/driver/stages.mjs +12 -8
  57. package/driver/suite-census.json +142 -64
  58. package/driver/systemd/README.md +7 -4
  59. package/driver/terminal-clamp.mjs +107 -1
  60. package/driver/tokens.mjs +169 -3
  61. package/driver/unit-environment.mjs +42 -15
  62. package/driver/unit-inventory.mjs +19 -2
  63. package/driver/verify.mjs +27 -0
  64. package/mcp-server/CHANGELOG.md +4 -0
  65. package/mcp-server/package.json +1 -1
  66. package/mcp-server/server.mjs +15 -1
  67. package/package.json +1 -1
  68. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
  69. package/portal-ui/dist/index.html +1 -1
  70. package/portal-ui/package.json +1 -1
  71. package/providers/jx/README.md +2 -1
  72. package/providers/jx/src/turn-envelope.mjs +8 -3
  73. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  74. package/providers/oauth-mcp-bridge/package.json +1 -1
  75. package/providers/uspto-local/README.md +1 -1
  76. package/scripts/authority-boundary-probe.mjs +4 -2
  77. package/scripts/env-audit.mjs +12 -6
  78. package/scripts/freeze-example-run.mjs +49 -16
  79. package/scripts/generated-files-are-current.mjs +69 -4
  80. package/scripts/settings-render-check.mjs +75 -2
  81. package/scripts/test-full.mjs +96 -3
  82. package/scripts/test-run.mjs +10 -0
  83. package/shared/deployment-box.mjs +7 -2
  84. package/shared/driver-dir.mjs +1 -1
  85. 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
  }
@@ -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
- for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) enumerated.push({ ...r, ...prov }); }
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:1439 validatePlanFeasibility already enforces the same rule one layer up
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.
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.7",
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": {
@@ -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 } from "./register-digest-record.mjs"; // conversion 11 — the render's facts sidecar and the accounting era stamp
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"; // — the stamp the late lanes never met
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
- const manifest = parseVariantManifestModel(readFileSync(P.variantManifestModel, "utf8"));
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: bandSizeForStage(name, P),
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:502), `open: true` on the
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
- clause: `${duty.unaccounted.length} of the ${duty.totals.owed} register records this search surfaced are neither addressed as findings nor expressly set aside in this report — they remain open points a reader must weigh`,
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
- clause: `${block.undischarged} of the ${block.floors} live registrations identical or near-identical to the mark are not individually addressed in this report — each remains an open point a reader must weigh`,
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
- if (coverageInsufficient) reasons.push(`the lawyer judged a material slice not fully cleared: ${coverageJudgment.reason || "register coverage gap"}`);
13075
- if (frameGap) reasons.push("the blind frame-diff flagged a dominant-element omission the reopen pass did not close");
13076
- else if (frameDeferrals.length) reasons.push(`follow-ups left open this run: ${frameDeferrals.map((d) => plainDirective(d.directive)).slice(0, 3).join(", ")}`);
13077
- if (screenGateGap) reasons.push(`${sgUnresolved.length} in-scope mark(s) dropped on goods could not be record_fetched (unverified): ${sgUnresolved.map((g) => g.mark).join(", ")}`);
13078
- if (seniorGap) reasons.push(`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(", ")}`);
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) reasons.push(`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`);
13081
- if (regGap.taintAxes.length) reasons.push(`the ${regGap.taintAxes.join(", ")} register pass was cut down at the timeout wall and its self-reported coverage is unverified`);
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) reasons.push(`a prior-confirmed live conflict was neither carried nor justified this run: ${regGap.recallRegressions.slice(0, 3).map(formatRecallRegression).join(", ")}`);
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) reasons.push(`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(", ")}`);
13088
- runLog(run.runDir, { event: "coverage-floor-clamp", 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 });
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