clearotron 0.5.0-beta.2 → 0.5.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +1 -0
- package/docs/architecture/05-config-governance.md +1 -1
- package/driver/CHANGELOG.md +11 -0
- package/driver/band-shape.mjs +4 -4
- package/driver/contract-vocabulary.mjs +3 -3
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +21 -2
- package/driver/coverage-ledger.mjs +7 -1
- package/driver/declination-call.mjs +33 -16
- package/driver/degraded-parts.mjs +6 -0
- package/driver/engine/mcp/clarivate-server.mjs +2 -0
- package/driver/engine/mcp/corsearch-server.mjs +2 -0
- package/driver/engine/mcp/declination-server.mjs +4 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +15 -0
- package/driver/engine/mcp/held-answers.mjs +11 -3
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +5 -2
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/envelope-settle.mjs +13 -3
- package/driver/gateway.mjs +31 -10
- package/driver/package.json +1 -1
- package/driver/parts-not-completed.mjs +52 -11
- package/driver/pipeline-knockout.mjs +5 -4
- package/driver/pipeline.mjs +436 -72
- package/driver/placement-decisions.mjs +4 -4
- package/driver/placement-form-io.mjs +21 -1
- package/driver/placement-form.mjs +25 -15
- package/driver/placement-union.mjs +53 -8
- package/driver/publish/index.mjs +7 -3
- package/driver/runner.mjs +4 -4
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-search/phase2-execution.md +3 -3
- package/driver/skills/clearance-search/synthesis-rules.md +3 -3
- package/driver/skills/placement-inquiry/SKILL.md +3 -3
- package/driver/stages.mjs +5 -5
- package/driver/suite-census.json +84 -12
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -0
- package/providers/_shared/enumerate.mjs +4 -3
- package/providers/_shared/execute-plan.mjs +24 -7
- package/providers/_shared/send-until.mjs +25 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/band-shape-probe.mjs +1 -1
- package/scripts/e2e-first-time.mjs +2 -1
- package/shared/names-in-force.mjs +1 -0
- package/shared/slices-taken-out.mjs +65 -0
- package/skills/clearotron-account/SKILL.md +1 -1
- package/skills/clearotron-ops/SKILL.md +1 -1
package/driver/pipeline.mjs
CHANGED
|
@@ -30,13 +30,15 @@ import { engineProvenance } from "./engine-build.mjs"; // —
|
|
|
30
30
|
import { preflightEngineTurn } from "./engine/probe.mjs"; // — …and the engine can complete a turn, not merely exist
|
|
31
31
|
import { rankClusterLegs, verdictDrivingFindings } from "./senior-rights.mjs";
|
|
32
32
|
import { buildRunContext, deriveSlug, kebab } from "./phase0.mjs";
|
|
33
|
-
import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveTier, lines,
|
|
33
|
+
import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveTier, lines, whatsappRouting,
|
|
34
34
|
chainEntries, stageOrdinal, stageInputs, stageOutputs, dependencyOrder, REGISTER_AXES, REGISTER_ENUMERATE_TOOL,
|
|
35
35
|
buildEscalationFollowup, buildEnvelopeCloseFollowup, buildFrameReopenFollowup,
|
|
36
36
|
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage, derivedLimitSec, limitExceedsCeiling, ceilingRefusal, stagesRefusingBand } from "./band-size.mjs";
|
|
37
37
|
import { handResearchAnswers } from "./research-answers.mjs"; // the research a stage's earlier attempts got, handed to a later one
|
|
38
38
|
import { IDENTITY_FILE as REPORT_IDENTITY_FILE, renderReportOverview } from "./report-overview-record.mjs";
|
|
39
|
-
import { writePartNotCompleted, readPartNotCompleted, clearPartNotCompleted, clearPartsNotCompleted, NOT_COMPLETED_DIR } from "./parts-not-completed.mjs";
|
|
39
|
+
import { writePartNotCompleted, readPartNotCompleted, clearPartNotCompleted, clearPartsNotCompleted, NOT_COMPLETED_DIR, slicesNotYetRecorded, axesNotYetDisclosed, unitsNotYetRecorded } from "./parts-not-completed.mjs";
|
|
40
|
+
import { renderUnitNote, readBandAccount } from "./register-unit-record.mjs"; // a unit that could not finish, its note rendered from its band
|
|
41
|
+
import { bandWithSlicesOut, slicesTakenOutByAxis } from "../shared/slices-taken-out.mjs"; // a band read with the slices a check could not prove ran taken out
|
|
40
42
|
import { dispatchRows, clearedSignatures } from "./seat-attempts.mjs";
|
|
41
43
|
import { CONTEXT_DERIVATIONS, DISPATCH_EXTRAS, INLINE_CONTEXT, sandboxManifest, sandboxGaps, derivationsFor } from "./stage-context.mjs"; // — what a stage is actually handed
|
|
42
44
|
import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections, validators, findReviewerCoherenceFlags, lateReviewAgainst, VERDICT_RANK } from "./verify.mjs";
|
|
@@ -458,6 +460,78 @@ export function loadCoverageLedger(runDir) { // @internal
|
|
|
458
460
|
return { rows: relabel(rows), source: "prose", dropped };
|
|
459
461
|
}
|
|
460
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Which still-tainted axes are disclosed and which are parked. Every axis once synthesis is done (the audit spine is
|
|
465
|
+
* locked); an axis the searches record lists, whose recovery is spent; and the rest parked, to be run again. PURE.
|
|
466
|
+
*/
|
|
467
|
+
export function taintDisposition(stillTainted, { digestLocked = false, disclosedAxes = new Set() } = {}) { // @internal
|
|
468
|
+
const disclose = (stillTainted ?? []).filter((a) => digestLocked || disclosedAxes.has(a));
|
|
469
|
+
return { disclose, park: (stillTainted ?? []).filter((a) => !disclose.includes(a)) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Put back what a taint park renamed, so the band and the digest are read as the last pass left them. A renamed copy
|
|
474
|
+
* whose original has since been written again stays where it is. Returns the paths put back. IMPURE.
|
|
475
|
+
*/
|
|
476
|
+
export function undoTaintRenames(renames) { // @internal
|
|
477
|
+
const back = [];
|
|
478
|
+
for (const r of Array.isArray(renames) ? renames : []) {
|
|
479
|
+
try { if (r?.to && r?.from && existsSync(r.to) && !existsSync(r.from)) { renameSync(r.to, r.from); back.push(r.from); } }
|
|
480
|
+
catch { /* best-effort: what stays renamed is re-run on the resume, as before */ }
|
|
481
|
+
}
|
|
482
|
+
return back;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// THE BANDS ARE READ WITH THE RECORDED SLICES OUT, whatever wrote them since. A unit re-run after the fan-in, by the
|
|
486
|
+
// envelope, the escalation or the taint chain, writes its axis's band whole, and a slice the searches' record took
|
|
487
|
+
// out would come back with the records nobody could prove were searched. So the band's two readers, the merge and the
|
|
488
|
+
// plan join, take the recorded slices out of what they read, in memory and never by writing the file: a unit or the
|
|
489
|
+
// executor may be writing that band at the same moment. Every other reader of a band as answers does the same, from
|
|
490
|
+
// the one definition (shared/slices-taken-out.mjs).
|
|
491
|
+
const recordedSlicesByAxis = (P) => slicesTakenOutByAxis(P?.runDir);
|
|
492
|
+
|
|
493
|
+
// A band's text with the slices taken out, for a reader that parses text. Text that does not parse is handed on whole,
|
|
494
|
+
// for the reader's own parse to refuse. PURE.
|
|
495
|
+
function withSlicesOut(raw, slices) {
|
|
496
|
+
if (!Array.isArray(slices) || !slices.length) return raw;
|
|
497
|
+
try {
|
|
498
|
+
const parsed = JSON.parse(raw);
|
|
499
|
+
if (!Array.isArray(parsed)) return raw;
|
|
500
|
+
const { blocks, taken } = bandWithSlicesOut(parsed, slices);
|
|
501
|
+
return taken.length ? JSON.stringify(blocks) : raw;
|
|
502
|
+
} catch { return raw; }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* THE SLICES A RUN COULD NOT PROVE IT SEARCHED, TAKEN OUT OF ITS BAND. Each recorded slice's block becomes the
|
|
507
|
+
* executor's own deferred block (providers/_shared/execute-plan.mjs), carrying the slice's reason, or is added as one
|
|
508
|
+
* where the band holds none for it. The plan join files it as deferred, the coverage form gives it an open row the
|
|
509
|
+
* write-up may not call clean, and the axis's other blocks stand. The band as it was is kept beside it the first time,
|
|
510
|
+
* as evidence. An axis whose band is absent or unreadable is left to the band's own checks. Returns the slices taken
|
|
511
|
+
* out on this call. IMPURE (rewrites the bands).
|
|
512
|
+
*/
|
|
513
|
+
export function takeSlicesOutOfTheBand(P, slices) { // @internal
|
|
514
|
+
const taken = [];
|
|
515
|
+
const byAxis = new Map();
|
|
516
|
+
for (const s of Array.isArray(slices) ? slices : []) {
|
|
517
|
+
if (!s?.axis || (!s?.qid && !s?.query)) continue;
|
|
518
|
+
if (!byAxis.has(s.axis)) byAxis.set(s.axis, []);
|
|
519
|
+
byAxis.get(s.axis).push(s);
|
|
520
|
+
}
|
|
521
|
+
for (const [axis, list] of byAxis) {
|
|
522
|
+
const at = P.registerBand(axis);
|
|
523
|
+
let blocks;
|
|
524
|
+
try { blocks = JSON.parse(readFileSync(at, "utf8")); } catch { continue; }
|
|
525
|
+
if (!Array.isArray(blocks)) continue;
|
|
526
|
+
const out = bandWithSlicesOut(blocks, list);
|
|
527
|
+
if (!out.taken.length) continue;
|
|
528
|
+
taken.push(...out.taken);
|
|
529
|
+
try { if (!existsSync(`${at}.before-taken-out`)) copyFileSync(at, `${at}.before-taken-out`); } catch { /* the evidence is best-effort */ }
|
|
530
|
+
atomicWrite(at, JSON.stringify(out.blocks, null, 2) + "\n");
|
|
531
|
+
}
|
|
532
|
+
return taken;
|
|
533
|
+
}
|
|
534
|
+
|
|
461
535
|
// ── judgment-relocation (2026-06-23): the COMPLETE NAMED BAND flow (lift the firewall) ───────────────
|
|
462
536
|
// The register-unit funnel writes register-units/<axis>-band.json (a JSON ARRAY of named-band blocks). The
|
|
463
537
|
// driver MERGES the per-axis bands into one complete band (register-named-band.json) and feeds it to Layer B
|
|
@@ -466,6 +540,7 @@ export function loadCoverageLedger(runDir) { // @internal
|
|
|
466
540
|
// de-dups enumerated records by record_id and preserves every crowd descriptor. Returns the merged band
|
|
467
541
|
// object {enumerated, crowds} plus { axes:[…], invalid:[{axis,reason}] } so the caller can flag a bad axis.
|
|
468
542
|
export function mergeRegisterBands(P, axes = []) { // @internal
|
|
543
|
+
const recorded = recordedSlicesByAxis(P);
|
|
469
544
|
const bands = [];
|
|
470
545
|
const merged = [];
|
|
471
546
|
const invalid = [];
|
|
@@ -473,7 +548,7 @@ export function mergeRegisterBands(P, axes = []) { // @internal
|
|
|
473
548
|
const bandPath = P.registerBand(a);
|
|
474
549
|
if (!existsSync(bandPath)) continue; // an axis that found nothing may legitimately write no band
|
|
475
550
|
try {
|
|
476
|
-
const band = parseNamedBand(readFileSync(bandPath, "utf8")); // throws named_band_* on a malformed artifact
|
|
551
|
+
const band = parseNamedBand(withSlicesOut(readFileSync(bandPath, "utf8"), recorded.get(a))); // throws named_band_* on a malformed artifact
|
|
477
552
|
bands.push(band);
|
|
478
553
|
merged.push(a);
|
|
479
554
|
} catch (e) {
|
|
@@ -489,7 +564,7 @@ export function mergeRegisterBands(P, axes = []) { // @internal
|
|
|
489
564
|
writeFileSync(`${bandPath}.tmp`, JSON.stringify(blocks, null, 2) + "\n");
|
|
490
565
|
renameSync(`${bandPath}.tmp`, bandPath);
|
|
491
566
|
runLog(P.runDir, { event: "named-band-state-quarantined", axis: a, states: quarantined.map((q) => q.state), site: "merge" });
|
|
492
|
-
const band = parseNamedBand(readFileSync(bandPath, "utf8"));
|
|
567
|
+
const band = parseNamedBand(withSlicesOut(readFileSync(bandPath, "utf8"), recorded.get(a)));
|
|
493
568
|
bands.push(band);
|
|
494
569
|
merged.push(a);
|
|
495
570
|
continue;
|
|
@@ -521,12 +596,13 @@ export function mergeRegisterBands(P, axes = []) { // @internal
|
|
|
521
596
|
// chain, the clean-gates, and dispatchPlanQids cover them with zero extra code. Run-local only: the
|
|
522
597
|
// slug store is never written (clearances run once; senior lawyer 2026-07-10).
|
|
523
598
|
export function readRegisterBands(P, axes) { // @internal
|
|
599
|
+
const recorded = recordedSlicesByAxis(P);
|
|
524
600
|
const byAxis = {};
|
|
525
601
|
for (const a of axes ?? []) {
|
|
526
602
|
if (!existsSync(P.registerBand(a))) continue;
|
|
527
603
|
try {
|
|
528
604
|
const parsed = JSON.parse(readFileSync(P.registerBand(a), "utf8"));
|
|
529
|
-
byAxis[a] = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.blocks) ? parsed.blocks : []);
|
|
605
|
+
byAxis[a] = bandWithSlicesOut(Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.blocks) ? parsed.blocks : []), recorded.get(a)).blocks;
|
|
530
606
|
} catch { byAxis[a] = []; }
|
|
531
607
|
}
|
|
532
608
|
return byAxis;
|
|
@@ -3465,7 +3541,7 @@ function findingsSurfaceRows(P) {
|
|
|
3465
3541
|
// cut, or none. Only the dispatch's copy carries it; the spec the declination tool reads is the positions alone.
|
|
3466
3542
|
// A list the label could not be added to keeps its lines as they were, and the run says so: a write-up handed no goods
|
|
3467
3543
|
// source must not read as one whose records have none.
|
|
3468
|
-
export function withGoodsFrom(ctx, P, rows) { // @internal — driven by its test with a run's own files
|
|
3544
|
+
export function withGoodsFrom(ctx, P, rows, read = null) { // @internal — driven by its test with a run's own files
|
|
3469
3545
|
const unlabelled = (reason) => {
|
|
3470
3546
|
try { runLog(P.runDir, { event: "write-up-goods-from-failed", rows: rows.length, reason: String(reason).slice(0, 120) }); } catch { /* the list still goes */ }
|
|
3471
3547
|
return rows;
|
|
@@ -3474,7 +3550,7 @@ export function withGoodsFrom(ctx, P, rows) { // @internal — driven by its t
|
|
|
3474
3550
|
const runPrefix = ctx.run?.slug && ctx.run?.codename ? `clearance-${ctx.run.slug}-${ctx.run.codename}-` : null;
|
|
3475
3551
|
if (!runPrefix) return unlabelled("the run has no session prefix to read its evidence by");
|
|
3476
3552
|
const uris = rows.filter((r) => r?.kind !== "page" && r?.uri).map((r) => r.uri);
|
|
3477
|
-
const evidence = goodsEvidenceOfDrops(P, runPrefix, uris);
|
|
3553
|
+
const evidence = read ?? goodsEvidenceOfDrops(P, runPrefix, uris);
|
|
3478
3554
|
return rows.map((r) => {
|
|
3479
3555
|
const e = r?.kind !== "page" && r?.uri ? evidence.get(r.uri) : null;
|
|
3480
3556
|
return e ? { ...r, goods_from: goodsFromLabel({ state: e.goods, source: e.source }) } : r;
|
|
@@ -3482,6 +3558,68 @@ export function withGoodsFrom(ctx, P, rows) { // @internal — driven by its t
|
|
|
3482
3558
|
} catch (e) { return unlabelled(e?.message ?? e); }
|
|
3483
3559
|
}
|
|
3484
3560
|
|
|
3561
|
+
// THE TOOL IS TOLD WHAT EACH RECORD'S GOODS REST ON, from the same derivation (ruled 2026-10-27). A goods decision on a
|
|
3562
|
+
// mark identical to the applied-for mark, live in a filed class, stands only where the run holds that record's goods
|
|
3563
|
+
// complete, so the spec the declination tool judges carries each record's state, a position's members' included. A
|
|
3564
|
+
// list the states could not be added to keeps its rows as they were: the refusal then reads no state and refuses, and
|
|
3565
|
+
// the run says so.
|
|
3566
|
+
export function withGoodsStates(ctx, P, rows, read = null) { // @internal — driven by its test with a run's own files
|
|
3567
|
+
const unstated = (reason) => {
|
|
3568
|
+
try { runLog(P.runDir, { event: "declination-goods-states-failed", rows: rows.length, reason: String(reason).slice(0, 120) }); } catch { /* the list still goes */ }
|
|
3569
|
+
return rows;
|
|
3570
|
+
};
|
|
3571
|
+
try {
|
|
3572
|
+
const runPrefix = ctx.run?.slug && ctx.run?.codename ? `clearance-${ctx.run.slug}-${ctx.run.codename}-` : null;
|
|
3573
|
+
if (!runPrefix) return unstated("the run has no session prefix to read its evidence by");
|
|
3574
|
+
const evidence = read ?? goodsEvidenceOfDrops(P, runPrefix, surfaceUris(rows));
|
|
3575
|
+
const state = (u) => evidence.get(u)?.goods ?? null;
|
|
3576
|
+
return rows.map((r) => (r?.kind === "page" || !r?.uri ? r : { ...r, goods: state(r.uri),
|
|
3577
|
+
...(Array.isArray(r.members) ? { members: r.members.map((m) => ({ ...m, goods: state(m?.uri) })) } : {}) }));
|
|
3578
|
+
} catch (e) { return unstated(e?.message ?? e); }
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/**
|
|
3582
|
+
* THE FLOORS THE WRITER DECLINED, AS THE REVIEW IS TOLD THEM (ruled 2026-10-27): one line per floor on a position the
|
|
3583
|
+
* writer declined, with its reason and its ground, after placement's decisions. The writer may decline a mark identical
|
|
3584
|
+
* to the applied-for mark on goods the run holds complete, and a review told only placement's decisions read such a
|
|
3585
|
+
* floor as one the narrative neither rates nor reasons away, and sent it back. [] when nothing stands. IMPURE.
|
|
3586
|
+
*/
|
|
3587
|
+
export function writerFloorDeclinationLines(P) { // @internal — driven by its test with a run's own files
|
|
3588
|
+
try {
|
|
3589
|
+
const floors = safeReadJson(P.bandShape)?.floors?.in_class_identical_or_near;
|
|
3590
|
+
if (!Array.isArray(floors) || !floors.length) return [];
|
|
3591
|
+
const declined = readDeclinations(P.runDir);
|
|
3592
|
+
if (!declined.byUri.size) return [];
|
|
3593
|
+
// A declination is the position's: each record of a row the writer was handed reads its row's.
|
|
3594
|
+
const rowOf = new Map();
|
|
3595
|
+
for (const row of findingsSurfaceRows(P))
|
|
3596
|
+
for (const u of (Array.isArray(row?.uris) && row.uris.length ? row.uris : [row?.uri])) rowOf.set(normalizeRecordUri(u), normalizeRecordUri(row?.uri));
|
|
3597
|
+
const out = [];
|
|
3598
|
+
for (const f of floors) {
|
|
3599
|
+
const u = normalizeRecordUri(f?.record_id);
|
|
3600
|
+
const d = u ? declined.byUri.get(rowOf.get(u) ?? u) : null;
|
|
3601
|
+
if (d) out.push(`- ${f.mark_text ?? "(unnamed)"}${f.owner_name ? ` — ${f.owner_name}` : ""} (${f.record_id}): declined by the writer as ${d.reason}: ${d.grounds}`);
|
|
3602
|
+
}
|
|
3603
|
+
return out;
|
|
3604
|
+
} catch { return []; }
|
|
3605
|
+
}
|
|
3606
|
+
/** Every record a list of the writer's rows names, a position's members included. PURE. */
|
|
3607
|
+
const surfaceUris = (rows) => [...new Set(rows.filter((r) => r?.kind !== "page" && r?.uri)
|
|
3608
|
+
.flatMap((r) => [r.uri, ...(Array.isArray(r.members) ? r.members.map((m) => m?.uri).filter(Boolean) : [])]))];
|
|
3609
|
+
/**
|
|
3610
|
+
* What the goods of the writer's records rest on, READ ONCE for the labels its message carries and the states its tool
|
|
3611
|
+
* judges: the read walks the run's held records, which run to hundreds of megabytes, and took half a minute a pass on a
|
|
3612
|
+
* test run. null where it cannot be read; each reader then reads for itself and says so as before. IMPURE.
|
|
3613
|
+
*/
|
|
3614
|
+
function surfaceGoodsEvidence(ctx, P, rows) {
|
|
3615
|
+
try {
|
|
3616
|
+
const runPrefix = ctx.run?.slug && ctx.run?.codename ? `clearance-${ctx.run.slug}-${ctx.run.codename}-` : null;
|
|
3617
|
+
return runPrefix ? goodsEvidenceOfDrops(P, runPrefix, surfaceUris(rows)) : null;
|
|
3618
|
+
} catch { return null; }
|
|
3619
|
+
}
|
|
3620
|
+
/** Every floor decision the review reads: placement's, then the writer's. IMPURE (reads the run). */
|
|
3621
|
+
const reviewFloorDecisionLines = (P) => [...placementFloorDecisionLines(P), ...writerFloorDeclinationLines(P)];
|
|
3622
|
+
|
|
3485
3623
|
export function prepareDeclinationSpec(ctx, P) { // @internal
|
|
3486
3624
|
const rows = [...findingsSurfaceRows(P), ...notesPageRows(existsSync(P.commonLaw) ? readFileSync(P.commonLaw, "utf8") : null)]; // records, then the web notes' pages
|
|
3487
3625
|
if (!rows.length) {
|
|
@@ -3497,9 +3635,10 @@ export function prepareDeclinationSpec(ctx, P) { // @internal
|
|
|
3497
3635
|
try { scope = JSON.parse(readFileSync(P.instructedScope, "utf8")) ?? {}; } catch { scope = {}; }
|
|
3498
3636
|
try {
|
|
3499
3637
|
ensureDriverDir(P.runDir);
|
|
3638
|
+
const read = surfaceGoodsEvidence(ctx, P, rows);
|
|
3500
3639
|
atomicWrite(driverDir(P.runDir, "declination-spec.json"),
|
|
3501
|
-
JSON.stringify({ ts: new Date().toISOString(), rows, scope: { marks: scope.marks ?? [], classes: scope.classes ?? [] } }, null, 2) + "\n");
|
|
3502
|
-
ctx.findingsSurface = withGoodsFrom(ctx, P, rows);
|
|
3640
|
+
JSON.stringify({ ts: new Date().toISOString(), rows: withGoodsStates(ctx, P, rows, read), scope: { marks: scope.marks ?? [], classes: scope.classes ?? [] } }, null, 2) + "\n");
|
|
3641
|
+
ctx.findingsSurface = withGoodsFrom(ctx, P, rows, read);
|
|
3503
3642
|
} catch (e) {
|
|
3504
3643
|
// The dispatch block is gated on ctx.findingsSurface, so a failed write leaves the stage UNORDERED
|
|
3505
3644
|
// rather than ordered to call a tool whose spec it cannot read. Loud in the log, never a dead order.
|
|
@@ -4570,6 +4709,46 @@ export function recoverWinningAttempt(runDir, label) { // @internal
|
|
|
4570
4709
|
return null;
|
|
4571
4710
|
}
|
|
4572
4711
|
|
|
4712
|
+
// ── A REGISTER UNIT THAT COULD NOT FINISH IS DELIVERED AS ITS BAND STANDS ─────────────────────────────
|
|
4713
|
+
//
|
|
4714
|
+
// A unit that wrote no result once its attempts were spent ended the run, though the run held every other unit: on a
|
|
4715
|
+
// production run on 2026-10-21 one unit's plan search timed out at the register on every attempt, and the client got
|
|
4716
|
+
// nothing. Such a unit now fails as the searches not completed (the gather barrier below), naming itself and the planned
|
|
4717
|
+
// slices its band never reached. Once its recovery is spent, the run-level catch records both, and the resumed pass
|
|
4718
|
+
// takes the unit as it stands rather than sending it again: its band is what the kept answers left, or empty, and its
|
|
4719
|
+
// note is the driver's own account of that band, with no word from the seat. The band merge then takes the recorded
|
|
4720
|
+
// slices out as deferred blocks, so they are delivered as open rows.
|
|
4721
|
+
|
|
4722
|
+
/**
|
|
4723
|
+
* The planned slices of `axis` that its band holds no block for, each `{axis, qid, reason}` with the shipped reason for a
|
|
4724
|
+
* search that never reached the register. A slice with a block, a timed-out one included, is not among them: the band
|
|
4725
|
+
* already accounts for it. A guarded child is left to the plan join, which reads it off its parent. PURE but for the read.
|
|
4726
|
+
*/
|
|
4727
|
+
export function slicesUnitNeverReached(P, plan, axis) { // @internal
|
|
4728
|
+
let band = [];
|
|
4729
|
+
try { const parsed = JSON.parse(readFileSync(P.registerBand(axis), "utf8")); band = Array.isArray(parsed) ? parsed : []; } catch { band = []; }
|
|
4730
|
+
const held = new Set(band.map((b) => b?.qid).filter(Boolean).map(String));
|
|
4731
|
+
return (plan?.entries ?? []).filter((e) => e?.axis === axis && e?.qid && !e.when && !held.has(String(e.qid)))
|
|
4732
|
+
.map((e) => ({ axis, qid: String(e.qid), reason: plainDeferralReason("slice-not-landed") }));
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
/** The unit a run records as unfinished, taken as its band stands: its note rendered from it, nothing dispatched. */
|
|
4736
|
+
export function unfinishedUnitAsItStands(ctx) { // @internal
|
|
4737
|
+
const P = ctx.paths, a = ctx.axis, label = `register-unit:${a}`;
|
|
4738
|
+
const t0 = Date.now();
|
|
4739
|
+
mkdirSync(join(P.runDir, "register-units"), { recursive: true });
|
|
4740
|
+
const bandPath = P.registerBand(a);
|
|
4741
|
+
if (!existsSync(bandPath)) { writeFileSync(`${bandPath}.tmp`, "[]\n"); renameSync(`${bandPath}.tmp`, bandPath); }
|
|
4742
|
+
const out = P.registerUnit(a);
|
|
4743
|
+
writeFileSync(`${out}.tmp`, renderUnitNote({ axis: a, account: readBandAccount(P.runDir, a) }));
|
|
4744
|
+
renameSync(`${out}.tmp`, out);
|
|
4745
|
+
runLog(P.runDir, { event: "unit-not-completed-delivery", stage: label, output: outputMeta(out), ...stageWallFields(t0) });
|
|
4746
|
+
note(`[${label}] could not be completed once its recovery was spent — taken as its band stands and not sent again`);
|
|
4747
|
+
(ctx.skippedStages ??= []).push(label);
|
|
4748
|
+
recordTransition(ctx, "register-unit");
|
|
4749
|
+
return { ok: true, skipped: true, notCompleted: true, sessionKey: null, model: null };
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4573
4752
|
// ── saturation-probe runs CODE-SIDE (2026-07-14, fabricated-band root cause) ────────────────────────
|
|
4574
4753
|
// The axis is fully dictated (count-only crowd descriptors; judgment proposes nothing on it), yet it was
|
|
4575
4754
|
// staffed by the cheapest model whose ONLY job was one register_execute_plan call — and the provider
|
|
@@ -4825,7 +5004,7 @@ function clearedForSeat(runDir, label) {
|
|
|
4825
5004
|
*/
|
|
4826
5005
|
export function messageCtxFor(name, ctx) { // @internal — driven by its test
|
|
4827
5006
|
if (!ctx?.paths) return ctx;
|
|
4828
|
-
if (name === "narrative-refutation") return { ...ctx, placementFloorDecisions:
|
|
5007
|
+
if (name === "narrative-refutation") return { ...ctx, placementFloorDecisions: reviewFloorDecisionLines(ctx.paths) };
|
|
4829
5008
|
// The reading step and the writer are told placement's accepted set-asides the same way, read when the message is built.
|
|
4830
5009
|
if (name === "register-digest") return { ...ctx, placementSetAsides: placementSetAsideLines(ctx.paths) };
|
|
4831
5010
|
// The writer is also handed the research its earlier attempts got (research-answers.mjs), written to the run folder when
|
|
@@ -5395,7 +5574,7 @@ export function terminalReasonFields(reason, { cap = 200, fullCap = 4000 } = {})
|
|
|
5395
5574
|
// every terminal failure to preserve. And per the payload stays OUT of the sentence — `whatsappText`
|
|
5396
5575
|
// and the CLIENT body are untouched; the staff body gets it in its own <pre>, beside the verbatim reason
|
|
5397
5576
|
// that body already quotes.
|
|
5398
|
-
export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortReason, reasonVerbatim, sig, failClass, terminalKind, repairs = [], priorAttempts = 0,
|
|
5577
|
+
export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortReason, reasonVerbatim, sig, failClass, terminalKind, repairs = [], priorAttempts = 0, reasonDetail = null, reasonQuantity = null, capWait = null }) {
|
|
5399
5578
|
const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<");
|
|
5400
5579
|
const detail = typeof reasonDetail === "string" && reasonDetail.trim() ? reasonDetail.trim() : null;
|
|
5401
5580
|
const quantity = Number.isFinite(reasonQuantity) ? reasonQuantity : null;
|
|
@@ -5514,7 +5693,10 @@ export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortR
|
|
|
5514
5693
|
: refused
|
|
5515
5694
|
? `<p>${esc(orderedIs)} for <b>${esc(mark)}</b> was REFUSED at stage <b>${esc(failedStage)}</b> before any work was done, and NOTHING was delivered.</p><p>${esc(kindLine)}</p><p>The driver reported, verbatim:</p><pre style="white-space:pre-wrap">${esc(reasonVerbatim)}</pre><p>The run directory is preserved; this is not a defect to diagnose.</p>`
|
|
5516
5695
|
: `<p>${esc(orderedIs)} for <b>${esc(mark)}</b> FAILED at stage <b>${esc(failedStage)}</b>${priorAttempts ? ` after <b>${priorAttempts} automatic recovery attempt${priorAttempts === 1 ? "" : "s"}</b>` : ""} and NOTHING was delivered.</p><p>${esc(kindLine)}</p><p>The driver reported, verbatim:</p><pre style="white-space:pre-wrap">${esc(reasonVerbatim)}</pre>${detailBlock}${attempted}<p>The run directory is preserved for diagnosis; re-trigger the run when the cause is addressed.</p>`,
|
|
5517
|
-
|
|
5696
|
+
// THE NOTICE GOES TO WHOEVER ASKED, as the completion notice does (whatsappRouting, stages.mjs): the requester's own
|
|
5697
|
+
// number where one is held, the gap stated where none is, and the operator's copy beside it. It went to the agent's
|
|
5698
|
+
// number, which every user of a deployment shares, so the operator was told of a failure the requester never heard.
|
|
5699
|
+
...whatsappRouting(job, agent),
|
|
5518
5700
|
whatsappText: refused
|
|
5519
5701
|
? `⛔ Clearotron search for ${mark} REFUSED at ${failedStage} [${terminalKind}]: ${shortReason} — nothing was delivered and nothing broke; ${retryHint}.`
|
|
5520
5702
|
: `❌ Clearotron search for ${mark} FAILED at ${failedStage}${priorAttempts ? ` after ${priorAttempts} automatic recovery attempt${priorAttempts === 1 ? "" : "s"}` : ""}${terminalKind ? ` [${terminalKind}]` : ""}: ${shortReason} — nothing was delivered; ${retryHint}.`,
|
|
@@ -6941,7 +7123,7 @@ async function runPostRepairReview(ctx, P, run, scope) {
|
|
|
6941
7123
|
const changedInputs = changedInputsOf(scope.changed, { narrative: P.narrative, findings: P.findings });
|
|
6942
7124
|
const message = repairFollowup("narrative-refutation:post-repair-review", {
|
|
6943
7125
|
findings: P.findings, ...list, registerOnly: Boolean(ctx.registerOnly), job: ctx.job ?? {},
|
|
6944
|
-
skepticFlags: P.skepticFlags, frameReopenReceipt: P.frameReopenReceipt, floorDecisions:
|
|
7126
|
+
skepticFlags: P.skepticFlags, frameReopenReceipt: P.frameReopenReceipt, floorDecisions: reviewFloorDecisionLines(P),
|
|
6945
7127
|
});
|
|
6946
7128
|
const { text: blocks } = composeDispatchExtra("narrative-refutation", ctx, { skip: ["refute-plain-register"] });
|
|
6947
7129
|
const t0 = Date.now();
|
|
@@ -7055,6 +7237,16 @@ function trustedStateBeforeCorrection(Pv, runDir, before) {
|
|
|
7055
7237
|
base: { accepted: k.accepted, narrative: k.narrative }, from: "state-before-the-review" };
|
|
7056
7238
|
}
|
|
7057
7239
|
} catch { /* no state kept, or it does not read: the saves below */ }
|
|
7240
|
+
return recordFromTheSaves(Pv, runDir, before);
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
/**
|
|
7244
|
+
* THE LAST RECORD A SAVE PASSED, rebuilt through the driver's own acceptance: the save the review read (`before`), then
|
|
7245
|
+
* the save kept from before the delivery-check repair (sources 3 and 4 of `trustedStateBeforeCorrection`, with the
|
|
7246
|
+
* same duty rule). Null when neither stands. Never throws.
|
|
7247
|
+
*/
|
|
7248
|
+
function recordFromTheSaves(Pv, runDir, before) {
|
|
7249
|
+
const sha = (t) => createHash("sha256").update(t).digest("hex");
|
|
7058
7250
|
const reads = (() => { try { return driverReadsFor(runDir); } catch { return null; } })();
|
|
7059
7251
|
const fromSave = (params, accepted, from) => {
|
|
7060
7252
|
if (!params || accepted == null || !reads) return null;
|
|
@@ -7075,6 +7267,46 @@ function trustedStateBeforeCorrection(Pv, runDir, before) {
|
|
|
7075
7267
|
?? fromSave(readRepairBase(runDir), bytes(repairBasePath(runDir)), "save-before-the-repair");
|
|
7076
7268
|
}
|
|
7077
7269
|
|
|
7270
|
+
/**
|
|
7271
|
+
* A FINDINGS RECORD BROKEN AFTER SYNTHESIS PASSED IT IS RESTORED AT DELIVERY, NOT ENDED ON. The delivery checks
|
|
7272
|
+
* find the record broken at the top level, so a write after synthesis broke it. The record comes back from the
|
|
7273
|
+
* last save that passed, by the corrective rollback's own search (`recordFromTheSaves`): the save the review read, then
|
|
7274
|
+
* the save kept from before the delivery-check repair. It is folded, and given the seam's writes, as after any save, and
|
|
7275
|
+
* the check runs again on the record that now stands. The broken record is kept under `_driver/` as evidence. Returns
|
|
7276
|
+
* {from, duty}, or null where no save stands or the record restored fails the check too. IMPURE (writes the run).
|
|
7277
|
+
*/
|
|
7278
|
+
async function restoreTheRecordThatPassed(ctx, P, runDir, check) {
|
|
7279
|
+
const pre = recordFromTheSaves(P, runDir, lastAcceptedCall(runDir));
|
|
7280
|
+
if (!pre?.raw) return null;
|
|
7281
|
+
try { copyFileSync(P.findings, driverDir(runDir, "findings-broken-at-delivery.json")); } catch { /* the evidence is best-effort */ }
|
|
7282
|
+
atomicWrite(P.findings, pre.raw);
|
|
7283
|
+
restoreSynthesisBase(runDir, pre.base);
|
|
7284
|
+
try { foldAfterSave(runDir, P.findings); } catch (e) { note(`[core-artifact] the restored record could not be folded (${String(e?.message ?? e).slice(0, 100)})`); }
|
|
7285
|
+
try { await ctx.afterSynthesisSave?.(); } catch (e) { note(`[core-artifact] the seam's writes could not be made again over the restored record (${String(e?.message ?? e).slice(0, 100)})`); }
|
|
7286
|
+
try { check(readFileSync(P.findings, "utf8")); } catch { return null; }
|
|
7287
|
+
return { from: pre.from, duty: pre.duty ?? "current" };
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// A correction row's outcomes that say the pass changed the record or the narrative (corrections-feedforward.mjs).
|
|
7291
|
+
const CORRECTION_CHANGED = new Set(["section-changed", "line-removed", "line-changed", "findings-removed", "findings-changed"]);
|
|
7292
|
+
|
|
7293
|
+
/**
|
|
7294
|
+
* THE CORRECTIONS A RECORD RESTORED FROM THE SAVE BEFORE THE REPAIR DOES NOT CARRY: what the correction after the review
|
|
7295
|
+
* of that repair changed, by the names its points targeted. The corrections' state is judged once, when the correction
|
|
7296
|
+
* runs, so nothing else would see that the restored record predates it. Written as that state, the client gate's
|
|
7297
|
+
* "findings-stale" reason closes the client export over it, as when a correction did not reach the findings. [] when
|
|
7298
|
+
* the record came from the accepted save, or no correction after the repair changed anything. Never throws.
|
|
7299
|
+
*/
|
|
7300
|
+
export function correctionsTheRestoreLacks(runDir, from) { // @internal
|
|
7301
|
+
if (from !== "save-before-the-repair") return [];
|
|
7302
|
+
try {
|
|
7303
|
+
const rec = JSON.parse(readFileSync(postRepairCorrectionsPath(runDir), "utf8"));
|
|
7304
|
+
const named = (r) => (Array.isArray(r.targets) && r.targets.length ? r.targets : [(rec.classes ?? []).find((c) => c?.n === r.n)?.target]);
|
|
7305
|
+
return [...new Set((rec.rows ?? []).filter((r) => CORRECTION_CHANGED.has(r?.outcome)).flatMap(named)
|
|
7306
|
+
.map((t) => String(t ?? "").trim()).filter(Boolean))];
|
|
7307
|
+
} catch { return []; }
|
|
7308
|
+
}
|
|
7309
|
+
|
|
7078
7310
|
/**
|
|
7079
7311
|
* A FAILED CORRECTION AFTER A POST-REPAIR REVIEW NEVER ENDS THE RUN (the owner's ruling of 2026-10-19: a run is never
|
|
7080
7312
|
* held). The trusted state comes back whole: the record, its narrative and the merge base, then the fold and the seam's
|
|
@@ -7083,13 +7315,14 @@ function trustedStateBeforeCorrection(Pv, runDir, before) {
|
|
|
7083
7315
|
* not waited for either: it is rolled back as a failed one is, since the report it would correct is already reviewed,
|
|
7084
7316
|
* and waiting for the reset would hold the run.
|
|
7085
7317
|
*
|
|
7086
|
-
* WITH NO TRUSTED STATE, THIS PASS DOES NOT DELIVER. That is the precise limit: the record, the state
|
|
7087
|
-
* the review, the save the review read and the save kept from before the repair are all unreadable
|
|
7088
|
-
* storage failure. Nothing the failed pass wrote is adopted: the narrative and the base
|
|
7318
|
+
* WITH NO TRUSTED STATE, THIS PASS DOES NOT DELIVER THE ASSESSMENT. That is the precise limit: the record, the state
|
|
7319
|
+
* kept whole before the review, the save the review read and the save kept from before the repair are all unreadable
|
|
7320
|
+
* or refused, a storage failure. Nothing the failed pass wrote is adopted: the narrative and the base
|
|
7089
7321
|
* go back to what they were, and the failed pass's record is set aside under `_driver/` as evidence. A pass that went on from there delivered a
|
|
7090
7322
|
* report with no findings, labelled with the verdict as it stood (measured on the mock pipeline, 2026-10-19): a clear
|
|
7091
|
-
* result over a record nobody can read. So
|
|
7092
|
-
*
|
|
7323
|
+
* result over a record nobody can read. So the failure is the assessment not completed (parts-not-completed.mjs): once
|
|
7324
|
+
* its recovery is spent, the run delivers what it holds without the assessment. Returns the rollback record, or null
|
|
7325
|
+
* for this case.
|
|
7093
7326
|
*/
|
|
7094
7327
|
async function recoverFromFailedCorrection(ctx, Pv, runDir, pre, pass, { baseBefore, findingsBefore }) {
|
|
7095
7328
|
// A rate limit is a failure of this pass like any other here: the shared rollback would leave it to wait.
|
|
@@ -7156,7 +7389,7 @@ async function runPostRepairCorrection(ctx, P, run, { rec, items, points, ground
|
|
|
7156
7389
|
...(rollback.duty ? { duty: rollback.duty } : {}) });
|
|
7157
7390
|
note(`[post-repair] the correction after the review of the repair failed (${rollback.reason}) — the record it started from is back, and the run delivers with the points recorded`);
|
|
7158
7391
|
} else {
|
|
7159
|
-
must(pass, "synthesis(post-repair)");
|
|
7392
|
+
await asPartNotCompleted("assessment", async () => must(pass, "synthesis(post-repair)"));
|
|
7160
7393
|
let preDocForNames = null;
|
|
7161
7394
|
try { preDocForNames = pre ? parseFindingsJsonLenient(pre.raw) : null; } catch { /* fall back to the file */ }
|
|
7162
7395
|
const repaired = repairUnnamedRemovals(Pv, runDir, pre, correctionNamedOrdinals(Pv), correctionNamedSet(Pv, preDocForNames), correctionNamedLines(Pv, preDocForNames));
|
|
@@ -10145,7 +10378,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10145
10378
|
const p = writeOutboxPacket(`${runId}.bindack`, {
|
|
10146
10379
|
kind: "late-bind-ack", runId, agent,
|
|
10147
10380
|
forwarder: job.forwarder ?? null, forwarderEmail: job.forwarderEmail ?? null, msgId: job.msgId ?? null,
|
|
10148
|
-
customer: bind.customer, action,
|
|
10381
|
+
customer: bind.customer, action, ...whatsappRouting(job, agent), text,
|
|
10149
10382
|
});
|
|
10150
10383
|
runLog(run.runDir, { event: "customer-late-bind-ack", sent: Boolean(p), via: "outbox" });
|
|
10151
10384
|
} catch (e) { note(`late-bind ack failed (non-fatal): ${e?.message ?? e}`); }
|
|
@@ -10653,9 +10886,13 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10653
10886
|
// opt out — their scenarios pre-date the code-side member and script the AGENT path (and an armed
|
|
10654
10887
|
// real executor lane would dial the provider from a test). Production default is ON.
|
|
10655
10888
|
const satProbeCodeSide = process.env.CLEAROTRON_SATPROBE_CODESIDE !== "0" && Boolean(planExec && ctx.registerPlan);
|
|
10889
|
+
// The units this run records as unfinished are taken as their bands stand (unfinishedUnitAsItStands, above).
|
|
10890
|
+
const unfinishedUnits = new Set(readPartNotCompleted(run.runDir, "searches")?.unfinished_units ?? []);
|
|
10656
10891
|
const [gatherResults, blindRes] = await Promise.all([
|
|
10657
10892
|
runBatched(gather, config.gatherConcurrency, (g) =>
|
|
10658
|
-
g.name === "register-unit" && g.axis
|
|
10893
|
+
g.name === "register-unit" && unfinishedUnits.has(g.axis)
|
|
10894
|
+
? unfinishedUnitAsItStands({ ...ctx, axis: g.axis })
|
|
10895
|
+
: g.name === "register-unit" && g.axis === "saturation-probe" && satProbeCodeSide
|
|
10659
10896
|
? runSaturationProbeCodeSide({ ...ctx, axis: g.axis }, planExec)
|
|
10660
10897
|
: stage(g.name, { ...ctx, axis: g.axis })),
|
|
10661
10898
|
stage("blind-frame", ctx).catch((e) => ({ ok: false, fail: String(e?.message ?? e) })),
|
|
@@ -10774,7 +11011,17 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10774
11011
|
runLog(run.runDir, { event: "connotation-reissue-failed", error: String(err?.message ?? err).slice(0, 160) });
|
|
10775
11012
|
}
|
|
10776
11013
|
}
|
|
10777
|
-
|
|
11014
|
+
// A REGISTER UNIT THAT WROTE NO RESULT is the searches not completed, after every other member has been held to its
|
|
11015
|
+
// own failure as before (unfinishedUnitAsItStands, above). A rate limit keeps its own path.
|
|
11016
|
+
const unfinished = gather.map((g, i) => i).filter((i) => gather[i].name === "register-unit" && gather[i].axis
|
|
11017
|
+
&& !gatherResults[i]?.ok && !gatherResults[i]?.resetsAt && !existsSync(P.registerUnit(gather[i].axis)));
|
|
11018
|
+
gather.forEach((g, i) => { if (!unfinished.includes(i)) must(gatherResults[i], g.name + (g.axis ? `:${g.axis}` : "")); });
|
|
11019
|
+
if (unfinished.length) {
|
|
11020
|
+
const axesOut = unfinished.map((i) => gather[i].axis), first = gatherResults[unfinished[0]];
|
|
11021
|
+
throw Object.assign(new StageFailure(`register-unit:${axesOut[0]}`, first?.fail ?? "failed", undefined, { quantity: first?.quantity }),
|
|
11022
|
+
{ notCompletedPart: "searches", unfinishedUnits: axesOut,
|
|
11023
|
+
excludedSlices: axesOut.flatMap((a) => slicesUnitNeverReached(P, ctx.registerPlan, a)) });
|
|
11024
|
+
}
|
|
10778
11025
|
// winning session key per axis → a later escalation resumes the EXACT key the unit succeeded on.
|
|
10779
11026
|
// (register units only — a common-law half's axis is its half id, tracked in clHalves, never here.)
|
|
10780
11027
|
const unitKey = {};
|
|
@@ -11227,6 +11474,16 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11227
11474
|
for (const f of [...(runCommonLaw ? [P.commonLaw] : []), ...axes.map((a) => P.registerUnit(a))]) {
|
|
11228
11475
|
if (!existsSync(f)) throw new StageFailure("fan-in", `missing ${f}`);
|
|
11229
11476
|
}
|
|
11477
|
+
// THE SLICES A CHECK COULD NOT PROVE RAN, ONCE ITS RECOVERY WAS SPENT: each is taken out of its axis's band and
|
|
11478
|
+
// delivered as an open row, before the band's checks and the plan join read the band. The checks below name the
|
|
11479
|
+
// slices they fail on, and the run-level catch records them (parts-not-completed.mjs).
|
|
11480
|
+
const searchesOpen = readPartNotCompleted(run.runDir, "searches");
|
|
11481
|
+
if (Array.isArray(searchesOpen?.slices) && searchesOpen.slices.length) {
|
|
11482
|
+
const taken = takeSlicesOutOfTheBand(P, searchesOpen.slices);
|
|
11483
|
+
runLog(run.runDir, { event: "searches-not-completed-delivery", slices: searchesOpen.slices.length, taken: taken.length,
|
|
11484
|
+
reasons: [...new Set(searchesOpen.slices.map((s) => s.reason))] });
|
|
11485
|
+
note(`register: ${searchesOpen.slices.length} slice(s) a check could not prove ran are taken out of the band and delivered as open rows`);
|
|
11486
|
+
}
|
|
11230
11487
|
// judgment-relocation LINCHPIN (2026-06-24): the load-bearing funnel artifact is the COMPLETE NAMED BAND
|
|
11231
11488
|
// (register-units/<axis>-band.json) — the real material that crosses the lifted firewall. A unit that RAN
|
|
11232
11489
|
// THIS process (unitKey[a] set) MUST have written it; a missing band would merge to empty and let a clean
|
|
@@ -11239,6 +11496,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11239
11496
|
// archived) keeps the lenient path. copper-spire's shipping path was exactly fail → manual resume →
|
|
11240
11497
|
// resumed-past leniency → degrade-with-note → ship.
|
|
11241
11498
|
const anyBands = axes.some((x) => existsSync(P.registerBand(x)));
|
|
11499
|
+
const unprovenBands = []; // every axis whose band the ledger cannot evidence, failed on together below
|
|
11500
|
+
const collapsedSlices = []; // every axis's qid-stamped slices that claimed hits and carried no record, likewise
|
|
11242
11501
|
for (const a of axes) {
|
|
11243
11502
|
if ((unitKey[a] || anyBands) && !existsSync(P.registerBand(a)))
|
|
11244
11503
|
throw new StageFailure("fan-in", `missing named band for axis "${a}" — the funnel produced no complete band (a clean can never ship over an unsearched band)`, undefined, { detail: `${P.registerBand(a)}${unitKey[a] ? "" : " — resumed-past axis on a run that carries bands: a hole, not legacy"}` });
|
|
@@ -11255,8 +11514,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11255
11514
|
// collapsed slices already had their re-run via the validator ladder — they stay terminal.
|
|
11256
11515
|
const collapsedQids = collapsed.map((x) => x.qid).filter(Boolean);
|
|
11257
11516
|
if (collapsedQids.length && (await dispatchPlanQids(a, collapsedQids, "collapsed-reexecute")) === "ok") collapsed = readCollapsed();
|
|
11258
|
-
|
|
11259
|
-
|
|
11517
|
+
// The collapsed slices are the searches not completed, failed on with every axis's below. One with no qid is named by
|
|
11518
|
+
// its query, and its block becomes a deferred block on that query.
|
|
11519
|
+
if (collapsed.length) collapsedSlices.push({ axis: a, collapsed });
|
|
11260
11520
|
}
|
|
11261
11521
|
// ── band-truth gate (2026-07-14, teal-foundry): the band must be EVIDENCED, not just well-formed ──
|
|
11262
11522
|
// A qid-stamped band whose unit lane logged ZERO real provider calls was AUTHORED, not executed —
|
|
@@ -11276,10 +11536,12 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11276
11536
|
if (process.env.CLEAROTRON_BAND_TRUTH_GATE !== "0"
|
|
11277
11537
|
&& unitKey[a] && existsSync(P.registerBand(a)) && (ctx.registerPlan?.entries ?? []).some((e) => e?.axis === a)) {
|
|
11278
11538
|
const laneKey = `clearance-${run.slug}-${run.codename}-register-unit-${a}`;
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11539
|
+
// A slice taken out of the band, or one the provider could not express, is a deferred block that made no call.
|
|
11540
|
+
const qidBlocks = (() => {
|
|
11541
|
+
try { const b = JSON.parse(readFileSync(P.registerBand(a), "utf8")); return Array.isArray(b) ? b.filter((x) => x && typeof x === "object" && x.qid && !(x.error === true && x.deferred === true)) : []; }
|
|
11542
|
+
catch { return []; /* parse defects are the stage validator's problem, not this gate's */ }
|
|
11282
11543
|
})();
|
|
11544
|
+
const bandHasQids = qidBlocks.length > 0;
|
|
11283
11545
|
if (bandHasQids && countLaneCalls(laneKey) === 0) {
|
|
11284
11546
|
note(`register-unit ${a}: band carries qid blocks but its lane logged ZERO provider calls — authored, not executed; direct re-execution from the frozen plan`);
|
|
11285
11547
|
runLog(run.runDir, { event: "band-truth-gate", axis: a, lane: laneKey, calls: 0 });
|
|
@@ -11289,11 +11551,36 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11289
11551
|
runLog(run.runDir, { event: "band-truth-rebuilt", axis: a, outcome, calls: callsNow ?? 0 });
|
|
11290
11552
|
note(`register-unit ${a}: band re-executed from the frozen plan (dispatch ${outcome ?? "unavailable"}, ${callsNow ?? 0} ledger call(s)) — fan-in continues over executor data`);
|
|
11291
11553
|
} else {
|
|
11292
|
-
|
|
11554
|
+
unprovenBands.push({ axis: a, laneKey, reexecution: outcome, qids: qidBlocks.map((x) => x.qid) });
|
|
11293
11555
|
}
|
|
11294
11556
|
}
|
|
11295
11557
|
}
|
|
11296
11558
|
}
|
|
11559
|
+
// THE SLICES THE BAND'S CHECKS COULD NOT PROVE RAN, failed on together, so one park takes every axis's. They are the
|
|
11560
|
+
// searches not completed: once this is terminal they are taken out of the band and delivered as open rows, each with
|
|
11561
|
+
// its shipped reason. A band no ledger call evidences reads "nothing in the run's own record confirms it was
|
|
11562
|
+
// searched"; a slice that claimed hits and carried no record reads "it could not be completed this run". A slice that
|
|
11563
|
+
// is both keeps the first.
|
|
11564
|
+
const notCompleted = [
|
|
11565
|
+
...unprovenBands.flatMap((u) => u.qids.map((qid) => ({ axis: u.axis, qid, reason: plainDeferralReason("not-verified-closed") }))),
|
|
11566
|
+
...collapsedSlices.flatMap((c) => c.collapsed.map((x) => (x.qid ? { axis: c.axis, qid: x.qid, reason: plainDeferralReason("collapsed-slice") }
|
|
11567
|
+
: { axis: c.axis, query: String(x.query ?? ""), reason: plainDeferralReason("collapsed-slice") }))),
|
|
11568
|
+
];
|
|
11569
|
+
if (unprovenBands.length) {
|
|
11570
|
+
const [first] = unprovenBands, more = unprovenBands.length - 1;
|
|
11571
|
+
throw Object.assign(new StageFailure("fan-in", `fabricated named band for ${more ? `${unprovenBands.length} axes` : `axis "${first.axis}"`} — qid-stamped blocks, ZERO provider calls in the ledger, re-execution not evidenced (a clean can never ship over an unexecuted band)`,
|
|
11572
|
+
undefined, { failClass: "deterministic", repairs: fanInRepairs,
|
|
11573
|
+
detail: `${P.registerBand(first.axis)} — lane "${first.laneKey}", re-execution ${first.reexecution ?? "no executor lane"}${more ? `; ${more} more axis(es): ${unprovenBands.slice(1).map((u) => u.axis).join(", ")}` : ""}` }),
|
|
11574
|
+
{ notCompletedPart: "searches", excludedSlices: notCompleted });
|
|
11575
|
+
}
|
|
11576
|
+
if (collapsedSlices.length) {
|
|
11577
|
+
const [{ axis: a, collapsed }] = collapsedSlices;
|
|
11578
|
+
const n = collapsedSlices.reduce((k, c) => k + c.collapsed.length, 0);
|
|
11579
|
+
throw Object.assign(new StageFailure("fan-in", `collapsed named band for axis "${a}" — ${n} enumerated slice(s) claimed hits but carried ZERO records into the band (a clean can never ship over a collapsed slice)`,
|
|
11580
|
+
undefined, { failClass: "deterministic", repairs: fanInRepairs,
|
|
11581
|
+
detail: `${P.registerBand(a)} — ${collapsed.slice(0, 6).map((x) => `${(x.query || "slice").slice(0, 40)}~${x.total_hits}`).join("; ")}${collapsedSlices.length > 1 ? `; and on ${collapsedSlices.length - 1} more axis(es)` : ""}` }),
|
|
11582
|
+
{ notCompletedPart: "searches", excludedSlices: notCompleted });
|
|
11583
|
+
}
|
|
11297
11584
|
|
|
11298
11585
|
// ── timeout-taint chain (copper-lattice 2026-07-08): unfinished ⇒ deferred ⇒ recover ⇒ converge ──────
|
|
11299
11586
|
// A register-unit pass SIGKILLed at the wall leaves partial band writes the merge preserves; the
|
|
@@ -11318,6 +11605,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11318
11605
|
const taintedNow = axes.filter((a) => taint0[a]?.tainted);
|
|
11319
11606
|
if (taintedNow.length) {
|
|
11320
11607
|
const recoveryAttempts = (() => { try { return Number(JSON.parse(readFileSync(join(run.runDir, "status.json"), "utf8")).recoveryAttempts) || 0; } catch { return 0; } })();
|
|
11608
|
+
// The axes whose recovery is spent: the searches record lists them, and each is read as it stands and disclosed.
|
|
11609
|
+
const disclosedAxes = new Set(Array.isArray(searchesOpen?.disclosed_axes) ? searchesOpen.disclosed_axes : []);
|
|
11321
11610
|
const axesRec = {};
|
|
11322
11611
|
const digestLocked = existsSync(P.narrative); // synthesis done — the audit spine is locked; disclose, never mutate under it
|
|
11323
11612
|
for (const a of taintedNow) {
|
|
@@ -11336,8 +11625,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11336
11625
|
}
|
|
11337
11626
|
}
|
|
11338
11627
|
} catch (e) { note(`register-unit ${a}: taint quarantine not applicable (${String(e.message).slice(0, 100)})`); }
|
|
11339
|
-
// T0.5 — deterministic re-execution of the dictated half (repair-ledger bounded)
|
|
11340
|
-
|
|
11628
|
+
// T0.5 — deterministic re-execution of the dictated half (repair-ledger bounded). Not for an axis the searches
|
|
11629
|
+
// record discloses: its recovery is spent, and its band is read as the last pass left it.
|
|
11630
|
+
const dispatched = disclosedAxes.has(a) ? null : await dispatchPlanQids(a, null, "taint-dispatch");
|
|
11341
11631
|
if (dispatched) repairs.push(`taint-dispatch:${dispatched}`);
|
|
11342
11632
|
// T1 — ONE fresh in-loop re-run, pre-synthesis only (a fresh full pass, full fallback chain)
|
|
11343
11633
|
// — held until the real result is in hand; see the push below the taint re-read.
|
|
@@ -11345,7 +11635,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11345
11635
|
// — ONE ceiling, named once and read by BOTH ends. A literal repeated at the record site is a
|
|
11346
11636
|
// second statement of the same rule, and two ends stating the same rule is how they drift apart.
|
|
11347
11637
|
const taintRerunMax = 1;
|
|
11348
|
-
if (!digestLocked && repairLedger.canAttempt("taint-rerun", a, { max: taintRerunMax, epoch: repairEpoch })) {
|
|
11638
|
+
if (!digestLocked && !disclosedAxes.has(a) && repairLedger.canAttempt("taint-rerun", a, { max: taintRerunMax, epoch: repairEpoch })) {
|
|
11349
11639
|
note(`register-unit ${a}: taint-rerun — one fresh re-run (fresh session; a killed session is never warm-resumed)`);
|
|
11350
11640
|
const r = await stage("register-unit", { ...ctx, axis: a },
|
|
11351
11641
|
{ force: true, trigger: "taint-rerun", sessionKey: `clearance-${run.slug}-${run.codename}-register-unit-${a}-taint-rerun-${recoveryAttempts}` });
|
|
@@ -11390,34 +11680,18 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11390
11680
|
const stillTainted = Object.entries(axesRec)
|
|
11391
11681
|
.filter(([a, v]) => v.status === "active" && !NON_MATERIAL_AXES.includes(String(a).toLowerCase()))
|
|
11392
11682
|
.map(([a]) => a);
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
//
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
try { if (existsSync(f)) renameSync(f, `${f}.tainted-${recoveryAttempts}`); } catch { /* best-effort */ }
|
|
11401
|
-
}
|
|
11402
|
-
}
|
|
11403
|
-
try { if (existsSync(P.planExecution)) rmSync(P.planExecution); } catch { /* best-effort */ }
|
|
11404
|
-
for (const f of taintParkJudgmentArtifacts(P, run.runDir)) {
|
|
11405
|
-
try { if (existsSync(f)) renameSync(f, `${f}.tainted-${recoveryAttempts}`); } catch { /* best-effort */ }
|
|
11406
|
-
}
|
|
11407
|
-
runLog(run.runDir, { event: "taint-park", axes: stillTainted, attempt: recoveryAttempts });
|
|
11408
|
-
throw new StageFailure(`register-unit:${stillTainted[0]}`,
|
|
11409
|
-
`register slice timeout-tainted and unrecovered — an unfinished pass cannot ship self-reported clean (axes: ${stillTainted.join(", ")})`,
|
|
11410
|
-
undefined, { failClass: "transient", repairs: fanInRepairs });
|
|
11411
|
-
}
|
|
11412
|
-
if (stillTainted.length && digestLocked) {
|
|
11413
|
-
// DISCLOSE — the sanctioned post-synthesis lever: the client export is withheld until the
|
|
11414
|
-
// search is re-run to closure (the same class as a failed escalation/envelope), the internal
|
|
11415
|
-
// report ships CONDITIONAL via the ledger relabel + registerGap clamp.
|
|
11683
|
+
// Disclosed: every axis once synthesis is done, and an axis the searches record lists once its recovery is spent.
|
|
11684
|
+
// Parked: the rest, while the run can still re-run them.
|
|
11685
|
+
const { disclose: toDisclose, park: toPark } = taintDisposition(stillTainted, { digestLocked, disclosedAxes });
|
|
11686
|
+
if (toDisclose.length) {
|
|
11687
|
+
// DISCLOSE — the sanctioned lever once synthesis is done, and once an axis's recovery is spent: the client
|
|
11688
|
+
// export is withheld until the search is re-run to closure (the same class as a failed escalation/envelope),
|
|
11689
|
+
// the internal report ships CONDITIONAL via the ledger relabel + registerGap clamp.
|
|
11416
11690
|
try {
|
|
11417
11691
|
const escPath = driverDir(run.runDir, "escalation-state.json");
|
|
11418
11692
|
let esc = {}; try { esc = JSON.parse(readFileSync(escPath, "utf8")); } catch { /* fresh */ }
|
|
11419
11693
|
const failed = Array.isArray(esc.failed) ? esc.failed : [];
|
|
11420
|
-
for (const a of
|
|
11694
|
+
for (const a of toDisclose) if (!failed.includes(`taint:${a}`)) failed.push(`taint:${a}`);
|
|
11421
11695
|
atomicWrite(escPath, JSON.stringify({ ...esc, ts: new Date().toISOString(), failed }, null, 2) + "\n"); // B5 — the client-gate reads this: never truncated
|
|
11422
11696
|
} catch (e) { note(`taint disclose: escalation-state append failed (${String(e.message).slice(0, 80)})`); }
|
|
11423
11697
|
// Receipt status → "disclosed": the LEDGER keeps treating the axis as a gap (deferred → clamp +
|
|
@@ -11426,11 +11700,31 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11426
11700
|
try {
|
|
11427
11701
|
const tp = driverDir(run.runDir, "register-taint.json");
|
|
11428
11702
|
const rec = JSON.parse(readFileSync(tp, "utf8"));
|
|
11429
|
-
for (const a of
|
|
11703
|
+
for (const a of toDisclose) if (rec?.axes?.[a]) rec.axes[a] = { ...rec.axes[a], status: "disclosed" };
|
|
11430
11704
|
writeFileSync(`${tp}.tmp`, JSON.stringify(rec, null, 2) + "\n"); renameSync(`${tp}.tmp`, tp);
|
|
11431
11705
|
} catch (e) { note(`register-taint receipt disclose update failed (non-fatal): ${String(e.message).slice(0, 80)}`); }
|
|
11432
|
-
runLog(run.runDir, { event: "taint-disclose", axes:
|
|
11433
|
-
note(`timeout-taint on ${
|
|
11706
|
+
runLog(run.runDir, { event: "taint-disclose", axes: toDisclose });
|
|
11707
|
+
note(`timeout-taint on ${toDisclose.join(", ")} ${digestLocked ? "past the locked audit spine" : "with its recovery spent"} — disclosed: client export withheld, verdict clamps CONDITIONAL (internal report still ships)`);
|
|
11708
|
+
}
|
|
11709
|
+
if (toPark.length) {
|
|
11710
|
+
// PARK — the converging outer net. Rename (never delete — forensics) so stageOnce's
|
|
11711
|
+
// exists-and-validates skip MISSES and the resume genuinely re-executes the tainted units;
|
|
11712
|
+
// drop the plan-execution receipt so the re-join runs over the fresh bands; a digest already
|
|
11713
|
+
// derived from tainted material is renamed too, else its skip would ship stale rows. The renames ride the
|
|
11714
|
+
// failure: once its recovery is spent, the run-level catch puts them back and the axes are disclosed instead.
|
|
11715
|
+
const undoRenames = [];
|
|
11716
|
+
const parkRename = (f) => {
|
|
11717
|
+
try { if (existsSync(f)) { renameSync(f, `${f}.tainted-${recoveryAttempts}`); undoRenames.push({ from: f, to: `${f}.tainted-${recoveryAttempts}` }); } }
|
|
11718
|
+
catch { /* best-effort */ }
|
|
11719
|
+
};
|
|
11720
|
+
for (const a of toPark) for (const f of [P.registerUnit(a), P.registerBand(a)]) parkRename(f);
|
|
11721
|
+
try { if (existsSync(P.planExecution)) rmSync(P.planExecution); } catch { /* best-effort */ }
|
|
11722
|
+
for (const f of taintParkJudgmentArtifacts(P, run.runDir)) parkRename(f);
|
|
11723
|
+
runLog(run.runDir, { event: "taint-park", axes: toPark, attempt: recoveryAttempts });
|
|
11724
|
+
throw Object.assign(new StageFailure(`register-unit:${toPark[0]}`,
|
|
11725
|
+
`register slice timeout-tainted and unrecovered — an unfinished pass cannot ship self-reported clean (axes: ${toPark.join(", ")})`,
|
|
11726
|
+
undefined, { failClass: "transient", repairs: fanInRepairs }),
|
|
11727
|
+
{ notCompletedPart: "searches", taintedAxes: toPark, undoRenames });
|
|
11434
11728
|
}
|
|
11435
11729
|
}
|
|
11436
11730
|
}
|
|
@@ -11743,7 +12037,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11743
12037
|
// The rows are excluded here and announced by their own note: they are NOT slices the provider
|
|
11744
12038
|
// cannot express — it accepted them and then failed — and one note claiming both would make the
|
|
11745
12039
|
// run's own record of WHY a territory went unsearched wrong.
|
|
11746
|
-
|
|
12040
|
+
// And the slices a check could not prove ran, taken out of the band (searchesOpen above): announced on their own.
|
|
12041
|
+
const takenOut = new Set((Array.isArray(searchesOpen?.slices) ? searchesOpen.slices : []).map((s) => s.qid));
|
|
12042
|
+
const capabilityDeferred = (joinRes.deferred ?? []).filter((d) => !isProviderHardErrorReason(d.reason) && !takenOut.has(d.qid));
|
|
11747
12043
|
if (capabilityDeferred.length) {
|
|
11748
12044
|
runLog(run.runDir, { event: "plan-qids-deferred", count: capabilityDeferred.length,
|
|
11749
12045
|
qids: capabilityDeferred.slice(0, 8).map((d) => d.qid) });
|
|
@@ -11792,12 +12088,17 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11792
12088
|
if (r.quote) return `${r.qid} ← ${r.quote.slice(0, 160)}`;
|
|
11793
12089
|
return `${r.qid} ← no error block and no repair outcome — nothing ran this slice`;
|
|
11794
12090
|
});
|
|
11795
|
-
|
|
12091
|
+
// Once their recovery is spent, these are the searches not completed: each is added to its axis's band as the
|
|
12092
|
+
// executor's deferred block and delivered as an open row, reading that it was planned and never reached the
|
|
12093
|
+
// register (parts-not-completed.mjs).
|
|
12094
|
+
throw Object.assign(new StageFailure("fan-in", `register plan unexecuted after direct dispatch + followup — ${joinRes.missing.length} dictated qid(s) own no band block: ${detail.join("; ")}${joinRes.missing.length > 6 ? " …" : ""} (a clean can never ship over a slice the plan dictated and nothing ran)`, undefined,
|
|
11796
12095
|
// `quantity` is the missing count — what CONVERGES across attempts (5 → 3 → 1 is a run closing
|
|
11797
12096
|
// on the hole; 5 → 5 → 5 is one that has stopped). repairs.mjs progressQuantity cannot read it
|
|
11798
12097
|
// out of this prose: its census table is an allowlist and this template is not in it, so the
|
|
11799
12098
|
// ladder has been reading `unknown` here and a converging run looked plateaued.
|
|
11800
|
-
{ failClass, repairs: fanInRepairs, quantity: joinRes.missing.length })
|
|
12099
|
+
{ failClass, repairs: fanInRepairs, quantity: joinRes.missing.length }),
|
|
12100
|
+
{ notCompletedPart: "searches", excludedSlices: joinRes.missing.map((qid) => ({ axis: byQid.get(qid)?.axis, qid, reason: plainDeferralReason("slice-not-landed") }))
|
|
12101
|
+
.filter((x) => x.axis) });
|
|
11801
12102
|
}
|
|
11802
12103
|
runLog(run.runDir, { event: "plan-execution", executed: joinRes.executed.length, skipped: joinRes.skipped.length, unplanned: joinRes.unplanned.length,
|
|
11803
12104
|
awaiting: joinRes.awaiting?.length ?? 0, asked: joinRes.asked?.length ?? 0,
|
|
@@ -14073,7 +14374,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14073
14374
|
+ `${correctiveRollback.restored ? "findings.json restored to the version the reviewer read" : "findings.json was never changed"}`
|
|
14074
14375
|
+ `; the run delivers with the reviewer's points printed rather than failing`);
|
|
14075
14376
|
} else {
|
|
14076
|
-
|
|
14377
|
+
// The rollback declined: a rate limit, which waits, or no record from before the pass to trust, which is the
|
|
14378
|
+
// assessment not completed, as for the correction after the review of a repair.
|
|
14379
|
+
await asPartNotCompleted("assessment", async () => must(correctivePass, `synthesis(${verdict.toLowerCase()})`));
|
|
14077
14380
|
// ── — THE THIRD BRANCH, AND IT IS ON THE SUCCESS SIDE ON PURPOSE ─────────
|
|
14078
14381
|
//
|
|
14079
14382
|
// A repair is not a rollback and must never be reached by falling through to one. The rollback
|
|
@@ -15216,6 +15519,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15216
15519
|
// P2 clause 3 — assigned in the lint block below, consumed by the freshness precondition before the
|
|
15217
15520
|
// client gate. Declared here so both live in the same scope regardless of the lint block's nesting.
|
|
15218
15521
|
let staleStages = [];
|
|
15522
|
+
let staleOnTheDeliveryPath = () => staleStages; // the same question, asked again: set where the lint block asks it
|
|
15219
15523
|
// ── §9 — THE CARD SET IS `fullProseOrdinals ∧ form === "full"` ───────────────────────────
|
|
15220
15524
|
//
|
|
15221
15525
|
// On one country every form is `full`, so this reduces to `fullProseOrdinals` — the same list, in the
|
|
@@ -15397,6 +15701,46 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15397
15701
|
// composeEmailHtml now takes its headline tier from the verdict sidecar and its conditions from
|
|
15398
15702
|
// the report's own `# Actions`.
|
|
15399
15703
|
|
|
15704
|
+
// A FINDINGS RECORD BROKEN AFTER SYNTHESIS PASSED IT IS RESTORED, NOT ENDED ON. It is checked as the delivery
|
|
15705
|
+
// gate checks it, before the audit and the delivery checks are built from it, and again at that gate. A broken record
|
|
15706
|
+
// comes back from the last save that passed (restoreTheRecordThatPassed). A save rebuilt with the duty set aside is
|
|
15707
|
+
// judged again here, as after a rollback, and the corrections a save from before the repair lacks close the client
|
|
15708
|
+
// export (correctionsTheRestoreLacks). With no save to restore, the failure is the assessment not completed: its
|
|
15709
|
+
// recovery re-runs synthesis, which a broken record does not let skip, and once that is spent the run delivers
|
|
15710
|
+
// without the assessment. Returns the restore, or null where no save passes.
|
|
15711
|
+
const restoreTheBrokenRecord = async (failures, where) => {
|
|
15712
|
+
const restored = await restoreTheRecordThatPassed(ctx, P, run.runDir, (raw) => { parseFindingsJsonLenient(raw); });
|
|
15713
|
+
if (!restored) {
|
|
15714
|
+
note(`core-artifact (${where}): ${failures.join("; ")} — no save passes either, so the assessment is not completed`);
|
|
15715
|
+
return null;
|
|
15716
|
+
}
|
|
15717
|
+
const lacks = correctionsTheRestoreLacks(run.runDir, restored.from);
|
|
15718
|
+
if (lacks.length) {
|
|
15719
|
+
// Joined to what the state already holds: an earlier correction's names stay in the reason.
|
|
15720
|
+
const at = driverDir(run.runDir, "corrections-state.json");
|
|
15721
|
+
const held = (() => { try { const s = JSON.parse(readFileSync(at, "utf8"))?.stale; return Array.isArray(s) ? s : []; } catch { return []; } })();
|
|
15722
|
+
try { atomicWrite(at, JSON.stringify({ ts: new Date().toISOString(), stale: [...new Set([...held, ...lacks])] }, null, 2)); }
|
|
15723
|
+
catch { /* the event below records it */ }
|
|
15724
|
+
}
|
|
15725
|
+
runLog(run.runDir, { event: "core-artifact-restored", where, from: restored.from, duty: restored.duty, ...(lacks.length ? { correctionsLacked: lacks.length } : {}) });
|
|
15726
|
+
note(`core-artifact (${where}): ${failures.join("; ")} — the record is restored from the ${restored.from}, and the run delivers from it`);
|
|
15727
|
+
if (restored.duty === "relaxed") {
|
|
15728
|
+
const was = verdict, stood = JSON.stringify(clampReasons);
|
|
15729
|
+
clampOnTheWritersDuty({ again: true });
|
|
15730
|
+
if ((JSON.stringify(clampReasons) !== stood || verdict !== was) && saveTheLabel("after the record was restored")) applyVerdictFrontMatter();
|
|
15731
|
+
}
|
|
15732
|
+
return restored;
|
|
15733
|
+
};
|
|
15734
|
+
if (existsSync(P.findings)) {
|
|
15735
|
+
let broken = null;
|
|
15736
|
+
try { parseFindingsJsonLenient(readFileSync(P.findings, "utf8")); } catch (e) { broken = `findings.json — ${String(e.message).replace(/\s+/g, " ").slice(0, 140)}`; }
|
|
15737
|
+
if (broken) {
|
|
15738
|
+
runLog(run.runDir, { event: "core-artifact-invalid", failures: [broken], where: "delivery-checks" });
|
|
15739
|
+
if (!(await restoreTheBrokenRecord([broken], "delivery-checks")))
|
|
15740
|
+
throw Object.assign(new StageFailure("core-artifact-gate", `a core machine artifact is invalid and cannot ship: ${broken}`), { notCompletedPart: "assessment" });
|
|
15741
|
+
}
|
|
15742
|
+
}
|
|
15743
|
+
|
|
15400
15744
|
// A2 — full audit: DETERMINISTIC CODE (parse the spine tables → audit.md, count-guarded). Replaces the LLM
|
|
15401
15745
|
// audit-emit (output varied 47↔69 on identical input). NON-FATAL: if it fails, the report still ships (publish
|
|
15402
15746
|
// renders the report with no xlsx when audit.md is absent), and the audit self-recovers on a re-run.
|
|
@@ -16298,8 +16642,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
16298
16642
|
// again, and nothing re-runs either.
|
|
16299
16643
|
const deliveryPathNow = () => deliveryPathStages().filter((l) => !(overviewOpen && l === "report-overview")
|
|
16300
16644
|
&& !(reviewOpen && l === "narrative-refutation"));
|
|
16301
|
-
|
|
16645
|
+
staleOnTheDeliveryPath = () => [...staleOnPath(run.runDir, deliveryPathNow(), deliveryInputsFor, { project: projectStageInput }),
|
|
16302
16646
|
...cardsNeverBuilt(P, ctx.depth, cardOrdinals)]; // a finding a later save added is owed its one card call
|
|
16647
|
+
staleStages = staleOnTheDeliveryPath();
|
|
16303
16648
|
// A DELIVERY-CHECK REPAIR THAT CHANGED ONLY THE FINDINGS RECORD leaves the narrative's bytes, and so the
|
|
16304
16649
|
// reviewer, fresh — the reviewer does not declare findings.json. The change is still content no review has
|
|
16305
16650
|
// read, so the reviewer is queued for the review after a repair, marked as the record's change.
|
|
@@ -16978,8 +17323,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
16978
17323
|
// doc-44 Fix C — machine-file DELIVERY gate (mechanism + a pluggable core-artifact seam). A top-level-invalid
|
|
16979
17324
|
// CORE machine artifact must NOT ship: it is repaired UPSTREAM (the synthesis quarantine + the corrective
|
|
16980
17325
|
// must() re-synthesis already fail a top-level-invalid findings.json), and THIS is the final backstop before
|
|
16981
|
-
// publish so nothing invalid slips through a post-synthesis mutation.
|
|
16982
|
-
//
|
|
17326
|
+
// publish so nothing invalid slips through a post-synthesis mutation. Never quarantine-and-ship: a broken
|
|
17327
|
+
// findings.json is restored from the last save that passed (restoreTheRecordThatPassed), and with none to restore
|
|
17328
|
+
// the failure is the assessment not completed. We validate with the LENIENT parser so a legitimate PER-FINDING quarantine
|
|
16983
17329
|
// (one bad finding dropped, the rest shipped) still passes — THAT (whether a dropped finding is a core failure)
|
|
16984
17330
|
// is a POLICY call the definition-of-done (spec 45) owns, not this gate. The CORE_ARTIFACTS list is the SEAM:
|
|
16985
17331
|
// spec 45 owns its contents; it is seeded with the machine findings file doc-44 names. This enforces machine-
|
|
@@ -16995,8 +17341,13 @@ async function pipelineInner(job, opts = {}) {
|
|
|
16995
17341
|
}
|
|
16996
17342
|
if (coreInvalid.length) {
|
|
16997
17343
|
runLog(run.runDir, { event: "core-artifact-invalid", failures: coreInvalid });
|
|
16998
|
-
|
|
16999
|
-
|
|
17344
|
+
// The record, broken after the delivery checks first read it, is restored as it is there (restoreTheBrokenRecord),
|
|
17345
|
+
// and what was built from it is judged again against the record that stands: the freshness check below parks a
|
|
17346
|
+
// stage it left stale, and the resumed pass rebuilds it.
|
|
17347
|
+
const failure = new StageFailure("core-artifact-gate", `a core machine artifact is invalid and cannot ship: ${coreInvalid.join("; ")}`);
|
|
17348
|
+
if (!coreInvalid.every((f) => f.startsWith("findings.json "))) throw failure; // only the findings record is the assessment's
|
|
17349
|
+
if (!(await restoreTheBrokenRecord(coreInvalid, "delivery-gate"))) throw Object.assign(failure, { notCompletedPart: "assessment" });
|
|
17350
|
+
staleStages = staleOnTheDeliveryPath();
|
|
17000
17351
|
}
|
|
17001
17352
|
|
|
17002
17353
|
// P2 clause 3 — FRESHNESS PRECONDITION, immediately before the client gate. copper-vault's gate
|
|
@@ -17350,17 +17701,30 @@ async function pipelineInner(job, opts = {}) {
|
|
|
17350
17701
|
// a run that delivers carries neither. A rate limit and a stop never reach this line (both are handled above), and
|
|
17351
17702
|
// a storage failure is not a covered part: no throw site names one.
|
|
17352
17703
|
const coveredPart = e instanceof StageFailure ? (e.notCompletedPart ?? null) : null;
|
|
17353
|
-
|
|
17704
|
+
const heldRecord = coveredPart && run?.runDir ? readPartNotCompleted(run.runDir, coveredPart) : null;
|
|
17705
|
+
// THE SEARCHES' RECORD GROWS: a failure is routed again while it names a slice the record does not hold yet.
|
|
17706
|
+
const addedSlices = coveredPart === "searches" ? slicesNotYetRecorded(heldRecord, e.excludedSlices) : [];
|
|
17707
|
+
const addedAxes = coveredPart === "searches" ? axesNotYetDisclosed(heldRecord, e.taintedAxes) : [];
|
|
17708
|
+
const addedUnits = coveredPart === "searches" ? unitsNotYetRecorded(heldRecord, e.unfinishedUnits) : [];
|
|
17709
|
+
if (coveredPart && run?.runDir && (coveredPart === "searches" ? addedSlices.length + addedAxes.length + addedUnits.length > 0 : !heldRecord)) {
|
|
17354
17710
|
let parked = null;
|
|
17355
17711
|
try {
|
|
17356
|
-
|
|
17712
|
+
// A TIMED-OUT AXIS IS READ AS THE LAST PASS LEFT IT: the park renamed its unit, band and digest so a resume would
|
|
17713
|
+
// run it again, and with its recovery spent nothing will, so they are put back.
|
|
17714
|
+
if (addedAxes.length) undoTaintRenames(e.undoRenames);
|
|
17715
|
+
writePartNotCompleted(run.runDir, coveredPart, { stage: failedStage, reason: String(reason), sig: failSig.sig, terminalKind,
|
|
17716
|
+
...(coveredPart === "searches" ? { slices: [...(Array.isArray(heldRecord?.slices) ? heldRecord.slices : []), ...addedSlices],
|
|
17717
|
+
disclosedAxes: [...(Array.isArray(heldRecord?.disclosed_axes) ? heldRecord.disclosed_axes : []), ...addedAxes],
|
|
17718
|
+
unfinishedUnits: [...(Array.isArray(heldRecord?.unfinished_units) ? heldRecord.unfinished_units : []), ...addedUnits] } : {}) });
|
|
17357
17719
|
const resumesAt = new Date().toISOString();
|
|
17358
17720
|
// The payload the runner's self-resume needs (job, agent, codename), and the recovery kind, whose stated
|
|
17359
17721
|
// clock it keeps exactly: now. No recovery attempt is counted, because nothing is being retried.
|
|
17360
17722
|
sentinel(run.runDir, ".postponed", { kind: "recovery", recoveryResumesAt: resumesAt, postponedAt: resumesAt,
|
|
17361
17723
|
codename: run.codename, job, agent, attempt: priorAttempts, reason: String(reason).slice(0, 300), sig: failSig.sig,
|
|
17362
17724
|
class: failClass, notCompleted: coveredPart });
|
|
17363
|
-
|
|
17725
|
+
// The cause's payload rides beside the sentence, as on the stop it replaces, so the record still points at it.
|
|
17726
|
+
writeRunStatus(ctx, { state: "recovering", failedStage, ...terminalReasonFields(reason), recoveryResumesAt: resumesAt, resetsAt: null,
|
|
17727
|
+
reasonDetail: reasonDetail ? abbrev(String(reasonDetail).replace(/\s+/g, " ").trim(), 600) : null });
|
|
17364
17728
|
rollupStatus(run.studioRoot);
|
|
17365
17729
|
runLog(run.runDir, { event: "part-not-completed", part: coveredPart, stage: failedStage, sig: failSig.sig, class: failClass,
|
|
17366
17730
|
terminalKind: terminalKind ?? null, resumesAt });
|
|
@@ -17449,7 +17813,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
17449
17813
|
runId, agent, job, failedStage, shortReason,
|
|
17450
17814
|
reasonVerbatim: String(reason).slice(0, 1000), sig: failSig.sig, failClass,
|
|
17451
17815
|
terminalKind: terminalKind ?? null, repairs: attemptedRepairs ?? [],
|
|
17452
|
-
priorAttempts,
|
|
17816
|
+
priorAttempts,
|
|
17453
17817
|
// Read off the park history rather than recomputed: the history is what survived the parks.
|
|
17454
17818
|
capWait: capWaitFrom(Array.isArray(statusSnapshot?.recoveryHistory) ? statusSnapshot.recoveryHistory : []),
|
|
17455
17819
|
reasonDetail: reasonDetailField, reasonQuantity: quantity, // — the payload reaches the notice
|
|
@@ -17506,7 +17870,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
17506
17870
|
const packet = buildFailurePacket({
|
|
17507
17871
|
runId: `${run.slug}-${run.date}-${run.codename}`, agent, job, failedStage, shortReason, // canonical runId form (charter P1 §3)
|
|
17508
17872
|
reasonVerbatim: String(reason).slice(0, 1000), sig: failSig.sig, failClass, terminalKind,
|
|
17509
|
-
repairs: attemptedRepairs ?? [], priorAttempts,
|
|
17873
|
+
repairs: attemptedRepairs ?? [], priorAttempts,
|
|
17510
17874
|
reasonDetail: reasonDetailField, reasonQuantity: quantity, // — the BACKSTOP notice carries it too
|
|
17511
17875
|
});
|
|
17512
17876
|
writeFileSync(driverDir(run.runDir, "failure.json"), JSON.stringify(packet, null, 2) + "\n");
|