clearotron 0.3.3-beta.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/onboard.mjs +80 -6
  2. package/build-info.json +2 -2
  3. package/docs/architecture/04-configuration-reference.md +28 -10
  4. package/docs/releases/0.3.3.md +52 -0
  5. package/driver/CHANGELOG.md +56 -0
  6. package/driver/citation-census.json +3 -3
  7. package/driver/config-inventory.mjs +1 -1
  8. package/driver/dev-portal.mjs +3 -3
  9. package/driver/driver.config.mjs +80 -9
  10. package/driver/engine/CONTRACT.md +3 -2
  11. package/driver/engine/anthropic-agent.mjs +34 -7
  12. package/driver/engine/mcp/probe-server.mjs +12 -3
  13. package/driver/package.json +1 -1
  14. package/driver/pipeline-knockout.mjs +3 -3
  15. package/driver/pipeline.mjs +14 -14
  16. package/driver/plan-run-agreement-verdict.mjs +49 -0
  17. package/driver/portal-service.mjs +8 -4
  18. package/driver/progress.mjs +14 -3
  19. package/driver/publish/index.mjs +1 -1
  20. package/driver/publish/report-data.mjs +4 -3
  21. package/driver/reference-score.mjs +10 -2
  22. package/driver/settle-stamp.mjs +10 -3
  23. package/driver/status-snapshot.mjs +2 -2
  24. package/driver/suite-census.json +61 -19
  25. package/mcp-server/CHANGELOG.md +8 -0
  26. package/mcp-server/lib/brief.mjs +11 -5
  27. package/mcp-server/package.json +1 -1
  28. package/package.json +2 -2
  29. package/portal-ui/package.json +1 -1
  30. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  31. package/providers/oauth-mcp-bridge/package.json +1 -1
  32. package/scripts/e2e.mjs +1 -1
  33. package/scripts/live-surface-check.mjs +6 -14
  34. package/scripts/repo-writes.mjs +1 -1
  35. package/scripts/report-sections-render-check.mjs +7 -3
  36. package/scripts/score.mjs +7 -1
  37. package/shared/scroll-settle.mjs +67 -0
@@ -639,7 +639,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
639
639
  // for reconcile-runs' exact liveness test. The stepper and the identity are separate calls now.
640
640
  ...identitySeed(),
641
641
  stepIndex: 0, stepLabel: STEPS[0], stepN: 1, stepTotal: STEPS.length,
642
- verdict: null, url: null, failedStage: null, reason: null, deliveredAt: null,
642
+ review: null, url: null, failedStage: null, reason: null, deliveredAt: null,
643
643
  // A5/A3: a re-run of a previously-terminal knockout may reopen the state ONLY because the resume
644
644
  // guard cleared the sentinel (ctx.stateReset threads that authority); startedAt is no longer
645
645
  // seeded anywhere — writeRunStatus backfills it first-write-wins.
@@ -1092,11 +1092,11 @@ export async function knockoutInner(ctx, job, opts = {}) {
1092
1092
  const deliveredAt = new Date().toISOString();
1093
1093
  // `tier` beside `verdict` — the same band word under the name the clearance lane records it by, so a
1094
1094
  // reader of either lane's status finds the rating in one place. `verdict` stays as it was.
1095
- writeRunStatus(ctx, { state: "delivered", verdict: overall, tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
1095
+ writeRunStatus(ctx, { state: "delivered", tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
1096
1096
  // — the knockout lane's pool copy learns its terminal state the same way,
1097
1097
  // for the same reason: publish returns the pool dir, and `state: "delivered"` is decided after it
1098
1098
  // returns. Same seam, same best-effort contract, no lane-specific exception to write down.
1099
- const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", verdict: overall, deliveredAt, runId: published.runId ?? run.runId, lane: "knockout" });
1099
+ const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", tier: overall, deliveredAt, runId: published.runId ?? run.runId, lane: "knockout" });
1100
1100
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
1101
1101
  const archived = archive(run);
1102
1102
  rollupStatus(run.studioRoot);
@@ -91,7 +91,7 @@ import { deriveScopeFacts } from "./scope-facts.mjs";
91
91
  import { documentGrowth } from "./gate-metrics.mjs";
92
92
  import { editRepairTail, abbrev } from "./repair-contract.mjs";
93
93
  import { repairFollowup } from "./repair-composers.mjs";
94
- import { seedRunStatus, recordTransition, writeRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState } from "./progress.mjs";
94
+ import { seedRunStatus, recordTransition, writeRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState, signoffPatch, readSignoff } from "./progress.mjs";
95
95
  import { batchMarkName } from "./mark-name.mjs";
96
96
  import { writeOutboxPacket } from "./outbox.mjs";
97
97
  import { publishReport, composeEmailHtml, deliverySubject } from "./publish/index.mjs";
@@ -12603,7 +12603,7 @@ async function pipelineInner(job, opts = {}) {
12603
12603
  : "failed after retries + fallback (the ladder recorded no condition)");
12604
12604
  let verdict = firstRef.verdict;
12605
12605
  runLog(run.runDir, { event: "verdict", verdict });
12606
- writeRunStatus(ctx, { verdict });
12606
+ writeRunStatus(ctx, signoffPatch(verdict));
12607
12607
  note(`refutation verdict: ${verdict}`);
12608
12608
  // qw/typed-correction-kinds — TELEMETRY ONLY (data first; the corrective-skip decision is a
12609
12609
  // separate owner-gated build): partition the review's flagged-correction lines by their
@@ -12847,7 +12847,7 @@ async function pipelineInner(job, opts = {}) {
12847
12847
  verdict = rc.ok ? (parseVerdict(readFileSync(P.seniorEyeReview, "utf8")) ?? entryVerdict) : entryVerdict;
12848
12848
  correctiveRecheckOk = rc.ok === true;
12849
12849
  runLog(run.runDir, { event: "verdict-2", verdict });
12850
- writeRunStatus(ctx, { verdict });
12850
+ writeRunStatus(ctx, signoffPatch(verdict));
12851
12851
  }
12852
12852
  // AD-2 A1 — the completion receipt, written ONLY when the cycle finished with a verified recheck:
12853
12853
  // a fallen-back recheck kept the entry verdict WITHOUT re-verifying, and cementing that would stop
@@ -13089,7 +13089,7 @@ async function pipelineInner(job, opts = {}) {
13089
13089
  if (d.clamped) {
13090
13090
  runLog(run.runDir, { event: "terminal-guard-clamp", defect: d.record.defect, from: verdict, to: d.verdict });
13091
13091
  verdict = d.verdict;
13092
- writeRunStatus(ctx, { verdict });
13092
+ writeRunStatus(ctx, signoffPatch(verdict));
13093
13093
  }
13094
13094
  };
13095
13095
 
@@ -13223,7 +13223,7 @@ async function pipelineInner(job, opts = {}) {
13223
13223
  runLog(run.runDir, { event: "coverage-floor-clamp", cause: "forward-actions", from: "CLEAR", to: "CONDITIONAL", legalActions: conditions.length });
13224
13224
  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).`);
13225
13225
  verdict = "CONDITIONAL";
13226
- writeRunStatus(ctx, { verdict });
13226
+ writeRunStatus(ctx, signoffPatch(verdict));
13227
13227
  } else if (fresh.length) {
13228
13228
  runLog(run.runDir, { event: "verdict-conditions-recorded", legalActions: fresh.length });
13229
13229
  }
@@ -13261,7 +13261,7 @@ async function pipelineInner(job, opts = {}) {
13261
13261
  if (droppedConditions.length && verdict === "CLEAR") {
13262
13262
  runLog(run.runDir, { event: "coverage-floor-clamp", cause: "dropped-actions", from: "CLEAR", to: "CONDITIONAL", actionsDropped: droppedConditions.length });
13263
13263
  verdict = "CONDITIONAL";
13264
- writeRunStatus(ctx, { verdict });
13264
+ writeRunStatus(ctx, signoffPatch(verdict));
13265
13265
  }
13266
13266
  }
13267
13267
  } catch (e) { note(`legal-actions floor skipped (${String(e.message).slice(0, 80)}) — never-kill; the predelivery coherence lint is the second net`); }
@@ -13309,7 +13309,7 @@ async function pipelineInner(job, opts = {}) {
13309
13309
  // re-ask can flip a clamped verdict back to CLEAR, and it must meet this floor again with the
13310
13310
  // reason ALREADY recorded. Folding these two into one guard would let the second pass skip on
13311
13311
  // the dedup and deliver CLEAR — the precise defect this issue is about.
13312
- if (clReason && verdict === "CLEAR") { verdict = "CONDITIONAL"; writeRunStatus(ctx, { verdict }); }
13312
+ if (clReason && verdict === "CLEAR") { verdict = "CONDITIONAL"; writeRunStatus(ctx, signoffPatch(verdict)); }
13313
13313
  }
13314
13314
  } catch (e) { note(`common-law downgrade floor skipped (${String(e.message).slice(0, 80)}) — never-kill`); }
13315
13315
  if (entryVerdict === "CLEAR") {
@@ -13403,7 +13403,7 @@ async function pipelineInner(job, opts = {}) {
13403
13403
  // The reason KINDS distinguish coverage/frame/screen-gate/senior-right/register residue for the
13404
13404
  // report bound line and the client conditions row (merged — legalActions survives).
13405
13405
  clampKinds = { ...clampKinds, coverage: coverageInsufficient || undefined, frame: frameResidual || undefined, screenGate: screenGateGap || undefined, seniorRight: seniorGap || undefined, registerGap: registerGap || undefined, deadlineCarry: deadlineGap || undefined };
13406
- writeRunStatus(ctx, { verdict });
13406
+ writeRunStatus(ctx, signoffPatch(verdict));
13407
13407
  }
13408
13408
  }
13409
13409
  };
@@ -13591,7 +13591,7 @@ async function pipelineInner(job, opts = {}) {
13591
13591
  if (rr.ok) {
13592
13592
  verdict = parseVerdict(readReview()) ?? "BLOCKING";
13593
13593
  runLog(run.runDir, { event: "verdict-3", verdict, trigger: "degenerate-reask" });
13594
- writeRunStatus(ctx, { verdict });
13594
+ writeRunStatus(ctx, signoffPatch(verdict));
13595
13595
  applyCoverageFloor(); // a re-asked CLEAR passes the same deliver-conditional floor
13596
13596
  try { writeVerdictSidecar(); }
13597
13597
  catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
@@ -14616,7 +14616,7 @@ async function pipelineInner(job, opts = {}) {
14616
14616
  applyCoverageFloor();
14617
14617
  try { writeVerdictSidecar(); }
14618
14618
  catch (e) { throw new StageFailure("verdict", `verdict sidecar re-derive failed (spec-64 coherence repair): ${String(e.message).slice(0, 120)}`); }
14619
- if (verdict !== beforeVerdict) writeRunStatus(ctx, { verdict });
14619
+ if (verdict !== beforeVerdict) writeRunStatus(ctx, signoffPatch(verdict));
14620
14620
  runLog(run.runDir, { event: "verdict-rederive-repair", from: beforeVerdict, to: verdict });
14621
14621
  lint = lintNow();
14622
14622
  }
@@ -14902,7 +14902,7 @@ async function pipelineInner(job, opts = {}) {
14902
14902
  runLog(run.runDir, { event: "verdict-hardened-by-repair", was: verdict, now: hardened, stage: s2.label, adopted: true });
14903
14903
  note(`stale-repair: the reviewer returned ${hardened} — adopting it as the run's verdict (the one settled earlier was decided against a review this repair has since rewritten) and rebuilding the label from it`);
14904
14904
  verdict = hardened;
14905
- writeRunStatus(ctx, { verdict });
14905
+ writeRunStatus(ctx, signoffPatch(verdict));
14906
14906
  try { writeVerdictSidecar(); }
14907
14907
  catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
14908
14908
  reassemble = true;
@@ -15468,13 +15468,13 @@ async function pipelineInner(job, opts = {}) {
15468
15468
  // .published + .delivered on disk must never read "7/9" on any status surface (nothing runs after
15469
15469
  // the packet, so no stage transition would ever finish the display sequence). finalStepFields ⇒ 9/9.
15470
15470
  const deliveredAt = new Date().toISOString();
15471
- writeRunStatus(ctx, { state: "delivered", verdict, statement: emailVerdictOpts.statement ?? undefined, caption: published.caption ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
15471
+ writeRunStatus(ctx, { state: "delivered", ...signoffPatch(verdict), statement: emailVerdictOpts.statement ?? undefined, caption: published.caption ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
15472
15472
  // — THE POOL COPY LEARNS ITS OWN TERMINAL STATE, HERE AND NOWHERE ELSE.
15473
15473
  // `meta.json` cannot carry this: it is composed inside publish, before this line runs, so the state
15474
15474
  // did not exist yet when it was written. This is the one moment where the terminal state and the
15475
15475
  // pool directory are both in hand. Best-effort by construction — the report is already published
15476
15476
  // and the run is already settled, so a failure to stamp is logged and the delivery proceeds.
15477
- const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", verdict, deliveredAt, runId: published.runId ?? run.runId, lane: "clearance" });
15477
+ const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", signoff: verdict, deliveredAt, runId: published.runId ?? run.runId, lane: "clearance" });
15478
15478
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
15479
15479
  const archived = archive(run);
15480
15480
  rollupStatus(run.studioRoot);
@@ -16043,7 +16043,7 @@ export function reconstructCtx(job, opts) { // @internal
16043
16043
  } catch { /* no/corrupt frozen plan — prose axes stand, as they always did */ }
16044
16044
  const readJson = (f) => { try { return JSON.parse(readFileSync(join(run.runDir, f), "utf8")); } catch { return null; } };
16045
16045
  const status = readJson("status.json");
16046
- if (status?.verdict) ctx.verdict = status.verdict;
16046
+ if (readSignoff(status)) ctx.verdict = readSignoff(status); // `review.signoff`, or `verdict` on a record written before the move
16047
16047
  const pub = readJson(".published");
16048
16048
  if (pub?.url) ctx.publishedUrl = pub.url;
16049
16049
  // ── THE CTX FIELDS THIS USED TO DROP (item) ───────────────────────────────────────────────────
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sarl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // DO describe_options AND plan_run AGREE ON WHICH PRODUCTS ARE AVAILABLE — the deployment check's arm,
5
+ // extracted so it can be driven without a door to call (the reason surface-exit-verdict.mjs gives).
6
+ //
7
+ // A plan_run that THROWS compared nothing. A 429 from the door's rate limit, a timeout, a refused
8
+ // connection: each means the question was never answered, and the check used to file it beside the real
9
+ // disagreements and exit 1, "drifted". Three runs of the check in two minutes exhausted the ops door's
10
+ // rate limit and the third reported a drift that a redeploy could not have fixed. So an unanswered ask is
11
+ // a marked skip, which exits 3, and only an answer that contradicts describe_options is a drift.
12
+
13
+ // The blocker wording plan_run uses for a product it will not run.
14
+ const UNAVAILABLE = /not part of the current release|not switched on|unavailable/i;
15
+
16
+ /**
17
+ * Ask plan_run about each product describe_options listed, and say whether the two agree.
18
+ *
19
+ * @param {{ keys: string[], doorSays: Map<string, boolean>, probeProfileKey: string|null,
20
+ * ask: (key: string) => Promise<{blockers?: unknown[]}> }} input
21
+ * `ask` is the plan_run call for one product; a throw is a question that was not answered.
22
+ * @returns {Promise<{state: "pass"|"fail"|"skip", message: string, blocked?: true}>}
23
+ */
24
+ export async function planRunAgreementVerdict({ keys = [], doorSays = new Map(), probeProfileKey = null, ask }) {
25
+ if (!probeProfileKey) {
26
+ return { state: "skip", message: "no customer resolved to plan against — see the roster check" };
27
+ }
28
+ const disagreements = [];
29
+ const unanswered = [];
30
+ for (const key of keys) {
31
+ let plan;
32
+ try { plan = await ask(key); } catch (e) { unanswered.push(`${key}: ${String(e?.message ?? e).slice(0, 120)}`); continue; }
33
+ const unavailable = (plan?.blockers ?? []).some((b) => UNAVAILABLE.test(String(b)));
34
+ if (unavailable !== !doorSays.get(key)) {
35
+ disagreements.push(`${key}: describe_options=${doorSays.get(key) ? "available" : "unavailable"} plan_run=${unavailable ? "unavailable" : "available"}`);
36
+ }
37
+ }
38
+ // A drift outranks an unanswered ask, as in exitFor: the drift is actionable now. The unanswered
39
+ // products stay on the line so the report does not read as though they were compared.
40
+ if (disagreements.length) {
41
+ return { state: "fail", message: disagreements.join(" · ")
42
+ + (unanswered.length ? ` · and plan_run gave no answer for ${unanswered.length}: ${unanswered.join(" · ")}` : "") };
43
+ }
44
+ if (unanswered.length) {
45
+ return { state: "skip", blocked: true, message: `could not ask plan_run about ${unanswered.length} of ${keys.length} product(s), `
46
+ + `so those were not compared — ${unanswered.join(" · ")}` };
47
+ }
48
+ return { state: "pass", message: `${keys.length} products checked through both code paths` };
49
+ }
@@ -350,7 +350,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
350
350
  const docs = reportsOf(meta).filter((r) => existsSync(join(poolRoot, name, r.file)));
351
351
  const hasReport = docs.length > 0;
352
352
  const { bands, toneFor } = ladderOf(meta);
353
- const band = meta.overall ?? meta.verdict ?? null;
353
+ const band = meta.overall ?? null; // the rating; a meta's retired `verdict` held the reviewer's sign-off on the full-search lane
354
354
  out.push({ runId: meta.runId ?? name, account: owner, ...(owner === "generic" ? { organisation: meta.organisation ?? null } : {}),
355
355
  title: meta.title ?? meta.matter ?? name, kind: meta.kind ?? "clearance",
356
356
  // THE MARK, separate from the report's headline.
@@ -540,7 +540,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
540
540
  // hide this. Staff only: the client redaction below replaces `reason` and must drop this too,
541
541
  // or the redaction would be defeated by the field that carries the raw words.
542
542
  reasonDetail: s.reasonDetail ?? null,
543
- date: (s.updatedAt ?? "").slice(0, 10), overall: s.verdict ?? null,
543
+ date: (s.updatedAt ?? "").slice(0, 10), overall: s.tier ?? null, // the rating, never the reviewer's sign-off word
544
544
  // A live run has not been issued, so its last progress write is the honest ordering key — the
545
545
  // same role issuedAt plays for a delivered one, never presented as a finish time.
546
546
  issuedAt: typeof s.updatedAt === "string" ? s.updatedAt : null,
@@ -4006,7 +4006,7 @@ ${signedIn
4006
4006
  <div><a href="/portal">Go to the portal</a></div>
4007
4007
  <form method="post" action="/portal/logout"><button type="submit">Sign out</button></form>`
4008
4008
  : `<p>This ${escHtml(BRAND.name)} has one user: <b class="who">${escHtml(email)}</b>. Enter its passphrase.</p>
4009
- ${discarded ? `<p class="hint">A session this portal did not start, from another ${escHtml(BRAND.name)} on this address or an expired one, was set aside. Sign in below.</p>` : ""}
4009
+ ${discarded ? `<p class="hint">Your earlier sign-in has expired. Sign in again.</p>` : ""}
4010
4010
  ${error ? `<p class="err">${escHtml(error)}</p>` : ""}
4011
4011
  <form method="post" action="/portal/login">
4012
4012
  <label for="passphrase">Passphrase</label>
@@ -4130,6 +4130,10 @@ async function readFormBody(req, limitBytes = 8192) {
4130
4130
  });
4131
4131
  }
4132
4132
 
4133
+ // The brief reader's model when PORTAL_READ_MODEL is unset: a TIER, like every stage's. An exact id goes to
4134
+ // the program as that model — past a cloud's ANTHROPIC_DEFAULT_SONNET_MODEL — and codex maps tiers only.
4135
+ export const PORTAL_READ_MODEL_DEFAULT = "sonnet";
4136
+
4133
4137
  export function makeHttpHandler({ verify, limiter, service, log = () => {}, devIdentity = null, localAuth = null, static: staticHandler = null,
4134
4138
  // item 1 — PASSED IN, because this handler is its own function and the bootstrap that reads
4135
4139
  // the environment is another. The default is the Cloudflare Access header, so a caller that does
@@ -5211,7 +5215,7 @@ const PORT = PORT_CHOICE.port;
5211
5215
  // two are parameters on the shared runner for exactly this reason: `compose-read.mjs` records that
5212
5216
  // Haiku 4.5 refuses a thinking block outright (400), so inheriting the jx constants here would
5213
5217
  // have been a failure on every press.
5214
- const model = process.env.PORTAL_READ_MODEL || "claude-sonnet-5";
5218
+ const model = process.env.PORTAL_READ_MODEL || PORTAL_READ_MODEL_DEFAULT;
5215
5219
  const runner = await makeJxTurnRunner({ model, thinking: "off", lane: "the brief reader" });
5216
5220
  if (runner?.error) {
5217
5221
  // NAMES THE CONDITION, on the operator's surface. The client-facing note stays client-facing;
@@ -194,6 +194,16 @@ const TERMINAL_STATES = new Set(["delivered", "failed", "cancelled"]);
194
194
  * preflight before an unattended run remains the operational guard for that case; this closes the
195
195
  * invisibility for every failure short of it, which is the honest claim.
196
196
  */
197
+ // ── THE REVIEWER'S SIGN-OFF IS NOT THE CLEARANCE'S ANSWER (ruled 2026-09-22) ─────────────────────────
198
+ //
199
+ // The narrative-refutation stage signs the draft off CLEAR, CONDITIONAL or BLOCKING. At the top of the run
200
+ // record as `verdict` it read as the clearance's answer, competed with the rating, and a BLOCKING one was
201
+ // printed to clients as "on hold" although nothing holds delivery on it. It now lives under the stage
202
+ // that produced it, named for what it is: `review.signoff`. The rating (`tier`) is the run's headline.
203
+ // A record written before the move carries the old field; `readSignoff` reads either.
204
+ export const signoffPatch = (signoff) => ({ review: { signoff: signoff ?? null } });
205
+ export const readSignoff = (s) => s?.review?.signoff ?? s?.verdict ?? null;
206
+
197
207
  export function writeRunStatus(ctx, patch = {}, runDirOverride = null, { critical = false } = {}) {
198
208
  const runDir = runDirOverride ?? ctx?.run?.runDir;
199
209
  // — A STATE WRITE THAT CANNOT FIND ITS RUN DIRECTORY SAYS SO. It used to `return` here, in
@@ -222,6 +232,7 @@ export function writeRunStatus(ctx, patch = {}, runDirOverride = null, { critica
222
232
  const { __stateReset, ...rest } = patch;
223
233
  const old = readRunStatus(runDir);
224
234
  const merged = { ...old, ...rest, updatedAt: nowISO() };
235
+ delete merged.verdict; // the retired top-level field: a record rewritten here carries `review.signoff` alone
225
236
  if (typeof rest.stepIndex === "number" && typeof old.stepIndex === "number" && old.stepIndex > rest.stepIndex) {
226
237
  // keep the furthest step ever reached (label/n/total move together with the index)
227
238
  merged.stepIndex = old.stepIndex;
@@ -378,7 +389,7 @@ export function identitySeed() {
378
389
  // first-write-wins makes it the honest wall-clock start across any number of resumes. A resume instead
379
390
  // records itself: resumedAt (this resume's clock) + attempts (fresh run = 1, each resume +1), and
380
391
  // threads __stateReset because the resume guard has deliberately cleared a terminal sentinel. The
381
- // verdict/failedStage/reason resets stay (a resumed run owes a fresh outcome);
392
+ // review/failedStage/reason resets stay (a resumed run owes a fresh outcome);
382
393
  // recoveryAttempts/recoveryHistory stay OUT of the seed (they are the park budget's memory).
383
394
  export function seedRunStatus(ctx, { resume = false } = {}) {
384
395
  const { job, run, agent } = ctx;
@@ -412,7 +423,7 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
412
423
  stepIndex: first.index, stepLabel: first.label, stepN: first.n, stepTotal: first.total,
413
424
  currentStep: currentStepOf(first),
414
425
  lastStage: null,
415
- verdict: null,
426
+ review: null,
416
427
  url: null,
417
428
  failedStage: null,
418
429
  reason: null,
@@ -489,7 +500,7 @@ export function lineFor(s) {
489
500
  // The rollup now carries the send state loudly; clearotron-deliver flips sendPending:false on send.
490
501
  const pending = s.sendPending === true ? " — 📮 SEND PENDING (email/WhatsApp NOT yet out — run clearotron-deliver)" : "";
491
502
  if (s.state === "delivered") {
492
- const v = s.verdict ? ` (${s.verdict})` : "";
503
+ const v = s.tier ? ` (${s.tier})` : ""; // the rating, not the reviewer's sign-off word
493
504
  return `- ${head} — delivered${v}${s.url ? ` — ${s.url}` : ""}${pending}`;
494
505
  }
495
506
  if (s.state === "failed") {
@@ -1352,7 +1352,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1352
1352
  // null-guard and old rows render byte-identically.
1353
1353
  statement: verdictInfo?.statement ?? undefined,
1354
1354
  caption: fm.overall_caption || undefined, // the report's own conclusion, which the run list quotes
1355
- verdict: verdictInfo?.verdict ?? undefined,
1355
+ review: verdictInfo?.verdict ? { signoff: verdictInfo.verdict } : undefined, // the reviewer's sign-off, not the clearance's answer (ruled 2026-09-22)
1356
1356
  // doc 50 — which framework rated this run (custom vs Generic default) + its ladder, for the archive
1357
1357
  // card, the email table sort/colour and the per-customer index; absent on archived runs forever.
1358
1358
  framework: framework ? { key: framework.framework_key, title: framework.title,
@@ -98,9 +98,10 @@ export function clearanceReportData({
98
98
  auditFile: auditFile || null,
99
99
  level: { searchLevel: searchLevel ?? null, stageLabel: stageLabel ?? null },
100
100
  framework: framework ? { key: framework.framework_key, title: framework.title, bands: framework.bands.map((b) => ({ label: b.label, tone: b.tone })) } : null,
101
- // The one risk statement + its derivation — the exact record every other surface joins (spec 64).
102
- verdict: verdictInfo ? {
103
- verdict: verdictInfo.verdict ?? null,
101
+ // The rating + the one risk statement — the exact record every other surface joins (spec 64). Named
102
+ // `rating` (ruled 2026-09-22): as `verdict` it read as the clearance's answer and carried the reviewer's
103
+ // sign-off word beside the band. The sign-off stays in the review stage's own record, not the client's.
104
+ rating: verdictInfo ? {
104
105
  tier: verdictInfo.tier ?? null,
105
106
  badge: verdictInfo.badge ?? null,
106
107
  band: verdictInfo.band ?? null,
@@ -45,6 +45,7 @@
45
45
 
46
46
  import { normalizeElement, consonantSkeleton } from "./form-neighbourhood.mjs";
47
47
  import { canonicalJurisdictionCode } from "./jurisdiction-codes.mjs";
48
+ import { territoryTier } from "./territory-tiers.mjs";
48
49
 
49
50
  export const REFERENCE_SCHEMA_VERSION = 1;
50
51
 
@@ -378,6 +379,12 @@ export function inScope(entry, scopeClasses = [], scopeTerritories = []) {
378
379
  // that scenario it is five of fifteen entries, which is the difference between a bad round and a fine
379
380
  // one. Same shape as the class rule: an entry naming no territory is in scope, and a run that recorded
380
381
  // no scope cannot exclude anything.
382
+ // A WORLDWIDE SCOPE EXCLUDES NOTHING ON TERRITORY, for the reason a missing scope excludes nothing: it
383
+ // names no territory an entry could fall outside. Without this a worldwide run compared every entry
384
+ // with the word itself (`GLOBAL`), no entry names that, and the whole reference landed in `excluded`,
385
+ // an empty score that read as a clean one. Which words mean worldwide is the intake's own list
386
+ // (`territoryTier`), not a pattern restated here; `canonTerritory` still guesses nothing about `intl`.
387
+ if (scopeTerritories.some((t) => territoryTier(t) === "worldwide")) return true;
381
388
  const terr = (entry?.jurisdictions ?? []).map(canonTerritory).filter(Boolean);
382
389
  const scopeTerr = scopeTerritories.map(canonTerritory).filter(Boolean);
383
390
  if (scopeTerr.length && terr.length && !terr.some((t) => scopeTerr.includes(t))) return false;
@@ -911,7 +918,8 @@ export function readVerdict({ verdictDoc = null, knockoutFindings = null, status
911
918
  const rows = marks
912
919
  .map((m) => [m.name, [m.rating, m.ratingQualifier ? `(${m.ratingQualifier})` : null].filter(Boolean).join(" ")].filter(Boolean).join(": "))
913
920
  .filter((s) => s.includes(":"));
914
- const text = [status?.verdict ? `worst band ${status.verdict}` : null, ...rows].filter(Boolean).join(" · ");
921
+ const worst = status?.tier ?? status?.verdict ?? null; // `verdict` on a quick-search record written before the move
922
+ const text = [worst ? `worst band ${worst}` : null, ...rows].filter(Boolean).join(" · ");
915
923
  if (text) return { clean: clean(text), text, source: "knockout-findings.json + status.json — this lane writes no _driver/verdict.json", why: null };
916
924
  }
917
925
  return { clean: null, text: null, source: null,
@@ -2194,7 +2202,7 @@ export function deliveryLine(run) {
2194
2202
  }
2195
2203
  if (run.poolMeta) {
2196
2204
  const issued = run.poolMeta.issuedAt ? ` published ${run.poolMeta.issuedAt}` : " publication time not recorded";
2197
- const verdict = run.poolMeta.verdict ?? run.poolMeta.overall;
2205
+ const verdict = run.poolMeta.review?.signoff ?? run.poolMeta.verdict ?? run.poolMeta.overall;
2198
2206
  // NOT PRESERVED stays the honest answer for a pool copy with no stamp — a run archived before the
2199
2207
  // stamp existed, or one whose best-effort write failed. An absent stamp is unknown, not a refusal.
2200
2208
  return `delivered: NOT PRESERVED — this is a pool copy carrying no settle stamp, so the terminal`
@@ -55,12 +55,15 @@ export const SETTLE_SCHEMA_VERSION = 1;
55
55
  * reads a delivery date (turnaround, an SLA, a client-facing "delivered on") would have inherited it.
56
56
  * A backfiller must not compose this value at all: call `backfillSettleStamp`, which reads it.
57
57
  */
58
- export function writeSettleStamp(poolRunDir, { state, verdict = null, deliveredAt = null, runId = null, lane = null } = {}) {
58
+ // The outcome words are named for what they are: the full-search lane's reviewer `signoff` (CLEAR /
59
+ // CONDITIONAL / BLOCKING) and the quick-search lane's rating `tier`. Both once shared one `verdict` field,
60
+ // which read as the clearance's answer and held a different vocabulary on each lane.
61
+ export function writeSettleStamp(poolRunDir, { state, signoff = null, tier = null, deliveredAt = null, runId = null, lane = null } = {}) {
59
62
  if (!poolRunDir) return { written: false, reason: "no pool run directory — the run published nowhere" };
60
63
  if (!state) return { written: false, reason: "no terminal state given — a stamp with no state is the absence it would be mistaken for" };
61
64
  const path = join(poolRunDir, SETTLE_FILE);
62
65
  try {
63
- writeFileSync(path, `${JSON.stringify({ schema_version: SETTLE_SCHEMA_VERSION, state, verdict, deliveredAt, runId, lane, stampedAt: new Date().toISOString() }, null, 2)}\n`);
66
+ writeFileSync(path, `${JSON.stringify({ schema_version: SETTLE_SCHEMA_VERSION, state, ...(signoff ? { signoff } : {}), ...(tier ? { tier } : {}), deliveredAt, runId, lane, stampedAt: new Date().toISOString() }, null, 2)}\n`);
64
67
  // Group-read like every other pool file. Best-effort on its own: a stamp nobody can chmod is still
65
68
  // a stamp, and the set-GID pool already grants the group.
66
69
  try { chmodSync(path, 0o640); } catch { /* best-effort, exactly as publish's writeRO does */ }
@@ -116,7 +119,11 @@ export function backfillSettleStamp(poolRunDir, runDir, { readStatus = defaultRe
116
119
  return { written: false, reason: "status.json says delivered but carries no deliveredAt — the delivery time this backfill exists to preserve is not recorded" };
117
120
  return writeSettleStamp(poolRunDir, {
118
121
  state: status.state,
119
- verdict: status.verdict ?? null,
122
+ // A status.json written before the sign-off moved carries it as `verdict`, and on the quick-search
123
+ // lane that field held the rating; each is read back into the field it always meant.
124
+ ...((status.lane ?? (status.marks ? "knockout" : "clearance")) === "knockout"
125
+ ? { tier: status.tier ?? status.verdict ?? null }
126
+ : { signoff: status.review?.signoff ?? status.verdict ?? null }),
120
127
  deliveredAt: status.deliveredAt ?? null,
121
128
  runId: status.runId ?? null,
122
129
  lane: status.lane ?? (status.marks ? "knockout" : "clearance"),
@@ -80,7 +80,7 @@ const slimRun = (r) => ({
80
80
  markName: r.markName, ref: r.ref, classes: r.classes,
81
81
  state: r.state, stepN: r.stepN, stepTotal: r.stepTotal, stepLabel: r.stepLabel,
82
82
  lastStage: r.lastStage, // spec 64 C — the RAW stage key ("register-unit:primary-sweep"): what the run is actually doing
83
- verdict: r.verdict, statement: r.statement, caption: r.caption, // spec 64 — THE one risk statement (absent on legacy runs); caption — the report's own conclusion
83
+ tier: r.tier, statement: r.statement, caption: r.caption, // the rating, never the reviewer's sign-off; spec 64 — THE one risk statement (absent on legacy runs); caption — the report's own conclusion
84
84
  url: r.url, failedStage: r.failedStage, reason: r.reason,
85
85
  resetsAt: r.resetsAt, // rate-limit POSTPONE ONLY: when the cap window clears + the run auto-resumes (ISO)
86
86
  recoveryResumesAt: r.recoveryResumesAt ?? null, // recovery park's backoff clock (A4 split — never conflated with a provider cap)
@@ -100,7 +100,7 @@ export function buildRecentActivity({ reports = [], limit = 12 } = {}) {
100
100
  // spec 64 — a delivered run's outcome is THE one risk statement when the run carries it (band +
101
101
  // stance in one sentence), never a bare disposition word beside a severity word on another page.
102
102
  out.push({ kind: "report", ok, when: r.deliveredAt || r.updatedAt || r.startedAt || "", label: r.markName || r.slug || r.runId,
103
- outcome: ok ? (r.statement || r.verdict || "delivered") : `failed${r.failedStage ? " · " + r.failedStage : ""}`,
103
+ outcome: ok ? (r.caption || r.statement || r.tier || "delivered") : `failed${r.failedStage ? " · " + r.failedStage : ""}`,
104
104
  url: r.url || (ok && r.runId ? `${r.runId}/report.html` : "") });
105
105
  }
106
106
  // ONE explicit time key per row: a report's `when` can come from any of three stamps, and they all sort on
@@ -621,6 +621,12 @@
621
621
  "skips": 0,
622
622
  "todos": 0
623
623
  },
624
+ "a-model-of-the-current-generation-is-named-in-every-spelling.test.mjs": {
625
+ "tests": 4,
626
+ "asserts": 13,
627
+ "skips": 0,
628
+ "todos": 0
629
+ },
624
630
  "a-name-documented-in-any-example-file-is-documented.test.mjs": {
625
631
  "tests": 7,
626
632
  "asserts": 16,
@@ -711,6 +717,12 @@
711
717
  "skips": 0,
712
718
  "todos": 0
713
719
  },
720
+ "a-plan-run-that-was-not-answered-is-not-a-drift.test.mjs": {
721
+ "tests": 5,
722
+ "asserts": 16,
723
+ "skips": 0,
724
+ "todos": 0
725
+ },
714
726
  "a-pool-copy-is-not-a-refusal.test.mjs": {
715
727
  "tests": 8,
716
728
  "asserts": 28,
@@ -723,12 +735,24 @@
723
735
  "skips": 0,
724
736
  "todos": 0
725
737
  },
738
+ "a-press-that-starts-late-is-not-a-page-that-did-not-move.test.mjs": {
739
+ "tests": 5,
740
+ "asserts": 13,
741
+ "skips": 0,
742
+ "todos": 0
743
+ },
726
744
  "a-printed-command-runs-from-where-the-reader-stands.test.mjs": {
727
745
  "tests": 15,
728
746
  "asserts": 62,
729
747
  "skips": 0,
730
748
  "todos": 0
731
749
  },
750
+ "a-program-already-here-is-checked-against-the-floor.test.mjs": {
751
+ "tests": 9,
752
+ "asserts": 38,
753
+ "skips": 1,
754
+ "todos": 0
755
+ },
732
756
  "a-project-cannot-break-the-tree-it-joins.test.mjs": {
733
757
  "tests": 13,
734
758
  "asserts": 37,
@@ -933,6 +957,12 @@
933
957
  "skips": 0,
934
958
  "todos": 0
935
959
  },
960
+ "a-run-records-the-tier-it-asked-for.test.mjs": {
961
+ "tests": 5,
962
+ "asserts": 18,
963
+ "skips": 0,
964
+ "todos": 0
965
+ },
936
966
  "a-run-records-the-tool-that-served-it.test.mjs": {
937
967
  "tests": 13,
938
968
  "asserts": 33,
@@ -1253,7 +1283,7 @@
1253
1283
  },
1254
1284
  "an-absent-portal-bundle-is-not-a-stale-one.test.mjs": {
1255
1285
  "tests": 4,
1256
- "asserts": 7,
1286
+ "asserts": 13,
1257
1287
  "skips": 0,
1258
1288
  "todos": 0
1259
1289
  },
@@ -1271,7 +1301,7 @@
1271
1301
  },
1272
1302
  "an-archived-run-says-whether-it-was-delivered.test.mjs": {
1273
1303
  "tests": 8,
1274
- "asserts": 42,
1304
+ "asserts": 44,
1275
1305
  "skips": 0,
1276
1306
  "todos": 0
1277
1307
  },
@@ -2452,8 +2482,8 @@
2452
2482
  "todos": 0
2453
2483
  },
2454
2484
  "engine-probe.test.mjs": {
2455
- "tests": 34,
2456
- "asserts": 166,
2485
+ "tests": 35,
2486
+ "asserts": 172,
2457
2487
  "skips": 0,
2458
2488
  "todos": 0
2459
2489
  },
@@ -2465,7 +2495,7 @@
2465
2495
  },
2466
2496
  "engine.anthropic.test.mjs": {
2467
2497
  "tests": 65,
2468
- "asserts": 239,
2498
+ "asserts": 245,
2469
2499
  "skips": 0,
2470
2500
  "todos": 0
2471
2501
  },
@@ -3412,8 +3442,8 @@
3412
3442
  "todos": 0
3413
3443
  },
3414
3444
  "pipeline.mock.test.mjs": {
3415
- "tests": 98,
3416
- "asserts": 820,
3445
+ "tests": 99,
3446
+ "asserts": 827,
3417
3447
  "skips": 0,
3418
3448
  "todos": 0
3419
3449
  },
@@ -3665,7 +3695,7 @@
3665
3695
  },
3666
3696
  "progress.test.mjs": {
3667
3697
  "tests": 26,
3668
- "asserts": 96,
3698
+ "asserts": 97,
3669
3699
  "skips": 0,
3670
3700
  "todos": 0
3671
3701
  },
@@ -3880,8 +3910,8 @@
3880
3910
  "todos": 0
3881
3911
  },
3882
3912
  "reference-score.test.mjs": {
3883
- "tests": 63,
3884
- "asserts": 213,
3913
+ "tests": 65,
3914
+ "asserts": 221,
3885
3915
  "skips": 0,
3886
3916
  "todos": 0
3887
3917
  },
@@ -4048,8 +4078,8 @@
4048
4078
  "todos": 0
4049
4079
  },
4050
4080
  "release-pipeline.test.mjs": {
4051
- "tests": 120,
4052
- "asserts": 452,
4081
+ "tests": 121,
4082
+ "asserts": 456,
4053
4083
  "skips": 0,
4054
4084
  "todos": 0
4055
4085
  },
@@ -4133,7 +4163,7 @@
4133
4163
  },
4134
4164
  "report-data.test.mjs": {
4135
4165
  "tests": 5,
4136
- "asserts": 55,
4166
+ "asserts": 56,
4137
4167
  "skips": 0,
4138
4168
  "todos": 0
4139
4169
  },
@@ -4715,7 +4745,7 @@
4715
4745
  },
4716
4746
  "status-snapshot.test.mjs": {
4717
4747
  "tests": 6,
4718
- "asserts": 39,
4748
+ "asserts": 40,
4719
4749
  "skips": 0,
4720
4750
  "todos": 0
4721
4751
  },
@@ -5121,6 +5151,12 @@
5121
5151
  "skips": 0,
5122
5152
  "todos": 0
5123
5153
  },
5154
+ "the-engine-floor-can-run-the-current-top-tier.test.mjs": {
5155
+ "tests": 2,
5156
+ "asserts": 5,
5157
+ "skips": 0,
5158
+ "todos": 0
5159
+ },
5124
5160
  "the-engine-step-cannot-loop-on-a-menu-nothing-changed.test.mjs": {
5125
5161
  "tests": 17,
5126
5162
  "asserts": 45,
@@ -5225,7 +5261,7 @@
5225
5261
  },
5226
5262
  "the-install-pages-describe-the-install-this-build-performs.test.mjs": {
5227
5263
  "tests": 11,
5228
- "asserts": 89,
5264
+ "asserts": 90,
5229
5265
  "skips": 0,
5230
5266
  "todos": 0
5231
5267
  },
@@ -5374,8 +5410,8 @@
5374
5410
  "todos": 0
5375
5411
  },
5376
5412
  "the-portal-shows-only-approved-strings.test.mjs": {
5377
- "tests": 15,
5378
- "asserts": 32,
5413
+ "tests": 20,
5414
+ "asserts": 46,
5379
5415
  "skips": 0,
5380
5416
  "todos": 0
5381
5417
  },
@@ -5517,6 +5553,12 @@
5517
5553
  "skips": 0,
5518
5554
  "todos": 0
5519
5555
  },
5556
+ "the-reviewers-sign-off-is-not-the-clearances-answer.test.mjs": {
5557
+ "tests": 3,
5558
+ "asserts": 19,
5559
+ "skips": 0,
5560
+ "todos": 0
5561
+ },
5520
5562
  "the-reviewing-pass-reads-the-lines-a-client-sees-first.test.mjs": {
5521
5563
  "tests": 10,
5522
5564
  "asserts": 27,
@@ -5591,7 +5633,7 @@
5591
5633
  },
5592
5634
  "the-short-surfaces-quote-the-report.test.mjs": {
5593
5635
  "tests": 3,
5594
- "asserts": 7,
5636
+ "asserts": 9,
5595
5637
  "skips": 0,
5596
5638
  "todos": 0
5597
5639
  },
@@ -5717,7 +5759,7 @@
5717
5759
  },
5718
5760
  "tokens.test.mjs": {
5719
5761
  "tests": 10,
5720
- "asserts": 51,
5762
+ "asserts": 54,
5721
5763
  "skips": 0,
5722
5764
  "todos": 0
5723
5765
  },