clearotron 0.4.0 → 0.4.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/build-info.json +2 -2
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -2
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +1 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +18 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +18 -18
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +1 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +31 -4
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/openai-agent.mjs +100 -17
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +3 -3
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +94 -12
- package/driver/named-band.mjs +2 -0
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +46 -33
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +1 -1
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +1 -1
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +18 -1
- package/driver/register-plan.mjs +71 -17
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/scope-facts.mjs +11 -3
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +27 -1
- package/driver/stages.mjs +12 -12
- package/driver/suite-census.json +175 -31
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/package.json +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +4 -1
- package/providers/_shared/enumerate.mjs +61 -22
- package/providers/_shared/execute-plan.mjs +2 -0
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +17 -2
- package/providers/signa/src/core.js +234 -37
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +16 -1
- package/scripts/freeze-example-run.mjs +2 -2
- package/shared/scope.mjs +18 -2
|
@@ -70,7 +70,7 @@ export function parseFrameDiff(raw) {
|
|
|
70
70
|
if (d.remedy != null) out.remedy = parseRemedy(d.remedy);
|
|
71
71
|
// ── THE ASK CONTRACT AT ASK-RAISE (charter P2e) ───────────────────────────────────────────────
|
|
72
72
|
// A firing directive is a REQUEST TO SEARCH SOMETHING. Until now the parser accepted one whose
|
|
73
|
-
// item was a display label ("
|
|
73
|
+
// item was a display label ("WAVA (famous cpg snack, …)") with no remedy, and the refusal came
|
|
74
74
|
// hours later at reopen — by which time the session that could have restated it had exited. Four
|
|
75
75
|
// of the five directives on the 2026-07-29 run died exactly there: loud (coverage rows, the clamp,
|
|
76
76
|
// the audit trail) but terminal, with the head noun of a perfect one-query term sitting in the
|
|
@@ -143,7 +143,7 @@ export function undispatchableThrow(offenders) {
|
|
|
143
143
|
* Every FIRING directive in a frame-diff document that cannot be dispatched — read LENIENTLY, from
|
|
144
144
|
* raw text or an already-parsed object. This is the enumeration the gateway hint and the pipeline's
|
|
145
145
|
* exhausted-ladder disclosure both run against the artifact ON DISK, because the fail string they
|
|
146
|
-
* would otherwise scrape is cut at 160 characters (verify.mjs:381) and cannot carry more than the
|
|
146
|
+
* would otherwise scrape is cut at 160 characters (verify.mjs:381 commonLawMeaningSeat) and cannot carry more than the
|
|
147
147
|
* first offender. Lenient on purpose: the artifact reaching these two callers has ALREADY failed
|
|
148
148
|
* validation, so it may also carry an unknown key or a bad gap flag — refusing to enumerate then
|
|
149
149
|
* would lose exactly the disclosures this exists to keep. Returns [{item, layer, severity, why}].
|
|
@@ -190,7 +190,7 @@ export function firingDirectivesLenient(rawOrDoc) {
|
|
|
190
190
|
*
|
|
191
191
|
* SCOPE IS DELIBERATELY THE VARIANT LAYER (plus any layer's own remedy):
|
|
192
192
|
* * VARIANT — the evidenced class. The directive says "search this near-form", so its item stands
|
|
193
|
-
* where a mark term stands, and a label there ("
|
|
193
|
+
* where a mark term stands, and a label there ("WAVA (famous cpg snack, …)") is the whole defect:
|
|
194
194
|
* it dispatches as a nil search that reads CLEAN, or it dies unasked at reopen.
|
|
195
195
|
* * FIELD — untouched. A class-gap directive whose item names no parseable class already has a
|
|
196
196
|
* principled ending: deriveDirectiveRemedy returns null and the directive is DISCLOSED rather
|
package/driver/framework.mjs
CHANGED
|
@@ -139,6 +139,28 @@ export function aboveLowestBand(manifest, label) {
|
|
|
139
139
|
return i !== -1 && i < manifest.bands.length - 1;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** The tones of the rungs whose findings are "material": medium risk or higher. A rung's tone is its own
|
|
143
|
+
* statement of its risk, so the line holds on any client's ladder. A line drawn by POSITION ("above the
|
|
144
|
+
* lowest band") did not: the bottom rung is "low" on a four-rung ladder and "minimal" on a five-rung one,
|
|
145
|
+
* so one client's low-risk rung was checked and another's was not (owner's ruling, 2026-10-09). */
|
|
146
|
+
export const MATERIAL_TONES = Object.freeze(["severe", "high", "medium"]);
|
|
147
|
+
|
|
148
|
+
/** True when the label is a band whose tone is medium risk or higher — the "material finding" predicate. */
|
|
149
|
+
export function materialBand(manifest, label) {
|
|
150
|
+
return MATERIAL_TONES.includes(bandTone(manifest, label));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The material bands' labels, highest first: what a dispatch names when it says which findings a check covers. */
|
|
154
|
+
export const materialBandLabels = (manifest) => (manifest?.bands ?? [])
|
|
155
|
+
.filter((b) => MATERIAL_TONES.includes(b?.tone)).map((b) => b.label);
|
|
156
|
+
|
|
157
|
+
/** The material bands as a reader's list — "Very High, High or Moderate" — or null when the ladder names none. */
|
|
158
|
+
export function materialBandPhrase(manifest) {
|
|
159
|
+
const xs = materialBandLabels(manifest);
|
|
160
|
+
if (!xs.length) return null;
|
|
161
|
+
return xs.length === 1 ? xs[0] : `${xs.slice(0, -1).join(", ")} or ${xs[xs.length - 1]}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
/** The most severe band label among the given labels (unknown labels ignored); null when none are bands. */
|
|
143
165
|
export function worstBand(manifest, labels) {
|
|
144
166
|
let best = -1;
|
package/driver/gateway.mjs
CHANGED
|
@@ -246,7 +246,8 @@ import { REGISTER_ENUMERATE_TOOL, SUPPLEMENTAL_LANE_STEERING } from "./stages.mj
|
|
|
246
246
|
// same thing to the exit-1 rescue below as it does to the register taint chain that reads the very
|
|
247
247
|
// same rows off disk. Acyclic: register-taint.mjs imports node builtins only.
|
|
248
248
|
import { isTaintRow } from "./register-taint.mjs";
|
|
249
|
-
import { recordDispatch } from "./dispatch-record.mjs";
|
|
249
|
+
import { recordDispatch, streamFilePath } from "./dispatch-record.mjs";
|
|
250
|
+
import { anyResultErrored, endingFields } from "./engine/session-record.mjs"; // a session that reached an error result is not an ok attempt, whatever its last result said
|
|
250
251
|
import { recordBestDraft } from "./best-draft.mjs";
|
|
251
252
|
//: the allowlisted progress-quantity extractor. Acyclic — repairs.mjs imports node builtins only
|
|
252
253
|
// (fs/crypto/path). Used only when the validator did not stamp its own count; never a digit hunt.
|
|
@@ -309,6 +310,37 @@ export function isTimeout({ killed, code, wall, stderr = "", timeoutSec }) {
|
|
|
309
310
|
// on a parsed 0-usage object. A slow-but-working model that times out returns a status_timeout envelope WITH
|
|
310
311
|
// usage (admitted) → NOT a wedge. Kept pure + exported so the classifier is unit-tested (it must not misfire on
|
|
311
312
|
// admitted turns, and it MUST fire on the null-usage hard-kill that the production incident actually produced).
|
|
313
|
+
/**
|
|
314
|
+
* Did this attempt spend its whole derived budget, exit cleanly, and still fail? PURE.
|
|
315
|
+
*
|
|
316
|
+
* THE CASE THIS NAMES, measured on one round: an attempt ran 958.6 seconds against a derived limit of 900,
|
|
317
|
+
* exited 0, wrote nothing, and was recorded as `missing_file` naming its own output. It was working the whole
|
|
318
|
+
* time — 35,341 output tokens, 119 tool calls, 947.7 seconds of active time. The hard kill is the limit plus
|
|
319
|
+
* 60, so it finished 1.4 seconds under it.
|
|
320
|
+
*
|
|
321
|
+
* WHY THE CLASSIFICATION ABOVE IS RIGHT AND THE RECORD WAS STILL WRONG. `isTimeout`'s third signal needs a
|
|
322
|
+
* non-zero exit as well as a wall past the budget, and the program exited 0 — so it is not a timeout, and
|
|
323
|
+
* nothing here argues it should be. The defect is that the row then read `missing_file`, which names an
|
|
324
|
+
* artifact and sends the next reader to look for a path bug or a race. The two numbers that say otherwise
|
|
325
|
+
* were both on the row — the wall beside the derived limit — and nobody was pointed at them.
|
|
326
|
+
*
|
|
327
|
+
* SO THIS RECORDS A FACT AND MOVES NO POLICY. It does not feed `isTimeout`, it does not change which retry
|
|
328
|
+
* the attempt earns, and the fail class is untouched. Whether a clean exit at the budget should also earn the
|
|
329
|
+
* longer retry a timeout earns is a question for the owner, and the issue that asked for this says so
|
|
330
|
+
* explicitly: it may be right that a clean exit is not a timeout. This is the record it would be decided on.
|
|
331
|
+
*
|
|
332
|
+
* Three-valued, like the model fields on the same row: null when the budget is unknown, so "nobody derived a
|
|
333
|
+
* limit for this stage" stays visibly different from "it did not spend it".
|
|
334
|
+
*/
|
|
335
|
+
export function spentBudgetCleanly({ fail, killed, code, wall, derivedLimitSec }) {
|
|
336
|
+
if (!fail) return false; // a passing attempt is not this shape
|
|
337
|
+
if (!Number.isFinite(derivedLimitSec) || derivedLimitSec <= 0) return null;
|
|
338
|
+
if (!Number.isFinite(wall)) return null;
|
|
339
|
+
if (killed) return false; // a kill is the timeout path, not this one
|
|
340
|
+
if (code !== 0) return false; // a dirty exit at the budget IS a timeout already
|
|
341
|
+
return wall >= derivedLimitSec;
|
|
342
|
+
}
|
|
343
|
+
|
|
312
344
|
export function isLaneWedge(fail, usage) {
|
|
313
345
|
if (fail !== "timeout") return false;
|
|
314
346
|
const moved = (usage?.input || 0) + (usage?.output || 0) + (usage?.cacheRead || 0) + (usage?.cacheWrite || 0);
|
|
@@ -389,9 +421,35 @@ export function toolGauge(turn) {
|
|
|
389
421
|
// engine cannot report", the same distinction toolWaitByTool draws one line up. RECORDING ONLY:
|
|
390
422
|
// toolWaitMs and the per-tool split are exactly what they were, because the kill clock reads them.
|
|
391
423
|
toolWaitUnmeasurable: Array.isArray(turn?.toolWaitUnmeasurable) ? [...turn.toolWaitUnmeasurable] : null, ...toolCallCounts(turn),
|
|
424
|
+
// The calls by tool name, beside the total, so a tool server's own log can be set against what the
|
|
425
|
+
// model asked for. An OBJECT is a measurement (`{}`: none); null is "this engine cannot report".
|
|
426
|
+
toolCallsByName: turn?.toolCallsByName && typeof turn.toolCallsByName === "object" && !Array.isArray(turn.toolCallsByName)
|
|
427
|
+
? { ...turn.toolCallsByName } : null,
|
|
392
428
|
};
|
|
393
429
|
}
|
|
394
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Whether the attempt is OK: the driver's verdict (`fail`) AND no result event of the session carried
|
|
433
|
+
* `is_error`. A session the vendor refused and a restart recovered ends in a successful LAST result, so
|
|
434
|
+
* the retry ladder (which reads that last result, unchanged) accepts it; the record says what happened.
|
|
435
|
+
* Readers that ask "did the stage fail" read `fail`, which this does not touch. PURE.
|
|
436
|
+
*/
|
|
437
|
+
export function attemptOk(fail, turn) {
|
|
438
|
+
return !fail && !anyResultErrored(turn?.session);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The stream record as a row carries it: the file relative to the run, as the dispatch record names its
|
|
443
|
+
* file, and the size — or why it was not kept. null when the engine kept none or ran no session. PURE.
|
|
444
|
+
*/
|
|
445
|
+
export function streamMeta(runDir, stream) {
|
|
446
|
+
if (!stream || typeof stream !== "object" || !stream.file) return null;
|
|
447
|
+
const dir = String(runDir ?? "");
|
|
448
|
+
const file = dir && String(stream.file).startsWith(dir) ? String(stream.file).slice(dir.length).replace(/^[\\/]+/, "") : String(stream.file);
|
|
449
|
+
return { file, present: stream.present === true, bytes: Number.isFinite(stream.bytes) ? stream.bytes : null,
|
|
450
|
+
...(stream.reason ? { reason: stream.reason } : {}) };
|
|
451
|
+
}
|
|
452
|
+
|
|
395
453
|
export function tokensPerSec(usage, wall) {
|
|
396
454
|
const w = Number(wall);
|
|
397
455
|
if (!usage || !Number.isFinite(w) || w <= 0) return null;
|
|
@@ -1073,7 +1131,10 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1073
1131
|
// run-integrity.mjs for why it is not a manifest file and why the append-only journals are excluded.
|
|
1074
1132
|
const integrityBefore = frozenSnapshot(runDir);
|
|
1075
1133
|
const turn = await engine.runTurn({ agent, sessionKey: key, message: effMessage, grantDispatch: base, model, thinking, timeoutSec: effTimeout, resumeRef: warm ? lastSessionRef : undefined, codexHome: stageCodexHome, mcpConfig: gatherMcpConfig, allowedTools: gatherAllowedTools, seatWrites: gatherSeatWrites, skillsDir: engineSkillsDir, skillsGrantRoots: engineSkillsGrantRoots, profilesDir: profilesStoreDir, resolveSkill: engineResolveSkill, runDir, stallSec,
|
|
1076
|
-
progressFiles: files
|
|
1134
|
+
progressFiles: files, // the no-progress watchdog's artifact-advance signal (anthropic-agent; other adapters ignore it)
|
|
1135
|
+
// The raw stream of this session, beside its dispatch record (engine/session-record.mjs). The engines
|
|
1136
|
+
// keep it while the disk has room and say on the tuple's `stream` what landed.
|
|
1137
|
+
streamFile: streamFilePath(runDir, name, attempt) });
|
|
1077
1138
|
const settledAt = Date.now(); //: zero point of the wall-rescue quiescence clock, read before anything else
|
|
1078
1139
|
// LOG-ONLY. Nothing here can fail a turn: the claim that the frozen set does not change across a seat
|
|
1079
1140
|
// turn is READ from the writers' call sites and not yet MEASURED on a running system, and arming an
|
|
@@ -1294,7 +1355,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1294
1355
|
const rt = await engine.runTurn({ agent, sessionKey: key, message: repairMessage, model, thinking,
|
|
1295
1356
|
timeoutSec: effTimeout, resumeRef: repairRef, codexHome: stageCodexHome, mcpConfig: gatherMcpConfig, allowedTools: gatherAllowedTools,
|
|
1296
1357
|
skillsDir: engineSkillsDir, skillsGrantRoots: engineSkillsGrantRoots, profilesDir: profilesStoreDir, resolveSkill: engineResolveSkill, runDir, stallSec,
|
|
1297
|
-
progressFiles: files });
|
|
1358
|
+
progressFiles: files, streamFile: streamFilePath(runDir, name, attempt, formRepairsUsed + 1) });
|
|
1298
1359
|
repairRef = rt.sessionRef ?? repairRef;
|
|
1299
1360
|
// A KILLED REPAIR TURN'S BYTES ARE NOT STAGE TRUTH. Same doctrine as the exit-1 rescue below: a
|
|
1300
1361
|
// stage validator is a SHAPE check, not a completeness proof, and a torn draft can pass its own
|
|
@@ -1338,8 +1399,9 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1338
1399
|
repair: formRepairsUsed, repairOf: MAX_FORM_REPAIRS, repairOutcome: outcome, fail: triggeredBy,
|
|
1339
1400
|
repairTarget: rel(target), // the file the repair was AIMED at (often a sibling, not the expectFile)
|
|
1340
1401
|
dispatch: repairDispatch, // — the verbatim message this repair turn was given
|
|
1341
|
-
stopReason: rt.json?.stopReason ?? undefined,
|
|
1402
|
+
stopReason: rt.json?.stopReason ?? undefined, ...endingFields(rt.json), // — and what ended it, in the program's words on an error
|
|
1342
1403
|
killed: rt.killed || undefined, signals: rt.signals ?? undefined,
|
|
1404
|
+
session: rt.session ?? null, stream: streamMeta(runDir, rt.stream), // — the repair session's own record, as on the attempt row
|
|
1343
1405
|
warm: true, // a repair turn always RESUMES the session that wrote the defect
|
|
1344
1406
|
runId: rt.json?.runId, engineSummary: rt.json?.summary, usage: rt.usage,
|
|
1345
1407
|
selfReportContradicted: (outcome !== "repaired" && rt.json?.status === "ok") || undefined, //
|
|
@@ -1707,7 +1769,13 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1707
1769
|
// attempt produce what the stage exists to produce". Sixty report-card attempts on round 892dd88e
|
|
1708
1770
|
// journalled status:"ok", summary:"success", code:0, wrote:true and output.present:true beside a
|
|
1709
1771
|
// populated `fail`, and a watcher keyed on the headline saw sixty successes.
|
|
1710
|
-
|
|
1772
|
+
// — AND NOT WHEN THE SESSION REACHED AN ERROR RESULT, whatever its last result said: a refusal a
|
|
1773
|
+
// restart recovered is a session that was refused (engine/session-record.mjs). `fail` is unchanged,
|
|
1774
|
+
// so the ladder and every reader asking "did the stage fail" read exactly what they read before.
|
|
1775
|
+
ok: attemptOk(fail, turn),
|
|
1776
|
+
// What the session went through — every result, classifier cut, refusal and restart, with when —
|
|
1777
|
+
// and where its raw stream is, or why it was not kept. null where the engine cannot report.
|
|
1778
|
+
session: turn.session ?? null, stream: streamMeta(runDir, turn.stream),
|
|
1711
1779
|
// The engine's claim about itself, KEPT and renamed to say whose claim it is. A self-report is
|
|
1712
1780
|
// evidence — deleting it would destroy the only record of what the seat believed it had done — and
|
|
1713
1781
|
// it is not a verdict. Still three-valued (ok | timeout | error). Nothing in the driver reads either
|
|
@@ -1743,7 +1811,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1743
1811
|
laneWaitMs, // WS-C turn-cap queue time — the live overlap validation reads this
|
|
1744
1812
|
// A6: the provider's stop reason, verbatim — the max_tokens fault line is countable from the
|
|
1745
1813
|
// rows alone (fail carries the named fault; this field carries the raw discriminator).
|
|
1746
|
-
stopReason: json?.stopReason ?? undefined,
|
|
1814
|
+
stopReason: json?.stopReason ?? undefined, ...endingFields(json), // — and beside it what ended the session: its kind, the vendor's status, and on an error the program's own words (session-record.mjs)
|
|
1747
1815
|
// taint durability (copper-lattice): the kill discriminators survive to disk — register-taint.mjs
|
|
1748
1816
|
// reads these rows across process restarts (--resume), where the in-memory turn object is gone.
|
|
1749
1817
|
// `followup` matters too: a followup success PATCHES the prior session's output, it never
|
|
@@ -1785,6 +1853,11 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1785
1853
|
// measure" stays visibly different from "this turn called no tools" — see toolGauge.
|
|
1786
1854
|
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1787
1855
|
inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
|
|
1856
|
+
// …and whether this attempt spent that limit and exited cleanly anyway. Written UNCONDITIONALLY and
|
|
1857
|
+
// three-valued, beside the two numbers it is about, so a failure that had used its whole budget is
|
|
1858
|
+
// not left looking like an artifact problem. It names a fact; it moves no retry policy. See
|
|
1859
|
+
// `spentBudgetCleanly`.
|
|
1860
|
+
budgetSpentCleanly: spentBudgetCleanly({ fail, killed, code, wall, derivedLimitSec: derivedLimit?.sec }),
|
|
1788
1861
|
// AD-4 emitted-vs-landed, UNCONDITIONAL (was success-only, which made a failed attempt's mid-write
|
|
1789
1862
|
// artifact invisible): `output` = what LANDED on disk after this attempt (null when the stage has no
|
|
1790
1863
|
// expected file); `wrote` = whether THIS attempt emitted it (see the computation above the runDir
|
|
@@ -1815,7 +1888,9 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1815
1888
|
// from "retries not recorded here"). Best-effort like every telemetry write.
|
|
1816
1889
|
try {
|
|
1817
1890
|
runLog(runDir, {
|
|
1818
|
-
event: "attempt", stage: name, attempt, of: maxRetries + 1, ok:
|
|
1891
|
+
event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: attemptOk(fail, turn), fail: fail ?? null,
|
|
1892
|
+
// The spine carries the one-word answer and the pointer; the per-stage row carries the record.
|
|
1893
|
+
interrupted: turn.session?.interrupted ?? null, stream: streamMeta(runDir, turn.stream)?.file ?? null,
|
|
1819
1894
|
//: the spine carries the same pair as the per-stage log, or the two disagree about what
|
|
1820
1895
|
// ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
|
|
1821
1896
|
model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
|
|
@@ -1850,6 +1925,9 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1850
1925
|
// — the spine carries them too, or a round has to join two files to ask why a stage was slow.
|
|
1851
1926
|
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1852
1927
|
inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
|
|
1928
|
+
// …and the same fact on this row, because one of the two carrying it records half the runs and
|
|
1929
|
+
// reads as done. Three-valued and beside the numbers it is about. See `spentBudgetCleanly`.
|
|
1930
|
+
budgetSpentCleanly: spentBudgetCleanly({ fail, killed, code, wall, derivedLimitSec: derivedLimit?.sec }),
|
|
1853
1931
|
formRepairs: formRepairsThisAttempt || undefined, //, see the stage row above
|
|
1854
1932
|
});
|
|
1855
1933
|
} catch { /* telemetry best-effort — never fail a turn over a journal line */ }
|
|
@@ -3037,7 +3115,7 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
|
|
|
3037
3115
|
// run the driver stamped as form-required, and the stamp is conditional. On an unstamped run
|
|
3038
3116
|
// validators.registerFindings demands the table exactly as it did before and emits this label,
|
|
3039
3117
|
// so dropping the arm left the one lane that can still fire it with a generic hint.
|
|
3040
|
-
hint = "the file has a findings heading plus a Coverage ledger with a status row (confirmed-clean / coverage-limited / deferred)"
|
|
3118
|
+
hint = "the file has a findings heading plus a Coverage ledger with a status row (confirmed-clean / coverage-limited / deferred)"; // the common-law clause went with that lane's status gate (ruling 2026-10-01)
|
|
3041
3119
|
} else if (/negative-results|coverage-ledger|audit-trail|findings-heading/.test(lastFail)) {
|
|
3042
3120
|
hint = "the findings file carries ALL required sections: a findings heading, the Negative results matrix " +
|
|
3043
3121
|
"(every variant × platform row), the Coverage ledger with a status row, and the Audit trail call log";
|
|
@@ -3114,7 +3192,7 @@ export function isFormClassFail(fail) {
|
|
|
3114
3192
|
return FORM_CLASS_RE.test(fail ?? "");
|
|
3115
3193
|
}
|
|
3116
3194
|
// How many in-dispatch repairs one stage run may buy. TWO, and the number has a cause: parseFrameDiff
|
|
3117
|
-
// is FAIL-FAST — it throws on the first bad severity (frame-diff-model.mjs:66) before it ever collects
|
|
3195
|
+
// is FAIL-FAST — it throws on the first bad severity (frame-diff-model.mjs:66 parseFrameDiff) before it ever collects
|
|
3118
3196
|
// the undispatchable directives (:100) — so ONE artifact carrying both defects surfaces them
|
|
3119
3197
|
// SEQUENTIALLY. That is exactly what the 08-02 frame-diff ladder did (severity on a1, undispatchable
|
|
3120
3198
|
// on a2), and a cap of 1 would have handed the second one straight back to the ladder. The codebase
|
|
@@ -3201,7 +3279,7 @@ const MAX_FORM_REPAIRS = 2;
|
|
|
3201
3279
|
// issue. The coverage-judgment family was COLD-ONLY — WARM_ELIGIBLE_RE carried seven coverage STRUCTURE
|
|
3202
3280
|
// tokens and not one `coverage_clean_*` — so every retry re-dispatched a fresh session that re-read a
|
|
3203
3281
|
// 1.9 MB band and re-derived a 160 KB document instead of editing it. The stage's own measured profile
|
|
3204
|
-
// (repair-contract.mjs
|
|
3282
|
+
// (repair-contract.mjs, 'WHY THIS EXISTS') is 105,747 out FAIL → 137,519 out FAIL → 36,362 out PASS, and the attempt
|
|
3205
3283
|
// that passed is the one that PATCHED. A cold ladder never patches.
|
|
3206
3284
|
// WHY THEY ARE SAFE TO WARM, on the same argument made one gate over: findCoverageFormViolations
|
|
3207
3285
|
// emits both tokens only over rows the DRIVER wrote from the frozen plan and the plan-execution
|
|
@@ -3605,11 +3683,15 @@ export function warmPatchMessage(lastFail, expectFile, { supplementalLane = fals
|
|
|
3605
3683
|
* calls the program refused (Claude's own denials; Codex's tool-server calls refused before reaching their
|
|
3606
3684
|
* server). `commandToolCalls`: calls to a tool that runs a command, which no Claude stage is offered, so
|
|
3607
3685
|
* zero is the expected reading; Codex keeps its shell and reports null. `mcpToolCalls`: the tool-server
|
|
3608
|
-
* calls Codex completed, its only count of calls made, since its `toolCalls` stays null.
|
|
3686
|
+
* calls Codex completed, its only count of calls made, since its `toolCalls` stays null.
|
|
3687
|
+
* `toolCallsErroredByName`: the tool-server calls that came back as an error, by tool name, so a stage that
|
|
3688
|
+
* had a call refused and made it again says so where its row otherwise reads ok on one attempt. Null, as in
|
|
3609
3689
|
* `toolGauge`, is "this engine does not report", never zero. RECORDING ONLY. It sits at the end of this
|
|
3610
3690
|
* file so that adding it moved no line another file cites.
|
|
3611
3691
|
*/
|
|
3612
3692
|
function toolCallCounts(turn) {
|
|
3613
3693
|
const n = (v) => (Number.isFinite(v) && v >= 0 ? v : null);
|
|
3614
|
-
|
|
3694
|
+
const errored = turn?.toolCallsErroredByName;
|
|
3695
|
+
return { toolCallsRefused: n(turn?.toolCallsRefused), commandToolCalls: n(turn?.commandToolCalls), mcpToolCalls: n(turn?.mcpToolCalls),
|
|
3696
|
+
toolCallsErroredByName: errored && typeof errored === "object" && !Array.isArray(errored) ? { ...errored } : null };
|
|
3615
3697
|
}
|
package/driver/named-band.mjs
CHANGED
|
@@ -129,6 +129,8 @@ export function parseNamedBand(raw) {
|
|
|
129
129
|
// truncate the covered_by pointers with the reason's 400-char cap. Old bands carry neither key.
|
|
130
130
|
...(b.class_counts && typeof b.class_counts === "object" && !Array.isArray(b.class_counts) ? { class_counts: b.class_counts } : {}),
|
|
131
131
|
...(Array.isArray(b.covered_by) && b.covered_by.length ? { covered_by: b.covered_by } : {}),
|
|
132
|
+
// what the total counts, when the register said — carried like the counts it qualifies
|
|
133
|
+
...(typeof b.total_counts === "string" ? { total_counts: b.total_counts } : {}),
|
|
132
134
|
// — THE REFUSAL STAMP SURVIVES THE PROJECTION. `execute-plan.mjs` writes an errored slice
|
|
133
135
|
// as `{state:"incomplete", total_hits:0, fetched:0, error:true}` and says why in its own header:
|
|
134
136
|
// "the error:true stamp (a provider error is never confusable with a sanctioned crowd)". The
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.0",
|
|
5
|
+
"version": "0.4.1-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
package/driver/phase0.mjs
CHANGED
|
@@ -76,15 +76,26 @@ const boundName = (s) => String(s).slice(0, PLAN_MAX_NAME_LENGTH);
|
|
|
76
76
|
// Refless jobs (ref/tmp absent — allowed since the intake relaxation) get `noref<6-hex>-<mark>`: the hash is
|
|
77
77
|
// of job.id, the sanitized email message-id and the queue's dedup key, so the slug stays unique across
|
|
78
78
|
// same-name marks AND stable across webhook re-delivery + resume. Refed jobs are byte-identical to before.
|
|
79
|
+
//
|
|
80
|
+
// THE SLUG IS THE FIRST PART OF THE RUN ID, AND THE RUN ID IS A URL PATH SEGMENT IN ASCII. Every route that
|
|
81
|
+
// serves a run, and every link a delivery carries, spells the run id as `[A-Za-z0-9._-]`
|
|
82
|
+
// (publish/index.mjs RUN_SEGMENT_RE, the portal's own gate), and refuses anything else: a link the portal
|
|
83
|
+
// would refuse is not built at all. `kebab` keeps every Unicode letter, so a mark written in Greek,
|
|
84
|
+
// Cyrillic, Han or with a letter such as Ł gave a run id no route accepts, and its run was delivered with
|
|
85
|
+
// no report link on any surface. So the mark's part of the slug keeps only what the routes accept. A name
|
|
86
|
+
// whose kebab is already ASCII is untouched, byte for byte, which is every run id minted before this; a
|
|
87
|
+
// name with nothing ASCII left reads `mark`, the knockout slug's own word for that case. The reference
|
|
88
|
+
// prefix and the codename keep the run id unique, as they always have: the mark only makes it readable.
|
|
89
|
+
const asciiMark = (slug) => slug.replace(/[^a-z0-9-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "") || "mark";
|
|
79
90
|
export function deriveSlug(job) {
|
|
80
91
|
const raw = boundName(String(job.ref ?? job.tmp ?? "").toLowerCase().replace(/[^a-z0-9]/g, ""));
|
|
81
92
|
const mark = kebab(boundName(job.markName ?? job.name ?? job.marks?.[0]?.name ?? "mark"));
|
|
82
93
|
if (!raw) {
|
|
83
94
|
const h = createHash("sha256").update(String(job.id ?? mark)).digest("hex").slice(0, 6);
|
|
84
|
-
return `noref${h}-${mark}`;
|
|
95
|
+
return `noref${h}-${asciiMark(mark)}`;
|
|
85
96
|
}
|
|
86
97
|
const tmp = raw.startsWith("tmp") ? raw : `tmp${raw}`;
|
|
87
|
-
return `${tmp}-${mark}`;
|
|
98
|
+
return `${tmp}-${asciiMark(mark)}`;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
// Calendar date in Europe/Zurich (the firm's timezone), YYYY-MM-DD.
|
|
@@ -1209,7 +1209,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
1209
1209
|
// ONE LINE, N LINKS. This said "Report: <one url>" and on a batch that url is now null, which would
|
|
1210
1210
|
// have read "Report: null" — the fail-visible shape doing its job, and still not a line to send. A
|
|
1211
1211
|
// batch names every report it produced, in the order the names were ordered.
|
|
1212
|
-
whatsappText: `✅ Knockout screen${refTag} of ${nMarks} mark(s) is done —
|
|
1212
|
+
whatsappText: `✅ Knockout screen${refTag} of ${nMarks} mark(s) is done — ${nMarks === 1 ? "rating" : "highest rating"} ${overall}. `
|
|
1213
1213
|
+ (published.reports.length === 1
|
|
1214
1214
|
? `Report: ${published.reports[0].url}`
|
|
1215
1215
|
: `Reports: ${published.reports.map((r) => `${r.mark} ${r.url}`).join(" · ")}`),
|
package/driver/pipeline.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// verdict, and never parks (there is no LLM continuation decision anywhere). CLI: `node pipeline.mjs --job <file.json>`.
|
|
6
6
|
|
|
7
7
|
import "../shared/env-local.mjs"; // side effect: apply <repo>/.env when THIS file is the CLI entry (never on library import)
|
|
8
|
-
import "./engine/mcp/http-dispatcher.mjs"; // side effect: raise undici headersTimeout (code-side fetches)
|
|
8
|
+
import "./engine/mcp/http-dispatcher.mjs"; import { recordStatusesFromFindings } from "./common-law-coverage-status.mjs"; // side effect: raise undici headersTimeout (code-side fetches); the second records a common-law ledger's statuses in code
|
|
9
9
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
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
|
|
@@ -148,6 +148,7 @@ import { beginAnswerMemory, endAnswerMemory } from "../providers/_shared/answer-
|
|
|
148
148
|
import { findScreenGateViolations, findScreenGateParseGaps, screenGateZeroCause } from "./screen-gate.mjs";
|
|
149
149
|
import { emptyQueue, coerceQueue, mintItem, pendingItems, markFlushed, receiptKeyFor,
|
|
150
150
|
buildFlushFollowup, runPostFlushGateRepair } from "./digest-queue.mjs"; // (t1cd) — the digest-trigger funnel
|
|
151
|
+
import { foldFindingsFile, foldAfterSave, openPass, modelRecordPath, snapshotFold, restoreFold } from "./record-fold.mjs"; // the folded record from the delivery seam on
|
|
151
152
|
import { writeStamp, stageStaleness, restamp, restampStage, staleOnPath, reconcileStamps, shaOf } from "./stage-freshness.mjs";
|
|
152
153
|
import { parseManifestVariants, variantsParseFailure, findCoverageLimitedCells, partitionClosableCells, findSimilarListingSignals,
|
|
153
154
|
GRID_HALVES, GRID_SEATS, MEANING_SEAT, splitGridSpec, halfOfTerm, balanceClosureCells, mergeGrids, mergeCommonLawFindings, openChannelRows,
|
|
@@ -2280,7 +2281,7 @@ export function deriveFormNeighbourhood(ctx) { // @internal — exported for i
|
|
|
2280
2281
|
} catch { /* scope ledger optional — markets are informational; no ledger ⇒ nothing dropped (fail-open) */ }
|
|
2281
2282
|
// NOT scoped here: the transliteration SCRIPT set. `markets` is passed through but never narrows
|
|
2282
2283
|
// SUPPORTED_SCRIPTS, so latin-diacritic/german/nordic/cyrillic/greek run on every matter regardless of the
|
|
2283
|
-
// jurisdictions judgment applied. That is what generated the five
|
|
2284
|
+
// jurisdictions judgment applied. That is what generated the five Drivers Haven diacritics — but the fold-dedupe
|
|
2284
2285
|
// in formNeighbourhood removes them at the source, and the surviving cross-script forms (ß→ss, Cyrillic and
|
|
2285
2286
|
// Greek homoglyphs) are genuinely distinct strings that cost one term each. Deferred deliberately: narrowing
|
|
2286
2287
|
// scripts by market is a coverage decision, and the harm it was meant to fix is already gone.
|
|
@@ -3933,7 +3934,7 @@ export function findFloorBreaches(ledger, floorAxes) { // @internal
|
|
|
3933
3934
|
* `[]`: no manifest, unreadable, unparseable. That is the honest default here and not an absence read as
|
|
3934
3935
|
* a pass, because the whole mechanism is opt-in — a run with no designation owes no floor, and a run whose
|
|
3935
3936
|
* manifest cannot be read has no designation to honour. The refusal for an absent or unparseable manifest
|
|
3936
|
-
* belongs to the stage that writes it and already exists there — verify.mjs:1335 runs the same parser
|
|
3937
|
+
* belongs to the stage that writes it and already exists there — verify.mjs:1335 CALL_REASON_SET runs the same parser
|
|
3937
3938
|
* through `checkSiblingJson` and fails clearance-variants with `variantmodel_missing`. Checked, because
|
|
3938
3939
|
* "something else refuses it" is exactly the assumption that turns a swallowed error into a silent pass.
|
|
3939
3940
|
*/
|
|
@@ -5922,6 +5923,14 @@ function snapshotFindingsForCorrections(P, runDir) {
|
|
|
5922
5923
|
try { raw = readFileSync(P.findings, "utf8"); } catch { /* legacy / not-yet-populated */ }
|
|
5923
5924
|
if (raw == null) return null;
|
|
5924
5925
|
const sha = createHash("sha256").update(raw).digest("hex");
|
|
5926
|
+
// THE RECORD IN THE MODEL'S NUMBERING, beside the one on disk. The review's flags name findings in the
|
|
5927
|
+
// model's numbering, so the corrective body compares the record the model wrote; once the delivered
|
|
5928
|
+
// record is folded the two differ (record-fold.mjs). The fold is held too, for a rollback to put back.
|
|
5929
|
+
const modelAt = modelRecordPath(runDir, P.findings);
|
|
5930
|
+
let modelRaw = raw;
|
|
5931
|
+
if (modelAt !== P.findings) { try { modelRaw = readFileSync(modelAt, "utf8"); } catch { modelRaw = raw; } }
|
|
5932
|
+
const model = { raw: modelRaw, sha: modelRaw === raw ? sha : createHash("sha256").update(modelRaw).digest("hex") };
|
|
5933
|
+
const fold = snapshotFold(runDir);
|
|
5925
5934
|
try {
|
|
5926
5935
|
const tmp = driverDir(runDir, "findings-pre-corrective.json.tmp");
|
|
5927
5936
|
// PR-4 (snapshot-before-overwrite): store the FULL pre-corrective text, not just its sha — a
|
|
@@ -5931,7 +5940,7 @@ function snapshotFindingsForCorrections(P, runDir) {
|
|
|
5931
5940
|
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), sha, text: raw }, null, 2));
|
|
5932
5941
|
renameSync(tmp, driverDir(runDir, "findings-pre-corrective.json"));
|
|
5933
5942
|
} catch { /* advisory sidecar */ }
|
|
5934
|
-
return { raw, sha };
|
|
5943
|
+
return { raw, sha, model, fold };
|
|
5935
5944
|
}
|
|
5936
5945
|
/**
|
|
5937
5946
|
*, T3b — THE CORRECTIVE PASS FAILED VALIDATION. RESTORE THE LAST GOOD STATE.
|
|
@@ -5979,6 +5988,7 @@ export function rollbackCorrectivePass(P, runDir, pre, fail) { // @internal
|
|
|
5979
5988
|
// Atomic, like every other write to this file: a half-restored findings.json is a worse state than
|
|
5980
5989
|
// either of the two this is choosing between.
|
|
5981
5990
|
atomicWrite(P.findings, pre.raw);
|
|
5991
|
+
if (pre.fold) restoreFold(runDir, pre.fold); // the record the model wrote and the map, as they stood
|
|
5982
5992
|
}
|
|
5983
5993
|
const record = { ts: new Date().toISOString(), reason: String(fail?.fail ?? "failed").slice(0, 200),
|
|
5984
5994
|
restored: changed, preSha: pre.sha, failedSha: nowSha };
|
|
@@ -6023,9 +6033,11 @@ export function rollbackCorrectivePass(P, runDir, pre, fail) { // @internal
|
|
|
6023
6033
|
*/
|
|
6024
6034
|
export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks, namedLines = []) { // @internal
|
|
6025
6035
|
if (!pre?.raw) return null; // nothing held — nothing to compare against
|
|
6036
|
+
// In the model's numbering: the flags name its ordinals, and a folded record renumbers, so a finding
|
|
6037
|
+
// removed from the folded record would shift every ordinal after it (record-fold.mjs).
|
|
6026
6038
|
let preDoc = null, postDoc = null, postRaw = null;
|
|
6027
|
-
try { preDoc = JSON.parse(pre.raw); } catch { return null; }
|
|
6028
|
-
try { postRaw = readFileSync(P.findings, "utf8"); postDoc = JSON.parse(postRaw); } catch { return null; }
|
|
6039
|
+
try { preDoc = JSON.parse(pre.model?.raw ?? pre.raw); } catch { return null; }
|
|
6040
|
+
try { postRaw = readFileSync(modelRecordPath(runDir, P.findings), "utf8"); postDoc = JSON.parse(postRaw); } catch { return null; }
|
|
6029
6041
|
if (!preDoc || !postDoc || typeof postDoc !== "object") return null;
|
|
6030
6042
|
|
|
6031
6043
|
const norm = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
@@ -6095,6 +6107,7 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks,
|
|
|
6095
6107
|
leftRemoved: leftRemoved.map(brief),
|
|
6096
6108
|
};
|
|
6097
6109
|
atomicWrite(P.findings, JSON.stringify(merged, null, 2) + "\n");
|
|
6110
|
+
foldAfterSave(runDir, P.findings); // after the seam the repaired record is folded again, as a save's is
|
|
6098
6111
|
try { atomicWrite(driverDir(runDir, "corrective-repair.json"), JSON.stringify(record, null, 2) + "\n"); }
|
|
6099
6112
|
catch { /* sidecar is best-effort; the runLog event is the record that decides */ }
|
|
6100
6113
|
return record;
|
|
@@ -6218,10 +6231,11 @@ async function enforceCorrectionsReachFindings(ctx, P, pre, resume) {
|
|
|
6218
6231
|
const named = correctionNamedSet(P);
|
|
6219
6232
|
const readState = () => {
|
|
6220
6233
|
let raw = null, corrections = null;
|
|
6221
|
-
|
|
6234
|
+
// the record the model wrote: a change the fold hides is still a change it made (record-fold.mjs)
|
|
6235
|
+
try { raw = readFileSync(modelRecordPath(run.runDir, P.findings), "utf8"); } catch { return { stale: false }; }
|
|
6222
6236
|
try { corrections = parseFindingsJsonLenient(raw).corrections; } catch { /* shape defects ride the normal ladder */ }
|
|
6223
6237
|
const sha = createHash("sha256").update(raw).digest("hex");
|
|
6224
|
-
return { stale: named.length > 0 && sha === pre.sha && !(corrections && corrections.applied), sha };
|
|
6238
|
+
return { stale: named.length > 0 && sha === (pre.model?.sha ?? pre.sha) && !(corrections && corrections.applied), sha };
|
|
6225
6239
|
};
|
|
6226
6240
|
if (!readState().stale) return;
|
|
6227
6241
|
note(`[corrections] findings.json unchanged after the corrective pass (review names: ${named.join(", ")}) — demanding the re-emit`);
|
|
@@ -7477,26 +7491,15 @@ function enrichFindingDeadlines(P, runDir, note, { nowMs = Date.now(), withinDay
|
|
|
7477
7491
|
}
|
|
7478
7492
|
}
|
|
7479
7493
|
|
|
7494
|
+
// THE SEAM'S FOLD. From here on every accepted save folds the record again on acceptance, so the record
|
|
7495
|
+
// the reviewer, the cards and the delivery read stays folded (record-fold.mjs, design 2026-10-03). doc 50 —
|
|
7496
|
+
// on a v4 record the merge base is the WORST BAND by the frozen manifest's order. No duplicates leave the
|
|
7497
|
+
// file byte-identical; a folded shape that does not validate keeps the record as written.
|
|
7480
7498
|
function consolidateFindingsFile(P, note, manifest = null) {
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
// doc 50 — on a v4 record the merge base is the WORST BAND by the frozen manifest's order
|
|
7486
|
-
const { findings, merges, ordinalMap } = consolidateFindings(doc.findings, (doc.schema_version ?? 1) >= 4 ? manifest : null);
|
|
7487
|
-
if (!merges.length) return; // no duplicates → leave the file byte-identical
|
|
7488
|
-
const next = { ...doc, findings };
|
|
7489
|
-
// spec 64 — actions follow the renumber: a merged-away ordinal remaps to its kept finding's new
|
|
7490
|
-
// ordinal (the action still applies to the consolidated conflict — dropping the reference would
|
|
7491
|
-
// silently un-condition a live demand).
|
|
7492
|
-
if (Array.isArray(doc.actions)) next.actions = remapActionOrdinals(doc.actions, ordinalMap);
|
|
7493
|
-
parseFindingsJson(JSON.stringify(next)); // re-validate the consolidated shape (throws → catch keeps original)
|
|
7494
|
-
atomicWrite(P.findings, `${JSON.stringify(next, null, 2)}\n`); // B5 — a crash mid-write must never leave a truncated findings.json
|
|
7495
|
-
for (const m of merges)
|
|
7496
|
-
note(`[consolidate] ${m.owner} — ${m.mark}: folded ${m.dropped.length} duplicate filing(s) (ord ${m.dropped.join(", ")}) into one card`);
|
|
7497
|
-
} catch (e) {
|
|
7498
|
-
note(`[consolidate] skipped: ${String(e?.message || e).replace(/\s+/g, " ").slice(0, 100)}`);
|
|
7499
|
-
}
|
|
7499
|
+
const { merges, error } = foldFindingsFile(P.runDir, P.findings, manifest, { seam: true });
|
|
7500
|
+
for (const m of merges)
|
|
7501
|
+
note(`[consolidate] ${m.owner} — ${m.mark}: folded ${m.dropped.length} duplicate filing(s) (ord ${m.dropped.join(", ")}) into one card`);
|
|
7502
|
+
if (error) note(`[consolidate] skipped: ${error.slice(0, 100)}`);
|
|
7500
7503
|
}
|
|
7501
7504
|
// minor (a): a finding earns a FULL prose card when composite ≥ 3 (on-field) OR level ∈ {A,B} (high legal
|
|
7502
7505
|
// exposure even at a low practical composite — a mis-rated-low mark must not be reduced to a structured-only row).
|
|
@@ -8793,6 +8796,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
8793
8796
|
// BEFORE doc 50 backfills its framework sidecar from the frozen frameworkPath (deploy-boundary case) so
|
|
8794
8797
|
// the v4 gates always have vocabulary; a run that already has one reads it verbatim (never re-derived).
|
|
8795
8798
|
attachFramework(ctx);
|
|
8799
|
+
// A pass opens before the delivery seam: until this pass's seam folds the record, a save writes the
|
|
8800
|
+
// model's record as it is (record-fold.mjs).
|
|
8801
|
+
openPass(ctx.paths.runDir);
|
|
8796
8802
|
|
|
8797
8803
|
// ── (t1cd): the digest-trigger FUNNEL ──────────────────────────────────────────────────────
|
|
8798
8804
|
// The digest-trigger funnel (; flag deleted post-E2E 2026-07-22): the queue
|
|
@@ -9217,7 +9223,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
9217
9223
|
: stage(g.name, { ...ctx, axis: g.axis })),
|
|
9218
9224
|
stage("blind-frame", ctx).catch((e) => ({ ok: false, fail: String(e?.message ?? e) })),
|
|
9219
9225
|
]);
|
|
9220
|
-
if (!blindRes.ok) {
|
|
9226
|
+
if (!clSplit && clMember()?.ok) recordStatusesFromFindings(P.commonLaw); if (!blindRes.ok) {
|
|
9221
9227
|
note(`blind-frame failed (non-fatal — no frame-diff/reopen this run): ${blindRes.fail}`);
|
|
9222
9228
|
runLog(run.runDir, { event: "blind-frame-skipped", reason: blindRes.fail });
|
|
9223
9229
|
}
|
|
@@ -9345,7 +9351,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
9345
9351
|
// synthesis, replay) reads exactly the artifacts a single-member run would have written. Re-run after
|
|
9346
9352
|
// every routed half followup (closure / frame-reopen) — the half files stay the source of truth and
|
|
9347
9353
|
// the canonical pair is always derived, which also makes the merge resume-idempotent.
|
|
9348
|
-
const mergeCommonLawArtifacts = !clSplit ? null : ({ discloseUnfinished = false } = {}) => {
|
|
9354
|
+
const mergeCommonLawArtifacts = !clSplit ? null : ({ discloseUnfinished = false } = {}) => { for (const h of GRID_HALVES) recordStatusesFromFindings(P.commonLawHalf(h), { half: h }); // each half's ledger statuses, recorded by code where the seat made no call (ruling 2026-10-01)
|
|
9349
9355
|
// Each half's coverage truth = its plugin-written main ledger FOLDED with any plugin-written
|
|
9350
9356
|
// supplementary sibling(s) the closure lane produced. Returns the batches as an ARRAY (mergeGrids'
|
|
9351
9357
|
// batchesOf flattens it, cells union across all of them); a missing/corrupt main leaves its cells to
|
|
@@ -9424,7 +9430,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
9424
9430
|
//
|
|
9425
9431
|
// The prior channel is the TRUSTED one and already carries everything needed. Half forms are
|
|
9426
9432
|
// driver-written, `seatFields(p, row)` carries their rulings by the same `pOk` path, and
|
|
9427
|
-
// gateway.mjs:
|
|
9433
|
+
// gateway.mjs:630 selectEngine already unions this exact shape — prior-only, `{ rows: null }` submitted — on
|
|
9428
9434
|
// every attempt. So this is that mode, not a new one.
|
|
9429
9435
|
//
|
|
9430
9436
|
// ONE METRIC MOVES, deliberately: with no submission, `carried` counts every ruled row, because
|
|
@@ -12644,9 +12650,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12644
12650
|
// always did, which is today's behaviour exactly.
|
|
12645
12651
|
try {
|
|
12646
12652
|
const rows = parseCorrections(readFileSync(P.seniorEyeReview, "utf8"));
|
|
12647
|
-
const preDoc = preCorrective ? parseFindingsJsonLenient(preCorrective.raw) : null;
|
|
12653
|
+
const preDoc = preCorrective ? parseFindingsJsonLenient(preCorrective.model?.raw ?? preCorrective.raw) : null; // the flags name the model's ordinals, so both sides are the record the model wrote (record-fold.mjs)
|
|
12648
12654
|
let postDoc = null;
|
|
12649
|
-
try { postDoc = parseFindingsJsonLenient(readFileSync(P.findings, "utf8")); } catch { /* shape defects ride the normal ladder */ }
|
|
12655
|
+
try { postDoc = parseFindingsJsonLenient(readFileSync(modelRecordPath(run.runDir, P.findings), "utf8")); } catch { /* shape defects ride the normal ladder */ }
|
|
12650
12656
|
correctionsApplied = buildCorrectionsApplied(rows, preDoc, postDoc);
|
|
12651
12657
|
// — A CLAIM MAY NOT OUTLIVE ITS EVIDENCE, AND A DEMOTED STAMP MAY NOT COME BACK.
|
|
12652
12658
|
//
|
|
@@ -14390,6 +14396,13 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14390
14396
|
lintActions = soft.actions ?? null; lintAskAnswers = soft.askAnswers ?? null; lintFourAnswers = soft.fourAnswers ?? null;
|
|
14391
14397
|
} } catch { /* validators.findings owns the unparseable case */ }
|
|
14392
14398
|
}
|
|
14399
|
+
// The narrative's finding headings are in the model's numbering: once the record is folded, its
|
|
14400
|
+
// write-ups join the record the model wrote (record-fold.mjs).
|
|
14401
|
+
let lintNarrativeFindings = null;
|
|
14402
|
+
const lintModelAt = modelRecordPath(run.runDir, P.findings);
|
|
14403
|
+
if (lintModelAt !== P.findings) {
|
|
14404
|
+
try { lintNarrativeFindings = parseFindingsJsonLenient(readFileSync(lintModelAt, "utf8")).findings; } catch { lintNarrativeFindings = null; }
|
|
14405
|
+
}
|
|
14393
14406
|
// wp50/wi2 — the sidecar tier joins the lint so overall-level prose can't contradict it.
|
|
14394
14407
|
let lintVerdict = null;
|
|
14395
14408
|
try { lintVerdict = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); } catch { /* legacy */ }
|
|
@@ -14413,7 +14426,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14413
14426
|
// is presence-gated and simply emits nothing here; they stay alive for replay-archive.mjs,
|
|
14414
14427
|
// which passes the archived summary.
|
|
14415
14428
|
reportMd, narrativeMd, auditMd, recordsByUri: assembled.records, searchedNames,
|
|
14416
|
-
fetchFailures: recordFetchFailures, findings: lintFindings, findingsRaw: lintFindingsRaw, actionsRegister: lintActions, verdictDoc: lintVerdict,
|
|
14429
|
+
fetchFailures: recordFetchFailures, findings: lintFindings, narrativeFindings: lintNarrativeFindings, findingsRaw: lintFindingsRaw, actionsRegister: lintActions, verdictDoc: lintVerdict,
|
|
14417
14430
|
// WP-56 B2 — fresh v4 runs expect the standing mark-assessment (structural banner flag when
|
|
14418
14431
|
// absent). The replay harness's runLint call never passes this, so archived runs never flip.
|
|
14419
14432
|
markAssessment: lintMarkAssessment, markAssessmentExpected: lintSv >= 4,
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// ── the shape ────────────────────────────────────────────────────────────────────────────────────────
|
|
19
19
|
//
|
|
20
20
|
// { "schema": 1,
|
|
21
|
-
// "families": { "
|
|
22
|
-
// "of": { "<runId>": "
|
|
21
|
+
// "families": { "aqua-range": { "name": "Aqua range", "account": "foxglade" } },
|
|
22
|
+
// "of": { "<runId>": "aqua-range" } }
|
|
23
23
|
//
|
|
24
24
|
// Keyed by RUN rather than by mark. A mark is not a thing the pool stores — it is a grouping the browser
|
|
25
25
|
// derives from run metadata — so a run id is the only stable handle the two sides already agree on. The
|
|
@@ -39,7 +39,7 @@ const FILE = "family-tags.json";
|
|
|
39
39
|
// Caught by a test asserting a malformed file degrades to no families; it degraded to someone else's.
|
|
40
40
|
const empty = () => ({ schema: 1, families: {}, of: {} });
|
|
41
41
|
|
|
42
|
-
/** Ids are derived from the name so that grouping into "
|
|
42
|
+
/** Ids are derived from the name so that grouping into "Aqua range" twice MERGES rather than duplicating. */
|
|
43
43
|
export function familyId(name) {
|
|
44
44
|
return String(name ?? "")
|
|
45
45
|
.normalize("NFKD")
|
|
@@ -3147,7 +3147,7 @@ async function connectorDoorKind(url) {
|
|
|
3147
3147
|
//
|
|
3148
3148
|
// STAFF ONLY, and by construction rather than by filtering: it hangs off /portal/admin, which
|
|
3149
3149
|
// asserted staffOnly above and 404s for everyone else. That is a decision, not an oversight. A
|
|
3150
|
-
// family name is staff shorthand for a brand line — "
|
|
3150
|
+
// family name is staff shorthand for a brand line — "Aqua range" can name a launch a client has
|
|
3151
3151
|
// not announced — and a grouping a client can see but not edit or explain raises more questions
|
|
3152
3152
|
// than it answers. Revisit when clients have a reason to care; until then the client's list is
|
|
3153
3153
|
// exactly the mark list, which is what the browser falls back to when this 404s.
|