clearotron 0.4.1 → 0.5.0-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/README.md +1 -0
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/start.mjs +33 -8
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +23 -0
- package/driver/ask-ledger.mjs +86 -15
- package/driver/band-shape.mjs +61 -11
- package/driver/band-size.mjs +10 -0
- package/driver/close-verify.mjs +29 -2
- package/driver/common-law-coverage-status.mjs +1 -1
- package/driver/connotation-search.mjs +7 -9
- package/driver/contract-e3-backlog.mjs +34 -34
- package/driver/contract-e3-baseline.json +5 -5
- package/driver/contract-vocabulary.mjs +12 -12
- package/driver/corrections-delivered.mjs +113 -0
- package/driver/corrections-feedforward.mjs +16 -6
- package/driver/corrective-arm.mjs +11 -8
- package/driver/coverage-form.mjs +3 -0
- package/driver/coverage-union.mjs +11 -2
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-call.mjs +1 -1
- package/driver/document-coverage.mjs +3 -2
- package/driver/effective-scope.mjs +2 -2
- package/driver/engine/mcp/band-server.mjs +77 -13
- package/driver/engine/mcp/dispositions-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +10 -6
- package/driver/findings-model.mjs +16 -4
- package/driver/floor-duty.mjs +9 -0
- package/driver/gate-metrics.mjs +7 -1
- package/driver/gateway.mjs +28 -14
- package/driver/goods-evidence.mjs +295 -0
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/narrative-refutation-record.mjs +40 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +1945 -314
- package/driver/placement-decisions.mjs +243 -0
- package/driver/placement-form.mjs +42 -8
- package/driver/placement-union.mjs +14 -6
- package/driver/provider-usage.mjs +10 -7
- package/driver/publish/audit-from-spine.mjs +5 -2
- package/driver/publish/index.mjs +19 -4
- package/driver/publish/xlsx.mjs +6 -6
- package/driver/recall-reconciliation.mjs +15 -5
- package/driver/record-carry.mjs +70 -6
- package/driver/record-discard.mjs +7 -1
- package/driver/record-fold.mjs +14 -0
- package/driver/record-goods.mjs +101 -5
- package/driver/register-plan.mjs +89 -0
- package/driver/registry-fidelity.mjs +34 -3
- package/driver/remedy-accounting.mjs +52 -14
- package/driver/repair-composers.mjs +265 -105
- package/driver/repair-scope.mjs +327 -0
- package/driver/repair-targets.mjs +92 -0
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/screen-gate.mjs +96 -0
- package/driver/skills/clearance-common-law/SKILL.md +8 -0
- package/driver/skills/clearance-register/SKILL.md +5 -5
- package/driver/skills/clearance-register/digest.md +14 -14
- package/driver/skills/clearance-register/unit.md +3 -3
- package/driver/skills/clearance-search/delivery-contract.md +2 -58
- package/driver/skills/clearance-search/report-prose.md +12 -3
- package/driver/skills/clearance-search/risk-framework-triage.md +1 -1
- package/driver/skills/clearance-search/synthesis-rules.md +10 -13
- package/driver/skills/clearance-variants/SKILL.md +4 -4
- package/driver/skills/knockout-assess/SKILL.md +9 -14
- package/driver/skills/knockout-frame/SKILL.md +5 -5
- package/driver/skills/narrative-refutation/SKILL.md +1 -1
- package/driver/skills/placement-inquiry/SKILL.md +1 -1
- package/driver/skills/whatif-memo/SKILL.md +1 -1
- package/driver/stage-context.mjs +2 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +95 -60
- package/driver/suite-census.json +367 -19
- package/driver/synthesis-record.mjs +310 -8
- package/driver/terminal-clamp.mjs +53 -2
- package/driver/verify-knockout.mjs +22 -4
- package/driver/verify.mjs +23 -3
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/driver.mjs +5 -0
- package/mcp-server/lib/events.mjs +25 -4
- package/mcp-server/lib/findings.mjs +4 -2
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/src/core.js +3 -4
- package/providers/jx/src/judge.js +2 -2
- package/providers/jx/src/nativeread.js +1 -2
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-first-time.mjs +29 -12
package/driver/pipeline.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { join, dirname, basename, resolve } from "node:path"; // resolve: the
|
|
|
12
12
|
import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs";
|
|
13
13
|
import { REVIEWER_OPEN_QUESTIONS_FILE } from "./reviewer-open-points.mjs"; // the run-record file the reviewer's open points go to, and the only thing that reads them now
|
|
14
14
|
import { goodsOf } from "./queue-markers.mjs"; // — one reading of "does this job name goods", shared with the intake gate
|
|
15
|
-
import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
15
|
+
import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect, gapPositivelyClosed, verdictWithoutTheClosedGap, jointUnaccounted } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
16
16
|
import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { runStage, correctionHint, gridLedgerNameFor, draftCarryEligible, toolWrittenArtifact, selectEngine, saidBackOnly } from "./gateway.mjs";
|
|
@@ -33,14 +33,15 @@ import { buildRunContext, deriveSlug, kebab } from "./phase0.mjs";
|
|
|
33
33
|
import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveTier, lines, AGENT_WHATSAPP, whatsappRouting,
|
|
34
34
|
chainEntries, stageOrdinal, stageInputs, stageOutputs, dependencyOrder, REGISTER_AXES, REGISTER_ENUMERATE_TOOL,
|
|
35
35
|
buildEscalationFollowup, buildEnvelopeCloseFollowup, buildFrameReopenFollowup,
|
|
36
|
-
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage, derivedLimitSec, limitExceedsCeiling, ceilingRefusal } from "./band-size.mjs";
|
|
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 { IDENTITY_FILE as REPORT_IDENTITY_FILE } from "./report-overview-record.mjs";
|
|
38
38
|
import { dispatchRows, clearedSignatures } from "./seat-attempts.mjs";
|
|
39
39
|
import { CONTEXT_DERIVATIONS, DISPATCH_EXTRAS, INLINE_CONTEXT, sandboxManifest, sandboxGaps, derivationsFor } from "./stage-context.mjs"; // — what a stage is actually handed
|
|
40
|
-
import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections, validators, findReviewerCoherenceFlags, lateReviewAgainst } from "./verify.mjs";
|
|
41
|
-
import { readAcceptedFlags } from "./narrative-refutation-record.mjs"; // T3b — the typed flags, not the re-parse
|
|
40
|
+
import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections, validators, findReviewerCoherenceFlags, lateReviewAgainst, VERDICT_RANK } from "./verify.mjs";
|
|
41
|
+
import { readAcceptedFlags, nextRefutationSeq, readAcceptedReviewSince } from "./narrative-refutation-record.mjs"; // T3b — the typed flags, not the re-parse
|
|
42
42
|
import { evidenceClaimViolations, evidenceClaimTable } from "./evidence-claim-invariant.mjs"; //
|
|
43
43
|
import { buildCorrectionsApplied, correctionsWorklist, correctionsAppliedTable, correctionScope, scopeDrift, unresolvedFlags, reportLines, linesOf, REPORT_LINE_KEY, REPORT_LINE_LABEL } from "./corrections-feedforward.mjs";
|
|
44
|
+
import { correctionTargets, deliveredOutcomes } from "./corrections-delivered.mjs"; // whether each correction reached the record the report was built from
|
|
44
45
|
import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, formRowUnitKey, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
|
|
45
46
|
import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection, readStickyGaps } from "./envelope-settle.mjs";
|
|
46
47
|
import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
|
|
@@ -58,7 +59,7 @@ import { reconcileRecall, parseFindingsEndings, parseCrowdRulings, readOkRecordU
|
|
|
58
59
|
import { buildCrowdContext, CROWD_CONTEXT_AXIS } from "./crowd-context.mjs";
|
|
59
60
|
import { writeUpForm, gradedWriteUpRung } from "./write-up-form.mjs"; // §8/§9 — which findings earn a full card
|
|
60
61
|
import { shortEntryBody, groupEntries, buildGradedEntriesSection } from "./write-up-render.mjs";
|
|
61
|
-
import { compileRegisterPlan, parseRegisterPlan, resolvePlanAgainstStore, joinPlanToBands, deriveCoverageSkeleton, validatePlanFeasibility, feasibilityMessages, foldSupplementalEntries, partitionFoldDirectives, screenThenCap, fullyDeferredAxes, variantTermIssue, searchedJurisdictionsFromPlan, PLAN_AUDIT_HEAD, PLAN_AUDIT_CLASSES, deferExhaustedProviderErrors, ladderExhaustedQids, isProviderHardErrorReason } from "./register-plan.mjs";
|
|
62
|
+
import { compileRegisterPlan, parseRegisterPlan, resolvePlanAgainstStore, joinPlanToBands, deriveCoverageSkeleton, validatePlanFeasibility, feasibilityMessages, foldSupplementalEntries, withheldFamilyTwins, pendingRequiredQids, refuseWithheldFamilies, partitionFoldDirectives, screenThenCap, fullyDeferredAxes, variantTermIssue, searchedJurisdictionsFromPlan, PLAN_AUDIT_HEAD, PLAN_AUDIT_CLASSES, deferExhaustedProviderErrors, ladderExhaustedQids, isProviderHardErrorReason } from "./register-plan.mjs";
|
|
62
63
|
// — ONE binding of the office split to this box's env, shared with Depth 2's two lanes.
|
|
63
64
|
import { registerCapabilities, registerUnavailableOffices } from "./register-unreachable.mjs";
|
|
64
65
|
import { parseVariantManifestModel, famousMarkElements } from "./variant-manifest-model.mjs";
|
|
@@ -77,8 +78,12 @@ import { unionCoverageForm } from "./coverage-union.mjs";
|
|
|
77
78
|
import { armCoverageForm, coverageFormInput, coverageFormPaths, coverageFormStamp, readCoverageForm, readCoverageFormInput, waitingFamilyStates, writeCoverageForm } from "./coverage-form-io.mjs";
|
|
78
79
|
import { releasedFamilyQids, readWithheldFamilies } from "./withheld-families.mjs"; // the waiting families the reading turn released join as dictated entries
|
|
79
80
|
import { unionPlacementForm } from "./placement-union.mjs";
|
|
81
|
+
import { placedUriRows, standingSetAside, placementSetAsideGrounds, placementFloorDecisionLines, placementSetAsideLines, placementAuditRows,
|
|
82
|
+
placementRecallEndings, writePlacementDecisions, rankingNow, rankedRecords, recordsReRanked, rankingToJson, rankingFromJson } from "./placement-decisions.mjs"; // placement's standing decisions, one check for every reader
|
|
83
|
+
export { placementSetAsideGrounds, placementFloorDecisionLines, placementSetAsideLines, placementAuditRows, placementRecallEndings,
|
|
84
|
+
standingPlacementDecisions, writePlacementDecisions } from "./placement-decisions.mjs";
|
|
80
85
|
import { readPlacementForm, readPlacementFormInput, writePlacementForm } from "./placement-form-io.mjs";
|
|
81
|
-
import { buildSelectionIndex,
|
|
86
|
+
import { buildSelectionIndex, setAsideKeysOf, ownerFates, servedLines, lineKey } from "./placement-form.mjs";
|
|
82
87
|
import { dictatedPaths, findStrayArtifacts, treeSnapshot, findStrayInTree, matterSiblings, findStrayMatterSiblings } from "./stray-artifacts.mjs"; // — a run dir holds no document no stage dictated; — nor does the doctrine tree
|
|
83
88
|
import { resolveProfile, resolveEffectiveProfile, derivedFloor, derivedBatchSize, gridBatchFor, applicantMatchesProfile, NEUTRAL_DELIVERY, deliveryForRun, recipeProseGuard, withRunPlatforms, profileStoreResolution } from "./profiles.mjs"; // adds profileStoreResolution — the CONFIG store's identity, beside the doctrine tree's
|
|
84
89
|
import { resolveSearchPolicy, gateResolvedPolicy, loadRecipes, policyFor, isRegisterOnly, reportIdentityFor, depthFor } from "./search-policy.mjs";
|
|
@@ -103,7 +108,7 @@ import { parseCaseLawProfiles, joinCaseLawProfiles } from "./publish/parse.mjs";
|
|
|
103
108
|
import { buildAuditMd, parseSpineFindingBlocks } from "./publish/audit-from-spine.mjs";
|
|
104
109
|
import { deriveRegisterPresence } from "./publish/register-presence.mjs"; // — the audit stores every live in-scope record
|
|
105
110
|
import { lastAcceptedMatterFrame, withFrameClasses, frameIdentifiedClassRows, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, each with its reason, which the plan compile gives one identical-mark question apiece (decision 18)
|
|
106
|
-
import { bandShapeGoods, instructedOrder } from "./record-goods.mjs"; // — the band's goods, read from the run's own records, so the shape can sort by the client's goods
|
|
111
|
+
import { bandShapeGoods, instructedOrder, goodsEvidence } from "./record-goods.mjs"; // — the band's goods, read from the run's own records, so the shape can sort by the client's goods
|
|
107
112
|
import { frameWebChoiceFor, frameAskedForWebGrid, webGridOf, closureBlocksOf } from "./web-grid.mjs"; // — the web grid the matter frame decides: its stores, its forms, what it set aside
|
|
108
113
|
import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs";
|
|
109
114
|
import { excludeHouseElement, verifyHouseElementOwnership, resolveRegions as resolvePlanRegions, HOUSE_ELEMENT_RECEIPT } from "./register-plan.mjs";
|
|
@@ -124,11 +129,12 @@ import { mintPresenceDoubts } from "./presence-reconciliation.mjs"; // presenc
|
|
|
124
129
|
import { escalatedAxes } from "./skeptic-record.mjs"; // THE escalation parse — shared with the record_skeptic transport so the rendered shape and this read cannot drift
|
|
125
130
|
// — every placed candidate ends somewhere a reader can see; the ones that do not are counted by name
|
|
126
131
|
import { PLACEMENT_CARRY_SCHEMA_VERSION, reconcilePlacementCarry, placementCarryEvent, mintPlacementCarryDoubts, entryUris } from "./placement-carry.mjs";
|
|
127
|
-
import { FLOOR_DUTY_SCHEMA_VERSION, reconcileFloorDuty, floorDutyEvent, armFloorDuty, floorDutyArmed, floorDutyBlock, floorDutyBlocksSkip } from "./floor-duty.mjs"; // — the era stamp that turns disclosure into a delivery floor
|
|
128
|
-
import { synthesisDutyForRun } from "./synthesis-record.mjs"; // — the duty checked against the DELIVERED document
|
|
129
|
-
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, bandRecordUri, placementIndex, findingUris , silentlyLostFindings, statedDivergenceFindings } from "./record-carry.mjs"; import { pickingExits, notesExits, notesPageRows, exitsForLog } from "./hand-off-exits.mjs";
|
|
132
|
+
import { FLOOR_DUTY_SCHEMA_VERSION, reconcileFloorDuty, floorDutyEvent, armFloorDuty, floorDutyArmed, floorDutyBlock, floorDutyClosed, floorDutyBlocksSkip } from "./floor-duty.mjs"; // — the era stamp that turns disclosure into a delivery floor
|
|
133
|
+
import { synthesisDutyForRun, snapshotSynthesisBase, restoreSynthesisBase, rebaseSynthesis, lastAcceptedCall, acceptSynthesis, driverReadsFor } from "./synthesis-record.mjs"; // — the duty checked against the DELIVERED document; the merge base a corrective pass holds and puts back
|
|
134
|
+
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, recordCarryDoubtsAgain, bandRecordUri, placementIndex, findingUris , silentlyLostFindings, statedDivergenceFindings } from "./record-carry.mjs"; import { pickingExits, notesExits, notesPageRows, exitsForLog } from "./hand-off-exits.mjs";
|
|
130
135
|
import { reconcileSurfaceDuty, surfaceDutyNote } from "./surface-duty.mjs"; // item 3 — silence at the findings surface, read off the rows above
|
|
131
136
|
import { DISCARD_LEDGER_NAME, seamRows, appendDiscardRows, foldDiscardLedger } from "./record-discard.mjs";
|
|
137
|
+
import { deriveGoodsEvidence, recordGoodsEvidence, heldRecordProvenance, recordFetchOutcomes, readRunCallRows, recordReads, linesRead, registerCallAccount, readGoodsEvidence, GOODS_EVIDENCE_FILE, goodsFromLabel } from "./goods-evidence.mjs"; // — what each band record's goods rest on, written beside the shape and served by no band tool
|
|
132
138
|
import { readDeclinations } from "./declination-tool.mjs"; // — synthesis's own stated declines
|
|
133
139
|
import { seamReasonFor } from "./declination-call.mjs"; // — the per-record judgment, pure and separately tested
|
|
134
140
|
import { traceCommonLawCarry, commonLawCarryEvent, mintCommonLawCarryDoubts, reconciliationVerdict, planVsExecutedChannels, CL_STAGES } from "./commonlaw-carry.mjs";
|
|
@@ -139,31 +145,33 @@ const COMMONLAW_CARRY_FLOOR = (() => {
|
|
|
139
145
|
try { return JSON.parse(readFileSync(new URL("./commonlaw-carry-floor.json", import.meta.url), "utf8")); }
|
|
140
146
|
catch { return null; }
|
|
141
147
|
})();
|
|
142
|
-
import { accountRemedyTerms, domClosedOverAccounted, remedyAccountingEvent, mintRemedyAccountingDoubts } from "./remedy-accounting.mjs"; // — every remedy term ends accounted (2026-08-03)
|
|
143
|
-
import { tallyRegisterCalls, DEFAULT_LEDGER_PATH,
|
|
148
|
+
import { accountRemedyTerms, domClosedOverAccounted, remedyAccountingEvent, mintRemedyAccountingDoubts, SETTLED_CLASSES } from "./remedy-accounting.mjs"; // — every remedy term ends accounted (2026-08-03)
|
|
149
|
+
import { tallyRegisterCalls, DEFAULT_LEDGER_PATH, countLaneCalls } from "./provider-usage.mjs";
|
|
144
150
|
// — the run's own record log. Every driver-side register call that can write a record BODY carries
|
|
145
151
|
// this path, because this process runs several runs' pipelines at once and a global address would file
|
|
146
152
|
// one run's evidence under another.
|
|
147
153
|
import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs";
|
|
148
154
|
import { beginAnswerMemory, endAnswerMemory } from "../providers/_shared/answer-memory.mjs";
|
|
149
|
-
import { findScreenGateViolations, findScreenGateParseGaps, screenGateZeroCause } from "./screen-gate.mjs";
|
|
155
|
+
import { findScreenGateViolations, findScreenGateParseGaps, screenGateZeroCause, screenGateEvidence, policedDropUris } from "./screen-gate.mjs";
|
|
150
156
|
import { emptyQueue, coerceQueue, mintItem, pendingItems, markFlushed, receiptKeyFor,
|
|
151
157
|
buildFlushFollowup, runPostFlushGateRepair } from "./digest-queue.mjs"; // (t1cd) — the digest-trigger funnel
|
|
152
|
-
import { foldFindingsFile, foldAfterSave, openPass, modelRecordPath, snapshotFold, restoreFold } from "./record-fold.mjs"; // the folded record from the delivery seam on
|
|
153
|
-
import { writeStamp, stageStaleness, restamp, restampStage, staleOnPath, reconcileStamps, shaOf } from "./stage-freshness.mjs";
|
|
158
|
+
import { foldFindingsFile, foldAfterSave, openPass, modelRecordPath, snapshotFold, restoreFold, readFold, keptModelOrdinal } from "./record-fold.mjs"; // the folded record from the delivery seam on
|
|
159
|
+
import { writeStamp, stageStaleness, restamp, restampStage, staleOnPath, reconcileStamps, shaOf, readStamp } from "./stage-freshness.mjs";
|
|
160
|
+
import { locateReportFaults, reportPartsOf } from "./repair-targets.mjs"; // a delivery-check fault reaches the part of the report that holds it
|
|
154
161
|
import { parseManifestVariants, variantsParseFailure, findCoverageLimitedCells, partitionClosableCells, findSimilarListingSignals,
|
|
155
162
|
GRID_HALVES, GRID_SEATS, MEANING_SEAT, splitGridSpec, halfOfTerm, balanceClosureCells, mergeGrids, mergeCommonLawFindings, openChannelRows,
|
|
156
163
|
routeHalfTermScopes, findDroppedConnotationQueries, findErroredConnotationQueries, findGridCandidateOmissions } from "./common-law-receipts.mjs";
|
|
157
164
|
import { stampTokenRollup } from "./tokens.mjs";
|
|
158
165
|
import { recordRunConsumption } from "./consumption-ledger.mjs";
|
|
159
166
|
import { quoteForJob, reconcileTurnaround } from "./run-quote.mjs";
|
|
160
|
-
import { assembleRunRecords, readRecordArtifacts, findRegistryArithmeticIssues, findRegistryViolations, applyRegistryCorrections, extractEnforcerSignals, collectOppositionDeadlines, normalizeRecordUri } from "./registry-fidelity.mjs";
|
|
167
|
+
import { assembleRunRecords, recordBodyHasContent, readRecordArtifacts, findRegistryArithmeticIssues, findRegistryViolations, applyRegistryCorrections, extractEnforcerSignals, collectOppositionDeadlines, normalizeRecordUri } from "./registry-fidelity.mjs";
|
|
161
168
|
import { recallReceiptForOwnCompany } from "./recall-receipt.mjs"; // a past run's recall receipt, as one company's audit may list it
|
|
162
169
|
import { runLint, flagLines, properNameCandidates } from "./predelivery-lint.mjs";
|
|
163
170
|
import { parsePlacementsJson } from "./placement-model.mjs"; // B2 — the structured tier mirror; the lint reads it only to flag an EMPTY one
|
|
164
171
|
import { readAnchors } from "./anchor-reader.mjs";
|
|
165
172
|
import { findEngagementReceipts } from "./engagement-receipt.mjs";
|
|
166
|
-
import { plainRegisterFlags } from "./plain-register.mjs";
|
|
173
|
+
import { plainRegisterFlags, SENTENCE_WORD_LIMIT } from "./plain-register.mjs";
|
|
174
|
+
import { snapshotRepairBase, readRepairBase, repairBasePath, changedContent, isEmptyChange, restingOn, scopeItems, anyRenumbered, classifyPoints, changedInputsOf, listForReview, writePostRepairRecord, postRepairRecordPath, NARRATIVE_PROSE, sectionText } from "./repair-scope.mjs";
|
|
167
175
|
import { parseFindingsJson, parseFindingsJsonLenient, consolidateFindings, deriveDisplayVerdict, bindRecommendation, compareBlockingPower, inDispositionMode, DISPOSITION_GROUP, deriveActionConditions, cardedParties, actionPartyReferences, quarantinedConditionRows, salvageRepairTargets, riskStatement, verdictStance, remapActionOrdinals, joinAskToAnswer, stripAskLabel, bandBorderlineDeclarations, reasonedNegativeGroups, blankForeignRecordLinks } from "./findings-model.mjs";
|
|
168
176
|
import { foldCaption, foldCardRead } from "./card-budget.mjs";
|
|
169
177
|
// S2 — the report card's mechanical frame, composed from the record instead of dictated (see below).
|
|
@@ -634,6 +642,7 @@ export function settleDerivedBases(ctx, at) { // @internal
|
|
|
634
642
|
});
|
|
635
643
|
let applied = 0;
|
|
636
644
|
let unwritable = 0;
|
|
645
|
+
let heldBase = null; // what the synthesis merge base took, when there was a demotion to give it
|
|
637
646
|
if (demotions.length) {
|
|
638
647
|
let onDisk = null;
|
|
639
648
|
try { onDisk = JSON.parse(raw); } catch { onDisk = null; }
|
|
@@ -648,6 +657,23 @@ export function settleDerivedBases(ctx, at) { // @internal
|
|
|
648
657
|
const next = applyDerivedBases(onDisk, demotions);
|
|
649
658
|
applied = next.applied;
|
|
650
659
|
atomicWrite(P.findings, JSON.stringify(next.doc, null, 2) + "\n");
|
|
660
|
+
// THE HELD BASE TAKES THE SAME DEMOTIONS. Every accepted synthesis patch merges onto
|
|
661
|
+
// `_driver/synthesis-calls/accepted.json` and re-renders the findings it does not name from there, so a
|
|
662
|
+
// demotion written to this file alone came back with the next patch, and the evidence check then told
|
|
663
|
+
// the reviewer the stamp had returned. In the model's numbering: under a fold, a demotion lands on the
|
|
664
|
+
// member the fold kept, whose object the folded finding is. The edit is judged as a call is; a refusal
|
|
665
|
+
// leaves the base byte-identical and is recorded below, and the run goes on as it did before.
|
|
666
|
+
if (applied) {
|
|
667
|
+
const fold = readFold(P.runDir);
|
|
668
|
+
const inModel = demotions.map((d) => ({ ...d, ordinal: keptModelOrdinal(fold, d.ordinal) })).filter((d) => d.ordinal != null);
|
|
669
|
+
const r = rebaseSynthesis(P.runDir, (held) => applyDerivedBases(held, inModel).doc);
|
|
670
|
+
heldBase = r.ok ? { base: r.changed ? "demoted" : "unchanged" }
|
|
671
|
+
: /^no_base:/.test(String(r.reason)) ? { base: "no-base" } : { base: "refused", baseReason: String(r.reason).slice(0, 200) };
|
|
672
|
+
if (heldBase.base === "refused") {
|
|
673
|
+
note(`basis derivation (${at}): the synthesis merge base could not take the demotions (${heldBase.baseReason.slice(0, 100)}) — `
|
|
674
|
+
+ `it is left as it was, and the next accepted patch re-renders the stamps it holds`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
651
677
|
// A SHORTFALL IS A FINDING. The demotions are computed against the lenient parse, which normalises
|
|
652
678
|
// — so a raw finding missing the `ordinal` the match keys on would leave an unprovable claim
|
|
653
679
|
// standing while `demoted` and `rows` sat next to each other looking consistent. Say it.
|
|
@@ -671,10 +697,11 @@ export function settleDerivedBases(ctx, at) { // @internal
|
|
|
671
697
|
.filter((m) => m?.basis === "verified-from-record").length, 0);
|
|
672
698
|
const record = { ts: new Date().toISOString(), at,
|
|
673
699
|
stamped_before: stampedBefore, stamped_after: stampedBefore - applied, demoted: applied,
|
|
674
|
-
...(unwritable ? { unwritable } : {}),
|
|
700
|
+
...(unwritable ? { unwritable } : {}), ...(heldBase ?? {}),
|
|
675
701
|
rows: demotions.map((d) => ({ ordinal: d.ordinal, mark: d.mark, meter: d.meter, uri: d.uri, why: d.why })) };
|
|
676
702
|
atomicWrite(P.basisDerivation, JSON.stringify(record, null, 2) + "\n");
|
|
677
703
|
runLog(P.runDir, { event: "basis-derivation", at, stamped_after: stampedBefore - applied, demoted: applied,
|
|
704
|
+
...(heldBase ? { base: heldBase.base } : {}),
|
|
678
705
|
never_fetched: demotions.filter((d) => d.why === "record-never-fetched").length,
|
|
679
706
|
never_read: demotions.filter((d) => d.why === "record-on-disk-never-read").length });
|
|
680
707
|
return record;
|
|
@@ -745,10 +772,96 @@ export function refreshSupplementalExecution(ctx) { // @internal
|
|
|
745
772
|
} catch (e) { note(`supplemental receipt refresh skipped (${String(e.message).slice(0, 80)}) — never-kill`); }
|
|
746
773
|
}
|
|
747
774
|
|
|
748
|
-
|
|
775
|
+
/** One axis band's bytes, or null where it has none: what a re-run's change is measured against. */
|
|
776
|
+
function bandBytesOf(P, axis) { try { return readFileSync(P.registerBand(axis), "utf8"); } catch { return null; } }
|
|
777
|
+
|
|
778
|
+
/** A fingerprint of every axis band file, in axis order: what a merge read. */
|
|
779
|
+
function axisBandsFingerprint(P, axes = []) {
|
|
780
|
+
const h = createHash("sha256");
|
|
781
|
+
for (const a of axes ?? []) { h.update(`${a}\0`); h.update(bandBytesOf(P, a) ?? "\0absent"); h.update("\0"); }
|
|
782
|
+
return h.digest("hex");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* THE NAMED BAND MERGED AGAIN WHEN AN AXIS BAND MOVED SINCE THE LAST MERGE. A unit re-run in its own session after the
|
|
787
|
+
* fan-in (the cross-check's questions, a frame-reopen sweep, the skeptic's escalation, the envelope close) can add blocks
|
|
788
|
+
* to its axis band whatever it does to its notes, and placement, the reading step and the writer read the named band.
|
|
789
|
+
* Only the code-side recheck merged again, so the records those re-runs found reached none of the three. Code only:
|
|
790
|
+
* nothing is dispatched. Returns whether the band was merged again.
|
|
791
|
+
*
|
|
792
|
+
* A BAND A READING STAGE WOULD REFUSE IS NOT ADOPTED. Placement, the reading step and the writer each refuse a band past
|
|
793
|
+
* the size ceiling before they dispatch, and that refusal ends the run with no report. These re-runs alone can grow a
|
|
794
|
+
* band the first merge kept under it past it, where without this merge the run would have gone on reading the band as
|
|
795
|
+
* first merged. So the band is measured before it is adopted, by the test each stage applies at dispatch, and a band any
|
|
796
|
+
* of the three would refuse is not adopted: the earlier merge stands, and the records the re-runs found stay unread. They
|
|
797
|
+
* are not lost silently: the records the refused band held and the band standing does not are kept, and the recall gate
|
|
798
|
+
* counts them into its approved sentence for records neither addressed nor set aside. `refusing` is that test,
|
|
799
|
+
* injectable so a test can drive a refusal without a band of that size.
|
|
800
|
+
*/
|
|
801
|
+
export function remergeIfBandsMoved(ctx, why, { refusing = (bytes) => stagesRefusingBand(bytes, (s) => STAGES[s]?.timeoutSec) } = {}) { // @internal — driven by its test with a run's own files
|
|
802
|
+
const P = ctx.paths;
|
|
803
|
+
if (ctx.bandsMergedAt == null) return false; // nothing merged yet this pass: the fan-in merge reads every band
|
|
804
|
+
const now = axisBandsFingerprint(P, ctx.axes);
|
|
805
|
+
if (now === ctx.bandsMergedAt || now === ctx.bandsRefusedAt) return false; // unmoved, or already refused as it stands
|
|
806
|
+
try {
|
|
807
|
+
const merged = mergeRegisterBands(P, ctx.axes ?? []);
|
|
808
|
+
if (!merged.invalid.length) {
|
|
809
|
+
const bytes = Buffer.byteLength(namedBandText(merged.band));
|
|
810
|
+
const stages = refusing(bytes);
|
|
811
|
+
if (stages.length) {
|
|
812
|
+
ctx.bandsRefusedAt = now;
|
|
813
|
+
const left = recordRefusedRecords(P, merged.band);
|
|
814
|
+
runLog(P.runDir, { event: "named-band-remerge-over-ceiling", why, bytes, stages, records: left });
|
|
815
|
+
note(`${why}: the band merged again would be ${(bytes / (1024 * 1024)).toFixed(1)} MB, more than ${stages.join(", ")} can be dispatched over — the earlier merge stands, and the records the re-run found stay unread`);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
deriveNamedBand(ctx, merged); runLog(P.runDir, { event: "named-band-remerged", why }); return true;
|
|
820
|
+
}
|
|
821
|
+
catch (e) { note(`${why}: the named band could not be merged again (${String(e?.message ?? e).slice(0, 100)}) — the earlier merge stands`); return false; }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Where the records a refused merge left out are kept, for the recall gate. */
|
|
825
|
+
const refusedRecordsPath = (P) => driverDir(P.runDir, "band-refused-records.json");
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* THE RECORDS A REFUSED MERGE LEAVES OUT: the ids the band merged again holds and the band standing does not, added to
|
|
829
|
+
* those an earlier refusal left out. Returns how many are kept, or null when they could not be written, which the run
|
|
830
|
+
* record names. Never throws.
|
|
831
|
+
*/
|
|
832
|
+
function recordRefusedRecords(P, band) {
|
|
833
|
+
try {
|
|
834
|
+
const key = (r) => String(r?.record_id ?? "").toLowerCase();
|
|
835
|
+
const standing = new Set((safeReadJson(P.registerNamedBand)?.enumerated ?? []).map(key));
|
|
836
|
+
const ids = [...new Set([...(safeReadJson(refusedRecordsPath(P))?.ids ?? []),
|
|
837
|
+
...(band?.enumerated ?? []).map(key).filter((id) => id && !standing.has(id))])];
|
|
838
|
+
atomicWrite(refusedRecordsPath(P), JSON.stringify({ ids, count: ids.length }, null, 2) + "\n");
|
|
839
|
+
return ids.length;
|
|
840
|
+
} catch (e) {
|
|
841
|
+
runLog(P.runDir, { event: "band-refused-records-failed", reason: String(e?.message ?? e).slice(0, 160) });
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* The records a refused merge left out that are still outside the named band: a record a later merge took in is read,
|
|
848
|
+
* and is not counted as left out. Never throws.
|
|
849
|
+
*/
|
|
850
|
+
export function refusedRecordsLeftOut(P) { // @internal — driven by its test with a run's own files
|
|
851
|
+
const ids = safeReadJson(refusedRecordsPath(P))?.ids;
|
|
852
|
+
if (!Array.isArray(ids) || !ids.length) return [];
|
|
853
|
+
const inBand = new Set((safeReadJson(P.registerNamedBand)?.enumerated ?? []).map((r) => String(r?.record_id ?? "").toLowerCase()));
|
|
854
|
+
return ids.map((x) => String(x).toLowerCase()).filter((id) => id && !inBand.has(id));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** The named band as it is written to disk: what a dispatch over it measures. */
|
|
858
|
+
const namedBandText = (band) => JSON.stringify(band, null, 2) + "\n";
|
|
859
|
+
|
|
860
|
+
export function deriveNamedBand(ctx, merged = null) { // @internal — driven by its test with a run's own files
|
|
749
861
|
const P = ctx.paths;
|
|
750
862
|
const axes = ctx.axes ?? [];
|
|
751
|
-
const { band, axes: ok, invalid } = mergeRegisterBands(P, axes);
|
|
863
|
+
const { band, axes: ok, invalid } = merged ?? mergeRegisterBands(P, axes); // a caller that measured the merge first hands it in
|
|
864
|
+
const mergedFrom = axisBandsFingerprint(P, axes);
|
|
752
865
|
if (invalid.length) {
|
|
753
866
|
const detail = invalid.map((x) => `${x.axis}:${x.reason}`).join("; ");
|
|
754
867
|
runLog(P.runDir, { event: "named-band-invalid", invalid });
|
|
@@ -756,8 +869,9 @@ function deriveNamedBand(ctx) {
|
|
|
756
869
|
}
|
|
757
870
|
try {
|
|
758
871
|
const tmp = P.registerNamedBand + ".tmp";
|
|
759
|
-
writeFileSync(tmp,
|
|
872
|
+
writeFileSync(tmp, namedBandText(band));
|
|
760
873
|
renameSync(tmp, P.registerNamedBand);
|
|
874
|
+
ctx.bandsMergedAt = mergedFrom;
|
|
761
875
|
runLog(P.runDir, { event: "named-band-merged", axes: ok, enumerated: band.enumerated.length, crowds: band.crowds.length });
|
|
762
876
|
} catch (e) {
|
|
763
877
|
note(`named-band merge write failed (non-fatal): ${String(e.message).slice(0, 100)}`);
|
|
@@ -923,6 +1037,7 @@ function recordDetailIndex(runDir) {
|
|
|
923
1037
|
|
|
924
1038
|
function deriveBandShape(ctx, band) {
|
|
925
1039
|
const P = ctx.paths;
|
|
1040
|
+
let evidenceDoc = null;
|
|
926
1041
|
try {
|
|
927
1042
|
const targets = [];
|
|
928
1043
|
const markNames = Array.isArray(ctx.job?.marks)
|
|
@@ -959,7 +1074,13 @@ function deriveBandShape(ctx, band) {
|
|
|
959
1074
|
runLog(P.runDir, { event: "register-positions-failed", fail: String(e?.message ?? e).slice(0, 160) });
|
|
960
1075
|
note(`register-positions derivation failed (non-fatal — the per-registration band stands): ${String(e?.message ?? e).slice(0, 100)}`);
|
|
961
1076
|
}
|
|
962
|
-
const
|
|
1077
|
+
const shapeGoods = bandShapeGoods(P, band?.enumerated ?? []);
|
|
1078
|
+
// What each record's goods rest on, derived first so the shape says it of each line the goods test could not
|
|
1079
|
+
// place; the same record is written beside the shape below.
|
|
1080
|
+
try { evidenceDoc = goodsEvidenceDocFor(ctx, band, shapeGoods.goods); } catch { evidenceDoc = null; }
|
|
1081
|
+
const evidenceBy = evidenceDoc ? new Map(evidenceDoc.records.map((r) => [r.uri, r])) : null;
|
|
1082
|
+
const evidenceOf = evidenceBy ? (r) => evidenceBy.get(normalizeRecordUri(r?.record_id) || String(r?.record_id ?? "")) ?? null : null;
|
|
1083
|
+
const { shape, md } = buildBandShape(band, { targets: targets.slice(0, 64), inScopeClasses, crowdContext, positions, ...shapeGoods, markets: clientMarkets(instructedOrder(P, ctx.job), ctx.profile), territories: orderTerritories(instructedOrder(P, ctx.job), ctx.profile), keysOf: setAsideKeysOf(positions?.positions ?? null), evidenceOf });
|
|
963
1084
|
mkdirSync(dirname(P.bandShape), { recursive: true });
|
|
964
1085
|
atomicWrite(P.bandShape, JSON.stringify(shape, null, 2) + "\n");
|
|
965
1086
|
atomicWrite(P.bandShapeMd, md + "\n");
|
|
@@ -967,6 +1088,8 @@ function deriveBandShape(ctx, band) {
|
|
|
967
1088
|
runLog(P.runDir, { event: "band-shape-derived", records: shape.totals.records, crowds: shape.totals.crowds,
|
|
968
1089
|
tiers: shape.totals.by_tier, floors: shape.floors.in_class_identical_or_near.length,
|
|
969
1090
|
goods_test: tested.source, goods_test_words: tested.words.length,
|
|
1091
|
+
// what each record's goods rest on: a held body, its band row's whole statements, statements cut short, or none
|
|
1092
|
+
goods_evidence: goodsEvidence(band?.enumerated ?? [], shapeGoods.goods),
|
|
970
1093
|
blind_spots: shape.blind_spots.map((b) => ({ kind: b.kind, count: b.count })) });
|
|
971
1094
|
// The goods test falls back where the variants stage named no product words; the run says so.
|
|
972
1095
|
if (tested.source !== "product_words")
|
|
@@ -975,6 +1098,48 @@ function deriveBandShape(ctx, band) {
|
|
|
975
1098
|
runLog(P.runDir, { event: "band-shape-failed", fail: String(e?.message ?? e).slice(0, 200) });
|
|
976
1099
|
note(`band-shape derivation failed (non-fatal — judgment still holds band_lookup over the band; machine checks read the complete band): ${String(e?.message ?? e).slice(0, 120)}`);
|
|
977
1100
|
}
|
|
1101
|
+
recordGoodsEvidenceFor(ctx, band, evidenceDoc);
|
|
1102
|
+
// Placement's standing decisions are read off the shape, so a shape derived again can change them with no placement
|
|
1103
|
+
// pass: a group's row checked again on the new shape may cover other lines, or stand no longer, and the floors it
|
|
1104
|
+
// leaves can grow. The decisions file the reading step, the writer and the review declare is written again with the
|
|
1105
|
+
// shape, so such a change makes a stage that already read them stale. Only once placement has written it.
|
|
1106
|
+
if (existsSync(P.placementDecisions)) {
|
|
1107
|
+
try { writePlacementDecisions(P); }
|
|
1108
|
+
catch (e) { runLog(P.runDir, { event: "placement-decisions-failed", at: "band-shape", reason: String(e?.message ?? e).slice(0, 120) }); }
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* WHAT EACH BAND RECORD'S GOODS REST ON, written beside the shape after every derivation (goods-evidence.mjs): the
|
|
1114
|
+
* goods reader the shape tests with, given the band's records so that a reader which reads a band row's own goods
|
|
1115
|
+
* reads them here too; the official records the run holds, with where each came from; and this run's record fetches,
|
|
1116
|
+
* from the register call ledger. The shape says the same of each line from the same derivation (its goods cell and
|
|
1117
|
+
* `goods_evidence`); the file itself is served by no band tool, gates nothing, and a failure costs the run this
|
|
1118
|
+
* record and nothing else.
|
|
1119
|
+
*/
|
|
1120
|
+
function goodsEvidenceDocFor(ctx, band, goods = null) {
|
|
1121
|
+
const P = ctx.paths;
|
|
1122
|
+
const runPrefix = ctx.run?.slug && ctx.run?.codename ? `clearance-${ctx.run.slug}-${ctx.run.codename}-` : null;
|
|
1123
|
+
const calls = runPrefix ? readRunCallRows(DEFAULT_LEDGER_PATH, runPrefix) : { rows: [], error: null };
|
|
1124
|
+
const records = Array.isArray(band?.enumerated) ? band.enumerated : [];
|
|
1125
|
+
const doc = deriveGoodsEvidence({ records, goods: goods ?? bandShapeGoods(P, records).goods, held: heldRecordProvenance(P.runDir),
|
|
1126
|
+
fetches: recordFetchOutcomes(calls.rows, { runPrefix }) });
|
|
1127
|
+
// Whether fetches could be read is part of the record: without the ledger, "not held" may hide a failed fetch.
|
|
1128
|
+
doc.fetches_read = runPrefix ? (calls.error ? `the register call ledger could not be read: ${calls.error}` : true) : "no run prefix";
|
|
1129
|
+
return doc;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function recordGoodsEvidenceFor(ctx, band, derived = null) {
|
|
1133
|
+
const P = ctx.paths;
|
|
1134
|
+
try {
|
|
1135
|
+
const doc = derived ?? goodsEvidenceDocFor(ctx, band);
|
|
1136
|
+
ensureDriverDir(P.runDir);
|
|
1137
|
+
atomicWrite(driverDir(P.runDir, GOODS_EVIDENCE_FILE), JSON.stringify(doc, null, 2) + "\n");
|
|
1138
|
+
runLog(P.runDir, { event: "goods-evidence", records: doc.totals.records, by_state: doc.totals.by_state,
|
|
1139
|
+
by_source: doc.totals.by_source, unreadable: doc.totals.unreadable, fetches_read: doc.fetches_read });
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
runLog(P.runDir, { event: "goods-evidence-failed", fail: String(e?.message ?? e).slice(0, 160) });
|
|
1142
|
+
}
|
|
978
1143
|
}
|
|
979
1144
|
|
|
980
1145
|
// The DETERMINISTIC GRID CONTRACT — extracted from pipelineInner unchanged (item).
|
|
@@ -1310,6 +1475,10 @@ export function composeDispatchExtra(name, ctx, opts = {}) { // @internal
|
|
|
1310
1475
|
const parts = [], ids = [], failed = [], empty = [];
|
|
1311
1476
|
for (const x of DISPATCH_EXTRAS) {
|
|
1312
1477
|
if (x.stage !== name) continue;
|
|
1478
|
+
// `skip`: blocks a dispatch deliberately does not send — the review after a delivery-check repair leaves
|
|
1479
|
+
// out the plain-words list, which covers every line a client meets and points at a check that review is
|
|
1480
|
+
// not given. Skipped is not built and not failed: it appears in neither list.
|
|
1481
|
+
if (Array.isArray(opts.skip) && opts.skip.includes(x.id)) continue;
|
|
1313
1482
|
try {
|
|
1314
1483
|
const built = DISPATCH_EXTRA_BUILDERS[x.id](ctx, opts);
|
|
1315
1484
|
recordEmptyReturn(built, x.id, { parts, ids, empty }); // EVERY builder that ran is recorded, not only those that produced text: `if (built)` alone put an empty return in neither list, so a block that ran with nothing to flag read exactly like one that never ran — and only the second is a defect. The three-way sort lives in stages.mjs beside the sentinels it reads.
|
|
@@ -2975,13 +3144,15 @@ function derivePlacementCarry(ctx, trigger) {
|
|
|
2975
3144
|
//
|
|
2976
3145
|
// NEVER THROWS and never gates — same doctrine as the trace it feeds. A ledger that cannot be written
|
|
2977
3146
|
// costs the run a `basis: "recorded"`, which the trace then states, and costs it nothing else.
|
|
2978
|
-
function recordSeamDiscards(ctx, { seam, stage: stageName, trigger = null, completed = true,
|
|
3147
|
+
function recordSeamDiscards(ctx, { seam, stage: stageName, trigger = null, completed = true, skipped = false,
|
|
2979
3148
|
saw = [], carried = [], reasonFor = null, evidence = "" } = {}) {
|
|
2980
3149
|
const P = ctx.paths;
|
|
2981
3150
|
try {
|
|
2982
3151
|
ctx._discardPass ??= {};
|
|
2983
3152
|
const pass = (ctx._discardPass[seam] = (ctx._discardPass[seam] ?? 0) + 1);
|
|
2984
|
-
|
|
3153
|
+
// A skipped pass leaves standing what an earlier pass recorded (record-discard.mjs, `seamRows`).
|
|
3154
|
+
const prior = skipped ? foldDiscardLedger(safeReadText(driverDir(P.runDir, DISCARD_LEDGER_NAME))) : null;
|
|
3155
|
+
const rows = seamRows({ seam, stage: stageName, trigger, pass, completed, skipped, prior, saw, carried, reasonFor, evidence });
|
|
2985
3156
|
const w = appendDiscardRows(P.runDir, rows);
|
|
2986
3157
|
const discarded = rows.filter((r) => r.verdict === "discarded").length;
|
|
2987
3158
|
runLog(P.runDir, {
|
|
@@ -3005,18 +3176,6 @@ function bandUriRows(P) {
|
|
|
3005
3176
|
} catch { return []; }
|
|
3006
3177
|
}
|
|
3007
3178
|
|
|
3008
|
-
/** Every record uri a placements.json names. */
|
|
3009
|
-
function placedUriRows(P) {
|
|
3010
|
-
try {
|
|
3011
|
-
const placements = parsePlacementsJson(readFileSync(P.placementModel, "utf8")).placements;
|
|
3012
|
-
const out = [];
|
|
3013
|
-
for (const p of Array.isArray(placements) ? placements : []) {
|
|
3014
|
-
for (const r of Array.isArray(p?.records) ? p.records : []) out.push({ uri: r, tier: p?.tier ?? null });
|
|
3015
|
-
}
|
|
3016
|
-
return out;
|
|
3017
|
-
} catch { return []; }
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
3179
|
/**
|
|
3021
3180
|
* PLACEMENT. Everything in the band was in front of this pass; what it placed is in placements.json.
|
|
3022
3181
|
* Recorded whether the pass completed or not — an incomplete pass discards nothing, and its rows say so
|
|
@@ -3027,7 +3186,7 @@ function recordPlacementSeam(ctx, r, trigger = null) {
|
|
|
3027
3186
|
const saw = bandUriRows(P);
|
|
3028
3187
|
if (!saw.length) return null;
|
|
3029
3188
|
return recordSeamDiscards(ctx, {
|
|
3030
|
-
seam: "placement", stage: "placement-inquiry", trigger, completed: r?.ok === true && !r?.skipped,
|
|
3189
|
+
seam: "placement", stage: "placement-inquiry", trigger, completed: r?.ok === true && !r?.skipped, skipped: r?.skipped === true,
|
|
3031
3190
|
saw, carried: placedUriRows(P),
|
|
3032
3191
|
evidence: r?.skipped ? "the pass was SKIPPED on an artifact already on disk, so it made no decision this pass" : (r?.fail ? String(r.fail).slice(0, 80) : ""),
|
|
3033
3192
|
reasonFor: placementSeamReason(placementSetAsideGrounds(P)),
|
|
@@ -3035,28 +3194,75 @@ function recordPlacementSeam(ctx, r, trigger = null) {
|
|
|
3035
3194
|
}
|
|
3036
3195
|
|
|
3037
3196
|
/**
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3197
|
+
* WHAT ONE PLACEMENT PASS DID, RECORDED: the ground every band record left it with (the seam rows) and the floors
|
|
3198
|
+
* it answered (the floor duty). The main path and the delivery gate's repeat of the stage both record through
|
|
3199
|
+
* here, so the ledger and the floor duty describe the placements the report is built from. Returns the pass
|
|
3200
|
+
* result.
|
|
3201
|
+
*
|
|
3202
|
+
* THE STAMP IS THE MAIN PATH'S. On the main path, a pass that actually ran arms the floor its account may be held
|
|
3203
|
+
* to. A SKIPPED pass leaves `placements.json` written by an earlier seat (on a resume, possibly one that never saw
|
|
3204
|
+
* the floors order), so it arms nothing, and its seam rows leave the earlier pass's grounds standing. The delivery
|
|
3205
|
+
* gate's repeat (`stale-repair`) arms nothing either: the floor it is held to is the one the run was already held
|
|
3206
|
+
* to, applied again to the repeated pass's account where the gate rebuilds the report from it. A run whose only
|
|
3207
|
+
* pass under the floors order is that repeat is held to no floor, as before.
|
|
3043
3208
|
*/
|
|
3044
|
-
function
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3209
|
+
export function recordPlacementPass(ctx, r, trigger = null) { // @internal — driven by its test with a run's own files
|
|
3210
|
+
recordPlacementSeam(ctx, r, trigger);
|
|
3211
|
+
deriveFloorDuty(ctx, r, trigger);
|
|
3212
|
+
// The decisions as they stand after this pass, where the reading step and the writer declare them: a pass that
|
|
3213
|
+
// changes only a set-aside's ground or a group's membership leaves a reader that already ran stale.
|
|
3214
|
+
try { writePlacementDecisions(ctx.paths); }
|
|
3215
|
+
catch (e) { runLog(ctx.paths.runDir, { event: "placement-decisions-failed", trigger, reason: String(e?.message ?? e).slice(0, 120) }); }
|
|
3216
|
+
if (r?.ok === true && r?.skipped !== true) recordPlacementBandIds(ctx.paths);
|
|
3217
|
+
if (r?.ok === true && r?.skipped !== true && trigger !== "stale-repair") armFloorDuty(ctx.paths.runDir);
|
|
3218
|
+
return r;
|
|
3050
3219
|
}
|
|
3051
3220
|
|
|
3052
|
-
|
|
3053
|
-
|
|
3221
|
+
const bandRecordIds = (band) => [...new Set((band?.enumerated ?? []).map((r) => String(r?.record_id ?? "").toLowerCase()).filter(Boolean))];
|
|
3222
|
+
|
|
3223
|
+
/** The named band's record ids a placement pass that ran read, so a record the band gains after it is known as new. */
|
|
3224
|
+
function recordPlacementBandIds(P) {
|
|
3054
3225
|
try {
|
|
3055
|
-
const
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3226
|
+
const ids = bandRecordIds(safeReadJson(P.registerNamedBand)).sort();
|
|
3227
|
+
atomicWrite(driverDir(P.runDir, "placement-band-ids.json"), JSON.stringify({ count: ids.length, ids }) + "\n");
|
|
3228
|
+
} catch (e) { runLog(P.runDir, { event: "placement-band-ids-failed", reason: String(e?.message ?? e).slice(0, 120) }); }
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
/**
|
|
3232
|
+
* The records the named band holds that the last placement pass that ran never read. [] when it read every one, and
|
|
3233
|
+
* when no pass recorded what it read (a run predating the record assesses nothing late). Ids, never the band's bytes:
|
|
3234
|
+
* a count that moved is not a record placement has not seen. PURE over the run's files.
|
|
3235
|
+
*/
|
|
3236
|
+
export function lateRecordIds(P) { // @internal — driven by its test with a run's own files
|
|
3237
|
+
const seen = safeReadJson(driverDir(P.runDir, "placement-band-ids.json"));
|
|
3238
|
+
if (!Array.isArray(seen?.ids)) return [];
|
|
3239
|
+
const had = new Set(seen.ids);
|
|
3240
|
+
return bandRecordIds(safeReadJson(P.registerNamedBand)).filter((id) => !had.has(id));
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
/**
|
|
3244
|
+
* THE LATE RECORDS THE REPORT DOES NOT ADDRESS: records the band holds that no placement pass read (`lateRecordIds`),
|
|
3245
|
+
* which neither the reading step's account nor the writer's findings name, and which no other clause names
|
|
3246
|
+
* (`alreadyNamed`, lowercased). A record either step names is addressed there, whatever placement did. Lowercased ids,
|
|
3247
|
+
* in the band's order. Never throws. PURE over the run's files.
|
|
3248
|
+
*/
|
|
3249
|
+
export function lateRecordsUnaddressed(P, alreadyNamed = new Set()) { // @internal — driven by its test with a run's own files
|
|
3250
|
+
const late = lateRecordIds(P);
|
|
3251
|
+
if (!late.length) return [];
|
|
3252
|
+
let endings = { carried: new Set(), dropRows: new Set() };
|
|
3253
|
+
try { endings = parseFindingsEndings(readFileSync(P.registerFindings, "utf8")); } catch { /* no account from the reading step */ }
|
|
3254
|
+
let named = new Set();
|
|
3255
|
+
try { named = new Set([...findingUris(parseFindingsJsonLenient(readFileSync(P.findings, "utf8"))?.findings ?? []).keys()].map((u) => String(u).toLowerCase())); }
|
|
3256
|
+
catch { /* no findings from the writer */ }
|
|
3257
|
+
return late.filter((id) => !endings.carried.has(id) && !endings.dropRows.has(id) && !named.has(id) && !alreadyNamed.has(id));
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
/** The records the writer was owed, by id: the population its duty is out of. Null without a spec. Never throws. */
|
|
3261
|
+
function owedUrisFor(runDir) {
|
|
3262
|
+
try {
|
|
3263
|
+
const owed = driverReadsFor(runDir).owed;
|
|
3264
|
+
return Array.isArray(owed) ? owed.map((row) => String(normalizeRecordUri(row?.uri ?? row?.record_id) ?? "").toLowerCase()).filter(Boolean) : null;
|
|
3265
|
+
} catch { return null; }
|
|
3060
3266
|
}
|
|
3061
3267
|
|
|
3062
3268
|
/**
|
|
@@ -3068,8 +3274,16 @@ export function placementOwnerFates(P, placements) { // @internal — driven b
|
|
|
3068
3274
|
try {
|
|
3069
3275
|
const input = readPlacementFormInput(P.runDir);
|
|
3070
3276
|
if (!input.groups) return { absent: "the band shape lists no groups" };
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3277
|
+
const index = buildSelectionIndex(input);
|
|
3278
|
+
// What placement read, by its own reading log, and what each line's goods rest on, where the run recorded it.
|
|
3279
|
+
const evidence = readGoodsEvidence(P.runDir);
|
|
3280
|
+
const stateOf = Array.isArray(evidence?.records) ? new Map(evidence.records.map((r) => [normalizeRecordUri(r?.uri), r?.state])) : null;
|
|
3281
|
+
const placedUris = (Array.isArray(placements) ? placements : []).flatMap((p) => (Array.isArray(p?.records) ? p.records : []));
|
|
3282
|
+
const held = readPlacementForm(P.runDir);
|
|
3283
|
+
const counted = ownerFates({ groups: input.groups, entries: standingSetAside(held.set_aside, input, { placedUris, rows: held.rows }), index,
|
|
3284
|
+
placedUris,
|
|
3285
|
+
served: servedLines(input.groups, input.served, index),
|
|
3286
|
+
lineEvidence: stateOf ? (l) => stateOf.get(normalizeRecordUri(l?.record_id)) ?? null : null });
|
|
3073
3287
|
return counted ? { counted } : { absent: "the band shape's groups could not be counted" };
|
|
3074
3288
|
} catch (e) { return { absent: `an input could not be read: ${String(e?.message ?? e).slice(0, 120)}` }; }
|
|
3075
3289
|
}
|
|
@@ -3112,7 +3326,7 @@ function recordDigestSeam(ctx, r, trigger = null) {
|
|
|
3112
3326
|
if (c === "carried") carried.push(row);
|
|
3113
3327
|
}
|
|
3114
3328
|
return recordSeamDiscards(ctx, {
|
|
3115
|
-
seam: "digest", stage: "register-digest", trigger, completed: r?.ok === true && !r?.skipped,
|
|
3329
|
+
seam: "digest", stage: "register-digest", trigger, completed: r?.ok === true && !r?.skipped, skipped: r?.skipped === true,
|
|
3116
3330
|
saw, carried,
|
|
3117
3331
|
evidence: r?.fail ? String(r.fail).slice(0, 80) : "",
|
|
3118
3332
|
reasonFor: (rec, uri) => {
|
|
@@ -3183,6 +3397,28 @@ function findingsSurfaceRows(P) {
|
|
|
3183
3397
|
* `scope` is instructed-scope.json, JOB-authored before any model ran. It is the only thing the
|
|
3184
3398
|
* contradiction refusals rest on, which is why it is read here rather than accepted from the seat.
|
|
3185
3399
|
*/
|
|
3400
|
+
// THE WRITE-UP IS TOLD WHAT EACH RECORD'S GOODS REST ON, beside its position (goods-evidence.mjs `goodsFromLabel`),
|
|
3401
|
+
// so a record declined on goods is declined knowing whether those goods were the official record's, a search answer's,
|
|
3402
|
+
// cut, or none. Only the dispatch's copy carries it; the spec the declination tool reads is the positions alone.
|
|
3403
|
+
// 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
|
|
3404
|
+
// source must not read as one whose records have none.
|
|
3405
|
+
export function withGoodsFrom(ctx, P, rows) { // @internal — driven by its test with a run's own files
|
|
3406
|
+
const unlabelled = (reason) => {
|
|
3407
|
+
try { runLog(P.runDir, { event: "write-up-goods-from-failed", rows: rows.length, reason: String(reason).slice(0, 120) }); } catch { /* the list still goes */ }
|
|
3408
|
+
return rows;
|
|
3409
|
+
};
|
|
3410
|
+
try {
|
|
3411
|
+
const runPrefix = ctx.run?.slug && ctx.run?.codename ? `clearance-${ctx.run.slug}-${ctx.run.codename}-` : null;
|
|
3412
|
+
if (!runPrefix) return unlabelled("the run has no session prefix to read its evidence by");
|
|
3413
|
+
const uris = rows.filter((r) => r?.kind !== "page" && r?.uri).map((r) => r.uri);
|
|
3414
|
+
const evidence = goodsEvidenceOfDrops(P, runPrefix, uris);
|
|
3415
|
+
return rows.map((r) => {
|
|
3416
|
+
const e = r?.kind !== "page" && r?.uri ? evidence.get(r.uri) : null;
|
|
3417
|
+
return e ? { ...r, goods_from: goodsFromLabel({ state: e.goods, source: e.source }) } : r;
|
|
3418
|
+
});
|
|
3419
|
+
} catch (e) { return unlabelled(e?.message ?? e); }
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3186
3422
|
export function prepareDeclinationSpec(ctx, P) { // @internal
|
|
3187
3423
|
const rows = [...findingsSurfaceRows(P), ...notesPageRows(existsSync(P.commonLaw) ? readFileSync(P.commonLaw, "utf8") : null)]; // records, then the web notes' pages
|
|
3188
3424
|
if (!rows.length) {
|
|
@@ -3200,7 +3436,7 @@ export function prepareDeclinationSpec(ctx, P) { // @internal
|
|
|
3200
3436
|
ensureDriverDir(P.runDir);
|
|
3201
3437
|
atomicWrite(driverDir(P.runDir, "declination-spec.json"),
|
|
3202
3438
|
JSON.stringify({ ts: new Date().toISOString(), rows, scope: { marks: scope.marks ?? [], classes: scope.classes ?? [] } }, null, 2) + "\n");
|
|
3203
|
-
ctx.findingsSurface = rows;
|
|
3439
|
+
ctx.findingsSurface = withGoodsFrom(ctx, P, rows);
|
|
3204
3440
|
} catch (e) {
|
|
3205
3441
|
// The dispatch block is gated on ctx.findingsSurface, so a failed write leaves the stage UNORDERED
|
|
3206
3442
|
// rather than ordered to call a tool whose spec it cannot read. Loud in the log, never a dead order.
|
|
@@ -3214,8 +3450,17 @@ export function prepareDeclinationSpec(ctx, P) { // @internal
|
|
|
3214
3450
|
* on a fenced or prose-wrapped file, and a synthesis pass whose output is merely untidy would otherwise
|
|
3215
3451
|
* record every record it delivered as discarded — the defect rebuilt one seam later.
|
|
3216
3452
|
*/
|
|
3453
|
+
/** Where the ranking the writer last read is kept, for the sentence on records re-ranked after it wrote. */
|
|
3454
|
+
const rankingReadByWriterPath = (P) => driverDir(P.runDir, "ranking-read-by-writer.json");
|
|
3455
|
+
/** Keeps where each record stood when a writer's pass completed. Never throws. */
|
|
3456
|
+
function keepRankingReadByWriter(P) {
|
|
3457
|
+
try { atomicWrite(rankingReadByWriterPath(P), JSON.stringify(rankingToJson(rankingNow(P)), null, 2) + "\n"); }
|
|
3458
|
+
catch (e) { runLog(P.runDir, { event: "ranking-read-by-writer-failed", reason: String(e?.message ?? e).slice(0, 160) }); }
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3217
3461
|
function recordSynthesisSeam(ctx, r, trigger = null) {
|
|
3218
3462
|
const P = ctx.paths;
|
|
3463
|
+
if (r?.ok) keepRankingReadByWriter(P); // where each record stood as this writer read it
|
|
3219
3464
|
const declined = readDeclinations(P.runDir); // — three-valued: absent ledger ≠ empty ledger
|
|
3220
3465
|
// — ONE DERIVATION, and it is CALLED rather than copied. This used to rebuild the carried set
|
|
3221
3466
|
// inline, in three lines that happened to match `findingsSurfaceRows`'s. They must match: that function
|
|
@@ -3232,7 +3477,7 @@ function recordSynthesisSeam(ctx, r, trigger = null) {
|
|
|
3232
3477
|
}
|
|
3233
3478
|
} catch { /* validators.findings' business; this seam records what it can */ }
|
|
3234
3479
|
return recordSeamDiscards(ctx, {
|
|
3235
|
-
seam: "synthesis", stage: "synthesis", trigger, completed: r?.ok === true && !r?.skipped,
|
|
3480
|
+
seam: "synthesis", stage: "synthesis", trigger, completed: r?.ok === true && !r?.skipped, skipped: r?.skipped === true,
|
|
3236
3481
|
saw, carried: delivered,
|
|
3237
3482
|
evidence: r?.fail ? String(r.fail).slice(0, 80) : "",
|
|
3238
3483
|
reasonFor: (rec, uri) => {
|
|
@@ -3618,7 +3863,8 @@ function deriveRecallReconciliation(ctx, trigger) {
|
|
|
3618
3863
|
}
|
|
3619
3864
|
}
|
|
3620
3865
|
if (!positions) return notComputable("no exact-identity positions projection — the join's unit is the POSITION and record-unit would block a compliant one-URI position row");
|
|
3621
|
-
const
|
|
3866
|
+
const placement = placementRecallEndings(P, candidates.map((c) => c.record_id));
|
|
3867
|
+
const artifact = reconcileRecall({ candidates, endings, crowdRulings, positions, dominantElement, inScopeClasses, placement });
|
|
3622
3868
|
writeArtifact(artifact);
|
|
3623
3869
|
runLog(P.runDir, recallReconciliationEvent({ trigger, artifact }));
|
|
3624
3870
|
if (artifact.totals.unended) note(`recall-reconciliation: ${artifact.totals.unended} screened-live dominant-element position(s) end NOWHERE (${artifact.totals.candidates} candidate record(s) in ${artifact.totals.positions} position(s); crowd declares ${artifact.crowd.declared} vs residual ${artifact.crowd.residual})`);
|
|
@@ -4436,6 +4682,22 @@ function clearedForSeat(runDir, label) {
|
|
|
4436
4682
|
catch { return []; }
|
|
4437
4683
|
}
|
|
4438
4684
|
|
|
4685
|
+
/**
|
|
4686
|
+
* The context a stage's message is composed from. The review's floor check lists placement's standing decisions, and
|
|
4687
|
+
* they are read here, from the run's files, each time its message is built: the review, its recheck, the delivery
|
|
4688
|
+
* loop's whole review after a repeated placement pass and a review dispatched from a rebuilt context all read the
|
|
4689
|
+
* decisions as they stand then, never a copy an earlier pass left on the context, and never "(none)" from a context
|
|
4690
|
+
* that never set one. The reading step and the writer read placement's accepted set-asides the same way. Every
|
|
4691
|
+
* other stage composes from the context as it is.
|
|
4692
|
+
*/
|
|
4693
|
+
export function messageCtxFor(name, ctx) { // @internal — driven by its test
|
|
4694
|
+
if (!ctx?.paths) return ctx;
|
|
4695
|
+
if (name === "narrative-refutation") return { ...ctx, placementFloorDecisions: placementFloorDecisionLines(ctx.paths) };
|
|
4696
|
+
// The reading step and the writer are told placement's accepted set-asides the same way, read when the message is built.
|
|
4697
|
+
if (name === "register-digest" || name === "synthesis") return { ...ctx, placementSetAsides: placementSetAsideLines(ctx.paths) };
|
|
4698
|
+
return ctx;
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4439
4701
|
async function stageOnce(name, ctx, opts = {}) {
|
|
4440
4702
|
const def = STAGES[name];
|
|
4441
4703
|
if (!def) throw new Error(`unknown stage ${name}`);
|
|
@@ -4615,8 +4877,15 @@ async function stageOnce(name, ctx, opts = {}) {
|
|
|
4615
4877
|
// is read HERE because this is the one site that composes a followup. NEVER-KILL: an unreadable
|
|
4616
4878
|
// or absent seat log yields no constraint and the dispatch is exactly what it was before.
|
|
4617
4879
|
cleared: clearedForSeat(P.runDir, label),
|
|
4880
|
+
// The review after a delivery-check repair names the files that repair changed; every other dispatch
|
|
4881
|
+
// passes nothing and keeps its own inputs line.
|
|
4882
|
+
changedInputs: opts.changedInputs ?? null,
|
|
4883
|
+
// The reading step's and the writer's fresh sessions are given placement's accepted set-asides as they stand,
|
|
4884
|
+
// except a narrow repair: it fixes only the faults or objects it names, and the general order to check each
|
|
4885
|
+
// ground would widen it. Its inputs, the decisions file among them, are named as for any other.
|
|
4886
|
+
decisions: NARROW_REPAIR_TRIGGERS.has(trigger) ? null : (messageCtxFor(name, ctx)?.placementSetAsides ?? null),
|
|
4618
4887
|
})
|
|
4619
|
-
: (def.message(ctx) + (opts.extra ? `\n\n${opts.extra}` : ""));
|
|
4888
|
+
: (def.message(messageCtxFor(name, ctx)) + (opts.extra ? `\n\n${opts.extra}` : ""));
|
|
4620
4889
|
// Every stage runs as the forwarding agent (derived from the queue location). An explicit opts.agent wins.
|
|
4621
4890
|
const execAgent = opts.agent ?? ctx.agent;
|
|
4622
4891
|
// thinking: ONLY Anthropic models get a --thinking token; a cross-provider model (gpt/gemini) NEVER does, so
|
|
@@ -4683,7 +4952,7 @@ async function stageOnce(name, ctx, opts = {}) {
|
|
|
4683
4952
|
// seat answering something it was no longer asked about is served from the ledger rather than refused
|
|
4684
4953
|
// as out-of-list — and `still_open` keeps naming everything genuinely open.
|
|
4685
4954
|
...(patch == null && typeof def.refreshCtx === "function"
|
|
4686
|
-
? { refreshMessage: () => def.message(def.refreshCtx(ctx)) + (opts.extra ? `\n\n${opts.extra}` : "") }
|
|
4955
|
+
? { refreshMessage: () => def.message(messageCtxFor(name, def.refreshCtx(ctx))) + (opts.extra ? `\n\n${opts.extra}` : "") }
|
|
4687
4956
|
: {}),
|
|
4688
4957
|
// #5b: a followup is a warm session resume — tell runStage so a hard-wall timeout breaks after ONE attempt
|
|
4689
4958
|
// (an already-over-budget resume can't fit the 1.5× extension; this restores ~1× the wall, was ~2.5×).
|
|
@@ -5972,7 +6241,7 @@ const UPSTREAM_STALE_REPAIR = {
|
|
|
5972
6241
|
// re-run could leave outvoting its prose, and it is re-runnable by contract (no one-shot receipt — the
|
|
5973
6242
|
// assertReceiptSettled call ahead of its normal dispatch settles the plan receipt, which is already
|
|
5974
6243
|
// settled by the time the delivery gate asks).
|
|
5975
|
-
"placement-inquiry": (ctx) => stage("placement-inquiry", ctx, { force: true, trigger: "stale-repair" }),
|
|
6244
|
+
"placement-inquiry": async (ctx) => recordPlacementPass(ctx, await stage("placement-inquiry", ctx, { force: true, trigger: "stale-repair" }), "stale-repair"),
|
|
5976
6245
|
// frame-diff is DELIBERATELY ABSENT and must stay absent. It is one-shot by contract (no in-process
|
|
5977
6246
|
// re-diff), so an entry here would re-dispatch a stage the design says runs once, for a model call
|
|
5978
6247
|
// that cannot change anything: nothing declares frame-diff's outputs as a stage input, so by the time
|
|
@@ -5994,14 +6263,16 @@ export function partitionDeliveryStale(staleStages) { // @internal
|
|
|
5994
6263
|
// file is byte-unchanged (or every named finding's object is) and no corrections marker landed, fire
|
|
5995
6264
|
// ONE warm followup demanding the re-emit; a terminal miss NEVER withholds (never-withhold) — it logs,
|
|
5996
6265
|
// flags the internal review bar, and closes the CLIENT export via _driver/corrections-state.json.
|
|
5997
|
-
function snapshotFindingsForCorrections(P, runDir) {
|
|
6266
|
+
export function snapshotFindingsForCorrections(P, runDir) { // @internal
|
|
5998
6267
|
let raw = null;
|
|
5999
6268
|
try { raw = readFileSync(P.findings, "utf8"); } catch { /* legacy / not-yet-populated */ }
|
|
6000
6269
|
if (raw == null) return null;
|
|
6001
6270
|
const sha = createHash("sha256").update(raw).digest("hex");
|
|
6002
6271
|
// THE RECORD IN THE MODEL'S NUMBERING, beside the one on disk. The review's flags name findings in the
|
|
6003
6272
|
// model's numbering, so the corrective body compares the record the model wrote; once the delivered
|
|
6004
|
-
// record is folded the two differ (record-fold.mjs). The fold is held too, for a rollback to put back
|
|
6273
|
+
// record is folded the two differ (record-fold.mjs). The fold is held too, for a rollback to put back,
|
|
6274
|
+
// and so are narrative.md and the transport's merge base (`base`): a rollback that put back findings.json
|
|
6275
|
+
// alone left the next patch to merge onto the failed pass's record.
|
|
6005
6276
|
const modelAt = modelRecordPath(runDir, P.findings);
|
|
6006
6277
|
let modelRaw = raw;
|
|
6007
6278
|
if (modelAt !== P.findings) { try { modelRaw = readFileSync(modelAt, "utf8"); } catch { modelRaw = raw; } }
|
|
@@ -6016,7 +6287,7 @@ function snapshotFindingsForCorrections(P, runDir) {
|
|
|
6016
6287
|
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), sha, text: raw }, null, 2));
|
|
6017
6288
|
renameSync(tmp, driverDir(runDir, "findings-pre-corrective.json"));
|
|
6018
6289
|
} catch { /* advisory sidecar */ }
|
|
6019
|
-
return { raw, sha, model, fold };
|
|
6290
|
+
return { raw, sha, model, fold, base: snapshotSynthesisBase(runDir) };
|
|
6020
6291
|
}
|
|
6021
6292
|
/**
|
|
6022
6293
|
*, T3b — THE CORRECTIVE PASS FAILED VALIDATION. RESTORE THE LAST GOOD STATE.
|
|
@@ -6042,6 +6313,11 @@ function snapshotFindingsForCorrections(P, runDir) {
|
|
|
6042
6313
|
* is the document every earlier stage's output was checked against. It is not a guess at a good state;
|
|
6043
6314
|
* it is the last one this run demonstrated.
|
|
6044
6315
|
*
|
|
6316
|
+
* The narrative beside it and the transport's merge base come back with it, whether or not findings.json
|
|
6317
|
+
* moved: a pass can save a narrative change alone, and a base left at the failed pass's record is what the
|
|
6318
|
+
* next patch would merge onto, which applies the discarded pass again. The snapshot holds both
|
|
6319
|
+
* (`snapshotFindingsForCorrections`); a snapshot taken without them leaves both files as they are.
|
|
6320
|
+
*
|
|
6045
6321
|
* ── TWO CASES THAT MUST STILL THROW, BECAUSE SWALLOWING THEM LOSES MORE THAN IT SAVES ────────────
|
|
6046
6322
|
*
|
|
6047
6323
|
* rate-limited The run is PARKED and auto-resumes after the cap resets, with every stage intact.
|
|
@@ -6066,8 +6342,10 @@ export function rollbackCorrectivePass(P, runDir, pre, fail) { // @internal
|
|
|
6066
6342
|
atomicWrite(P.findings, pre.raw);
|
|
6067
6343
|
if (pre.fold) restoreFold(runDir, pre.fold); // the record the model wrote and the map, as they stood
|
|
6068
6344
|
}
|
|
6345
|
+
// `restored` keeps speaking for findings.json alone; what happened to the narrative and the base is its own field.
|
|
6346
|
+
const base = restoreSynthesisBase(runDir, pre.base);
|
|
6069
6347
|
const record = { ts: new Date().toISOString(), reason: String(fail?.fail ?? "failed").slice(0, 200),
|
|
6070
|
-
restored: changed, preSha: pre.sha, failedSha: nowSha };
|
|
6348
|
+
restored: changed, preSha: pre.sha, failedSha: nowSha, ...(base ? { base } : {}) };
|
|
6071
6349
|
try { atomicWrite(driverDir(runDir, "corrective-rollback.json"), JSON.stringify(record, null, 2) + "\n"); }
|
|
6072
6350
|
catch { /* sidecar is best-effort; the runLog event below is the record that decides */ }
|
|
6073
6351
|
return record;
|
|
@@ -6154,26 +6432,35 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks,
|
|
|
6154
6432
|
|
|
6155
6433
|
if (!restoredFindings.length && !restoredKeys.length && !restoredRows.length) return null;
|
|
6156
6434
|
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
const
|
|
6162
|
-
const
|
|
6163
|
-
const
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6435
|
+
// ONE RESTORATION, applied to a record in the model's numbering: the file's here, and the merge base's
|
|
6436
|
+
// below. Whatever a record already holds stays as it holds it; only what came back is added.
|
|
6437
|
+
const byOrdinal = new Map(preList.map((f, i) => [String(f?.ordinal), i]));
|
|
6438
|
+
const restoreInto = (doc) => {
|
|
6439
|
+
const merged = { ...doc };
|
|
6440
|
+
for (const k of restoredKeys) if (!(k in merged)) merged[k] = preDoc[k];
|
|
6441
|
+
for (const [reg, back] of Object.entries(rowsBack)) {
|
|
6442
|
+
// In the snapshot's order, the post-pass version of every row it kept, then any row the pass added.
|
|
6443
|
+
const keyOf = REPORT_LINE_KEY[reg];
|
|
6444
|
+
const now = Array.isArray(doc[reg]) ? doc[reg] : [];
|
|
6445
|
+
const post = new Map(now.map((r) => [keyOf(r), r]));
|
|
6446
|
+
const restored = new Set(back);
|
|
6447
|
+
const inPre = new Set(preDoc[reg].map(keyOf));
|
|
6448
|
+
merged[reg] = [
|
|
6449
|
+
...preDoc[reg].flatMap((r) => restored.has(r) ? [r] : post.has(keyOf(r)) ? [post.get(keyOf(r))] : []),
|
|
6450
|
+
...now.filter((r) => !inPre.has(keyOf(r))),
|
|
6451
|
+
];
|
|
6452
|
+
}
|
|
6453
|
+
if (restoredFindings.length) {
|
|
6454
|
+
// Back in the order the reviewer read them, by ordinal — a restored finding appended to the end
|
|
6455
|
+
// would be a different document from the one that was correct.
|
|
6456
|
+
const list = Array.isArray(doc.findings) ? doc.findings : [];
|
|
6457
|
+
const has = new Set(list.map((f) => String(f?.ordinal)));
|
|
6458
|
+
merged.findings = [...list, ...restoredFindings.filter((f) => !has.has(String(f?.ordinal)))]
|
|
6459
|
+
.sort((a, b) => (byOrdinal.get(String(a?.ordinal)) ?? 0) - (byOrdinal.get(String(b?.ordinal)) ?? 0));
|
|
6460
|
+
}
|
|
6461
|
+
return merged;
|
|
6462
|
+
};
|
|
6463
|
+
const merged = restoreInto(postDoc);
|
|
6177
6464
|
const brief = (f) => ({ ordinal: f?.ordinal ?? null, mark: f?.mark ?? null });
|
|
6178
6465
|
const record = {
|
|
6179
6466
|
ts: new Date().toISOString(),
|
|
@@ -6184,11 +6471,33 @@ export function repairUnnamedRemovals(P, runDir, pre, namedOrdinals, namedMarks,
|
|
|
6184
6471
|
};
|
|
6185
6472
|
atomicWrite(P.findings, JSON.stringify(merged, null, 2) + "\n");
|
|
6186
6473
|
foldAfterSave(runDir, P.findings); // after the seam the repaired record is folded again, as a save's is
|
|
6474
|
+
// THE MERGE BASE TAKES THE SAME RESTORATION. The patch that follows (the corrections gate's re-emit) merges
|
|
6475
|
+
// onto the base and re-renders the file from it, so a restore written to the file alone is dropped again.
|
|
6476
|
+
// The edit is judged as a call is; a refusal leaves the base as it was, and the record says so.
|
|
6477
|
+
record.base = rebaseSynthesis(runDir, restoreInto);
|
|
6187
6478
|
try { atomicWrite(driverDir(runDir, "corrective-repair.json"), JSON.stringify(record, null, 2) + "\n"); }
|
|
6188
6479
|
catch { /* sidecar is best-effort; the runLog event is the record that decides */ }
|
|
6189
6480
|
return record;
|
|
6190
6481
|
}
|
|
6191
6482
|
|
|
6483
|
+
/**
|
|
6484
|
+
* THE REPAIR'S RUN-LOG ROW, built from what `repairUnnamedRemovals` returned. PURE.
|
|
6485
|
+
*
|
|
6486
|
+
* `base` says what the merge base took, in the words the basis derivation's row uses for the same edit:
|
|
6487
|
+
* `restored`, or `unchanged` where the base already held what came back; `refused` where the acceptance boundary
|
|
6488
|
+
* refused the edit and the base was left as it was, so the next accepted patch drops the restore again; and
|
|
6489
|
+
* `no-base` where the run holds no base, so a later patch is refused for having none. The sidecar keeps the reason.
|
|
6490
|
+
*/
|
|
6491
|
+
export function removalRepairedRow(repaired) { // @internal
|
|
6492
|
+
const b = repaired?.base;
|
|
6493
|
+
const base = !b ? null : b.ok ? (b.changed ? "restored" : "unchanged") : /^no_base:/.test(String(b.reason)) ? "no-base" : "refused";
|
|
6494
|
+
return { event: "corrective-unnamed-removal-repaired", defect: true,
|
|
6495
|
+
restored: repaired.restoredFindings.length, keys: repaired.restoredKeys.length,
|
|
6496
|
+
rows: repaired.restoredRows.length,
|
|
6497
|
+
leftRemoved: repaired.leftRemoved.length,
|
|
6498
|
+
findings: repaired.restoredFindings.map((f) => `${f.ordinal}:${f.mark}`), ...(base ? { base } : {}) };
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6192
6501
|
/**
|
|
6193
6502
|
* The ordinals the reviewer's flags name, from the typed call where the run has one and the rendered
|
|
6194
6503
|
* parse where it does not — the same two sources, in the same order, that `correctionsExtra` uses to
|
|
@@ -6240,7 +6549,9 @@ export function restoredFindingsTable(repair) { // @internal
|
|
|
6240
6549
|
function correctionNamedOrdinals(P) {
|
|
6241
6550
|
const out = [];
|
|
6242
6551
|
try {
|
|
6243
|
-
|
|
6552
|
+
// `P.reviewFlags`: the pass after a delivery-check repair applies a LATER review's flags, which
|
|
6553
|
+
// `readAcceptedFlags` (the first review's call) cannot see. Absent everywhere else.
|
|
6554
|
+
const typed = Array.isArray(P.reviewFlags) ? P.reviewFlags : readAcceptedFlags(P.runDir);
|
|
6244
6555
|
if (typed) { for (const f of typed) if (Array.isArray(f?.on)) out.push(...f.on); return out; }
|
|
6245
6556
|
} catch { /* fall through to the rendered parse, exactly as the worklist does */ }
|
|
6246
6557
|
try {
|
|
@@ -6301,6 +6612,426 @@ function correctionNamedSet(P, doc0 = null) {
|
|
|
6301
6612
|
}
|
|
6302
6613
|
return [...named];
|
|
6303
6614
|
}
|
|
6615
|
+
// ── THE REVIEW AFTER A DELIVERY-CHECK REPAIR, AND ITS ONE CORRECTION ─────────────────────────────────────
|
|
6616
|
+
//
|
|
6617
|
+
// When the delivery check finds a fault in the narrative, the writer repairs its record and the narrative's
|
|
6618
|
+
// bytes move, so the reviewer reads stale. It used to be sent the whole review again, cold, and nothing applied
|
|
6619
|
+
// what it flagged then. Now, where the scope can be established, it reads what the repair changed and every
|
|
6620
|
+
// statement resting on it, worked out by value from the writer's record before and after the repair
|
|
6621
|
+
// (repair-scope.mjs), and its flags go to ONE correction by the writer that nothing reviews again.
|
|
6622
|
+
//
|
|
6623
|
+
// THE FIRST REVIEW STAYS WHOLE, and so does this one whenever the scope is not certain: no record kept from
|
|
6624
|
+
// before the repair, another of the reviewer's inputs moved, a whole re-synthesis ran in this pass, or the
|
|
6625
|
+
// record's version or framework changed. A rewrite with the same values is no change and no review runs.
|
|
6626
|
+
|
|
6627
|
+
/** Which review the reviewer's staleness after a delivery-check repair calls for. Reads the run; never throws. */
|
|
6628
|
+
/**
|
|
6629
|
+
* A SAVE TRIED AGAIN BEFORE IT IS GIVEN UP. Calls `save` up to `attempts` times, waiting `waitMs` between tries, and
|
|
6630
|
+
* says how it went: `{ ok, attempts, error }`. Never throws. `wait` is injectable so a test does not sleep.
|
|
6631
|
+
*/
|
|
6632
|
+
export function saveWithRetries(save, { attempts = 3, waitMs = 250, wait = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) } = {}) { // @internal — driven by its test
|
|
6633
|
+
let error = null;
|
|
6634
|
+
for (let n = 1; n <= attempts; n++) {
|
|
6635
|
+
try { save(); return { ok: true, attempts: n, error }; }
|
|
6636
|
+
catch (e) { error = e; if (n < attempts) wait(waitMs); }
|
|
6637
|
+
}
|
|
6638
|
+
return { ok: false, attempts, error };
|
|
6639
|
+
}
|
|
6640
|
+
|
|
6641
|
+
/**
|
|
6642
|
+
* THE STAGES STALE ON WHAT PLACEMENT WROTE, which join the chain of a changed decision whoever moved it: the delivery
|
|
6643
|
+
* check's own repeat, the decisions written again when the shape is derived again, or a late pass whose readers had
|
|
6644
|
+
* already run. `names` are the base names of placement's files. PURE.
|
|
6645
|
+
*/
|
|
6646
|
+
export function stagesStaleOnPlacement(stale = [], names = new Set()) { // @internal — driven by its test
|
|
6647
|
+
return stale.filter((x) => (x?.changed ?? []).some((c) => names.has(String(c?.name ?? "")))).map((x) => x.label);
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
/**
|
|
6651
|
+
* A CLAUSE JUDGED AGAIN REPLACES THE ONE IT REPEATS, BY ITS KIND. Withdraws every entry whose reason opens with
|
|
6652
|
+
* `<token>:` from the index-aligned reasons and clauses, and a withdrawn clause from the guard set once no entry left
|
|
6653
|
+
* carries it. Returns the arrays as they now stand and how many entries went. PURE.
|
|
6654
|
+
*/
|
|
6655
|
+
export function withdrawByToken(reasons = [], clauses = [], guard = new Set(), token) { // @internal — driven by its test
|
|
6656
|
+
const keep = reasons.map((r) => !String(r ?? "").startsWith(`${token}:`));
|
|
6657
|
+
const out = { reasons: reasons.filter((_, i) => keep[i]), clauses: clauses.filter((_, i) => keep[i]), withdrawn: keep.filter((k) => !k).length };
|
|
6658
|
+
for (const [i, c] of clauses.entries()) if (!keep[i] && !out.clauses.includes(c)) guard.delete(c);
|
|
6659
|
+
return out;
|
|
6660
|
+
}
|
|
6661
|
+
|
|
6662
|
+
/**
|
|
6663
|
+
* WHAT A REPAIR IN THE DELIVERY CHECK LEFT STALE, AND WHAT OF IT IS QUEUED. A stage is this repair's when it is stale now
|
|
6664
|
+
* and was not before the repair, or is stale on an input that has moved since; a stage stale before for the same reasons
|
|
6665
|
+
* is not, and stays the guard's. Every stage the repair left stale joins the chain, so its own re-run is followed in turn.
|
|
6666
|
+
* Of those, a stage already queued ahead runs where it stands, and one this pass already re-did is re-done once more at
|
|
6667
|
+
* most. PURE.
|
|
6668
|
+
*/
|
|
6669
|
+
export function whatTheRepairLeftStale({ before = [], after = [], repaired, ahead = new Set(), processed = new Set(), requeued = new Set() }) { // @internal — driven by its test
|
|
6670
|
+
const keys = (x) => (x?.changed ?? []).map((c) => `${c?.name}@${c?.now}`);
|
|
6671
|
+
const was = new Map(before.map((x) => [x.label, new Set(keys(x))]));
|
|
6672
|
+
const chain = [], queue = [], requeue = [];
|
|
6673
|
+
for (const x of after) {
|
|
6674
|
+
if (x.label === repaired) continue;
|
|
6675
|
+
const prior = was.get(x.label);
|
|
6676
|
+
if (prior && keys(x).every((k) => prior.has(k))) continue; // stale before, for the same reasons: not this repair's
|
|
6677
|
+
chain.push(x.label);
|
|
6678
|
+
if (ahead.has(x.label)) continue;
|
|
6679
|
+
if (processed.has(x.label)) { if (requeued.has(x.label)) continue; requeue.push(x.label); }
|
|
6680
|
+
queue.push(x);
|
|
6681
|
+
}
|
|
6682
|
+
return { chain, queue, requeue };
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6685
|
+
export function postRepairScope(runDir, P, s2, { staleRepaired = [], baseKept = false } = {}) { // @internal
|
|
6686
|
+
if (!baseKept) return { mode: "whole", why: "no-repair-base" };
|
|
6687
|
+
const narrative = basename(String(P?.narrative ?? "narrative.md"));
|
|
6688
|
+
const moved = (s2?.changed ?? []).filter((c) => c?.via !== "repair-record" && basename(String(c?.name ?? "")) !== narrative);
|
|
6689
|
+
if (moved.length) return { mode: "whole", why: "another-input-moved", moved: moved.map((c) => String(c?.name ?? "")) };
|
|
6690
|
+
if (staleRepaired.includes("synthesis")) return { mode: "whole", why: "re-synthesised" };
|
|
6691
|
+
// Any other step this loop re-ran ahead of the reviewer (the digest, placement, the skeptic) moved what the review
|
|
6692
|
+
// stands on beyond the repair's own change, so the scope the queue captured no longer says what changed.
|
|
6693
|
+
const upstream = staleRepaired.filter((l) => l !== "narrative-refutation" && !/^report-(overview|card:)/.test(l));
|
|
6694
|
+
if (upstream.length) return { mode: "whole", why: "upstream-re-run", upstream };
|
|
6695
|
+
let before = null, after = null;
|
|
6696
|
+
try { before = readRepairBase(runDir); after = lastAcceptedCall(runDir); } catch { /* read as no base */ }
|
|
6697
|
+
const changed = changedContent(before, after);
|
|
6698
|
+
if (changed.whole) return { mode: "whole", why: changed.why };
|
|
6699
|
+
if (isEmptyChange(changed)) return { mode: "none", why: "repair-changed-no-content" };
|
|
6700
|
+
return { mode: "narrowed", changed, after, findingOnly: queuedForTheRecordOnly(s2) };
|
|
6701
|
+
}
|
|
6702
|
+
|
|
6703
|
+
/**
|
|
6704
|
+
* Whether the delivery-check repair's change to the findings record is owed a review when the narrative did not move:
|
|
6705
|
+
* any change but the same values saved again. A change the scope cannot narrow (a moved version or framework, or a
|
|
6706
|
+
* record that could not be compared) is owed one too, and the loop gives it the whole review. Never throws.
|
|
6707
|
+
*/
|
|
6708
|
+
export function recordChangeOwesReview(runDir) { // @internal
|
|
6709
|
+
let c = null;
|
|
6710
|
+
try { c = changedContent(readRepairBase(runDir), lastAcceptedCall(runDir)); } catch { c = null; }
|
|
6711
|
+
return !isEmptyChange(c);
|
|
6712
|
+
}
|
|
6713
|
+
|
|
6714
|
+
/**
|
|
6715
|
+
* An open ask as the closure step is shown it: its id, where it was raised and its text, and for a request kept as
|
|
6716
|
+
* parts, which part it is and the questions that part holds, so two parts of one request never read as one line.
|
|
6717
|
+
*/
|
|
6718
|
+
export function openAskForClosure(a) { // @internal — driven by its test
|
|
6719
|
+
return { ask_id: a.ask_id, born: a.born, ask: a.ask,
|
|
6720
|
+
...(a.part ? { part: a.part, qids: Array.isArray(a.qids) ? a.qids : [] } : {}), ...(a.family ? { family: a.family } : {}) };
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
/** The change a whole review's record lists when its scope could not be compared: none. */
|
|
6724
|
+
const NO_CHANGE = Object.freeze({ findings: [], lines: [], registers: [], sections: [], whole: true, why: "not-compared" });
|
|
6725
|
+
|
|
6726
|
+
/** Whether the reviewer was queued for a change to the findings record only: its narrative did not move. */
|
|
6727
|
+
function queuedForTheRecordOnly(s2) {
|
|
6728
|
+
const changed = s2?.changed ?? [];
|
|
6729
|
+
return changed.length > 0 && changed.every((c) => c?.via === "repair-record");
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
/** What the record of an unread review says its change was: findings only, or more. null with no unread record. */
|
|
6733
|
+
function unreadRecordFindingOnly(runDir) {
|
|
6734
|
+
try {
|
|
6735
|
+
const r = JSON.parse(readFileSync(postRepairRecordPath(runDir), "utf8"));
|
|
6736
|
+
return r?.status === "unread" && typeof r.findingOnly === "boolean" ? r.findingOnly : null;
|
|
6737
|
+
} catch { return null; }
|
|
6738
|
+
}
|
|
6739
|
+
|
|
6740
|
+
/**
|
|
6741
|
+
* A review after a repair that could not be read, ON RECORD. Its change went unreviewed, and the reviewer's own stamp
|
|
6742
|
+
* cannot be trusted to say so: a findings-only change does not move the files the reviewer declares, and a stage
|
|
6743
|
+
* that ends with no accepted call still reads ok whenever the earlier review's file stands, so its stamp is
|
|
6744
|
+
* refreshed over the repaired narrative. This record is never handed to the delivery guard: a run is not held on an
|
|
6745
|
+
* unread review (owner's ruling, 2026-10-19), and such a run delivers as the released engine delivers it. The one
|
|
6746
|
+
* boundary is the released engine's own: a reviewer whose re-run fails outright leaves its stamp stale on a changed
|
|
6747
|
+
* narrative, and the freshness guard decides that, as it always has. The record keeps what went unreviewed, and a
|
|
6748
|
+
* resume for any other cause reviews that change whole, until a review that is read replaces the record.
|
|
6749
|
+
*/
|
|
6750
|
+
export function unreadRecordReview(runDir) { // @internal
|
|
6751
|
+
try { return JSON.parse(readFileSync(postRepairRecordPath(runDir), "utf8"))?.status === "unread"; }
|
|
6752
|
+
catch { return false; }
|
|
6753
|
+
}
|
|
6754
|
+
|
|
6755
|
+
const reviewBeforeRepairPath = (runDir) => driverDir(runDir, "review-before-repair.md");
|
|
6756
|
+
const reviewAfterRepairPath = (runDir) => driverDir(runDir, "review-after-repair.md");
|
|
6757
|
+
const postRepairCorrectionsPath = (runDir) => driverDir(runDir, "post-repair-corrections.json");
|
|
6758
|
+
const scopeRecord = (scope, items) => ({
|
|
6759
|
+
findingOnly: Boolean(scope.findingOnly),
|
|
6760
|
+
changed: { findings: scope.changed.findings.map((f) => ({ ordinal: f.ordinal, change: f.change, fields: f.fields })),
|
|
6761
|
+
lines: scope.changed.lines.map((l) => ({ area: l.area, change: l.change })),
|
|
6762
|
+
sections: scope.changed.sections.map((x) => x.title), registers: scope.changed.registers.map((r) => r.label) },
|
|
6763
|
+
items: items.map((i) => ({ kind: i.kind, role: i.role, label: i.label, ...(i.delivered != null ? { delivered: i.delivered } : {}) })),
|
|
6764
|
+
});
|
|
6765
|
+
|
|
6766
|
+
/**
|
|
6767
|
+
* The narrowed review. Keeps the whole review aside first, so whatever this one writes, the run can put the
|
|
6768
|
+
* whole review back: `senior-eye-review.md` holds the review whose verdict the run carries, and the caller
|
|
6769
|
+
* decides that after the late-verdict rule. Returns `{reviewed: false}` when no accepted review came back —
|
|
6770
|
+
* the label then stays stale and the delivery guard decides, as for a whole review that failed.
|
|
6771
|
+
*/
|
|
6772
|
+
async function runPostRepairReview(ctx, P, run, scope) {
|
|
6773
|
+
const runDir = run.runDir;
|
|
6774
|
+
const kept = existsSync(P.seniorEyeReview) ? readFileSync(P.seniorEyeReview, "utf8") : null;
|
|
6775
|
+
if (kept != null) atomicWrite(reviewBeforeRepairPath(runDir), kept);
|
|
6776
|
+
const putBack = () => {
|
|
6777
|
+
if (kept == null) return;
|
|
6778
|
+
let now = null;
|
|
6779
|
+
try { now = readFileSync(P.seniorEyeReview, "utf8"); } catch { now = null; }
|
|
6780
|
+
if (now !== kept) atomicWrite(P.seniorEyeReview, kept);
|
|
6781
|
+
};
|
|
6782
|
+
const seq0 = nextRefutationSeq(runDir);
|
|
6783
|
+
const fold = readFold(runDir);
|
|
6784
|
+
const resting = restingOn(scope.changed, scope.after);
|
|
6785
|
+
const items = scopeItems(scope.changed, resting, fold?.map);
|
|
6786
|
+
const list = listForReview(scope.changed, resting, fold?.map);
|
|
6787
|
+
const changedInputs = changedInputsOf(scope.changed, { narrative: P.narrative, findings: P.findings });
|
|
6788
|
+
const message = repairFollowup("narrative-refutation:post-repair-review", {
|
|
6789
|
+
findings: P.findings, ...list, registerOnly: Boolean(ctx.registerOnly), job: ctx.job ?? {},
|
|
6790
|
+
skepticFlags: P.skepticFlags, frameReopenReceipt: P.frameReopenReceipt, floorDecisions: placementFloorDecisionLines(P),
|
|
6791
|
+
});
|
|
6792
|
+
const { text: blocks } = composeDispatchExtra("narrative-refutation", ctx, { skip: ["refute-plain-register"] });
|
|
6793
|
+
const t0 = Date.now();
|
|
6794
|
+
const r = await stage("narrative-refutation", ctx, {
|
|
6795
|
+
force: true, trigger: "post-repair-review", freshMessage: message, ...(blocks ? { extra: blocks } : {}), changedInputs,
|
|
6796
|
+
});
|
|
6797
|
+
const rec = r?.ok ? readAcceptedReviewSince(runDir, seq0) : null;
|
|
6798
|
+
const base = { seq0, ...scopeRecord(scope, items), changedInputs: changedInputs.map((x) => basename(String(x))), ms: Date.now() - t0 };
|
|
6799
|
+
if (!rec) {
|
|
6800
|
+
// NOT A PASS: no review the driver can act on came back. The whole review goes back in place, the record
|
|
6801
|
+
// says the review could not be read, and the label stays stale for the delivery guard.
|
|
6802
|
+
putBack();
|
|
6803
|
+
writePostRepairRecord(runDir, { status: "unread", ...base, fail: r?.ok ? "no-accepted-call" : String(r?.fail ?? "failed") });
|
|
6804
|
+
runLog(runDir, { event: "post-repair-review", read: false, fail: r?.ok ? "no-accepted-call" : String(r?.fail ?? "failed").slice(0, 120) });
|
|
6805
|
+
note(`[post-repair] the review after the delivery-check repair could not be read back — the whole review stays in place and the delivery guard decides`);
|
|
6806
|
+
return { reviewed: false };
|
|
6807
|
+
}
|
|
6808
|
+
const reviewText = readFileSync(P.seniorEyeReview, "utf8");
|
|
6809
|
+
atomicWrite(reviewAfterRepairPath(runDir), reviewText);
|
|
6810
|
+
const points = classifyPoints(rec.flags, items);
|
|
6811
|
+
const by = points.reduce((a, x) => ((a[x.class] = (a[x.class] ?? 0) + 1), a), {});
|
|
6812
|
+
writePostRepairRecord(runDir, { status: "read", ...base, verdict: rec.verdict, seq: rec.seq,
|
|
6813
|
+
points: points.map((x) => ({ n: x.n, kind: x.kind, on: x.on, target: x.target.label, class: x.class, text: String(rec.flags[x.n - 1]?.text ?? "") })) });
|
|
6814
|
+
runLog(runDir, { event: "post-repair-review", read: true, verdict: rec.verdict, flags: rec.flags.length, classes: by,
|
|
6815
|
+
changed: { findings: scope.changed.findings.length, lines: scope.changed.lines.length, sections: scope.changed.sections.length, registers: scope.changed.registers.length },
|
|
6816
|
+
findingOnly: Boolean(scope.findingOnly), ms: base.ms });
|
|
6817
|
+
return { reviewed: true, rec, reviewText, items, points, putBack, base };
|
|
6818
|
+
}
|
|
6819
|
+
|
|
6820
|
+
/**
|
|
6821
|
+
* THE WHOLE REVIEW OF A REPAIR'S CHANGE: when the narrowed one could not be read, and when the change cannot be
|
|
6822
|
+
* narrowed at all (a change to the record the scope cannot compare, or one an earlier review could not read, as on
|
|
6823
|
+
* a resume). It returns what runPostRepairReview returns, so its points go to the same one correction; adopting its
|
|
6824
|
+
* verdict alone would leave its flags unapplied. When it cannot be read, the record says the change was not reviewed
|
|
6825
|
+
* (`unreadRecordReview`). No unread review is a pass, and the record never holds the run; a re-run that failed outright
|
|
6826
|
+
* leaves the reviewer's stamp as the released engine leaves it, for the freshness guard. `rerun`
|
|
6827
|
+
* is the reviewer's own stale-repair arm.
|
|
6828
|
+
*/
|
|
6829
|
+
async function runWholeReviewForRecord(ctx, P, run, scope, rerun) {
|
|
6830
|
+
const runDir = run.runDir;
|
|
6831
|
+
const seq0 = nextRefutationSeq(runDir);
|
|
6832
|
+
const t0 = Date.now();
|
|
6833
|
+
const r = await rerun();
|
|
6834
|
+
const rec = r?.ok ? readAcceptedReviewSince(runDir, seq0) : null;
|
|
6835
|
+
const fold = readFold(runDir);
|
|
6836
|
+
const items = scopeItems(scope.changed, restingOn(scope.changed, scope.after), fold?.map);
|
|
6837
|
+
const base = { seq0, ...scopeRecord(scope, items), whole: true, ms: Date.now() - t0 };
|
|
6838
|
+
if (!rec) {
|
|
6839
|
+
const fail = r?.ok ? "no-accepted-call" : String(r?.fail ?? "failed");
|
|
6840
|
+
writePostRepairRecord(runDir, { status: "unread", ...base, fail });
|
|
6841
|
+
runLog(runDir, { event: "post-repair-review", read: false, whole: true, fail: fail.slice(0, 120) });
|
|
6842
|
+
note(`[post-repair] neither review of what the repair changed could be read back — it was not reviewed, and the run's record says so; ${r?.ok ? "the run delivers" : "the reviewer's re-run failed, so its stamp stays as it was and the freshness guard decides, as in the released engine"}`);
|
|
6843
|
+
return { reviewed: false };
|
|
6844
|
+
}
|
|
6845
|
+
const reviewText = readFileSync(P.seniorEyeReview, "utf8");
|
|
6846
|
+
atomicWrite(reviewAfterRepairPath(runDir), reviewText);
|
|
6847
|
+
const points = classifyPoints(rec.flags, items);
|
|
6848
|
+
writePostRepairRecord(runDir, { status: "read", ...base, verdict: rec.verdict, seq: rec.seq,
|
|
6849
|
+
points: points.map((x) => ({ n: x.n, kind: x.kind, on: x.on, target: x.target.label, class: x.class, text: String(rec.flags[x.n - 1]?.text ?? "") })) });
|
|
6850
|
+
runLog(runDir, { event: "post-repair-review", read: true, whole: true, verdict: rec.verdict, flags: rec.flags.length, ms: base.ms });
|
|
6851
|
+
return { reviewed: true, rec, reviewText, items, points, putBack: () => {}, base };
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
/** Where the state the review after a repair reads is kept whole, before the review and its correction run. */
|
|
6855
|
+
const postRepairStatePath = (runDir) => driverDir(runDir, "post-repair-state.json");
|
|
6856
|
+
|
|
6857
|
+
/**
|
|
6858
|
+
* THE STATE THE REVIEW AFTER A REPAIR READS, KEPT WHOLE before the review and its correction can lose any of it: the
|
|
6859
|
+
* findings record, its narrative, the merge base the next patch builds on, and the fold, in one file under `_driver/`,
|
|
6860
|
+
* so the four are kept together or not at all. Where a part cannot be read, no state is kept, and an earlier one is
|
|
6861
|
+
* removed so that it never stands for this review. Returns whether a state was kept. Never throws.
|
|
6862
|
+
*/
|
|
6863
|
+
function retainStateBeforeReview(runDir, P) {
|
|
6864
|
+
try {
|
|
6865
|
+
const findings = readFileSync(P.findings, "utf8");
|
|
6866
|
+
const base = snapshotSynthesisBase(runDir);
|
|
6867
|
+
if (base.accepted == null || base.narrative == null) throw new Error("a part of the state could not be read");
|
|
6868
|
+
atomicWrite(postRepairStatePath(runDir), JSON.stringify({ ts: new Date().toISOString(), findings, narrative: base.narrative,
|
|
6869
|
+
accepted: base.accepted, fold: snapshotFold(runDir) }) + "\n");
|
|
6870
|
+
return true;
|
|
6871
|
+
} catch (e) {
|
|
6872
|
+
try { rmSync(postRepairStatePath(runDir), { force: true }); } catch { /* nothing to remove */ }
|
|
6873
|
+
runLog(runDir, { event: "post-repair-state-not-kept", reason: String(e?.message ?? e).slice(0, 160) });
|
|
6874
|
+
return false;
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
/**
|
|
6879
|
+
* THE TRUSTED STATE BEFORE THE CORRECTION AFTER A POST-REPAIR REVIEW, for its rollback, from the first source that
|
|
6880
|
+
* stands, in this order:
|
|
6881
|
+
* 1. the findings record as it stands, with its fold, narrative and base (the usual snapshot);
|
|
6882
|
+
* 2. the state kept whole before the review (`retainStateBeforeReview`), byte for byte;
|
|
6883
|
+
* 3. the save the review read (`before`, read before the correction starts), rebuilt through the driver's own
|
|
6884
|
+
* acceptance, never from a save the correction may write afterwards;
|
|
6885
|
+
* 4. the save kept from before the delivery-check repair, rebuilt the same way.
|
|
6886
|
+
* A save that the run's CURRENT duty refuses is not lost: it was accepted when it was made, and the records owed since
|
|
6887
|
+
* are evidence added later. It is rebuilt with the duty set aside and marked so (`duty: "relaxed"`); once the correction
|
|
6888
|
+
* settles, the writer's duty is judged again on the record that stands, and names what it leaves unaccounted in the
|
|
6889
|
+
* approved sentence. A save that cannot be read, or that
|
|
6890
|
+
* the acceptance refuses for anything else, is not a source. Null only when none of the four stands. Never throws.
|
|
6891
|
+
*/
|
|
6892
|
+
function trustedStateBeforeCorrection(Pv, runDir, before) {
|
|
6893
|
+
const held = snapshotFindingsForCorrections(Pv, runDir);
|
|
6894
|
+
if (held) return held;
|
|
6895
|
+
const sha = (t) => createHash("sha256").update(t).digest("hex");
|
|
6896
|
+
try {
|
|
6897
|
+
const k = JSON.parse(readFileSync(postRepairStatePath(runDir), "utf8"));
|
|
6898
|
+
if (typeof k?.findings === "string" && typeof k.narrative === "string" && typeof k.accepted === "string") {
|
|
6899
|
+
const h = sha(k.findings);
|
|
6900
|
+
return { raw: k.findings, sha: h, model: { raw: k.findings, sha: h }, fold: k.fold ?? null,
|
|
6901
|
+
base: { accepted: k.accepted, narrative: k.narrative }, from: "state-before-the-review" };
|
|
6902
|
+
}
|
|
6903
|
+
} catch { /* no state kept, or it does not read: the saves below */ }
|
|
6904
|
+
const reads = (() => { try { return driverReadsFor(runDir); } catch { return null; } })();
|
|
6905
|
+
const fromSave = (params, accepted, from) => {
|
|
6906
|
+
if (!params || accepted == null || !reads) return null;
|
|
6907
|
+
const accept = (r) => { try { return acceptSynthesis(params, r); } catch { return null; } };
|
|
6908
|
+
let v = accept(reads), duty = "current", refused = null;
|
|
6909
|
+
if (!v?.ok && reads.owed != null) {
|
|
6910
|
+
refused = v?.reason ?? null;
|
|
6911
|
+
const w = accept({ ...reads, owed: null, declined: null });
|
|
6912
|
+
if (w?.ok) { v = w; duty = "relaxed"; }
|
|
6913
|
+
}
|
|
6914
|
+
if (!v?.ok || typeof v.findings !== "string" || typeof v.narrative !== "string") return null;
|
|
6915
|
+
const h = sha(v.findings);
|
|
6916
|
+
return { raw: v.findings, sha: h, model: { raw: v.findings, sha: h }, fold: null, base: { accepted, narrative: v.narrative }, from, duty,
|
|
6917
|
+
...(duty === "relaxed" ? { refusedUnderCurrentDuty: String(refused ?? "").slice(0, 160) } : {}) };
|
|
6918
|
+
};
|
|
6919
|
+
const bytes = (at) => { try { return readFileSync(at, "utf8"); } catch { return null; } };
|
|
6920
|
+
return fromSave(before, snapshotSynthesisBase(runDir).accepted, "accepted-save")
|
|
6921
|
+
?? fromSave(readRepairBase(runDir), bytes(repairBasePath(runDir)), "save-before-the-repair");
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
/**
|
|
6925
|
+
* A FAILED CORRECTION AFTER A POST-REPAIR REVIEW NEVER ENDS THE RUN (the owner's ruling of 2026-10-19: a run is never
|
|
6926
|
+
* held). The trusted state comes back whole: the record, its narrative and the merge base, then the fold and the seam's
|
|
6927
|
+
* writes made again over a record rebuilt from a save, as after any save, so the delivered verdict is derived from the
|
|
6928
|
+
* record that stands. The correction's points stay in the run's open-questions record. A rate-limited correction is
|
|
6929
|
+
* not waited for either: it is rolled back as a failed one is, since the report it would correct is already reviewed,
|
|
6930
|
+
* and waiting for the reset would hold the run.
|
|
6931
|
+
*
|
|
6932
|
+
* WITH NO TRUSTED STATE, THIS PASS DOES NOT DELIVER. That is the precise limit: the record, the state kept whole before
|
|
6933
|
+
* the review, the save the review read and the save kept from before the repair are all unreadable or refused, a
|
|
6934
|
+
* storage failure. Nothing the failed pass wrote is adopted: the narrative and the base
|
|
6935
|
+
* 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
|
|
6936
|
+
* report with no findings, labelled with the verdict as it stood (measured on the mock pipeline, 2026-10-19): a clear
|
|
6937
|
+
* result over a record nobody can read. So this case alone keeps the failure it always had. Returns the rollback
|
|
6938
|
+
* record, or null for this case.
|
|
6939
|
+
*/
|
|
6940
|
+
async function recoverFromFailedCorrection(ctx, Pv, runDir, pre, pass, { baseBefore, findingsBefore }) {
|
|
6941
|
+
// A rate limit is a failure of this pass like any other here: the shared rollback would leave it to wait.
|
|
6942
|
+
const failure = pass?.fail === "rate_limited" || pass?.resetsAt ? { fail: "rate_limited, not waited for" } : pass;
|
|
6943
|
+
if (pre?.raw) {
|
|
6944
|
+
const rollback = rollbackCorrectivePass(Pv, runDir, pre, failure);
|
|
6945
|
+
if (rollback && pre.from) rollback.from = pre.from;
|
|
6946
|
+
if (rollback && pre.duty) rollback.duty = pre.duty;
|
|
6947
|
+
if (rollback && (pre.from === "accepted-save" || pre.from === "save-before-the-repair")) {
|
|
6948
|
+
// A record rebuilt from a save is folded, and given the seam's writes, as the save itself was.
|
|
6949
|
+
try { foldAfterSave(runDir, Pv.findings); } catch (e) { note(`[post-repair] the restored record could not be folded (${String(e?.message ?? e).slice(0, 100)})`); }
|
|
6950
|
+
try { await ctx.afterSynthesisSave?.(); } catch (e) { note(`[post-repair] the seam's writes could not be made again over the restored record (${String(e?.message ?? e).slice(0, 100)})`); }
|
|
6951
|
+
}
|
|
6952
|
+
return rollback;
|
|
6953
|
+
}
|
|
6954
|
+
// NO TRUSTED STATE: nothing the failed pass wrote is adopted, and the pass ends as it always did.
|
|
6955
|
+
const base = restoreSynthesisBase(runDir, baseBefore);
|
|
6956
|
+
let findings = "unchanged";
|
|
6957
|
+
if (!findingsBefore) {
|
|
6958
|
+
let now = null;
|
|
6959
|
+
try { now = readFileSync(Pv.findings, "utf8"); } catch { now = null; }
|
|
6960
|
+
// Set aside, never deleted: the failed pass's record stays beside the run's record for whoever diagnoses the run.
|
|
6961
|
+
if (now != null) {
|
|
6962
|
+
try { renameSync(Pv.findings, driverDir(runDir, "findings-failed-correction.json")); findings = "set-aside"; }
|
|
6963
|
+
catch { try { rmSync(Pv.findings, { force: true }); findings = "removed"; } catch { findings = "failed"; } }
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
runLog(runDir, { event: "post-repair-correction-unrecoverable", reason: String(failure?.fail ?? "failed").slice(0, 200), findings, ...(base ? { base } : {}) });
|
|
6967
|
+
note(`[post-repair] the correction after the review of the repair failed with no trusted state to restore (the record and both saves unreadable) — nothing it wrote is kept, and this pass does not deliver a report without its record`);
|
|
6968
|
+
return null;
|
|
6969
|
+
}
|
|
6970
|
+
|
|
6971
|
+
/**
|
|
6972
|
+
* The one correction after that review: every accepted point goes to it, whatever its class, and nothing
|
|
6973
|
+
* reviews it again. It runs the first correction's own backstops — the rollback, the put-back of a removal no
|
|
6974
|
+
* flag named, the follow-up when a correction missed the findings — on a view of the run's paths that points at
|
|
6975
|
+
* the review after the repair, and records what each flag led to beside the first correction's record.
|
|
6976
|
+
*/
|
|
6977
|
+
async function runPostRepairCorrection(ctx, P, run, { rec, items, points, grounds = false, whole = false, synthesisRef }) {
|
|
6978
|
+
const runDir = run.runDir;
|
|
6979
|
+
const Pv = { ...P, seniorEyeReview: reviewAfterRepairPath(runDir), correctionsApplied: postRepairCorrectionsPath(runDir), reviewFlags: rec.flags };
|
|
6980
|
+
const named = [...new Set(points.filter((x) => x.target.kind !== "none").map((x) => x.target.label))];
|
|
6981
|
+
const rows = rec.flags.map((f, i) => ({ n: i + 1, kind: f.kind, text: f.text, fix: f.fix ?? null,
|
|
6982
|
+
ordinals: Array.isArray(f.on) && f.on.length ? f.on : null }));
|
|
6983
|
+
const review = readFileSync(Pv.seniorEyeReview, "utf8");
|
|
6984
|
+
const rulingsTail = extractRulingsTail(existsSync(P.placement) ? readFileSync(P.placement, "utf8") : "");
|
|
6985
|
+
const followup = repairFollowup("synthesis:post-repair-corrective", {
|
|
6986
|
+
narrative: P.narrative, findings: P.findings, placement: P.placement, rulingsTail,
|
|
6987
|
+
items: named, renumbered: anyRenumbered(items), worklist: correctionsWorklist(rows, { afterRepair: true }), review, whole,
|
|
6988
|
+
});
|
|
6989
|
+
const before = lastAcceptedCall(runDir);
|
|
6990
|
+
// What stood before the correction, held before it can alter any of it: the files as they are, for a pass that
|
|
6991
|
+
// fails with no trusted state to restore, and the trusted state itself.
|
|
6992
|
+
const baseBefore = snapshotSynthesisBase(runDir);
|
|
6993
|
+
const findingsBefore = (() => { try { readFileSync(Pv.findings); return true; } catch { return false; } })();
|
|
6994
|
+
const pre = trustedStateBeforeCorrection(Pv, runDir, before);
|
|
6995
|
+
prepareDeclinationSpec(ctx, P);
|
|
6996
|
+
const pass = await stage("synthesis", ctx, { force: true, followup: followup + stageCharter("synthesis", ctx.depth, ctx.framework), sessionKey: synthesisRef.key,
|
|
6997
|
+
model: synthesisRef.model, thinking: synthesisRef.thinking, trigger: "post-repair-corrective" });
|
|
6998
|
+
recordSynthesisSeam(ctx, pass, "post-repair-corrective");
|
|
6999
|
+
const rollback = pass.ok ? null : await recoverFromFailedCorrection(ctx, Pv, runDir, pre, pass, { baseBefore, findingsBefore });
|
|
7000
|
+
if (rollback) {
|
|
7001
|
+
runLog(runDir, { event: "corrective-rollback", reason: rollback.reason, restored: rollback.restored, pass: "post-repair", from: rollback.from ?? "record",
|
|
7002
|
+
...(rollback.duty ? { duty: rollback.duty } : {}) });
|
|
7003
|
+
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`);
|
|
7004
|
+
} else {
|
|
7005
|
+
must(pass, "synthesis(post-repair)");
|
|
7006
|
+
let preDocForNames = null;
|
|
7007
|
+
try { preDocForNames = pre ? parseFindingsJsonLenient(pre.raw) : null; } catch { /* fall back to the file */ }
|
|
7008
|
+
const repaired = repairUnnamedRemovals(Pv, runDir, pre, correctionNamedOrdinals(Pv), correctionNamedSet(Pv, preDocForNames), correctionNamedLines(Pv, preDocForNames));
|
|
7009
|
+
if (repaired) runLog(runDir, removalRepairedRow(repaired));
|
|
7010
|
+
await enforceCorrectionsReachFindings(ctx, Pv, pre, { synthesisKey: synthesisRef.key, synthesisModel: synthesisRef.model, synthesisThinking: synthesisRef.thinking });
|
|
7011
|
+
}
|
|
7012
|
+
let applied = null;
|
|
7013
|
+
try {
|
|
7014
|
+
const preDoc = pre ? parseFindingsJsonLenient(pre.model?.raw ?? pre.raw) : null;
|
|
7015
|
+
let postDoc = null;
|
|
7016
|
+
try { postDoc = parseFindingsJsonLenient(readFileSync(modelRecordPath(runDir, P.findings), "utf8")); } catch { /* shape defects ride the normal ladder */ }
|
|
7017
|
+
const after = lastAcceptedCall(runDir);
|
|
7018
|
+
const sections = points.filter((x) => x.target.kind === "section").map((x) => {
|
|
7019
|
+
const sec = NARRATIVE_PROSE.find((y) => y.key === x.target.key);
|
|
7020
|
+
return { n: x.n, label: x.target.label, before: sectionText(sec?.read(before?.narrative)), after: sectionText(sec?.read(after?.narrative)) };
|
|
7021
|
+
});
|
|
7022
|
+
applied = buildCorrectionsApplied(rows, preDoc, postDoc, { sections });
|
|
7023
|
+
// `grounds`: the review these rows answer hardened the run's verdict, so its points are that verdict's grounds.
|
|
7024
|
+
atomicWrite(Pv.correctionsApplied, JSON.stringify({ ts: new Date().toISOString(), pass: "post-repair", ...(grounds ? { grounds: true } : {}),
|
|
7025
|
+
rows: applied, classes: points.map((x) => ({ n: x.n, class: x.class, target: x.target.label })) }, null, 2) + "\n");
|
|
7026
|
+
const outcomes = applied.reduce((a, x) => ((a[x.outcome] = (a[x.outcome] ?? 0) + 1), a), {});
|
|
7027
|
+
runLog(runDir, { event: "post-repair-fix-pass", handed: rows.length, ok: Boolean(pass.ok), rolledBack: Boolean(rollback), outcomes });
|
|
7028
|
+
} catch (e) {
|
|
7029
|
+
note(`[post-repair] the observation of the correction could not be built (${String(e?.message ?? e).slice(0, 100)}) — its points print as not checked`);
|
|
7030
|
+
runLog(runDir, { event: "post-repair-fix-pass", handed: rows.length, ok: Boolean(pass.ok), rolledBack: Boolean(rollback), observation: false });
|
|
7031
|
+
}
|
|
7032
|
+
return { ran: true, ok: Boolean(pass.ok), rollback, applied };
|
|
7033
|
+
}
|
|
7034
|
+
|
|
6304
7035
|
async function enforceCorrectionsReachFindings(ctx, P, pre, resume) {
|
|
6305
7036
|
if (!pre) return; // no findings.json before the pass — nothing to enforce
|
|
6306
7037
|
const { run } = ctx;
|
|
@@ -6318,7 +7049,7 @@ async function enforceCorrectionsReachFindings(ctx, P, pre, resume) {
|
|
|
6318
7049
|
await stage("synthesis", ctx, {
|
|
6319
7050
|
force: true, sessionKey: resume.synthesisKey, model: resume.synthesisModel, thinking: resume.synthesisThinking,
|
|
6320
7051
|
trigger: "corrective-findings",
|
|
6321
|
-
followup: repairFollowup("synthesis:corrective-findings", { findings: P.findings, named }) + stageCharter("synthesis", ctx.depth, ctx.framework),
|
|
7052
|
+
followup: repairFollowup("synthesis:corrective-findings", { findings: P.findings, named, seniorEyeReview: P.seniorEyeReview }) + stageCharter("synthesis", ctx.depth, ctx.framework),
|
|
6322
7053
|
});
|
|
6323
7054
|
if (readState().stale) {
|
|
6324
7055
|
runLog(run.runDir, { event: "corrective-findings-stale", named });
|
|
@@ -6534,15 +7265,22 @@ function plainRegisterExtra(ctx) {
|
|
|
6534
7265
|
// The surfaces a clearance reader meets before opening anything, named one by one so adding a
|
|
6535
7266
|
// field to the page is a deliberate addition here too.
|
|
6536
7267
|
const visible = [];
|
|
6537
|
-
const add = (where, v) => { const t = String(v ?? "").trim(); if (t) visible.push({ where, text: t }); };
|
|
6538
|
-
for (const f of doc.findings ?? []) add(`conflict ${f?.ordinal ?? "?"}'s
|
|
7268
|
+
const add = (where, v, field = null) => { const t = String(v ?? "").trim(); if (t) visible.push({ where, text: t, field }); };
|
|
7269
|
+
for (const f of doc.findings ?? []) add(`conflict ${f?.ordinal ?? "?"}'s finding sentence`, f?.net, "net");
|
|
6539
7270
|
// The lines that are not a finding, labelled by `reportLines` — the labels the corrective cycle's
|
|
6540
7271
|
// observation joins a flag back to, so what the reviewer is handed and what the driver reads agree.
|
|
6541
7272
|
for (const l of reportLines(doc)) add(l.label, l.text);
|
|
6542
7273
|
|
|
7274
|
+
// A FINDING SENTENCE IS AT MOST TWO SENTENCES, so its length hit cannot say "split it": a finding sentence
|
|
7275
|
+
// already at two would be told to grow a third. What does not fit goes to the legal and practical
|
|
7276
|
+
// positions, in the approved paragraph's own words. Every other visible line, and the knockout's, keeps the
|
|
7277
|
+
// shared hit word for word — `plainRegisterFlags` is not changed.
|
|
7278
|
+
const findingSentenceLength = (n) => `${n} words in one sentence, and a visible line takes ${SENTENCE_WORD_LIMIT}. The finding sentence is at most two sentences: every fact that does not fit goes in the legal and practical positions, never dropped.`;
|
|
6543
7279
|
const hits = [];
|
|
6544
|
-
for (const { where, text } of visible) {
|
|
6545
|
-
for (const flag of plainRegisterFlags(text, about))
|
|
7280
|
+
for (const { where, text, field } of visible) {
|
|
7281
|
+
for (const flag of plainRegisterFlags(text, about)) {
|
|
7282
|
+
hits.push(`- ${where} — ${field === "net" && flag.kind === "length" ? findingSentenceLength(flag.words) : flag.say}`);
|
|
7283
|
+
}
|
|
6546
7284
|
}
|
|
6547
7285
|
if (!hits.length) return nothingFound(`no plain-words flag over ${visible.length} visible line(s)`); // a NEGATIVE FINDING, and the count travels with it so a clean read is not a read of nothing
|
|
6548
7286
|
|
|
@@ -7483,6 +8221,79 @@ export function injectLaneDepthCoverage(P, runDir, note) { // @internal
|
|
|
7483
8221
|
}
|
|
7484
8222
|
}
|
|
7485
8223
|
|
|
8224
|
+
/**
|
|
8225
|
+
* WHAT EACH POLICED GOODS DROP RESTS ON, recorded beside the screen gate (screen-gate.mjs `screenGateEvidence`).
|
|
8226
|
+
* Taken in two halves: `screenGateSnapshot` at the moment the gate judges, before it fetches anything (the findings
|
|
8227
|
+
* it judged, each policed drop's goods evidence, and the reading log), and the record after the gate's fetches,
|
|
8228
|
+
* which reads each drop's goods again only where the gate fetched. Disclosure only: the gate's rule and its
|
|
8229
|
+
* outcome are decided above and nothing here changes them.
|
|
8230
|
+
*/
|
|
8231
|
+
function goodsEvidenceOfDrops(P, runPrefix, uris, gateFetched = null) {
|
|
8232
|
+
const out = new Map();
|
|
8233
|
+
if (!uris.length) return out;
|
|
8234
|
+
const calls = readRunCallRows(DEFAULT_LEDGER_PATH, runPrefix);
|
|
8235
|
+
const held = heldRecordProvenance(P.runDir);
|
|
8236
|
+
// Each drop's own band row, so that a goods reader which reads a row's goods reads this one's too.
|
|
8237
|
+
const band = safeReadJson(P.registerNamedBand);
|
|
8238
|
+
const bandRows = Array.isArray(band?.enumerated) ? band.enumerated : [];
|
|
8239
|
+
const rowOf = new Map(bandRows.map((r) => [normalizeRecordUri(r?.record_id), r]));
|
|
8240
|
+
const goods = bandShapeGoods(P, bandRows).goods;
|
|
8241
|
+
const fetches = recordFetchOutcomes(calls.rows, { runPrefix });
|
|
8242
|
+
// The gate's own fetches are this run's too, recorded by the gate whatever the register's ledger kept of them.
|
|
8243
|
+
for (const [u, r] of gateFetched ?? []) {
|
|
8244
|
+
const key = normalizeRecordUri(u) || String(u ?? "").toLowerCase();
|
|
8245
|
+
const o = fetches.get(key) ?? { sent: 0, from_memory: 0, failed: 0, ok: false, last_status: null };
|
|
8246
|
+
if (r?.ok === true) o.ok = true; else o.failed = Math.max(o.failed, 1);
|
|
8247
|
+
fetches.set(key, o);
|
|
8248
|
+
}
|
|
8249
|
+
for (const uri of uris) {
|
|
8250
|
+
const e = recordGoodsEvidence(rowOf.get(normalizeRecordUri(uri)) ?? { record_id: uri }, { goods, held, fetches });
|
|
8251
|
+
out.set(uri, { goods: e.state, source: e.source });
|
|
8252
|
+
}
|
|
8253
|
+
return out;
|
|
8254
|
+
}
|
|
8255
|
+
|
|
8256
|
+
function screenGateSnapshot(ctx, runPrefix) {
|
|
8257
|
+
const P = ctx.paths;
|
|
8258
|
+
try {
|
|
8259
|
+
const findings = existsSync(P.registerFindings) ? readFileSync(P.registerFindings, "utf8") : "";
|
|
8260
|
+
let readingLog = "";
|
|
8261
|
+
try { readingLog = readFileSync(P.readingLog, "utf8"); } catch { readingLog = ""; }
|
|
8262
|
+
return { findings, readingLog, goods: goodsEvidenceOfDrops(P, runPrefix, policedDropUris(findings)) };
|
|
8263
|
+
} catch (e) {
|
|
8264
|
+
return { findings: "", readingLog: "", goods: new Map(), error: String(e?.message ?? e).slice(0, 160) };
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
|
|
8268
|
+
function recordScreenGateEvidence(ctx, { runPrefix, before, heldBeforeGate, heldAfterGate, gateFetched }) {
|
|
8269
|
+
const P = ctx.paths;
|
|
8270
|
+
try {
|
|
8271
|
+
const uris = policedDropUris(before.findings);
|
|
8272
|
+
const after = gateFetched.size ? goodsEvidenceOfDrops(P, runPrefix, uris, gateFetched) : before.goods;
|
|
8273
|
+
// The line each drop's record sits on, as placement's fate count joins it, so a part of the shape or a page
|
|
8274
|
+
// of a group that served the line counts as the goods words shown.
|
|
8275
|
+
let lineOf = () => null, lineReads = "read";
|
|
8276
|
+
try {
|
|
8277
|
+
const index = buildSelectionIndex(readPlacementFormInput(P.runDir));
|
|
8278
|
+
lineOf = (uri) => lineKey(index.resolve(uri));
|
|
8279
|
+
} catch (e) { lineReads = `the band shape's lines could not be read, so only record opens and lookups count: ${String(e?.message ?? e).slice(0, 100)}`; }
|
|
8280
|
+
const ev = screenGateEvidence({ findingsContent: before.findings, heldBefore: heldBeforeGate, heldAfter: heldAfterGate,
|
|
8281
|
+
before: (uri) => before.goods.get(uri), after: (uri) => after.get(uri),
|
|
8282
|
+
reads: recordReads(before.readingLog), lines: linesRead(before.readingLog), lineOf, readerStage: "register-digest", gateFetched });
|
|
8283
|
+
const calls = readRunCallRows(DEFAULT_LEDGER_PATH, runPrefix);
|
|
8284
|
+
const gateCalls = registerCallAccount(calls.rows.filter((r) => String(r?.sessionKey ?? "").includes("screen-gate-refetch")));
|
|
8285
|
+
const doc = { schema_version: 2, ...ev, gate_calls: gateCalls, line_reads: lineReads,
|
|
8286
|
+
reading_note: "A drop with no recorded read has no band_record open or search-answer serve, and no lookup, part of the shape or page of a group serving its line, in the reading log for the reading step. The log is best-effort: this does not show the goods were never read.",
|
|
8287
|
+
...(before.error ? { snapshot_error: before.error } : {}), ...(calls.error ? { call_ledger_error: calls.error } : {}) };
|
|
8288
|
+
ensureDriverDir(P.runDir);
|
|
8289
|
+
atomicWrite(driverDir(P.runDir, "screen-gate-evidence.json"), JSON.stringify({ ts: new Date().toISOString(), ...doc }, null, 2) + "\n");
|
|
8290
|
+
runLog(P.runDir, { event: "screen-gate-evidence", ...ev.counts, unnamed: ev.unnamed, fetches: ev.fetches,
|
|
8291
|
+
alternatives: ev.alternatives, gate_calls: gateCalls });
|
|
8292
|
+
} catch (e) {
|
|
8293
|
+
runLog(P.runDir, { event: "screen-gate-evidence-failed", fail: String(e?.message ?? e).slice(0, 160) });
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
|
|
7486
8297
|
// ── screen-gate unexamined-disclosure readers ────────────────────────────────────────────────────────
|
|
7487
8298
|
// The screen-gate's disclose-and-continue terminal writes _driver/screen-gate-unresolved.json (atomic,
|
|
7488
8299
|
// the receipt idiom) the moment its post-repair re-check still fails, so the disclosure survives a
|
|
@@ -7536,7 +8347,7 @@ export function injectScreenGateCoverage(P, runDir, note, ctx = {}) { // @inte
|
|
|
7536
8347
|
// the EARLIEST qualifying window end. Code fills ABSENCE only (an authored deadline always wins);
|
|
7537
8348
|
// out-of-window dates never inject (an archived 2015 window is history, not urgency). Receipt:
|
|
7538
8349
|
// _driver/deadline-enrich.json. NEVER-KILL: any defect leaves findings.json untouched.
|
|
7539
|
-
function enrichFindingDeadlines(P, runDir, note, { nowMs = Date.now(), withinDays = 60, graceDays = 14 } = {}) {
|
|
8350
|
+
export function enrichFindingDeadlines(P, runDir, note, { nowMs = Date.now(), withinDays = 60, graceDays = 14 } = {}) { // @internal
|
|
7540
8351
|
try {
|
|
7541
8352
|
if (!existsSync(P.findings)) return;
|
|
7542
8353
|
const deadlines = collectOppositionDeadlines(runDir);
|
|
@@ -7560,7 +8371,8 @@ function enrichFindingDeadlines(P, runDir, note, { nowMs = Date.now(), withinDay
|
|
|
7560
8371
|
if (!enriched.length) return;
|
|
7561
8372
|
parseFindingsJson(JSON.stringify(doc)); // re-validate the shape (throws → catch keeps original)
|
|
7562
8373
|
atomicWrite(P.findings, `${JSON.stringify(doc, null, 2)}\n`); // B5 — a crash mid-write must never leave a truncated findings.json
|
|
7563
|
-
|
|
8374
|
+
const base = rebaseEnrichedDeadlines(runDir, enriched); // a later patch keeps what the file now carries
|
|
8375
|
+
atomicWrite(driverDir(runDir, "deadline-enrich.json"), JSON.stringify({ schema_version: 1, ts: new Date().toISOString(), judged_at: new Date(nowMs).toISOString(), enriched, base }, null, 2) + "\n");
|
|
7564
8376
|
note(`[deadline-enrich] ${enriched.length} finding(s) gained the register's own opposition window (${enriched.map((e) => `${e.mark}: ${e.date}`).join("; ").slice(0, 140)}) — a recorded deadline never silently disappears`);
|
|
7565
8377
|
} catch (e) {
|
|
7566
8378
|
note(`[deadline-enrich] skipped: ${String(e?.message || e).replace(/\s+/g, " ").slice(0, 100)}`);
|
|
@@ -7843,6 +8655,8 @@ export function buildOnlyYouSection(actions, findings, { nowMs = Date.now(), wit
|
|
|
7843
8655
|
//
|
|
7844
8656
|
// BLOCKING every ground the reviewer cites — unchanged, the reviewer refused to sign
|
|
7845
8657
|
// ANY VERDICT plus every objection the run tried and could not show it closed
|
|
8658
|
+
// A HARDENED VERDICT plus every point of the review after a repair that hardened it, even one its correction
|
|
8659
|
+
// changed: nothing reviewed that change, so the label keeps a visible ground (`ground`)
|
|
7846
8660
|
//
|
|
7847
8661
|
// The second source is `corrections-applied.json`, the driver's own flag-by-flag observation,
|
|
7848
8662
|
// which already exists and is already written on both the CONDITIONAL and the BLOCKING path. What
|
|
@@ -7867,7 +8681,7 @@ export function buildOnlyYouSection(actions, findings, { nowMs = Date.now(), wit
|
|
|
7867
8681
|
export function buildReviewerOpenPointsSection(reviewMd, appliedRows = null) { // @internal
|
|
7868
8682
|
const blocking = parseVerdict(reviewMd) === "BLOCKING";
|
|
7869
8683
|
const cited = blocking ? parseCorrections(reviewMd) : [];
|
|
7870
|
-
const unfixed =
|
|
8684
|
+
const unfixed = (Array.isArray(appliedRows) ? appliedRows : []).filter((r) => r?.ground === true || unresolvedFlags([r]).length);
|
|
7871
8685
|
const seen = new Set();
|
|
7872
8686
|
const rows = [];
|
|
7873
8687
|
for (const r of [...cited, ...unfixed]) {
|
|
@@ -7917,6 +8731,8 @@ export function buildReviewerOpenPointsSection(reviewMd, appliedRows = null) {
|
|
|
7917
8731
|
"not-entity-scoped": "the run could not check whether this was addressed",
|
|
7918
8732
|
"not-checkable": "the run could not check whether this was addressed",
|
|
7919
8733
|
"line-unchanged": "the line this named was left as written",
|
|
8734
|
+
// A narrative section a flag of the review after a repair named, left as written: the shipped words.
|
|
8735
|
+
"section-unchanged": "the line this named was left as written",
|
|
7920
8736
|
"line-removed": "the line this named was removed rather than corrected",
|
|
7921
8737
|
};
|
|
7922
8738
|
const items = rows.map((r) => {
|
|
@@ -7924,7 +8740,9 @@ export function buildReviewerOpenPointsSection(reviewMd, appliedRows = null) {
|
|
|
7924
8740
|
if (Array.isArray(r.ordinals) && r.ordinals.length) bits.push(`finding ${r.ordinals.join(", ")}`);
|
|
7925
8741
|
// An outcome nobody has classified takes the unchecked wording, never the checked one — the same
|
|
7926
8742
|
// direction `unresolvedFlags` fails in, for the same reason.
|
|
7927
|
-
|
|
8743
|
+
// A ground the correction changed keeps its observed outcome in the record and prints as unchecked: no review
|
|
8744
|
+
// read the change, so the run cannot say it was addressed, and "did not change" would be false.
|
|
8745
|
+
if (r.outcome) bits.push((r.ground === true ? null : QUALIFIER[r.outcome]) ?? "the run could not check whether this was addressed");
|
|
7928
8746
|
const tail = bits.length ? ` *(${bits.join(" — ")})*` : "";
|
|
7929
8747
|
return `- ${ADVISORY_TAG["client-fact"]} ${String(r.text).trim()}${tail}`;
|
|
7930
8748
|
});
|
|
@@ -8060,6 +8878,54 @@ export function buildReasonedNegativesSection(findings) { // @internal
|
|
|
8060
8878
|
return `${CODE_BUILT_SECTIONS.reasonedNegatives}\n\n${lines.join("\n\n")}`;
|
|
8061
8879
|
}
|
|
8062
8880
|
|
|
8881
|
+
// ── WHETHER EACH REVIEWER CORRECTION REACHED THE RECORD THE REPORT WAS BUILT FROM ───────────────────────
|
|
8882
|
+
//
|
|
8883
|
+
// The corrective pass keeps a basis — what each flag changed in the fields a report prints from the record —
|
|
8884
|
+
// in `_driver/corrections-delivered.json`, and every assembly reads it against the record the report was
|
|
8885
|
+
// built from and writes the outcome beside it (corrections-delivered.mjs). The last assembly's answer stands.
|
|
8886
|
+
// corrections-applied.json, which the open points and the verdict re-check read, is not touched.
|
|
8887
|
+
export const CORRECTIONS_DELIVERED_FILE = "corrections-delivered.json";
|
|
8888
|
+
|
|
8889
|
+
/** Keep the basis at the corrective pass, from the two records the applied table was built from. Throws on a write failure; the caller is never-kill. */
|
|
8890
|
+
export function recordCorrectionsBasis(runDir, rows, preDoc, postDoc) { // @internal
|
|
8891
|
+
const basis = correctionTargets(rows, preDoc, postDoc);
|
|
8892
|
+
atomicWrite(driverDir(runDir, CORRECTIONS_DELIVERED_FILE), JSON.stringify({
|
|
8893
|
+
_provenance: "whether each reviewer correction reached the record the report was built from: the basis is kept at the corrective pass, and every assembly reads it against the delivered record (corrections-delivered.mjs)",
|
|
8894
|
+
basisAt: new Date().toISOString(), basis,
|
|
8895
|
+
}, null, 2) + "\n");
|
|
8896
|
+
return basis;
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
/**
|
|
8900
|
+
* Read the basis against the record this assembly was built from, and record the outcome: the file, and one
|
|
8901
|
+
* run-log row. A run with no basis records nothing. A delivered record that cannot be read is recorded as a
|
|
8902
|
+
* failure to look, never as an outcome. Never throws.
|
|
8903
|
+
*/
|
|
8904
|
+
export function recordCorrectionsDelivered(P) { // @internal
|
|
8905
|
+
if (!P?.runDir) return null;
|
|
8906
|
+
const at = driverDir(P.runDir, CORRECTIONS_DELIVERED_FILE);
|
|
8907
|
+
let held = null;
|
|
8908
|
+
try { held = JSON.parse(readFileSync(at, "utf8")); } catch { return null; } // no corrective basis on this run
|
|
8909
|
+
if (!Array.isArray(held?.basis)) return null;
|
|
8910
|
+
let deliveredDoc = null, reportMd = null;
|
|
8911
|
+
try { deliveredDoc = parseFindingsJsonLenient(readFileSync(P.findings, "utf8")); } catch { deliveredDoc = null; }
|
|
8912
|
+
try { reportMd = readFileSync(P.report, "utf8"); } catch { reportMd = null; }
|
|
8913
|
+
let foldMap = null;
|
|
8914
|
+
try { foldMap = readFold(P.runDir)?.map ?? null; } catch { foldMap = null; }
|
|
8915
|
+
const rows = deliveredOutcomes({ basis: held.basis, deliveredDoc, foldMap, reportMd });
|
|
8916
|
+
const { rows: _rows, error: _error, assembledAt: _assembledAt, ...kept } = held;
|
|
8917
|
+
const out = { ...kept, assembledAt: new Date().toISOString(), rows: rows ?? null,
|
|
8918
|
+
...(rows ? {} : { error: "the record the report was built from could not be read, so no correction was compared" }) };
|
|
8919
|
+
try { atomicWrite(at, JSON.stringify(out, null, 2) + "\n"); } catch { /* the run-log row below is the record */ }
|
|
8920
|
+
try {
|
|
8921
|
+
const outcomes = (rows ?? []).reduce((a, r) => (a[r.outcome] = (a[r.outcome] ?? 0) + 1, a), {});
|
|
8922
|
+
runLog(P.runDir, rows
|
|
8923
|
+
? { event: "corrections-delivered", ok: true, flags: rows.length, outcomes, reportMdRead: reportMd != null }
|
|
8924
|
+
: { event: "corrections-delivered", ok: false, error: out.error });
|
|
8925
|
+
} catch { /* a log that cannot be written must not stop the report it describes */ }
|
|
8926
|
+
return out;
|
|
8927
|
+
}
|
|
8928
|
+
|
|
8063
8929
|
export function assembleReportMd(P, findings, cardOrdinals, { grouped = [], byRight = false } = {}) { // @internal
|
|
8064
8930
|
const overviewRaw = existsSync(P.reportOverview) ? readFileSync(P.reportOverview, "utf8") : "---\n---\n";
|
|
8065
8931
|
let overview = overviewRaw.split(/^#\s+Marks\b/m)[0].replace(/\s*$/, ""); // defensive: overview owns down to # Marks
|
|
@@ -8164,6 +9030,26 @@ export function assembleReportMd(P, findings, cardOrdinals, { grouped = [], byRi
|
|
|
8164
9030
|
let applied = null;
|
|
8165
9031
|
try { applied = JSON.parse(readFileSync(P.correctionsApplied, "utf8"))?.rows ?? null; }
|
|
8166
9032
|
catch { /* absent or malformed — the record falls back to the review alone */ }
|
|
9033
|
+
// …and the correction after the review of a delivery-check repair, which records its rows beside them. Its
|
|
9034
|
+
// points print through the same rule: anything not positively resolved. Which review raised a point stays in
|
|
9035
|
+
// the run's own records (_driver/post-repair-review.json); the printed line adds nothing.
|
|
9036
|
+
// Where that review hardened the verdict its points are the verdict's grounds, and print even when changed.
|
|
9037
|
+
try {
|
|
9038
|
+
const doc = JSON.parse(readFileSync(driverDir(dirname(P.report), "post-repair-corrections.json"), "utf8"));
|
|
9039
|
+
const after = doc?.rows;
|
|
9040
|
+
if (Array.isArray(after) && after.length) applied = [...(applied ?? []), ...(doc.grounds === true ? after.map((r) => ({ ...r, ground: true })) : after)];
|
|
9041
|
+
} catch { /* no correction after a repair on this run */ }
|
|
9042
|
+
// …and a review after a repair that was read and never corrected: the run was parked before its one correction,
|
|
9043
|
+
// and nothing on a resume repeats it. Its points are not shown closed, so they print through the same rule.
|
|
9044
|
+
try {
|
|
9045
|
+
const runDir = dirname(P.report);
|
|
9046
|
+
const rec = JSON.parse(readFileSync(driverDir(runDir, "post-repair-review.json"), "utf8"));
|
|
9047
|
+
if (rec?.status === "read" && !existsSync(driverDir(runDir, "post-repair-corrections.json"))) {
|
|
9048
|
+
const rows = (Array.isArray(rec.points) ? rec.points : []).filter((x) => String(x?.text ?? "").trim())
|
|
9049
|
+
.map((x) => ({ text: String(x.text), kind: x.kind ?? null, outcome: "not-checkable" }));
|
|
9050
|
+
if (rows.length) applied = [...(applied ?? []), ...rows];
|
|
9051
|
+
}
|
|
9052
|
+
} catch { /* no review after a repair on this run */ }
|
|
8167
9053
|
const openPoints = existsSync(P.seniorEyeReview)
|
|
8168
9054
|
? buildReviewerOpenPointsSection(readFileSync(P.seniorEyeReview, "utf8"), applied)
|
|
8169
9055
|
: "";
|
|
@@ -8268,6 +9154,9 @@ export function assembleReportMd(P, findings, cardOrdinals, { grouped = [], byRi
|
|
|
8268
9154
|
ensureDriverDir(P.runDir);
|
|
8269
9155
|
writeFileSync(driverDir(P.runDir, "card-folds.json"), `${JSON.stringify({ schema: 1, folds }, null, 2)}\n`);
|
|
8270
9156
|
} catch { /* observability only */ }
|
|
9157
|
+
// Whether each reviewer correction reached the record this report was built from: rewritten on every
|
|
9158
|
+
// assembly, like the fold record above, so the last assembly's answer stands. Nothing without a basis.
|
|
9159
|
+
try { recordCorrectionsDelivered(P); } catch { /* observability only: the report is assembled either way */ }
|
|
8271
9160
|
return { cards: cards.length, folds };
|
|
8272
9161
|
}
|
|
8273
9162
|
|
|
@@ -10219,9 +11108,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10219
11108
|
if (unitKey[axis]) try {
|
|
10220
11109
|
const byQid = new Map(ctx.registerPlan.entries.map((e) => [e.qid, e]));
|
|
10221
11110
|
const from = new Map([...(Array.isArray(prior?.directives) ? prior.directives : []), ...kept].map((d) => [d.qid, d.source?.url ?? null]));
|
|
10222
|
-
const followup = repairFollowup("register-unit:xcheck-decide", { axis, entries: waiting.map((q) => ({ ...byQid.get(q), from: from.get(q) ?? null })) });
|
|
11111
|
+
const followup = repairFollowup("register-unit:xcheck-decide", { axis, registerPlan: P.registerPlan, bandPath: P.registerBand(axis), entries: waiting.map((q) => ({ ...byQid.get(q), from: from.get(q) ?? null })) });
|
|
10223
11112
|
const r = await stage("register-unit", { ...ctx, axis }, { force: true, followup, sessionKey: unitKey[axis], trigger: "xcheck-decide" });
|
|
10224
11113
|
asked = r?.ok === true;
|
|
11114
|
+
remergeIfBandsMoved(ctx, "xcheck-decide");
|
|
10225
11115
|
} catch (e) { runLog(run.runDir, { event: "register-xcheck-decide-failed", fail: String(e?.message ?? e).slice(0, 160) }); }
|
|
10226
11116
|
const released = releasedFamilyQids(run.runDir), withheld = readWithheldFamilies(run.runDir);
|
|
10227
11117
|
const ran = undecidedCrossChecks(ctx.registerPlan, { released, withheld });
|
|
@@ -10922,7 +11812,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10922
11812
|
// (`x:0` vs `x:null` must differ by value, never by presence). The demote paths carry the SAME
|
|
10923
11813
|
// rows forward unchanged, deliberately — a failed re-digest demotes CLOSURE, not searchedness:
|
|
10924
11814
|
// a query that ran still ran.
|
|
10925
|
-
const writeReopenReceipt = (swept, deferrals, dom, partials = [], accounting = null, answered = []) => {
|
|
11815
|
+
const writeReopenReceipt = (swept, deferrals, dom, partials = [], accounting = null, answered = [], withheld = []) => {
|
|
10926
11816
|
try {
|
|
10927
11817
|
writeFileSync(reopenReceiptPath, JSON.stringify({
|
|
10928
11818
|
ts: new Date().toISOString(), requested: firing.map(reopenKey),
|
|
@@ -10930,6 +11820,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10930
11820
|
...(partials.length ? { partials } : {}),
|
|
10931
11821
|
// a closed directive whose slice the fold refused as already-asked, with the plan row that answered it
|
|
10932
11822
|
...(answered.length ? { answered_elsewhere: answered } : {}),
|
|
11823
|
+
// a closed directive with a slice settled by the reading turn's judgment: the slice and the family
|
|
11824
|
+
// it repeats, and nothing else. The reason stays on the turn's own record.
|
|
11825
|
+
...(withheld.length ? { withheld_by_judgment: withheld } : {}),
|
|
10933
11826
|
dominant_element_gap, domClosed: dom,
|
|
10934
11827
|
directive_qids: reopenQids, born: reopenBorn,
|
|
10935
11828
|
remedy_terms: accounting?.computable === true ? accounting.rows : null,
|
|
@@ -10976,6 +11869,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10976
11869
|
// folded into the defer reason otherwise), never dropped into the aggregate count alone.
|
|
10977
11870
|
const regPartialMint = new Map();
|
|
10978
11871
|
const regAnsweredBy = new Map(); // reopenKey → the plan rows that answered its slices the fold refused as already-asked
|
|
11872
|
+
const regWithheldBy = new Map(); // reopenKey → its slices refused as families the reading turn withheld, with each family
|
|
10979
11873
|
// — the per-TERM rows the remedy ledger accounts. One row per remedy term per directive,
|
|
10980
11874
|
// recorded at the mint seam (the only place the term→qid mapping exists) so the receipt can
|
|
10981
11875
|
// later say what each term asked and what came back. reopenKey → [{term, predicates, qids,
|
|
@@ -11093,18 +11987,28 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11093
11987
|
note(`frame-reopen: ${regDirectives.length} register omission(s) → ${allMinted.length} supplemental entr${allMinted.length === 1 ? "y" : "ies"} minted (structured remedy), dispatched as pure code (no agent sweep)`);
|
|
11094
11988
|
const beforeBand = existsSync(P.registerBand(a)) ? readFileSync(P.registerBand(a), "utf8") : null;
|
|
11095
11989
|
if (allMinted.length) {
|
|
11096
|
-
|
|
11990
|
+
// A QUESTION THE READING TURN WITHHELD IS NOT ASKED AGAIN HERE. The fold lets a question through
|
|
11991
|
+
// when it matches a waiting family, because the reading turn's own ask is what the family waits
|
|
11992
|
+
// for; this sweep asks on nobody's judgment, so its exact repeat of a family the turn withheld is
|
|
11993
|
+
// refused before the fold, with the family as its twin and the turn's recorded reason. The record
|
|
11994
|
+
// is read here, at the fold, so a family the turn has since released is asked as before.
|
|
11995
|
+
const withheldTwins = withheldFamilyTwins(ctx.registerPlan, readWithheldFamilies(run.runDir), releasedFamilyQids(run.runDir));
|
|
11996
|
+
const { kept: toFold, refused: withheldRefused } = refuseWithheldFamilies(allMinted, withheldTwins, ctx.registerPlan);
|
|
11997
|
+
const folded = foldSupplementalEntries(ctx.registerPlan, toFold);
|
|
11998
|
+
const { plan, added } = folded;
|
|
11999
|
+
const refused = [...withheldRefused, ...folded.refused];
|
|
11097
12000
|
if (added.length) {
|
|
11098
12001
|
writeFileSync(`${P.registerPlan}.tmp`, JSON.stringify(plan, null, 2) + "\n");
|
|
11099
12002
|
renameSync(`${P.registerPlan}.tmp`, P.registerPlan);
|
|
11100
12003
|
ctx.registerPlan = plan;
|
|
11101
12004
|
}
|
|
11102
|
-
// — expected
|
|
11103
|
-
//
|
|
11104
|
-
//
|
|
11105
|
-
// that
|
|
12005
|
+
// — expected to hold only the two refusals that mean the question is settled already: a
|
|
12006
|
+
// duplicate of the plan's own question, and a family the reading turn withheld. Every other kind
|
|
12007
|
+
// is a term mintSupplementalEntries should have caught (it runs entryTermIssues at the mint, and
|
|
12008
|
+
// its rejects are already in the sidecar above). Logged anyway, because that is a belief about
|
|
12009
|
+
// two screens agreeing, and the day they stop agreeing this is the only place that would say so.
|
|
11106
12010
|
if (refused.length) {
|
|
11107
|
-
for (const r of refused) if (r?.qid) regFoldRefusals.set(r.qid, { kind: r.kind ?? null, twin: r.twin ?? null });
|
|
12011
|
+
for (const r of refused) if (r?.qid) regFoldRefusals.set(r.qid, { kind: r.kind ?? null, twin: r.twin ?? null, reason: r.reason ?? null });
|
|
11108
12012
|
// THE REASON THE FOLD GAVE IS LOGGED, not just the qid it refused. The fold is the only
|
|
11109
12013
|
// place that knows WHY, and for a duplicate it knows WHICH plan row already holds the
|
|
11110
12014
|
// answer. Logging the qid alone threw that away: recovering it on one run meant
|
|
@@ -11119,15 +12023,25 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11119
12023
|
// in the plan and its existing row carries the coverage. An identity collision or a
|
|
11120
12024
|
// malformed term IS the mint screen and the fold screen disagreeing, because the mint
|
|
11121
12025
|
// runs entryTermIssues and should have caught it.
|
|
12026
|
+
// A withheld family's refusal is neither: it is the reading turn's decision standing, recorded on
|
|
12027
|
+
// the run log row above with its kind and its family, and settled by that turn's reason.
|
|
11122
12028
|
const dupes = refused.filter((r) => r.kind === "duplicate-question");
|
|
11123
|
-
const faults = refused.filter((r) => r.kind !== "duplicate-question");
|
|
12029
|
+
const faults = refused.filter((r) => r.kind !== "duplicate-question" && r.kind !== "withheld-by-judgment");
|
|
11124
12030
|
if (dupes.length) note(`frame-reopen: ${dupes.length} minted entr${dupes.length === 1 ? "y" : "ies"} refused at the fold as already-asked — the plan row${dupes.length === 1 ? "" : "s"} ${dupes.map((r) => `"${r.twin ?? "(twin unstated)"}"`).join(", ")} carr${dupes.length === 1 ? "ies" : "y"} that coverage; the term is answered, not unsearched`);
|
|
11125
12031
|
if (faults.length) note(`frame-reopen: ${faults.length} minted entr${faults.length === 1 ? "y" : "ies"} REFUSED at the fold (${[...new Set(faults.map((r) => r.kind ?? "unstated"))].join(", ")}) — the mint screen and the fold screen disagree, which is a wiring finding`);
|
|
11126
12032
|
}
|
|
11127
12033
|
// Dispatch → derive band → VERIFY per directive (qid-landed + non-collapse + class-scope).
|
|
11128
12034
|
// A byte-changed band with only a wrong-scope/empty/error block closes NOTHING.
|
|
11129
12035
|
const dispatchAndVerify = async (qids) => {
|
|
11130
|
-
|
|
12036
|
+
// NOTHING ADDED, NOTHING REPEATED. The executor reads an empty question list as every entry on the
|
|
12037
|
+
// axis, so a fold that refused every slice (the plan's own questions, or families the reading
|
|
12038
|
+
// turn withheld) handed the whole primary sweep back to the executor to run again. It now sends
|
|
12039
|
+
// only the axis's required questions that have not landed (pendingRequiredQids), so a required
|
|
12040
|
+
// search still gets its retry and nothing landed is asked twice. The verify below reads the join
|
|
12041
|
+
// either way, so a refused slice is still checked through its twin or its family.
|
|
12042
|
+
let send = qids;
|
|
12043
|
+
if (!send.length) send = pendingRequiredQids(ctx.registerPlan, a, readRegisterBands(P, axes), releasedFamilyQids(ctx.paths.runDir));
|
|
12044
|
+
const outcome = send.length ? await dispatchPlanQids(a, send, "frame-reopen-sweep", 2) : "ok";
|
|
11131
12045
|
refreshSupplementalExecution(ctx);
|
|
11132
12046
|
try { deriveNamedBand(ctx); } catch (e) { regMechFail = `named-band:${String(e.message).slice(0, 80)}`; }
|
|
11133
12047
|
const bands = readRegisterBands(P, axes);
|
|
@@ -11143,6 +12057,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11143
12057
|
executedQids: ev.executedQids, blocksByQid: ev.blocksByQid, foldRefusals: regFoldRefusals,
|
|
11144
12058
|
});
|
|
11145
12059
|
if (v.closed && v.answered_by) regAnsweredBy.set(reopenKey(d), v.answered_by);
|
|
12060
|
+
if (v.closed && v.withheld_by_judgment) regWithheldBy.set(reopenKey(d), v.withheld_by_judgment);
|
|
11146
12061
|
return v;
|
|
11147
12062
|
};
|
|
11148
12063
|
const mintedDirectives = regDirectives.filter((d) => (directiveQids.get(reopenKey(d)) ?? []).length);
|
|
@@ -11204,6 +12119,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11204
12119
|
const a = "primary-sweep";
|
|
11205
12120
|
const regDom = regDirectives.filter((d) => d.severity === "dominant-element");
|
|
11206
12121
|
const beforeUnit = existsSync(P.registerUnit(a)) ? readFileSync(P.registerUnit(a), "utf8") : null;
|
|
12122
|
+
const beforeBand = bandBytesOf(P, a); // a sweep that adds blocks moves the band, whatever it does to the notes
|
|
11207
12123
|
note(`frame-reopen: ${regDirectives.length} register omission(s) → resuming ${a} to ${regDom.length ? "ENUMERATE the dominant-element floor to exhaustion" : "fold in a targeted supplemental sweep"}`);
|
|
11208
12124
|
// Lane flag read from the SAME key the tool exclusion keys on (see the stage dispatcher) —
|
|
11209
12125
|
// this arm is reachable on a lane run whenever the code-side dispatch arm above could not
|
|
@@ -11212,7 +12128,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11212
12128
|
supplementalLane: !!ctx.registerPlan?.contract?.supplemental_lane });
|
|
11213
12129
|
const r = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "frame-reopen" });
|
|
11214
12130
|
if (r.ok) {
|
|
11215
|
-
if (readFileSync(P.registerUnit(a), "utf8") !== beforeUnit) { changedAxes.push(a); regChanged = true; }
|
|
12131
|
+
if (readFileSync(P.registerUnit(a), "utf8") !== beforeUnit || bandBytesOf(P, a) !== beforeBand) { changedAxes.push(a); regChanged = true; }
|
|
11216
12132
|
else note(`frame-reopen: ${a} unchanged after the supplemental sweep`);
|
|
11217
12133
|
} else {
|
|
11218
12134
|
regMechFail = r.fail; note(`frame-reopen: ${a} supplemental sweep failed mechanically (${r.fail})`);
|
|
@@ -11226,7 +12142,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11226
12142
|
const retryMsg = repairFollowup("register-unit:frame-reopen-retry", { paths: P, axis: a, directives: regDirectives, reopenFetchCap,
|
|
11227
12143
|
supplementalLane: !!ctx.registerPlan?.contract?.supplemental_lane });
|
|
11228
12144
|
const r2 = await stage("register-unit", { ...ctx, axis: a }, { force: true, freshMessage: retryMsg, sessionKey: `clearance-${ctx.run.slug}-${ctx.run.codename}-register-unit-${a}-reopen-retry`, trigger: "frame-reopen-retry" });
|
|
11229
|
-
if (r2.ok && existsSync(P.registerUnit(a)) && readFileSync(P.registerUnit(a), "utf8") !== beforeUnit) {
|
|
12145
|
+
if (r2.ok && existsSync(P.registerUnit(a)) && (readFileSync(P.registerUnit(a), "utf8") !== beforeUnit || bandBytesOf(P, a) !== beforeBand)) {
|
|
11230
12146
|
changedAxes.push(a); regChanged = true; regMechFail = null;
|
|
11231
12147
|
note(`frame-reopen: ${a} fresh scoped retry closed the gap`);
|
|
11232
12148
|
} else if (!r2.ok) {
|
|
@@ -11240,6 +12156,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11240
12156
|
regNoSession = true;
|
|
11241
12157
|
note(`frame-reopen: ${regDirectives.length} register omission(s) — no resumable primary-sweep/digest session in this process`);
|
|
11242
12158
|
}
|
|
12159
|
+
if (regArm === "resume") remergeIfBandsMoved(ctx, "frame-reopen");
|
|
11243
12160
|
|
|
11244
12161
|
// SOURCE channels: a warm RESUME is taken when the common-law session is live; with no
|
|
11245
12162
|
// resumable session the omission becomes a DISCLOSED DEFERRAL (open coverage row + the
|
|
@@ -11431,7 +12348,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11431
12348
|
return "unchanged-after-resume";
|
|
11432
12349
|
});
|
|
11433
12350
|
// — THE REMEDY TERM LEDGER, built before the closure read because the closure read now
|
|
11434
|
-
// depends on it. Every term a firing directive restated ends in one of
|
|
12351
|
+
// depends on it. Every term a firing directive restated ends in one of six named states, and
|
|
11435
12352
|
// `searched-empty` carries the executed query and its counted zero — the state that used to
|
|
11436
12353
|
// leave no trace anywhere. Non-fatal end to end: a ledger that cannot build leaves the
|
|
11437
12354
|
// receipt's fields NULL (never absent) and the closure read falls back to its prior rule,
|
|
@@ -11486,7 +12403,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11486
12403
|
const partials = swept.filter((k) => regPartialMint.has(k))
|
|
11487
12404
|
.map((k) => ({ directive: k, note: `verified-closed on the minted subset — ${regPartialMint.get(k)}` }));
|
|
11488
12405
|
const answered = swept.filter((k) => regAnsweredBy.has(k)).map((k) => ({ directive: k, slices: regAnsweredBy.get(k) }));
|
|
11489
|
-
|
|
12406
|
+
const withheldBy = swept.filter((k) => regWithheldBy.has(k)).map((k) => ({ directive: k, slices: regWithheldBy.get(k) }));
|
|
12407
|
+
writeReopenReceipt(swept, deferrals, domClosed, partials, remedyAccounting, answered, withheldBy);
|
|
11490
12408
|
runLog(run.runDir, { event: "frame-reopen", requested: firing.length, swept: swept.length, deferred: deferrals.length, remaining: deferrals.length, dominant_element_gap, domClosed, reopenFetchCap: regDirectives.some((d) => d.severity === "dominant-element") ? reopenFetchCap : undefined });
|
|
11491
12409
|
// C2 (review fix, 2026-07-29) — the redigest-fail demotion invariant, LOCKED path. The
|
|
11492
12410
|
// unlocked path demotes verified-closed directives inline when its pre-synthesis flush
|
|
@@ -11583,23 +12501,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11583
12501
|
// Step 2C placement → Step 2D digest
|
|
11584
12502
|
await assertReceiptSettled(ctx, "placement-inquiry");
|
|
11585
12503
|
const placementPass = await stage("placement-inquiry", ctx);
|
|
11586
|
-
//
|
|
11587
|
-
//
|
|
11588
|
-
//
|
|
11589
|
-
|
|
11590
|
-
// — and whether the floors came back, on the SAME seam and for the same reason: before
|
|
11591
|
-
// anything can overwrite placements.json, and ahead of `must`, because a pass that failed or
|
|
11592
|
-
// skipped is exactly the pass whose floor account is worth having.
|
|
11593
|
-
deriveFloorDuty(ctx, placementPass);
|
|
11594
|
-
// ── — ARM THE FLOOR, AND ONLY ON A PASS THAT ACTUALLY RAN ─────────────────────────────────
|
|
11595
|
-
//
|
|
11596
|
-
// The stamp says "this run's placement ran under code carrying the floors order", which is the one
|
|
11597
|
-
// fact a delivery floor may rest on. A SKIPPED pass leaves `placements.json` written by an earlier
|
|
11598
|
-
// seat — on a resume, potentially one that never saw the dictation — so arming there would hold a
|
|
11599
|
-
// seat to an order it never got. `ok:false` never reaches here anyway (`must` throws on the next
|
|
11600
|
-
// line), and it is written out rather than relied on, because the reason it cannot reach here is a
|
|
11601
|
-
// property of the line below rather than of this one.
|
|
11602
|
-
if (placementPass?.ok === true && placementPass?.skipped !== true) armFloorDuty(P.runDir);
|
|
12504
|
+
// What THIS pass did, recorded before anything else can overwrite placements.json, and ahead of `must`
|
|
12505
|
+
// deliberately: a pass that failed or skipped on a partial artifact is exactly the pass whose account is
|
|
12506
|
+
// worth having, and `must` throws. The delivery gate's repeat of this stage records through the same helper.
|
|
12507
|
+
recordPlacementPass(ctx, placementPass);
|
|
11603
12508
|
must(placementPass, "placement-inquiry");
|
|
11604
12509
|
await checkLateBind("pre-digest"); // B5b checkpoint 2: a bind here rides the digest message as a re-classification block
|
|
11605
12510
|
const digestKey = must(await runDigest(ctx), "register-digest").sessionKey;
|
|
@@ -11766,6 +12671,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11766
12671
|
// Only axes whose unit file MATERIALLY changed stay in escalated[]; content-compare is conservative —
|
|
11767
12672
|
// any byte difference re-fires, so a genuine recall fix is never skipped. Floor-neutral.
|
|
11768
12673
|
const beforeUnit = existsSync(P.registerUnit(a)) ? readFileSync(P.registerUnit(a), "utf8") : null;
|
|
12674
|
+
const beforeBand = bandBytesOf(P, a); // a narrow sub-query adds blocks to the band, whatever it does to the notes
|
|
11769
12675
|
note(`skeptic escalated ${a} → resuming its session to defend/adjust (warm, same tier)`);
|
|
11770
12676
|
let followup = repairFollowup("register-unit:escalation", { paths: P, axis: a, flags,
|
|
11771
12677
|
supplementalLane: !!ctx.registerPlan?.contract?.supplemental_lane });
|
|
@@ -11783,7 +12689,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11783
12689
|
const r = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "escalation" });
|
|
11784
12690
|
if (!r.ok) return { axis: a, changed: false, ok: false };
|
|
11785
12691
|
const afterUnit = readFileSync(P.registerUnit(a), "utf8");
|
|
11786
|
-
const changed = afterUnit !== beforeUnit;
|
|
12692
|
+
const changed = afterUnit !== beforeUnit || bandBytesOf(P, a) !== beforeBand;
|
|
11787
12693
|
if (!changed) {
|
|
11788
12694
|
note(`escalation ${a}: unit defended in place (unchanged) — Opus re-digest not needed`);
|
|
11789
12695
|
runLog(run.runDir, { event: "escalation-noop", axis: a });
|
|
@@ -11791,6 +12697,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11791
12697
|
return { axis: a, changed, ok: true };
|
|
11792
12698
|
});
|
|
11793
12699
|
const escalated = escResults.filter((e) => e.changed).map((e) => e.axis);
|
|
12700
|
+
remergeIfBandsMoved(ctx, "escalation");
|
|
11794
12701
|
// W-3 input (D1): persist the escalation outcome so the client-export gate can WITHHOLD when a
|
|
11795
12702
|
// load-bearing escalation the system ITSELF raised did not complete. requested = axes attempted this
|
|
11796
12703
|
// session; failed = those whose register-unit stage returned !r.ok (never ran to a result). The
|
|
@@ -11882,6 +12789,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11882
12789
|
if (closeableAxes.length && decision.close && !digestLockedEnv) {
|
|
11883
12790
|
const closeResults = await runBatched(closeableAxes, config.gatherConcurrency, async (a) => {
|
|
11884
12791
|
const beforeUnit = existsSync(P.registerUnit(a)) ? readFileSync(P.registerUnit(a), "utf8") : null;
|
|
12792
|
+
const beforeBand = bandBytesOf(P, a); // a close that runs the deferred slices adds blocks to the band
|
|
11885
12793
|
// quote the axis's own deferred ledger rows + ⭐-floor breach rows verbatim so the unit closes
|
|
11886
12794
|
// exactly what was deferred (or mislabelled away). WS-A: the quoted rows come from the SAME
|
|
11887
12795
|
// loadCoverageLedger read the gate decided on — never a separate prose grep that could diverge.
|
|
@@ -11903,8 +12811,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11903
12811
|
const r = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "envelope" });
|
|
11904
12812
|
if (!r.ok) return { axis: a, changed: false, fail: r.fail };
|
|
11905
12813
|
const afterUnit = readFileSync(P.registerUnit(a), "utf8");
|
|
11906
|
-
return { axis: a, changed: afterUnit !== beforeUnit, fail: null };
|
|
12814
|
+
return { axis: a, changed: afterUnit !== beforeUnit || bandBytesOf(P, a) !== beforeBand, fail: null };
|
|
11907
12815
|
});
|
|
12816
|
+
remergeIfBandsMoved(ctx, "envelope");
|
|
11908
12817
|
const closedChanged = closeResults.filter((e) => e.changed).map((e) => e.axis);
|
|
11909
12818
|
const closeFails = closeResults.filter((e) => e.fail);
|
|
11910
12819
|
if (closedChanged.length) {
|
|
@@ -12007,14 +12916,17 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12007
12916
|
// Re-read the findings + recompute the fetched universe on EACH check (the recovery re-digest rewrites
|
|
12008
12917
|
// the findings AND may record_fetch the flagged URIs — both must be picked up on the post-recovery
|
|
12009
12918
|
// check). V4-1: the fetched universe is the DELIVERED ARTIFACT SET's records (run-dir _records/ ∪ this
|
|
12010
|
-
// session's ledger rows)
|
|
12919
|
+
// session's ledger rows) — not the session-prefixed ledger alone. A failed fetch is not a retrieval, and a
|
|
12920
|
+
// body that carries no record is not one either (ruled 2026-10-18): neither answers the gate, so such a drop
|
|
12921
|
+
// gets the gate's own recovery fetch and, failing that, its disclosure. A valid copy the run already holds
|
|
12922
|
+
// still answers it, whatever a later attempt did. A call the register answered `ok` is no record on its own:
|
|
12923
|
+
// every register files the body it fetched, and a 200 carrying an empty record files an empty one, so the
|
|
12924
|
+
// body is what is read, never the call's status.
|
|
12925
|
+
const fetchedUniverse = () => new Set(
|
|
12926
|
+
[...assembleRunRecords(run.runDir, runPrefix).records].filter(([, body]) => recordBodyHasContent(body)).map(([uri]) => uri));
|
|
12011
12927
|
const checkScreenGate = () => {
|
|
12012
12928
|
const findings = existsSync(P.registerFindings) ? readFileSync(P.registerFindings, "utf8") : "";
|
|
12013
|
-
|
|
12014
|
-
...fetchedRecordUris(DEFAULT_LEDGER_PATH, runPrefix),
|
|
12015
|
-
...assembleRunRecords(run.runDir, runPrefix).records.keys(),
|
|
12016
|
-
]);
|
|
12017
|
-
return findScreenGateViolations(findings, fetched);
|
|
12929
|
+
return findScreenGateViolations(findings, fetchedUniverse());
|
|
12018
12930
|
};
|
|
12019
12931
|
|
|
12020
12932
|
// PARSE-GAP LEDGER (2026-07-21): records the gate is blind to because URI_RE's `[a-z]{2}` cannot read
|
|
@@ -12114,7 +13026,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12114
13026
|
// Re-stamp the disclosure to the CURRENT leftover set — the flush may have corrected some
|
|
12115
13027
|
// rows while new ones appeared; the floor and the sidecar must describe what is true now.
|
|
12116
13028
|
// A row already disclosed keeps its original (more specific) fetch-failure cause.
|
|
12117
|
-
const priorCause = new Map((ctx.screenGateUnresolved ?? []).map((u) => [u.uri ?? u.mark, u
|
|
13029
|
+
const priorCause = new Map((ctx.screenGateUnresolved ?? []).map((u) => [u.uri ?? u.mark, u]));
|
|
12118
13030
|
// — the PROVIDER's own words first. `runPostFlushGateRepair` now returns what each fetch
|
|
12119
13031
|
// said instead of discarding it, so a row disclosed on this path names the register's error
|
|
12120
13032
|
// rather than the driver's generic string. `cause_source` says which of the three it is,
|
|
@@ -12125,7 +13037,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12125
13037
|
const fresh = v.uri ? postFlushFailures?.get(v.uri) : null;
|
|
12126
13038
|
const prior = priorCause.get(key);
|
|
12127
13039
|
if (fresh) return { mark: v.mark, uri: v.uri ?? null, cause: fresh, cause_source: "provider" };
|
|
12128
|
-
|
|
13040
|
+
// An earlier pass's cause keeps the source it was given: a refusal is the provider's on that earlier
|
|
13041
|
+
// pass, and a fetch that reported success, or a row naming no record, stays what it was.
|
|
13042
|
+
if (prior?.cause) return { mark: v.mark, uri: v.uri ?? null, cause: prior.cause,
|
|
13043
|
+
cause_source: !prior.cause_source || prior.cause_source === "provider" ? "provider-earlier-pass" : prior.cause_source };
|
|
12129
13044
|
return {
|
|
12130
13045
|
mark: v.mark, uri: v.uri ?? null,
|
|
12131
13046
|
cause: v.uri
|
|
@@ -12150,6 +13065,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12150
13065
|
}
|
|
12151
13066
|
};
|
|
12152
13067
|
let violations = enforcedViolations();
|
|
13068
|
+
// What the gate's answer stood on before it fetched anything, and each fetch it makes: the evidence below.
|
|
13069
|
+
const heldBeforeGate = fetchedUniverse();
|
|
13070
|
+
const gateBefore = screenGateSnapshot(ctx, runPrefix);
|
|
13071
|
+
const gateFetched = new Map();
|
|
12153
13072
|
if (violations.length) {
|
|
12154
13073
|
// REPAIR-OR-DISCLOSE, NEVER BLOCK (2026-06-18). The digest intermittently drops an in-scope-live mark on
|
|
12155
13074
|
// goods WITHOUT record_fetching it, and a generic re-digest re-runs identically (the model skips the same
|
|
@@ -12172,7 +13091,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12172
13091
|
try {
|
|
12173
13092
|
const r = await fetcher(v.uri, { agentId: agent, sessionKey: `${runPrefix}screen-gate-refetch`, recordLog: runRecordLogPath(run.runDir) });
|
|
12174
13093
|
if (!r?.ok) fetchFailures.set(v.uri, r?.cause ?? "record_fetch returned not-ok");
|
|
12175
|
-
|
|
13094
|
+
gateFetched.set(v.uri, { ok: r?.ok === true, cause: r?.ok ? null : (r?.cause ?? "record_fetch returned not-ok") });
|
|
13095
|
+
} catch (e) { fetchFailures.set(v.uri, String(e?.message ?? e).slice(0, 120)); gateFetched.set(v.uri, { ok: false, cause: String(e?.message ?? e).slice(0, 120) }); }
|
|
12176
13096
|
}
|
|
12177
13097
|
// The re-decide text — shared verbatim between the legacy immediate re-digest (funnel OFF) and
|
|
12178
13098
|
// the queued settlement-flush section (funnel ON).
|
|
@@ -12244,6 +13164,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12244
13164
|
...screenGateZeroCause({ findingsPresent: fPresent,
|
|
12245
13165
|
findingsContent: fPresent ? readFileSync(P.registerFindings, "utf8") : "", unnamedArmed }) });
|
|
12246
13166
|
}
|
|
13167
|
+
recordScreenGateEvidence(ctx, { runPrefix, before: gateBefore, heldBeforeGate, heldAfterGate: fetchedUniverse(), gateFetched });
|
|
12247
13168
|
}
|
|
12248
13169
|
|
|
12249
13170
|
// ──: STANDALONE SETTLEMENT / LATE FLUSH ───────────────────────────────────────────────────
|
|
@@ -12261,6 +13182,32 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12261
13182
|
// never re-fire on any later resume.
|
|
12262
13183
|
if (!ctx.digestSettled) {
|
|
12263
13184
|
const digestLockedFlush = existsSync(P.narrative) && !forceFromActive(ctx, "synthesis");
|
|
13185
|
+
// ── LATE EVIDENCE IS ASSESSED BEFORE THE READING STEP'S LAST PASS AND THE WRITER (owner's ruling, 2026-10-19) ──
|
|
13186
|
+
// A record the band gained after placement's pass (a unit's re-run, the code-side recheck) is placed or set aside
|
|
13187
|
+
// by placement itself, through its own stage, before the flush below hands the reading step the placements and
|
|
13188
|
+
// before the writer reads them. The form is a union, so every row placement accepted stands. Only ids placement
|
|
13189
|
+
// never read run it again: a band whose counts alone moved costs nothing. Every path that grows the band after
|
|
13190
|
+
// placement queues the reading step's re-run, so the flush below follows; one that did not is recorded.
|
|
13191
|
+
if (!digestLockedFlush) {
|
|
13192
|
+
const late = lateRecordIds(P);
|
|
13193
|
+
if (late.length) {
|
|
13194
|
+
runLog(run.runDir, { event: "late-evidence", records: late.length });
|
|
13195
|
+
note(`late evidence: ${late.length} record(s) reached the band after placement's pass — placement assesses them before the reading step's next pass and the writer`);
|
|
13196
|
+
// A PASS THAT FAILS, OR IS REFUSED BEFORE IT STARTS, DOES NOT END THE RUN HERE. A run is never held for a recovery
|
|
13197
|
+
// that could not run (the owner's ruling of 2026-10-19): placement's earlier account stands, and the floor and the
|
|
13198
|
+
// recall gate state what it leaves open. The stage can throw before it dispatches, as it does for a band past the
|
|
13199
|
+
// size ceiling, so the throw is caught and recorded as the failure it is.
|
|
13200
|
+
let pass;
|
|
13201
|
+
try { pass = recordPlacementPass(ctx, await stage("placement-inquiry", ctx, { force: true, trigger: "late-evidence" }), "late-evidence"); }
|
|
13202
|
+
catch (e) { pass = { ok: false, fail: String(e?.message ?? e) }; }
|
|
13203
|
+
if (!pass?.ok) {
|
|
13204
|
+
runLog(run.runDir, { event: "late-evidence-failed", records: late.length, fail: String(pass?.fail ?? "failed").slice(0, 200) });
|
|
13205
|
+
ctx.latePassFailed = { records: late.length, fail: String(pass?.fail ?? "failed").slice(0, 200) }; // the attempt, for the delivery record
|
|
13206
|
+
note(`late evidence: placement's pass did not complete (${String(pass?.fail ?? "").slice(0, 100)}) — its earlier account stands, and the floor and the recall gate state what it leaves open`);
|
|
13207
|
+
}
|
|
13208
|
+
if (!pendingItems(ctx.digestQueue).length) runLog(run.runDir, { event: "late-evidence-no-digest-work", records: late.length });
|
|
13209
|
+
}
|
|
13210
|
+
}
|
|
12264
13211
|
if (!digestLockedFlush) await flushDigestQueue("pre-synthesis");
|
|
12265
13212
|
else if (pendingItems(ctx.digestQueue).length) await flushDigestQueue("late");
|
|
12266
13213
|
ctx.digestSettled = true;
|
|
@@ -12410,8 +13357,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12410
13357
|
const { text: synthExtra } = composeDispatchExtra("synthesis", ctx);
|
|
12411
13358
|
const synth = await quarantineSynth(await stage("synthesis", ctx, synthExtra ? { extra: synthExtra } : {}), ctx, "synthesis"); // A3: last-resort quarantine instead of must(); A4: one targeted re-emit inside
|
|
12412
13359
|
recordSynthesisSeam(ctx, synth); // half one — which surfaced records this pass delivered, and which it did not
|
|
12413
|
-
// The corrective re-synthesis
|
|
12414
|
-
//
|
|
13360
|
+
// The corrective re-synthesis is dispatched on the WINNING attempt's key AND model/thinking. A followup's first
|
|
13361
|
+
// attempt starts fresh, and it never resumes the synthesis session (no engine resumes by key); but if synthesis
|
|
13362
|
+
// fell over to a cross-provider model, a dispatch on the base model would correct a record another model wrote.
|
|
12415
13363
|
const synthesisKey = synth.sessionKey, synthesisModel = synth.model, synthesisThinking = synth.thinking;
|
|
12416
13364
|
|
|
12417
13365
|
// A2/A4 — the v3 evidence gates (verified-needs-source, symmetric use-check, client-tier
|
|
@@ -12475,7 +13423,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12475
13423
|
await stage("synthesis", ctx, {
|
|
12476
13424
|
force: true, sessionKey: synthesisKey, model: synthesisModel, thinking: synthesisThinking,
|
|
12477
13425
|
trigger: "actions-missing",
|
|
12478
|
-
followup: repairFollowup("synthesis:actions-missing", { findings: P.findings }) + stageCharter("synthesis", ctx.depth, ctx.framework),
|
|
13426
|
+
followup: repairFollowup("synthesis:actions-missing", { findings: P.findings, narrative: P.narrative }) + stageCharter("synthesis", ctx.depth, ctx.framework),
|
|
12479
13427
|
});
|
|
12480
13428
|
const after = readActionsPresence();
|
|
12481
13429
|
if (after && !after.hasActions) runLog(run.runDir, { event: "findings-actions-absent" });
|
|
@@ -12658,8 +13606,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12658
13606
|
note(`corrective cycle already settled for these exact review/narrative bytes — not re-firing (a stale-artifact recovery re-does what went stale, never the settled corrective loop)`);
|
|
12659
13607
|
}
|
|
12660
13608
|
}
|
|
12661
|
-
// One corrective re-synthesis on CONDITIONAL/BLOCKING, feeding the reviewer's flags back in
|
|
12662
|
-
// WINNING synthesis attempt
|
|
13609
|
+
// One corrective re-synthesis on CONDITIONAL/BLOCKING, feeding the reviewer's flags back in, dispatched on the
|
|
13610
|
+
// WINNING synthesis attempt's key, model and thinking, so the model that wrote a fallback-won synthesis corrects
|
|
13611
|
+
// it. Its first attempt is a fresh turn handed the flags and the inputs; it never resumes the synthesis session.
|
|
12663
13612
|
let correctionsApplied = null; // — the driver's flag-by-flag observation, shared by both legs
|
|
12664
13613
|
let evidenceViolations = []; // — claims that moved against their own evidence, carried to the recheck
|
|
12665
13614
|
let correctionsScope = null; // — the declared per-finding scope, and whether the pass held to it
|
|
@@ -12719,11 +13668,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12719
13668
|
// A DEFECT SIGNAL, not a success. After the schema fix a corrective pass sends a targeted edit
|
|
12720
13669
|
// and cannot remove a finding at all, so e2e asserts this count is ZERO — a firing reports the
|
|
12721
13670
|
// primary fix not holding, never this backstop working.
|
|
12722
|
-
runLog(run.runDir,
|
|
12723
|
-
restored: repaired.restoredFindings.length, keys: repaired.restoredKeys.length,
|
|
12724
|
-
rows: repaired.restoredRows.length,
|
|
12725
|
-
leftRemoved: repaired.leftRemoved.length,
|
|
12726
|
-
findings: repaired.restoredFindings.map((f) => `${f.ordinal}:${f.mark}`) });
|
|
13671
|
+
runLog(run.runDir, removalRepairedRow(repaired)); // with `base`: whether the merge base took the restore
|
|
12727
13672
|
note(`[corrections] the corrective pass removed ${repaired.restoredFindings.length} finding(s) `
|
|
12728
13673
|
+ `no flag named — restored whole from the pre-corrective snapshot: `
|
|
12729
13674
|
+ `${repaired.restoredFindings.map((f) => `#${f.ordinal} ${f.mark}`).join(", ")}`
|
|
@@ -12791,7 +13736,16 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12791
13736
|
runLog(run.runDir, { event: "corrections-applied", flags: correctionsApplied.length, outcomes: by,
|
|
12792
13737
|
scoped: correctionsScope.scoped, named: correctionsScope.named.length,
|
|
12793
13738
|
movedOutsideScope: correctionsScope.moved.length, unboundOrdinals: correctionsScope.unbound.length });
|
|
13739
|
+
// The basis for the delivery check each assembly makes (recordCorrectionsDelivered): what each flag
|
|
13740
|
+
// changed in the fields a report prints from the record, from the same two records. Its own file.
|
|
13741
|
+
try { recordCorrectionsBasis(run.runDir, correctionsApplied, preDoc, postDoc); }
|
|
13742
|
+
catch (e) {
|
|
13743
|
+
try { rmSync(driverDir(run.runDir, CORRECTIONS_DELIVERED_FILE), { force: true }); } catch { /* best-effort */ }
|
|
13744
|
+
note(`[corrections] the delivery basis could not be kept (${String(e?.message ?? e).slice(0, 100)}) — this pass has no delivery record`);
|
|
13745
|
+
}
|
|
12794
13746
|
} catch (e) {
|
|
13747
|
+
// A basis from an earlier pass would be read against this pass's report as if it were this pass's.
|
|
13748
|
+
try { rmSync(driverDir(run.runDir, CORRECTIONS_DELIVERED_FILE), { force: true }); } catch { /* best-effort */ }
|
|
12795
13749
|
correctionsApplied = null;
|
|
12796
13750
|
note(`[corrections] the applied table could not be built (${String(e?.message ?? e).slice(0, 100)}) — the recheck reads both files, as before`);
|
|
12797
13751
|
runLog(run.runDir, { event: "corrections-applied-failed", error: String(e?.message ?? e).slice(0, 160) });
|
|
@@ -12801,9 +13755,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12801
13755
|
// read, so a recheck spends a dispatch to re-derive the verdict it already gave. The entry verdict
|
|
12802
13756
|
// stands, which is also the fail-safe direction this block uses everywhere else.
|
|
12803
13757
|
if (!correctiveRollback && !correctiveCycleSettled && (verdict === "CONDITIONAL" || verdict === "BLOCKING")) {
|
|
12804
|
-
// A1: re-check after the corrective pass
|
|
12805
|
-
//
|
|
12806
|
-
//
|
|
13758
|
+
// A1: re-check after the corrective pass with a lean follow-up on the refutation's winning key and model —
|
|
13759
|
+
// NOT a full re-run of the stage, and NOT the base model when another model won. The follow-up's first
|
|
13760
|
+
// attempt starts fresh, and it never resumes the refutation's session (no engine resumes by key). (With no
|
|
13761
|
+
// winning key, as on a crash-resume skip, a clean forced re-run on the same model.)
|
|
12807
13762
|
// A1: the recheck also runs on the CONDITIONAL path (the VENZY shipping path — previously the
|
|
12808
13763
|
// corrective output was never re-verified there) and reads the updated FINDINGS too, since the client
|
|
12809
13764
|
// surfaces render from findings.json, not the narrative. Failure falls back to the ENTRY verdict
|
|
@@ -12953,31 +13908,12 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12953
13908
|
// are settled by now; the corrective ladder above re-emitted findings last).
|
|
12954
13909
|
enrichFindingDeadlines(P, run.runDir, note);
|
|
12955
13910
|
|
|
12956
|
-
// ── P2-A pre-verdict floor: the reconciliation's hard discrepancy list
|
|
12957
|
-
// screened-live dominant-element candidates ∖ endings
|
|
12958
|
-
//
|
|
12959
|
-
//
|
|
12960
|
-
//
|
|
12961
|
-
// (
|
|
12962
|
-
// never ships). Absent/not-computable artifacts never block (legacy runs, prose-only manifests,
|
|
12963
|
-
// register-gap runs — the gate polices only what this code derived).
|
|
12964
|
-
//
|
|
12965
|
-
// ROUND-2 FIX (review problem 7): the throw stamps failClass "deterministic", like every other
|
|
12966
|
-
// code-derived floor of this kind (the fan-in throws at :320/:3723/:4061). The list is computed
|
|
12967
|
-
// from the run's own artifacts by pure code, so a fresh sample re-derives it identically — the
|
|
12968
|
-
// park the "unknown" classification used to buy (this reason text misses repairs.mjs
|
|
12969
|
-
// DETERMINISTIC_RE, so classifyFailureReason returned "unknown" and decideRecovery granted a full
|
|
12970
|
-
// pipeline resume) is provably futile. Throw-site stamping always wins over the text guess, which
|
|
12971
|
-
// is why the token is NOT added to DETERMINISTIC_RE.
|
|
12972
|
-
{
|
|
12973
|
-
const rec = safeReadJson(P.recallReconciliation);
|
|
12974
|
-
if (rec?.computable === true && (rec.totals?.unended ?? 0) > 0) {
|
|
12975
|
-
const sample = (rec.unended ?? []).slice(0, 4).map((u) => `${u.mark_text} (${u.record_id})`).join("; ");
|
|
12976
|
-
throw new StageFailure("recall-reconciliation",
|
|
12977
|
-
`recall_reconciliation_unended:${rec.totals.unended} — retrieved and screened in-scope-live on the dominant element but ending nowhere a reader can see (no finding row, no drop row, no counted crowd ruling): ${sample}${rec.totals.unended > 4 ? " …" : ""} — a report never ships losing screened candidates silently`,
|
|
12978
|
-
undefined, { failClass: "deterministic" });
|
|
12979
|
-
}
|
|
12980
|
-
}
|
|
13911
|
+
// ── P2-A pre-verdict floor: the reconciliation's hard discrepancy list ─────────────────────────
|
|
13912
|
+
// screened-live dominant-element candidates ∖ endings is read here as it always was, and it no longer THROWS:
|
|
13913
|
+
// the owner's ruling of 2026-10-19 that a run is never held replaces the Round-2 rule that this one class never
|
|
13914
|
+
// ships. The run first recovers what it can in-run (the digest seam's warm followups; placement assessing late
|
|
13915
|
+
// evidence before the writer); whatever is still unended ships NAMED, through the terminal guards' deliver-and-clamp
|
|
13916
|
+
// (`clampOnTheRecallGate`, beside the floor duty's clamp below), never as a run that delivers nothing.
|
|
12981
13917
|
|
|
12982
13918
|
// ── — THE TERMINAL GUARDS DELIVER AND CLAMP; THEY DO NOT WITHHOLD ───────────
|
|
12983
13919
|
//
|
|
@@ -13002,6 +13938,11 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13002
13938
|
// run earlier in this function. The sidecar write sorts guard clauses AFTER the opinion's, stated
|
|
13003
13939
|
// there; this set is how it tells them apart without a parallel index to keep aligned.
|
|
13004
13940
|
const guardClauses = new Set();
|
|
13941
|
+
// THE ACCEPTED ASSESSMENT: the result the review reached, before any guard or floor clamped it. It moves with the
|
|
13942
|
+
// review alone (the degenerate re-ask, a late review that is harsher), and a gap a guard judged again finds closed is
|
|
13943
|
+
// withdrawn against it (ruled 2026-10-19).
|
|
13944
|
+
let assessed = verdict;
|
|
13945
|
+
let restoredClear = false; // a closed gap took this run back to CLEAR, so a later writer's judgment meets the floor again
|
|
13005
13946
|
|
|
13006
13947
|
/**
|
|
13007
13948
|
* Deliver, clamp conservatively, and name the defect in the run record.
|
|
@@ -13018,14 +13959,20 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13018
13959
|
// could only be pinned by source text, and a source-text pin cannot tell an armed check from one
|
|
13019
13960
|
// somebody disarmed while leaving its words in place. This closure is the wiring: run record, note,
|
|
13020
13961
|
// clamp lists, status. The judgement is all in the module.
|
|
13021
|
-
const deliverAndClamp = ({ event, defect, reason, clause, detail = {} }) => {
|
|
13962
|
+
const deliverAndClamp = ({ event, defect, reason, clause, detail = {}, again = false, statedElsewhere = false }) => {
|
|
13963
|
+
// A GUARD JUDGED AGAIN in this run says nothing new when its judgment is unchanged, and otherwise replaces its
|
|
13964
|
+
// earlier clause of the same kind, so a record is never counted twice and no count stands from an earlier pass.
|
|
13965
|
+
if (again && clampReasons.includes(String(reason ?? "").trim())) return;
|
|
13966
|
+
if (again) replaceClauses(String(defect ?? "").split(":")[0]);
|
|
13022
13967
|
// — reason and clause are SEPARATE inputs now: the run record and the ops note keep
|
|
13023
13968
|
// the token sentence; the clause list (which riskStatement renders into the client's Verdict row)
|
|
13024
13969
|
// receives only the reader's sentence. terminalClampDecision refuses a fused clause by shape.
|
|
13025
13970
|
const d = terminalClampDecision({ verdict, defect, reason, clause, detail });
|
|
13026
13971
|
runLog(run.runDir, { event, ...d.record });
|
|
13027
13972
|
note(d.reason);
|
|
13028
|
-
|
|
13973
|
+
// A gap stated in another entry's sentence keeps its reason for the run record, and its clause is that sentence's.
|
|
13974
|
+
const stored = statedElsewhere ? null : d.clause;
|
|
13975
|
+
if (!clampReasons.includes(d.reason)) { clampClauses.push(stored); clampReasons.push(d.reason); guardClauses.add(stored); }
|
|
13029
13976
|
if (d.clamped) {
|
|
13030
13977
|
runLog(run.runDir, { event: "terminal-guard-clamp", defect: d.record.defect, from: verdict, to: d.verdict });
|
|
13031
13978
|
verdict = d.verdict;
|
|
@@ -13033,31 +13980,96 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13033
13980
|
}
|
|
13034
13981
|
};
|
|
13035
13982
|
|
|
13036
|
-
//
|
|
13037
|
-
//
|
|
13038
|
-
//
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
//
|
|
13049
|
-
//
|
|
13050
|
-
//
|
|
13051
|
-
//
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13983
|
+
// A CLAUSE JUDGED AGAIN REPLACES THE ONE IT REPEATS. A guard judged a second time in this run, after a repeat or a
|
|
13984
|
+
// re-run, withdraws its earlier clause of the same kind before it states the current one, so a record is never
|
|
13985
|
+
// counted twice and a count never stands from an earlier judgment. Returns how many entries it withdrew.
|
|
13986
|
+
const replaceClauses = (token, { event = "guard-clause-replaced" } = {}) => {
|
|
13987
|
+
const w = withdrawByToken(clampReasons, clampClauses, guardClauses, token);
|
|
13988
|
+
if (!w.withdrawn) return 0;
|
|
13989
|
+
clampReasons = w.reasons;
|
|
13990
|
+
clampClauses = w.clauses;
|
|
13991
|
+
runLog(run.runDir, { event, token, withdrawn: w.withdrawn });
|
|
13992
|
+
return w.withdrawn;
|
|
13993
|
+
};
|
|
13994
|
+
|
|
13995
|
+
// A GAP JUDGED AGAIN AND POSITIVELY CLOSED takes its own clause with it, and the result is recomputed from the
|
|
13996
|
+
// accepted assessment and the conditions still standing (ruled 2026-10-19). Only a CONDITIONAL that this run's
|
|
13997
|
+
// guards alone made goes back to CLEAR, and that CLEAR then meets the same deliver-conditional floor any CLEAR meets.
|
|
13998
|
+
// A gap whose account is absent, unreadable or not computable is not closed: its clause stands.
|
|
13999
|
+
const withdrawClosedGap = (token) => {
|
|
14000
|
+
if (!replaceClauses(token, { event: "guard-gap-closed" })) return;
|
|
14001
|
+
const now = verdictWithoutTheClosedGap({ verdict, assessed, reasons: clampReasons });
|
|
14002
|
+
if (now === verdict) return;
|
|
14003
|
+
runLog(run.runDir, { event: "verdict-recomputed-gap-closed", token, from: verdict, to: now });
|
|
14004
|
+
note(`the ${token.split("_")[0]} gap this run named is closed, and nothing else conditions the result — ${verdict} goes back to ${now}, the result the review reached`);
|
|
14005
|
+
verdict = now;
|
|
14006
|
+
restoredClear = now === "CLEAR";
|
|
14007
|
+
writeRunStatus(ctx, signoffPatch(verdict));
|
|
14008
|
+
applyCoverageFloor(); // the floor any CLEAR meets: the opinion's forward actions, the coverage gaps
|
|
14009
|
+
};
|
|
14010
|
+
|
|
14011
|
+
// THE RECALL GATE, DELIVERED AND CLAMPED (the owner's ruling of 2026-10-19: a run is never held). A retrieved record still
|
|
14012
|
+
// ending nowhere a reader can see after the run's recovery ships named, in the approved sentence for the same state,
|
|
14013
|
+
// counted in records out of the records the gate weighed. A record another clause already names (the writer's duty,
|
|
14014
|
+
// the floor) is left to it, so no record is counted twice. Returns the count it clamped on, or 0.
|
|
14015
|
+
// THE RECALL GATE'S GAP AS IT STANDS, judged and not yet clamped, so the writer's duty can be stated with it.
|
|
14016
|
+
const recallGapNow = (duty = null) => {
|
|
14017
|
+
const rec = safeReadJson(P.recallReconciliation);
|
|
14018
|
+
const computable = rec?.computable === true;
|
|
14019
|
+
// A record the writer's duty or the floor's clamp already names is left to that clause: one record, one sentence.
|
|
14020
|
+
// The floor's rows count only where its clamp fires, so a record is never left unnamed by both.
|
|
14021
|
+
const floorNames = floorDutyBlock(safeReadJson(P.floorDuty), { armed: floorDutyArmed(run.runDir) })
|
|
14022
|
+
? (safeReadJson(P.floorDuty)?.rows ?? []).filter((r) => r?.disposition !== "accounted").map((r) => String(r?.record_id ?? "").toLowerCase())
|
|
14023
|
+
: [];
|
|
14024
|
+
const alreadyNamed = new Set([...(duty?.computable === true ? duty.unaccounted : []).map((u) => String(u?.uri ?? "").toLowerCase()), ...floorNames]);
|
|
14025
|
+
// A RECORD A REFUSED MERGE LEFT OUT IS UNREAD, and it is counted here, in the same sentence, out of a total that
|
|
14026
|
+
// includes it: the band could not take it without passing the size its readers can be dispatched over.
|
|
14027
|
+
const left = refusedRecordsLeftOut(P);
|
|
14028
|
+
// A RECORD NO PLACEMENT PASS READ, and that the report does not address, is counted in the same sentence: the band
|
|
14029
|
+
// gained it after placement's last pass, and the pass that would have assessed it did not complete.
|
|
14030
|
+
const late = lateRecordsUnaddressed(P, alreadyNamed);
|
|
14031
|
+
const unended = (computable ? rec.unended ?? [] : []).filter((u) => !(u.candidate_records ?? [u.record_id])
|
|
14032
|
+
.some((x) => alreadyNamed.has(String(x).toLowerCase())));
|
|
14033
|
+
const weighedIds = new Set(unended.flatMap((u) => u.candidate_records ?? [u.record_id]).map((x) => String(x).toLowerCase()));
|
|
14034
|
+
const records = [...new Set([...weighedIds, ...left, ...late])];
|
|
14035
|
+
// Out of the records the gate weighed or, where it could weigh none, the records the band holds; and the records
|
|
14036
|
+
// left out besides. A late record outside the records the gate weighed is added to the total; where the gate
|
|
14037
|
+
// weighed none, the band's count already holds it.
|
|
14038
|
+
const base = computable ? Number(rec.totals?.candidates ?? 0) : (safeReadJson(P.registerNamedBand)?.enumerated ?? []).length;
|
|
14039
|
+
const weighed = base + left.length + (computable ? late.filter((id) => !weighedIds.has(id)).length : 0);
|
|
14040
|
+
// The same population by id, where the gate kept it: what a sentence stated with the writer's duty counts once.
|
|
14041
|
+
const population = computable
|
|
14042
|
+
? (Array.isArray(rec.candidate_ids) ? [...rec.candidate_ids, ...left, ...late] : null)
|
|
14043
|
+
: [...(safeReadJson(P.registerNamedBand)?.enumerated ?? []).map((r) => String(r?.record_id ?? "").toLowerCase()), ...left];
|
|
14044
|
+
return { rec, computable, left, late, unended, records, base, weighed, population };
|
|
14045
|
+
};
|
|
14046
|
+
// THE RECALL GATE, DELIVERED AND CLAMPED (the owner's ruling of 2026-10-19: a run is never held). A retrieved record still
|
|
14047
|
+
// ending nowhere a reader can see after the run's recovery ships named, in the approved sentence for the same state,
|
|
14048
|
+
// counted in records out of the records the gate weighed. A record another clause already names (the writer's duty,
|
|
14049
|
+
// the floor) is left to it, so no record is counted twice. Where the writer's duty names records too, the one sentence
|
|
14050
|
+
// is the duty's, stated with these (`joint`). Returns the count it clamped on, or 0.
|
|
14051
|
+
const clampOnTheRecallGate = (duty = null, { again = false, gap = recallGapNow(duty), joint = null } = {}) => {
|
|
14052
|
+
const { rec, computable, left, late, unended, records, base, weighed } = gap;
|
|
14053
|
+
if (!left.length && !late.length && (!computable || !((rec.totals?.unended ?? 0) > 0))) {
|
|
14054
|
+
if (again && gapPositivelyClosed("recall_reconciliation_unended", { recall: rec, leftOut: [...left, ...late] })) withdrawClosedGap("recall_reconciliation_unended");
|
|
14055
|
+
return 0;
|
|
14056
|
+
}
|
|
14057
|
+
// Every record the gate would name is named by a clause standing now: its own earlier clause goes, so the record
|
|
14058
|
+
// is counted once. The gap is not closed, and the result is not recomputed.
|
|
14059
|
+
if (again && !records.length && unended.length < (rec?.unended ?? []).length) replaceClauses("recall_reconciliation_unended", { event: "guard-clause-named-elsewhere" });
|
|
14060
|
+
if (!records.length || !(weighed > 0)) return 0;
|
|
14061
|
+
const sample = [...unended.slice(0, 4).map((u) => `${u.mark_text} (${u.record_id})`), ...left.slice(0, 2), ...late.slice(0, 2)].join("; ");
|
|
14062
|
+
deliverAndClamp({
|
|
14063
|
+
event: "recall-reconciliation-unended-delivered",
|
|
14064
|
+
defect: `recall_reconciliation_unended:${records.length}`,
|
|
14065
|
+
detail: { positions: unended.length, records: records.length, candidates: base, refused: left.length, late: late.length, ...(joint ? { stated_with_duty: joint } : {}) },
|
|
14066
|
+
reason: `recall_reconciliation_unended:${records.length} of ${weighed} record(s) — retrieved and screened in-scope-live on the dominant element but ending nowhere a reader can see (no finding row, no drop row, no counted crowd ruling) after the run's recovery${left.length ? `, ${left.length} of them found by a unit's re-run and left out of a band its readers could not have been dispatched over` : ""}${late.length ? `, ${late.length} of them reaching the band after placement's last pass, whose pass to assess them did not complete` : ""}: ${sample}. The report ships with these named rather than being withheld.${joint ? ` Stated with the writer's duty, once, as ${joint.n} of ${joint.m} record(s).` : ""}`,
|
|
14067
|
+
again,
|
|
14068
|
+
clause: clauseForDefect("recall_reconciliation_unended", records.length, weighed),
|
|
14069
|
+
statedElsewhere: Boolean(joint),
|
|
14070
|
+
});
|
|
14071
|
+
return records.length;
|
|
14072
|
+
};
|
|
13061
14073
|
// ── — THE DECLINATION DUTY, CHECKED AGAINST THE DELIVERED DOCUMENT ────────────────────────
|
|
13062
14074
|
//
|
|
13063
14075
|
// A SECOND NET OVER ONE REACHABLE PATH, and it exists because I argued it could not fire and was
|
|
@@ -13070,10 +14082,19 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13070
14082
|
// boundary ever saw a call about the records added since, because the only call ordered against them
|
|
13071
14083
|
// was the one that was refused.
|
|
13072
14084
|
//
|
|
14085
|
+
// JUDGED AGAIN after the delivery check repeats placement or re-runs the writer, with `again`: a gap that remains
|
|
14086
|
+
// replaces its earlier clause with the current count, and a gap positively closed withdraws it (withdrawClosedGap).
|
|
14087
|
+
//
|
|
13073
14088
|
// NO ERA STAMP, and it needs none: an archived run carries no declination spec, so `owed` is null,
|
|
13074
14089
|
// the reconcile is `computable:false` and this never blocks. Archive compatibility by construction.
|
|
13075
|
-
{
|
|
14090
|
+
const clampOnTheWritersDuty = ({ again = false } = {}) => {
|
|
13076
14091
|
const duty = synthesisDutyForRun(run.runDir);
|
|
14092
|
+
// THE TWO GAPS STATED ONCE when both name records: the approved sentence, with each record counted once out of the
|
|
14093
|
+
// records either required to be assessed (ruled 2026-10-19). The run record keeps each gap's own counts.
|
|
14094
|
+
const gap = recallGapNow(duty);
|
|
14095
|
+
const joint = duty?.computable === true && duty.unaccounted.length
|
|
14096
|
+
? jointUnaccounted({ dutyIds: duty.unaccounted.map((u) => u.uri), owedIds: owedUrisFor(run.runDir), recallIds: gap.records, weighedIds: gap.population })
|
|
14097
|
+
: null;
|
|
13077
14098
|
if (duty?.computable === true && duty.unaccounted.length) {
|
|
13078
14099
|
const sample = duty.unaccounted.slice(0, 4).map((u) => `${u.mark ? `${u.mark} ` : ""}(${u.uri})`).join("; ");
|
|
13079
14100
|
// DELIVERS AND CLAMPS. It used to throw. The defect is real and stays
|
|
@@ -13083,18 +14104,48 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13083
14104
|
event: "synthesis-duty-undischarged",
|
|
13084
14105
|
defect: `synthesis_unaccounted_delivered:${duty.unaccounted.length}`,
|
|
13085
14106
|
detail: { unaccounted: duty.unaccounted.length, owed: duty.totals.owed, uris: duty.unaccounted.slice(0, 6).map((u) => u.uri) },
|
|
13086
|
-
reason: `synthesis_unaccounted_delivered:${duty.unaccounted.length} of ${duty.totals.owed} record(s) reached the findings surface and the delivered document accounts for none of them — neither a finding that names them nor a declination with a ground: ${sample}${duty.unaccounted.length > 4 ? " …" : ""}. The report ships with this named rather than being withheld; these records are open points a reader must weigh
|
|
14107
|
+
reason: `synthesis_unaccounted_delivered:${duty.unaccounted.length} of ${duty.totals.owed} record(s) reached the findings surface and the delivered document accounts for none of them — neither a finding that names them nor a declination with a ground: ${sample}${duty.unaccounted.length > 4 ? " …" : ""}. The report ships with this named rather than being withheld; these records are open points a reader must weigh.${joint ? ` Stated with the recall gate's records, once, as ${joint.n} of ${joint.m} record(s).` : ""}`,
|
|
14108
|
+
again,
|
|
13087
14109
|
// — the READER's sentence: what is open, in a lawyer's nouns. Counts survive;
|
|
13088
14110
|
// the token, the record ids and the engine's nouns stay in `reason` and the run record.
|
|
13089
14111
|
// COMPOSED BY THE AUTHORITY, NOT SPELLED HERE. The same sentence has to be reachable from a
|
|
13090
14112
|
// run that stored no clause, where these two objects are long gone and only the reason's own
|
|
13091
14113
|
// counts survive; `terminal-clamp.mjs` composes it from the counts either way, so the fresh
|
|
13092
14114
|
// run and the republished archive cannot drift apart.
|
|
13093
|
-
clause: clauseForDefect("synthesis_unaccounted_delivered",
|
|
14115
|
+
clause: joint ? clauseForDefect("synthesis_unaccounted_delivered", joint.n, joint.m)
|
|
14116
|
+
: clauseForDefect("synthesis_unaccounted_delivered", duty.unaccounted.length, duty.totals.owed),
|
|
13094
14117
|
});
|
|
13095
|
-
}
|
|
13096
|
-
|
|
13097
|
-
|
|
14118
|
+
} else if (again && gapPositivelyClosed("synthesis_unaccounted_delivered", { duty })) withdrawClosedGap("synthesis_unaccounted_delivered");
|
|
14119
|
+
const recall = clampOnTheRecallGate(duty, { again, gap, joint }); // the recall gate's gap, beside the writer's duty
|
|
14120
|
+
return { duty, recall };
|
|
14121
|
+
};
|
|
14122
|
+
// ── — THE FLOOR DUTY, DISCLOSED AND CLAMPED AT DELIVERY (it BLOCKED until) ───
|
|
14123
|
+
//
|
|
14124
|
+
// The floors are placement-inquiry's one UNCAPPED obligation — every live in-class identical or
|
|
14125
|
+
// near-identical record, listed individually — and the duty has been measured and never enforced.
|
|
14126
|
+
// Measured on two delivered runs before it was even derived: 45 of 207 floors on one and 99 of 225
|
|
14127
|
+
// on the next never reached the placement form, every one a LIVE filing.
|
|
14128
|
+
//
|
|
14129
|
+
// ARMED BY THE STAMP, NOT BY THE ARTIFACT'S PRESENCE. Across the 24 run directories on the test box,
|
|
14130
|
+
// 19 carry no floor-duty artifact at all — and on this box "absent" means a knockout run far more
|
|
14131
|
+
// often than an old one, so absence cannot be read as either "clean" or "old". The stamp is written
|
|
14132
|
+
// only by a placement pass that actually ran under the code carrying the order.
|
|
14133
|
+
//
|
|
14134
|
+
// IT COUNTS THE SEAT'S OWN SHARE. A floor row the band gave no record id is undischarged and
|
|
14135
|
+
// UNANSWERABLE — no seat behaviour could have closed it, and the repair it implies can be performed
|
|
14136
|
+
// by nobody. Blocking on it would fail a run for a band defect under a rule about seat behaviour, so
|
|
14137
|
+
// it is disclosed in the message and excluded from the count that blocks. That is a stated limit,
|
|
14138
|
+
// not an oversight: if the unanswerable count is ever the large one, it wants its own issue against
|
|
14139
|
+
// the band rather than a wider floor here.
|
|
14140
|
+
//
|
|
14141
|
+
// NO REPAIR RUNG, deliberately and on a measurement (ruling relayed 2026-08-27). Every
|
|
14142
|
+
// registered repair composer belongs to another stage; placement has none, and building one is a
|
|
14143
|
+
// larger piece of work than this floor. The blast radius said it could wait: on the day this was
|
|
14144
|
+
// written today's run discharged all 60 of its floors, and the only two runs with undischarged rows
|
|
14145
|
+
// carried 5 and 9 out of 222 and 352. A rung is a follow-up, never a precondition.
|
|
14146
|
+
// THE FLOOR DUTY'S CLAMP, callable: applied where the terminal checks run, and again to a placement pass the
|
|
14147
|
+
// delivery gate repeats, whose account the report is then rebuilt from. Returns the block it clamped on, or null.
|
|
14148
|
+
const clampOnTheFloorDuty = ({ again = false } = {}) => {
|
|
13098
14149
|
// THE PREDICATE IS `floorDutyBlock`, called not copied — it lives in floor-duty.mjs so the arms can
|
|
13099
14150
|
// DRIVE it. Inline here it could only be pinned by source text, and a source-text pin cannot tell
|
|
13100
14151
|
// an armed check from one somebody disarmed while leaving its words in place.
|
|
@@ -13113,6 +14164,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13113
14164
|
+ `${block.named_without_ground ? ` (${block.named_without_ground} named with no ground)` : ""}`
|
|
13114
14165
|
+ `${block.unanswerable ? ` — a further ${block.unanswerable} floor row(s) carry no record id at all and are excluded from this count: the band gave the seat nothing to name, so those are a band defect and not this seat's to answer` : ""}`
|
|
13115
14166
|
+ `. The report ships with these named rather than being withheld.`,
|
|
14167
|
+
again,
|
|
13116
14168
|
// — the READER's sentence. "Floor row" is an engine noun; what the fact IS for a
|
|
13117
14169
|
// lawyer: live registrations identical or near-identical to the mark that the report does not
|
|
13118
14170
|
// individually address. Counts survive; token, ids and engine nouns stay in `reason`.
|
|
@@ -13120,7 +14172,14 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13120
14172
|
// gives: a republished pre-split run has to reach the same sentence from the counts alone.
|
|
13121
14173
|
clause: clauseForDefect("floor_duty_undischarged", block.undischarged, block.floors),
|
|
13122
14174
|
});
|
|
13123
|
-
}
|
|
14175
|
+
} else if (again && gapPositivelyClosed("floor_duty_undischarged", { floorClosed: floorDutyClosed(safeReadJson(P.floorDuty), { armed: floorDutyArmed(run.runDir) }) }))
|
|
14176
|
+
withdrawClosedGap("floor_duty_undischarged");
|
|
14177
|
+
return block;
|
|
14178
|
+
};
|
|
14179
|
+
{
|
|
14180
|
+
clampOnTheWritersDuty(); // the writer's duty and, beside it, the recall gate (defined above the floor's block)
|
|
14181
|
+
|
|
14182
|
+
clampOnTheFloorDuty();
|
|
13124
14183
|
}
|
|
13125
14184
|
|
|
13126
14185
|
// PR-3 (report voice) — the factual open-state clause per reason, INDEX-ALIGNED with clampReasons.
|
|
@@ -13350,7 +14409,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13350
14409
|
// MANDATORY now (was best-effort): the sidecar IS the single authority; a failed write is a run
|
|
13351
14410
|
// failure, never a silent fallback to divergent labels. Re-invoked after findings consolidation
|
|
13352
14411
|
// (the delivery block) so the sidecar always reflects the FINAL findings set.
|
|
13353
|
-
const
|
|
14412
|
+
const composeVerdictSidecar = () => {
|
|
13354
14413
|
let findingsArr = [], sv = 1, actionsArr = null, actionsDropped = [];
|
|
13355
14414
|
try {
|
|
13356
14415
|
const doc = parseFindingsJsonLenient(readFileSync(P.findings, "utf8"));
|
|
@@ -13447,10 +14506,16 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13447
14506
|
// consumer keys on (never the statement's wording — the magic-string coupling is retired).
|
|
13448
14507
|
const statement = riskStatement({ tier: derived.tier, verdict, reasons: reasonsOut, clauses: clausesOut,
|
|
13449
14508
|
basis: isRegisterOnly(ctx.searchPolicy) ? "register-only" : null });
|
|
13450
|
-
|
|
13451
|
-
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses: clausesOut, kinds: kindsOut,
|
|
14509
|
+
return { derived, statement, doc: { ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses: clausesOut, kinds: kindsOut,
|
|
13452
14510
|
tier: derived.tier, badge: derived.badge, gaugeIndex: derived.gaugeIndex, maxComposite: derived.maxComposite,
|
|
13453
|
-
band: derived.band ?? null, statement, stance: verdictStance(verdict) }
|
|
14511
|
+
band: derived.band ?? null, statement, stance: verdictStance(verdict) } };
|
|
14512
|
+
};
|
|
14513
|
+
// The label, composed above and written here. Composing is apart so the delivery check can show the stages it re-runs
|
|
14514
|
+
// the label it could not yet save.
|
|
14515
|
+
const writeVerdictSidecar = () => {
|
|
14516
|
+
const { derived, statement, doc } = composeVerdictSidecar();
|
|
14517
|
+
const tmp = driverDir(run.runDir, "verdict.json.tmp");
|
|
14518
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2));
|
|
13454
14519
|
renameSync(tmp, driverDir(run.runDir, "verdict.json"));
|
|
13455
14520
|
// — THE STATUS RECORD CARRIES THE BAND AND THE SENTENCE, so a reader of the run does not
|
|
13456
14521
|
// have to reach for `verdict` to say something about the outcome. `verdict` is the gate's decision
|
|
@@ -13460,6 +14525,26 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13460
14525
|
writeRunStatus(ctx, { tier: derived.tier ?? null, statement: statement ?? null });
|
|
13461
14526
|
return derived;
|
|
13462
14527
|
};
|
|
14528
|
+
// THE LABEL, SAVED AGAIN IN THE DELIVERY CHECK, WITHOUT ENDING THE RUN (2026-10-19). After a clamp there the label
|
|
14529
|
+
// has to reach every surface the report is published from, and they all read it from the one file. A save that
|
|
14530
|
+
// fails is tried again. One that still fails is owed: the stages the check re-runs are shown the label as composed,
|
|
14531
|
+
// and the save is made once more before anything is published, so no surface is built from the label settled
|
|
14532
|
+
// before the clamp. Returns whether it is saved.
|
|
14533
|
+
let labelOwed = null;
|
|
14534
|
+
let rankingBeforeRepeat = null; // where each record stood before the delivery check repeated placement
|
|
14535
|
+
const saveTheLabel = (why) => {
|
|
14536
|
+
const saved = saveWithRetries(() => writeVerdictSidecar());
|
|
14537
|
+
if (saved.ok) {
|
|
14538
|
+
if (saved.attempts > 1) runLog(run.runDir, { event: "verdict-write-retried", why, attempts: saved.attempts });
|
|
14539
|
+
try { displayVerdict = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); } catch { /* the label is re-read at reassembly */ }
|
|
14540
|
+
return true;
|
|
14541
|
+
}
|
|
14542
|
+
labelOwed = labelOwed ?? why;
|
|
14543
|
+
runLog(run.runDir, { event: "verdict-write-deferred", why, attempts: saved.attempts, fail: String(saved.error?.message ?? saved.error).slice(0, 160) });
|
|
14544
|
+
note(`delivery: the label could not be saved ${why} (${String(saved.error?.message ?? saved.error).slice(0, 100)}) — it is saved again before the report is published`);
|
|
14545
|
+
try { displayVerdict = composeVerdictSidecar().doc; } catch { /* composed again when it is saved */ }
|
|
14546
|
+
return false;
|
|
14547
|
+
};
|
|
13463
14548
|
try { writeVerdictSidecar(); }
|
|
13464
14549
|
catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
|
|
13465
14550
|
|
|
@@ -13524,6 +14609,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13524
14609
|
reviewLedger.record("reviewer-degenerate-reask", run.codename, rr.ok ? "ok" : `failed: ${rr.fail}`, { max: degenerateReaskMax });
|
|
13525
14610
|
if (rr.ok) {
|
|
13526
14611
|
verdict = parseVerdict(readReview()) ?? "BLOCKING";
|
|
14612
|
+
assessed = verdict; // the re-asked review is the assessment now
|
|
13527
14613
|
runLog(run.runDir, { event: "verdict-3", verdict, trigger: "degenerate-reask" });
|
|
13528
14614
|
writeRunStatus(ctx, signoffPatch(verdict));
|
|
13529
14615
|
applyCoverageFloor(); // a re-asked CLEAR passes the same deliver-conditional floor
|
|
@@ -13632,12 +14718,19 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13632
14718
|
catch (e2) { throw new StageFailure("verdict", `verdict sidecar refresh failed after consolidation: ${String(e2.message).slice(0, 120)}`); }
|
|
13633
14719
|
}
|
|
13634
14720
|
};
|
|
14721
|
+
// THE RECORD THE CARDS ARE BUILT ON CARRIES EVERY DEMOTION. The derivation settled the record after the first
|
|
14722
|
+
// synthesis save; a corrective pass saves again before this seam, and a whole re-send brings back every stamp it had
|
|
14723
|
+
// demoted. Built on those, every card whose finding carried one read as stale after the derivation before delivery
|
|
14724
|
+
// demoted them again, and was rebuilt with the overview for a finding nothing had changed. Settled here, once more,
|
|
14725
|
+
// before the overview and the cards; idempotent on a record that already carries every demotion.
|
|
14726
|
+
settleDerivedBases(ctx, "seam");
|
|
13635
14727
|
await seamWrites();
|
|
13636
14728
|
// Every write above is idempotent: each injection skips a row the record already carries, the stamp
|
|
13637
14729
|
// replaces its rows, and the fold leaves a folded record as it is.
|
|
13638
14730
|
ctx.afterSynthesisSave = async () => {
|
|
13639
14731
|
note(`[seam] a synthesis save after the seam rewrote findings.json — re-applying the seam's writes`);
|
|
13640
14732
|
runLog(run.runDir, { event: "seam-reapplied", after: "synthesis-save" });
|
|
14733
|
+
rewriteFromBeforeTheSeam(ctx); // the basis derivation and the deadline enrichment, as the pass made them
|
|
13641
14734
|
await seamWrites();
|
|
13642
14735
|
};
|
|
13643
14736
|
// wp50/wi2 — the overview PROSE must speak the derived tier (VENZY: caption said "High risk" while
|
|
@@ -13703,13 +14796,11 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13703
14796
|
// plausible zero, and a zero here means every floor row quietly loses the full card the owner made
|
|
13704
14797
|
// non-negotiable. An absent band-shape yields an empty map and every finding reads as holding no
|
|
13705
14798
|
// floor — which is what it did before this existed.
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
const
|
|
13710
|
-
const cardOrdinals
|
|
13711
|
-
// The remainder, in the same order, for the short-entry and grouped-row render below.
|
|
13712
|
-
const entryOrdinals = proseOrdinals.filter((ord) => formOf(byOrdinal.get(ord)) !== "full");
|
|
14799
|
+
// The plan is made in one place (planWriteUps), the same way here and wherever a later save changes the record:
|
|
14800
|
+
// the full cards in the record's order, then the remainder, in the same order, for the short-entry and grouped-row
|
|
14801
|
+
// render below.
|
|
14802
|
+
const writeUps = planWriteUps(findingsForCards, ctx.depth, safeReadJson(P.bandShape));
|
|
14803
|
+
const { cardOrdinals, entryOrdinals } = writeUps;
|
|
13713
14804
|
// §9 — WRITTEN ON EVERY GRADED PRODUCT, INCLUDING ONE WHOSE ENTRY COUNT IS 0. The old gate was
|
|
13714
14805
|
// `entryOrdinals.length`, which conflated two different empties: one country is empty BY CONSTRUCTION
|
|
13715
14806
|
// (every form is `full`, so the row would report the product's own definition back), while a graded run
|
|
@@ -13780,17 +14871,11 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13780
14871
|
// The bodies are written to the SAME card files a dispatched card would use, so `assembleReportMd`
|
|
13781
14872
|
// frames them with the same composer and the ordinal binding is untouched. What they never get is a
|
|
13782
14873
|
// model turn — the whole saving.
|
|
13783
|
-
const entryFindings
|
|
13784
|
-
|
|
13785
|
-
const { groups: entryGroups, singles: entrySingles } = groupEntries(entryFindings, { byRight });
|
|
13786
|
-
for (const f of entrySingles) {
|
|
13787
|
-
try { writeFileSync(P.reportCard(String(f.ordinal)), `${shortEntryBody(f)}\n`); }
|
|
13788
|
-
catch (e) { note(`short entry ${f.ordinal} not written (${String(e?.message ?? e).slice(0, 80)}) — it renders structured-only, as an absent card always has`); }
|
|
13789
|
-
}
|
|
14874
|
+
const { entryFindings, byRight, entrySingles } = writeUps;
|
|
14875
|
+
writeShortEntries(P, entrySingles);
|
|
13790
14876
|
// Assembly sees full cards AND short entries; the DISPATCH set and the freshness stage list stay
|
|
13791
14877
|
// `cardOrdinals`, because a short entry has no stage and never had one.
|
|
13792
|
-
const assembledOrdinals
|
|
13793
|
-
const groupedMembers = entryGroups.flatMap((g) => g.findings);
|
|
14878
|
+
const { assembledOrdinals, groupedMembers } = writeUps;
|
|
13794
14879
|
if (entryFindings.length)
|
|
13795
14880
|
note(`write-up forms: ${cardOrdinals.length} full card(s), ${entrySingles.length} short entr(y/ies), ${groupedMembers.length} grouped line(s)`);
|
|
13796
14881
|
assembleReportMd(P, findingsForCards, assembledOrdinals, { grouped: groupedMembers, byRight });
|
|
@@ -13912,6 +14997,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13912
14997
|
// A2 — full audit: DETERMINISTIC CODE (parse the spine tables → audit.md, count-guarded). Replaces the LLM
|
|
13913
14998
|
// audit-emit (output varied 47↔69 on identical input). NON-FATAL: if it fails, the report still ships (publish
|
|
13914
14999
|
// renders the report with no xlsx when audit.md is absent), and the audit self-recovers on a re-run.
|
|
15000
|
+
// `rebuildTheDeliveredRecord` is set by the block once the audit is written: the delivery check calls it after a repair.
|
|
15001
|
+
let rebuildTheDeliveredRecord = null;
|
|
15002
|
+
let auditWrittenThisPass = false, earlierPairKept = null;
|
|
13915
15003
|
try {
|
|
13916
15004
|
// One resolution per claim: hand the builder the run's OWN dispositions so a withdrawn conflict reads
|
|
13917
15005
|
// as withdrawn on the audit surface too (it already does on report + client summary). Best-effort —
|
|
@@ -13932,6 +15020,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13932
15020
|
// mint below, the predelivery lint's `recordCarry` arms) reads THIS artifact rather than deriving
|
|
13933
15021
|
// its own: two derivations from different inputs is what let the doubt ledger be roughly right
|
|
13934
15022
|
// while the artifact and every lint arm were inverted.
|
|
15023
|
+
// A resumed pass writes both again: what the earlier pass wrote is kept under _history first.
|
|
15024
|
+
earlierPairKept = keepDeliveredRecordAsHistory(P, "earlier-pass");
|
|
13935
15025
|
deriveRecordCarry(ctx, "publish", { findings: auditRunFindings ?? [] });
|
|
13936
15026
|
// ── — AND NOW THE JOIN BETWEEN THE TWO ARTIFACTS THAT WERE EACH RIGHT ────────
|
|
13937
15027
|
//
|
|
@@ -14062,6 +15152,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14062
15152
|
supplementalPlans,
|
|
14063
15153
|
xcheck: readJson(driverDir(run.runDir, "register-xcheck.json")),
|
|
14064
15154
|
recall: recallReceiptForOwnCompany(readJson(driverDir(run.runDir, "register-recall.json"))), // never another company's recall line
|
|
15155
|
+
withheldFamilies: readWithheldFamilies(run.runDir), // the reading turn's reasons, for a frame ask its judgment settled
|
|
14065
15156
|
planExecution: ctx.planExecution ?? readJson(P.planExecution),
|
|
14066
15157
|
}, { ts: askTs });
|
|
14067
15158
|
} catch (e) {
|
|
@@ -14163,12 +15254,13 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14163
15254
|
// a record of what the EXECUTOR did, and nothing after the reopen can retroactively make an
|
|
14164
15255
|
// undispatched term dispatched, so the receipt is the authority and re-deriving would need band
|
|
14165
15256
|
// blocks the audit seam has no reason to hold. A `searched-empty` term mints nothing — it is
|
|
14166
|
-
// answered
|
|
15257
|
+
// answered — and nor does one the reading turn's recorded judgment settled (SETTLED_CLASSES). A run
|
|
15258
|
+
// with no reopen, or a legacy receipt with no term rows, mints nothing.
|
|
14167
15259
|
try {
|
|
14168
15260
|
const fr = JSON.parse(readFileSync(driverDir(run.runDir, "frame-reopen.json"), "utf8"));
|
|
14169
15261
|
if (Array.isArray(fr?.remedy_terms)) {
|
|
14170
15262
|
doubts.push(...mintRemedyAccountingDoubts(
|
|
14171
|
-
{ not_accounted: fr.remedy_terms.filter((r) => !
|
|
15263
|
+
{ not_accounted: fr.remedy_terms.filter((r) => !SETTLED_CLASSES.includes(r?.class)) },
|
|
14172
15264
|
{ sourceName: "_driver/frame-reopen.json" },
|
|
14173
15265
|
));
|
|
14174
15266
|
}
|
|
@@ -14223,7 +15315,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14223
15315
|
? openAfterStitch.filter((d) => doubtSel.ids.includes(d.id))
|
|
14224
15316
|
: openAfterStitch;
|
|
14225
15317
|
ctx.openDoubts = dispatchDoubts.map((d) => ({ id: d.id, birth: d.birth, subject: d.subject }));
|
|
14226
|
-
ctx.openAsks = openAsksForStage.map(
|
|
15318
|
+
ctx.openAsks = openAsksForStage.map(openAskForClosure);
|
|
14227
15319
|
// THE SPEC SIDECAR, WRITTEN BEFORE THE DISPATCH. The seat supplies none of its own context:
|
|
14228
15320
|
// the ids it may speak about and the files it may cite are the driver's, so a doubt it was not
|
|
14229
15321
|
// given is refused by name and a file it was not given cannot be expressed at all. Both
|
|
@@ -14326,16 +15418,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14326
15418
|
// _driver/reading-log.jsonl). Passed on any run that derived a band shape — an EMPTY array renders
|
|
14327
15419
|
// the honest zero ("no lookups recorded"), because a band-consuming stage that read nothing is a
|
|
14328
15420
|
// fact the audit must show, not hide. Legacy runs (no shape) pass null → no section, byte-identical.
|
|
14329
|
-
|
|
14330
|
-
if (existsSync(P.bandShape)) {
|
|
14331
|
-
readingLog = [];
|
|
14332
|
-
try {
|
|
14333
|
-
for (const line of readFileSync(P.readingLog, "utf8").split("\n")) {
|
|
14334
|
-
if (!line.trim()) continue;
|
|
14335
|
-
try { readingLog.push(JSON.parse(line)); } catch { /* a torn tail line never kills the audit */ }
|
|
14336
|
-
}
|
|
14337
|
-
} catch { /* no lookups recorded — the empty array IS the disclosure */ }
|
|
14338
|
-
}
|
|
15421
|
+
const readingLog = readReadingLog(P);
|
|
14339
15422
|
// item 33 — hand the builder the machine grid so the common-law search log is derived from the
|
|
14340
15423
|
// tool-written per-term cells rather than from a heading the tagger has to match. Absent (pre-grid
|
|
14341
15424
|
// archive, register-only run) it falls back to the prose tables, which is honest: those runs have
|
|
@@ -14377,13 +15460,25 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14377
15460
|
runLog(run.runDir, { event: "register-presence-skipped", reason: "no register-named-band.json" });
|
|
14378
15461
|
}
|
|
14379
15462
|
const a = buildAuditMd(registerFindingsText, commonLawText,
|
|
14380
|
-
{ findings: auditRunFindings, doubts: runDoubts, asks: runAsks, readingLog, commonLawGrid, doubtTruncations, registerPresence
|
|
15463
|
+
{ findings: auditRunFindings, doubts: runDoubts, asks: runAsks, readingLog, commonLawGrid, doubtTruncations, registerPresence,
|
|
15464
|
+
placementRows: placementAuditRows(P) });
|
|
14381
15465
|
if (!a.counts.findings) throw new Error("0 findings parsed from the spine tables"); // count-guard
|
|
14382
15466
|
writeFileSync(P.audit, a.md);
|
|
15467
|
+
auditWrittenThisPass = true;
|
|
14383
15468
|
runLog(run.runDir, { event: "audit", ...a.counts }); // — counts now carry doubtsOmitted
|
|
15469
|
+
// The trace and the Audit Trail, written again over a repair (`rebuildDeliveredRecord`), from the inputs this
|
|
15470
|
+
// pass built them from; the doubts minted from the trace follow the trace written again.
|
|
15471
|
+
rebuildTheDeliveredRecord = (trigger) => rebuildDeliveredRecord(ctx,
|
|
15472
|
+
{ commonLawText, doubts: runDoubts, asks: runAsks, commonLawGrid, doubtTruncations, registerPresence }, trigger);
|
|
14384
15473
|
note(`audit (code): ${a.counts.findings} findings, ${a.counts.negatives} negatives, ${a.counts.audit} audit-trail${a.counts.withdrawnStamped ? `, ${a.counts.withdrawnStamped} withdrawn-stamped` : ""}${a.counts.resolutionStamped ? `, ${a.counts.resolutionStamped} resolution-stamped` : ""}${runDoubts ? `, doubts ${a.counts.doubtsSettled} settled / ${a.counts.doubtsOpen} open` : ""}${a.counts.presenceRows != null ? `, register presence ${a.counts.presenceRows} row(s)` : ""}`);
|
|
14385
15474
|
} catch (e) {
|
|
14386
15475
|
note(`audit build failed (NON-FATAL — report still ships, audit will regenerate on re-run): ${e.message}`);
|
|
15476
|
+
// A findings record with no finding row trips the count guard, and on a clean run it can: that is recorded apart,
|
|
15477
|
+
// as a note on the run's content, and only a build that broke is recorded as a failure.
|
|
15478
|
+
const noFindings = /^0 findings parsed from the spine tables/.test(String(e?.message ?? ""));
|
|
15479
|
+
runLog(run.runDir, { event: noFindings ? "audit-not-built-no-findings" : "audit-build-failed", reason: String(e?.message ?? e).slice(0, 160) });
|
|
15480
|
+
// An audit an earlier pass wrote describes that pass, not this one: the report ships without it, kept under _history.
|
|
15481
|
+
if (!auditWrittenThisPass) withdrawSupersededAudit(P, "audit-build-failed", { keep: !earlierPairKept });
|
|
14387
15482
|
}
|
|
14388
15483
|
|
|
14389
15484
|
// ── Spec A1 + A2: pre-delivery verification — pure code, before ANYTHING outward-facing. Per the
|
|
@@ -14667,6 +15762,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14667
15762
|
// (composeEmailHtml opts.verdict, from _driver/verdict.json), the pre-delivery lint below, and
|
|
14668
15763
|
// the client gate. On the R2 evidence run this ladder was where the cost landed: three
|
|
14669
15764
|
// byte-identical failures before it exhausted.
|
|
15765
|
+
// The writer's accepted record as it stood just before the delivery-check repair, kept only when that
|
|
15766
|
+
// repair runs: the review after the repair is handed what changed between the two (repair-scope.mjs).
|
|
15767
|
+
let repairBaseKept = false;
|
|
14670
15768
|
if (lint.failures.length) {
|
|
14671
15769
|
runLog(run.runDir, { event: "predelivery-lint-failed", failures: lint.failures.map((f) => f.id) });
|
|
14672
15770
|
const bySurface = (s) => lint.failures.filter((f) => f.surface === s || (s === "report" && f.surface === "all"));
|
|
@@ -14684,13 +15782,13 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14684
15782
|
// `Write` and `Edit` out of its grant. Same class as the ten anchor sites and the max-tokens
|
|
14685
15783
|
// corrective before it: a retirement reaches the first-attempt surfaces and misses a correction
|
|
14686
15784
|
// surface. Keyed on the ARTIFACT rather than on the label, so conversions 5 and 6 inherit it.
|
|
14687
|
-
const redo = async (label, failures, key, model, thinking, file) => {
|
|
15785
|
+
const redo = async (label, failures, key, model, thinking, file, more = {}) => {
|
|
14688
15786
|
if (!failures.length || !key) return;
|
|
14689
15787
|
note(`pre-delivery lint: ${failures.length} failed check(s) on ${label} — one warm named-correction redo`);
|
|
14690
15788
|
const toolWritten = file ? toolWrittenArtifact(file) : null;
|
|
14691
15789
|
const r = await stage(label, ctx, {
|
|
14692
15790
|
force: true, sessionKey: key, model, thinking, trigger: "lint-repair",
|
|
14693
|
-
followup: repairFollowup("*:lint-repair", { label, file, toolWritten, failures })
|
|
15791
|
+
followup: repairFollowup("*:lint-repair", { label, file, toolWritten, failures, ...more })
|
|
14694
15792
|
+ stageCharter(label, ctx.depth, ctx.framework),
|
|
14695
15793
|
});
|
|
14696
15794
|
if (r.ok) repaired.push(label);
|
|
@@ -14705,7 +15803,14 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14705
15803
|
// finding-provenance bleed) re-renders THAT card alone, FRESH (isolated from its own record, so the bleed
|
|
14706
15804
|
// cannot recur); a shell failure (no ordinal) re-runs report-overview. Then re-assemble. Bounded: one pass.
|
|
14707
15805
|
let reassembled = false;
|
|
14708
|
-
|
|
15806
|
+
// Each fault goes to the part that holds it (repair-targets.mjs): a card's to that card, with the detail its
|
|
15807
|
+
// check gives on that card alone; the overview's to the overview; and a fault only the lines composed from the
|
|
15808
|
+
// record hold to the writer's repair below, since the record is the writer's. One no single part holds keeps the
|
|
15809
|
+
// overview's repair.
|
|
15810
|
+
const targets = locateReportFaults(reportFixable, reportPartsOf(P, findingsForCards, writeUps));
|
|
15811
|
+
if (reportFixable.length) runLog(run.runDir, { event: "report-repair-targets", cards: [...targets.cards.keys()],
|
|
15812
|
+
overview: targets.overview.map((f) => f.id), stamped: targets.stamped.map((f) => f.id), unlocated: targets.unlocated.map((f) => f.id) });
|
|
15813
|
+
const badOrdinals = [...targets.cards.keys()];
|
|
14709
15814
|
for (const ord of badOrdinals) {
|
|
14710
15815
|
const f = findingsForCards.find((x) => x.ordinal === ord);
|
|
14711
15816
|
if (!f) continue;
|
|
@@ -14718,7 +15823,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14718
15823
|
// isolation from the contaminated rendering is the entire point — patching the bad card would
|
|
14719
15824
|
// preserve exactly what the re-render exists to discard. The redo() ladder below is the opposite
|
|
14720
15825
|
// case (a warm resume of a session that owns its file) and patches.
|
|
14721
|
-
const why =
|
|
15826
|
+
const why = targets.cards.get(ord).map((x) => x.detail).filter(Boolean);
|
|
14722
15827
|
const rc = await stage("report-card", { ...ctx, axis: String(ord), finding: f }, {
|
|
14723
15828
|
force: true, trigger: "lint-repair",
|
|
14724
15829
|
...(why.length ? { extra: lines(
|
|
@@ -14729,14 +15834,26 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14729
15834
|
});
|
|
14730
15835
|
if (rc.ok) { repaired.push(`report-card:${ord}`); reassembled = true; }
|
|
14731
15836
|
}
|
|
14732
|
-
const shellFixable =
|
|
15837
|
+
const shellFixable = targets.overview;
|
|
14733
15838
|
if (shellFixable.length) { await redo("report-overview", shellFixable, rov.sessionKey, rov.model, rov.thinking, P.reportOverview); if (repaired.includes("report-overview")) reassembled = true; }
|
|
14734
15839
|
// — THE NARRATIVE REDO ROUTE. The same mechanism report-overview already has, re-aimed at
|
|
14735
15840
|
// the seat that wrote narrative.md. Without it a depth check could only FLAG, and a flag on a
|
|
14736
15841
|
// narrative that broke the rules is the run shipping the defect with a note attached. ONE warm
|
|
14737
15842
|
// pass, named violations; a second overrun ships and the lint row is the record.
|
|
14738
|
-
|
|
14739
|
-
|
|
15843
|
+
//
|
|
15844
|
+
// A FAULT IN A LINE COMPOSED FROM THE RECORD (a finding's own sentence, a short entry, a grouped line) is the
|
|
15845
|
+
// writer's too: no card or overview repair can change what the renderer stamps from the record. It joins the
|
|
15846
|
+
// narrative's faults in the writer's one repair, each named by the finding that owns it, in the numbering the
|
|
15847
|
+
// writer's save takes. Its consumers then rebuild, and its change is reviewed, as for any repair of the record.
|
|
15848
|
+
const fold = readFold(run.runDir);
|
|
15849
|
+
const sentenceFixable = targets.stamped.flatMap((f) => (f.parts ?? []).map((x) => ({ ...f, finding: keptModelOrdinal(fold, x.ordinal), detail: x.detail })));
|
|
15850
|
+
const narrativeFixable = [...modelFixable(bySurface("narrative")), ...sentenceFixable];
|
|
15851
|
+
if (narrativeFixable.length && synthesisKey) repairBaseKept = snapshotRepairBase(run.runDir);
|
|
15852
|
+
// A writer fault with no writer session to repair it is not repaired, and the run says so.
|
|
15853
|
+
if (narrativeFixable.length && !synthesisKey)
|
|
15854
|
+
runLog(run.runDir, { event: "lint-redo-skipped", surface: "writer", why: "no-writer-session", failures: narrativeFixable.map((f) => f.id) });
|
|
15855
|
+
if (narrativeFixable.length) await redo("synthesis", narrativeFixable, synthesisKey, synthesisModel, synthesisThinking, P.narrative,
|
|
15856
|
+
{ findings: modelRecordPath(run.runDir, P.findings), narrative: P.narrative });
|
|
14740
15857
|
if (reassembled) assembleReportMd(P, findingsForCards, assembledOrdinals, { grouped: groupedMembers, byRight });
|
|
14741
15858
|
// The client-summary redo arm is retired with the stage (2026-08-01). Its `email`-surface
|
|
14742
15859
|
// failures have not gone unowned: the `:email` reference check now runs over the report-derived
|
|
@@ -14763,16 +15880,35 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14763
15880
|
// lint-repair-tax metric: recording the verdict here means no consumer can read the failure list of
|
|
14764
15881
|
// a stale-blocked run and mistake it for a delivery-quality signal. The receipt itself stays
|
|
14765
15882
|
// ALWAYS-WRITTEN (spec A2 auditor evidence) — blocking a run must not cost us the evidence of why.
|
|
14766
|
-
|
|
15883
|
+
// The cards on the path are the ones the record owes as it stands, read again each time the path is judged: a
|
|
15884
|
+
// synthesis save after the cards can add a finding that is owed one, or withdraw one whose card is owed no longer.
|
|
15885
|
+
const deliveryPathStages = () => [...new Set([
|
|
14767
15886
|
...(ctx.skippedStages ?? []), "register-digest", "synthesis", "narrative-refutation",
|
|
14768
|
-
"report-overview", ...
|
|
15887
|
+
"report-overview", ...owedCardOrdinals(P, ctx.depth).map((o) => `report-card:${o}`),
|
|
14769
15888
|
])];
|
|
14770
15889
|
const deliveryInputsFor = (label) => {
|
|
14771
15890
|
const bare = label.replace(/:.*$/, "");
|
|
14772
15891
|
const axis = label.includes(":") ? label.slice(label.indexOf(":") + 1) : null;
|
|
14773
15892
|
try { return stageInputs(bare, P, { axes: ctx.axes, axis, registerOnly: ctx.registerOnly }); } catch { return []; }
|
|
14774
15893
|
};
|
|
14775
|
-
staleStages = staleOnPath(run.runDir, deliveryPathStages, deliveryInputsFor, { project: projectStageInput })
|
|
15894
|
+
staleStages = [...staleOnPath(run.runDir, deliveryPathStages(), deliveryInputsFor, { project: projectStageInput }),
|
|
15895
|
+
...cardsNeverBuilt(P, ctx.depth, cardOrdinals)]; // a finding a later save added is owed its one card call
|
|
15896
|
+
// A DELIVERY-CHECK REPAIR THAT CHANGED ONLY THE FINDINGS RECORD leaves the narrative's bytes, and so the
|
|
15897
|
+
// reviewer, fresh — the reviewer does not declare findings.json. The change is still content no review has
|
|
15898
|
+
// read, so the reviewer is queued for the review after a repair, marked as the record's change.
|
|
15899
|
+
if (repairBaseKept && !staleStages.some((s2) => s2.label === "narrative-refutation")) {
|
|
15900
|
+
if (recordChangeOwesReview(run.runDir)) {
|
|
15901
|
+
// At the FRONT, so the dependency order below puts it after the writer and ahead of the overview and the
|
|
15902
|
+
// cards, which then rebuild from whatever its correction changes.
|
|
15903
|
+
staleStages.unshift({ label: "narrative-refutation", changed: [{ name: basename(P.findings), via: "repair-record" }] });
|
|
15904
|
+
runLog(run.runDir, { event: "post-repair-review-queued", why: "the repair changed the findings record only" });
|
|
15905
|
+
}
|
|
15906
|
+
}
|
|
15907
|
+
// A RESUME AFTER SUCH A CHANGE WENT UNREVIEWED: its record queues the reviewer again, so this pass reviews it.
|
|
15908
|
+
if (unreadRecordReview(run.runDir) && !staleStages.some((s2) => s2.label === "narrative-refutation")) {
|
|
15909
|
+
staleStages.unshift({ label: "narrative-refutation", changed: [{ name: basename(P.findings), via: "repair-record" }] });
|
|
15910
|
+
runLog(run.runDir, { event: "post-repair-review-queued", why: "a change to the findings record went unreviewed" });
|
|
15911
|
+
}
|
|
14776
15912
|
// ── AD-2 A1 (E2E-R2, ordering) — repair-owned staleness is re-done IN-PASS, never parked. The
|
|
14777
15913
|
// pre-delivery repair can move an input under the tail this pass already built (the observed
|
|
14778
15914
|
// case: the report-overview lint redo rewrote findings.json, staling every report card + the
|
|
@@ -14816,20 +15952,232 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14816
15952
|
if (existsSync(P.caseLaw)) caseLawNow = joinCaseLawProfiles(parseCaseLawProfiles(readFileSync(P.caseLaw, "utf8")), findingsNow);
|
|
14817
15953
|
} catch { /* no case-law layer */ }
|
|
14818
15954
|
let reassemble = false;
|
|
15955
|
+
// THE LATE VERDICT, ONE RULE FOR BOTH REVIEWS THIS LOOP CAN RUN — the whole review and the review after
|
|
15956
|
+
// a delivery-check repair. A late review may only harden the run's verdict, and a softer one is logged,
|
|
15957
|
+
// never adopted. Returns what it found, so the review after a repair can keep the review whose verdict
|
|
15958
|
+
// the run carries in senior-eye-review.md.
|
|
15959
|
+
const adoptLateReview = (reviewNow, stageLabel) => {
|
|
15960
|
+
const { hardened, softened: reviewSays } = lateReviewAgainst(verdict, reviewNow);
|
|
15961
|
+
// A late review harsher than the assessment is a ground of its own: a gap closed later never lifts it.
|
|
15962
|
+
const says = parseVerdict(reviewNow);
|
|
15963
|
+
if ((VERDICT_RANK[says] ?? -1) > (VERDICT_RANK[assessed] ?? -1)) assessed = says;
|
|
15964
|
+
// A REVIEW THAT SOFTENED IS NOT ADOPTED, AND THE RUN SAYS SO. The ratchet below only
|
|
15965
|
+
// tightens, so a re-review that comes back softer leaves the settled verdict in force while
|
|
15966
|
+
// senior-eye-review.md, rewritten by this repair, opens with the softer word. That is the
|
|
15967
|
+
// design; recording nothing was the defect: a reader of the run found two answers and no
|
|
15968
|
+
// line saying which one governs (measured in testing, 2026-09-23: BLOCKING recorded, CONDITIONAL on disk).
|
|
15969
|
+
if (reviewSays) {
|
|
15970
|
+
runLog(run.runDir, { event: "verdict-softening-not-adopted", was: verdict, reviewSays, stage: stageLabel });
|
|
15971
|
+
note(`stale-repair: the reviewer now returns ${reviewSays}; the run keeps ${verdict}, because a late re-review may only harden the verdict — senior-eye-review.md now disagrees with verdict.json, and verdict.json governs`);
|
|
15972
|
+
}
|
|
15973
|
+
if (hardened) {
|
|
15974
|
+
// ── T3a — THE LATE VERDICT IS ADOPTED, NOT THROWN, AND ADOPTION IS THE WHOLE JOB ──
|
|
15975
|
+
//
|
|
15976
|
+
// built the throw here because a late hardening was an input the gate had been
|
|
15977
|
+
// DENIED: the verdict settles thousands of lines earlier, nothing re-reads the review,
|
|
15978
|
+
// so a reviewer refusing to sign DURING this repair "is written to disk and never
|
|
15979
|
+
// heard". Measured on `bf21580e` — the narrative printed a registration date of
|
|
15980
|
+
// 1 March 2011 against a record reading 25 February 2011, the reviewer said so, and
|
|
15981
|
+
// the run delivered anyway.
|
|
15982
|
+
//
|
|
15983
|
+
// DELETING THE THROW WITHOUT A DESTINATION RE-OPENS EXACTLY THAT HOLE, and that is
|
|
15984
|
+
// why this branch is not the same edit as the gate's. The owner's ruling removes the
|
|
15985
|
+
// REFUSAL; it does not say the reviewer stops being heard. So the late verdict is
|
|
15986
|
+
// adopted as the run's own and everything derived from it is rebuilt:
|
|
15987
|
+
//
|
|
15988
|
+
// · `verdict` and status.json — the run's own record
|
|
15989
|
+
// · verdict.json — "the single label authority" (its words)
|
|
15990
|
+
// · `reassemble` — assembleReportMd re-runs, so the open-points
|
|
15991
|
+
// section is built from THIS review, and
|
|
15992
|
+
// applyVerdictFrontMatter re-reads the sidecar
|
|
15993
|
+
// and re-stamps overall_label / overall_badge
|
|
15994
|
+
//
|
|
15995
|
+
// Without the sidecar write the report would ship BADGED WITH THE VERDICT SETTLED
|
|
15996
|
+
// BEFORE THE REVIEWER HARDENED — refused by the reviewer, labelled CONDITIONAL. That
|
|
15997
|
+
// is worse than either behaviour this replaces, and it is `bf21580e`'s shape one
|
|
15998
|
+
// layer up: the reviewer's word on disk and the client's document disagreeing.
|
|
15999
|
+
//
|
|
16000
|
+
// `verdictHardenedTo` still ratchets ONE WAY, so a review that SOFTENED during the
|
|
16001
|
+
// repair still cannot lift a clamp. This branch only ever tightens.
|
|
16002
|
+
runLog(run.runDir, { event: "verdict-hardened-by-repair", was: verdict, now: hardened, stage: stageLabel, adopted: true });
|
|
16003
|
+
note(`stale-repair: the reviewer returned ${hardened} — adopting it as the run's verdict (the one settled earlier was decided against a review this repair has since rewritten) and rebuilding the label from it`);
|
|
16004
|
+
verdict = hardened;
|
|
16005
|
+
writeRunStatus(ctx, signoffPatch(verdict));
|
|
16006
|
+
try { writeVerdictSidecar(); }
|
|
16007
|
+
catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
|
|
16008
|
+
reassemble = true;
|
|
16009
|
+
}
|
|
16010
|
+
return { hardened, softened: reviewSays };
|
|
16011
|
+
};
|
|
16012
|
+
const processedLabels = new Set(); // the labels this loop has re-done, for the re-queue after a correction
|
|
16013
|
+
const requeuedLabels = new Set();
|
|
16014
|
+
const repairFailed = new Map(); // label → why its re-run in this loop did not complete
|
|
16015
|
+
// A CHANGED DECISION IS READ AGAIN IN THIS PASS. A repeated placement pass whose decisions moved leaves the reading
|
|
16016
|
+
// step, the writer and the review stale, and each of them leaves what reads it stale in turn. They are queued here,
|
|
16017
|
+
// in dependency order after the stage that moved them, each re-done at most once more; one already queued runs where
|
|
16018
|
+
// it stands, and a stage stale before the repair for the same reasons is not the repair's. The reviewer gets the whole
|
|
16019
|
+
// review and its one correction, and the writer is judged again against its duty and the recall gate. The pass
|
|
16020
|
+
// then ends with all of them rebuilt from the decision that stands. The guard below used to park the run on them,
|
|
16021
|
+
// and the resume re-ran them, until the owner's ruling of 2026-10-19 that a run is never held. A stage still stale
|
|
16022
|
+
// after its re-run still falls to the guard: no approved sentence states that case yet.
|
|
16023
|
+
const chain = new Set();
|
|
16024
|
+
const staleNow = () => staleOnPath(run.runDir, deliveryPathStages(), deliveryInputsFor, { project: projectStageInput });
|
|
16025
|
+
const queueWhatItLeftStale = (s2, before) => {
|
|
16026
|
+
const at = staleStages.indexOf(s2) + 1;
|
|
16027
|
+
const left = whatTheRepairLeftStale({ before, after: staleNow(), repaired: s2.label,
|
|
16028
|
+
ahead: new Set(staleStages.slice(at).map((x) => x.label)), processed: processedLabels, requeued: requeuedLabels });
|
|
16029
|
+
for (const l of left.chain) chain.add(l);
|
|
16030
|
+
for (const l of left.requeue) requeuedLabels.add(l);
|
|
16031
|
+
if (!left.queue.length) return;
|
|
16032
|
+
const rest = [...staleStages.slice(at), ...left.queue];
|
|
16033
|
+
const order = [...new Set(dependencyOrder(rest.map((x) => x.label), P, { axes: ctx.axes }))]; // each label once
|
|
16034
|
+
staleStages.splice(at, staleStages.length - at, ...order.map((l) => rest.find((x) => x.label === l)).filter(Boolean));
|
|
16035
|
+
runLog(run.runDir, { event: "delivery-stale-requeued", after: s2.label, stages: left.queue.map((x) => x.label) });
|
|
16036
|
+
};
|
|
16037
|
+
// A STAGE STALE ON WHAT PLACEMENT WROTE IS THE CHAIN'S FROM THE START, whoever moved it: the decisions written
|
|
16038
|
+
// again when the shape is derived again, or a late pass whose readers had already run, as much as this check's
|
|
16039
|
+
// own repeat. Its re-run is followed, and a re-run that does not complete delivers.
|
|
16040
|
+
const placementWrote = new Set([...stageOutputs("placement-inquiry", P, { axes: ctx.axes }).map((f) => basename(f)), basename(P.placementDecisions)]);
|
|
16041
|
+
for (const l of stagesStaleOnPlacement(staleStages, placementWrote)) chain.add(l);
|
|
14819
16042
|
for (const s2 of staleStages) {
|
|
16043
|
+
processedLabels.add(s2.label);
|
|
14820
16044
|
if (s2.label === "report-overview") {
|
|
14821
16045
|
const r = await stage("report-overview", { ...ctx, displayVerdict }, { force: true, trigger: "stale-repair" });
|
|
14822
16046
|
if (r.ok) { staleRepaired.push(s2.label); reassemble = true; }
|
|
14823
16047
|
else note(`stale-repair: report-overview re-emit failed (${r.fail}) — stays stale, the guard below decides`);
|
|
16048
|
+
} else if (s2.label === "narrative-refutation"
|
|
16049
|
+
&& (chain.has(s2.label) || postRepairScope(run.runDir, P, s2, { staleRepaired, baseKept: repairBaseKept }).mode !== "whole"
|
|
16050
|
+
|| queuedForTheRecordOnly(s2) || unreadRecordReview(run.runDir))) {
|
|
16051
|
+
// A REVIEWER A CHANGED DECISION QUEUED is given the whole review and its one correction here, as the pass that
|
|
16052
|
+
// re-ran it on a resume gave it a review and a correction.
|
|
16053
|
+
// THE REVIEW AFTER A DELIVERY-CHECK REPAIR (see runPostRepairReview). Reached where the scope is certain, and
|
|
16054
|
+
// where a change to the record, or one an earlier review could not read, is reviewed whole: that review's
|
|
16055
|
+
// flags go to the same one correction. Every other staleness of the reviewer takes the arm below, as before.
|
|
16056
|
+
// A CHANGE NO REVIEW READ IS REVIEWED WHOLE, whatever this pass's own repair changed: the base this pass
|
|
16057
|
+
// kept follows the earlier change, so a narrowed review would read only the later one, and saving the same
|
|
16058
|
+
// values again would settle a stamp over a change nobody read.
|
|
16059
|
+
const unreadOnEntry = unreadRecordReview(run.runDir);
|
|
16060
|
+
const narrowedScope = postRepairScope(run.runDir, P, s2, { staleRepaired, baseKept: repairBaseKept });
|
|
16061
|
+
const scope = unreadOnEntry && narrowedScope.mode !== "whole" ? { mode: "whole", why: "an earlier change went unreviewed" } : narrowedScope;
|
|
16062
|
+
if (scope.mode === "none") {
|
|
16063
|
+
// A FILE WRITTEN AGAIN WITH THE SAME VALUES IS NOT A CHANGE: no review runs, and the reviewer's stamp
|
|
16064
|
+
// takes the rewrite as accounted for, so the delivery guard does not block on it.
|
|
16065
|
+
settleOneShotStamp(run.runDir, "narrative-refutation", [P.narrative], "repair-changed-no-content");
|
|
16066
|
+
runLog(run.runDir, { event: "post-repair-review-not-needed", why: scope.why });
|
|
16067
|
+
note(`[post-repair] the repair saved the same values again — no review needed`);
|
|
16068
|
+
staleRepaired.push(s2.label);
|
|
16069
|
+
continue;
|
|
16070
|
+
}
|
|
16071
|
+
const rerun = () => UPSTREAM_STALE_REPAIR[s2.label](ctx, { displayVerdict });
|
|
16072
|
+
// THE STATE THIS REVIEW READS IS KEPT WHOLE before it runs, so a correction that fails after it has a trusted
|
|
16073
|
+
// state to come back to even where the record itself is lost meanwhile.
|
|
16074
|
+
retainStateBeforeReview(run.runDir, P);
|
|
16075
|
+
// A scope that cannot be narrowed is reviewed whole at once, and its record lists no change it could compare.
|
|
16076
|
+
let review = scope.mode === "whole"
|
|
16077
|
+
// A resume reviews the change its earlier record names, so that record says what the change was.
|
|
16078
|
+
? await runWholeReviewForRecord(ctx, P, run, { ...scope, changed: scope.changed ?? NO_CHANGE,
|
|
16079
|
+
findingOnly: unreadRecordFindingOnly(run.runDir) ?? queuedForTheRecordOnly(s2) }, rerun)
|
|
16080
|
+
: await runPostRepairReview(ctx, P, run, scope);
|
|
16081
|
+
// A narrowed review that could not be read falls back to the whole review, whose points go to the same one
|
|
16082
|
+
// correction below: neither the stamp of a findings-only change nor that of a stage that ends ok on the
|
|
16083
|
+
// earlier review's file can show the change unread. When the whole review cannot be read either, its
|
|
16084
|
+
// record keeps the change no review read; the record holds nothing (see `unreadRecordReview`).
|
|
16085
|
+
if (!review.reviewed && scope.mode !== "whole") review = await runWholeReviewForRecord(ctx, P, run, scope, rerun);
|
|
16086
|
+
if (!review.reviewed) continue;
|
|
16087
|
+
staleRepaired.push(s2.label);
|
|
16088
|
+
reassemble = true;
|
|
16089
|
+
// senior-eye-review.md HOLDS THE REVIEW WHOSE VERDICT THE RUN CARRIES. A narrowed review that hardened
|
|
16090
|
+
// the verdict stays in place, so the open questions print its grounds; otherwise the whole review goes
|
|
16091
|
+
// back, and the narrowed one is kept under _driver/.
|
|
16092
|
+
const { hardened } = adoptLateReview(review.reviewText, s2.label);
|
|
16093
|
+
if (!hardened) review.putBack();
|
|
16094
|
+
if (review.rec.flags.length) {
|
|
16095
|
+
const pass = await runPostRepairCorrection(ctx, P, run, { rec: review.rec, items: review.items, points: review.points,
|
|
16096
|
+
grounds: Boolean(hardened), whole: review.base?.whole === true, synthesisRef: { key: synthesisKey, model: synthesisModel, thinking: synthesisThinking } });
|
|
16097
|
+
// NOTHING REVIEWS THE CORRECTION: the reviewer's stamp takes what it changed as accounted for, and so
|
|
16098
|
+
// does the first cycle's receipt, so a resume re-fires neither.
|
|
16099
|
+
settleOneShotStamp(run.runDir, "narrative-refutation", [P.narrative], "post-repair-fix");
|
|
16100
|
+
try {
|
|
16101
|
+
const at = driverDir(run.runDir, "corrective-cycle.json");
|
|
16102
|
+
let receipt = null;
|
|
16103
|
+
try { receipt = JSON.parse(readFileSync(at, "utf8")); } catch { receipt = null; }
|
|
16104
|
+
atomicWrite(at, JSON.stringify({ ...(receipt ?? { entryVerdict: verdict }), ts: new Date().toISOString(), verdict,
|
|
16105
|
+
shas: { review: shaOf(P.seniorEyeReview), narrative: shaOf(P.narrative) }, settledBy: "post-repair-cycle" }, null, 2) + "\n");
|
|
16106
|
+
} catch (e) { note(`[post-repair] the cycle receipt could not be rewritten (${String(e?.message ?? e).slice(0, 100)}) — a resume may re-fire the first correction`); }
|
|
16107
|
+
// ITS CONSUMERS REBUILD FROM WHAT IT WROTE: re-read the findings, and queue the overview and any card it
|
|
16108
|
+
// left stale that this loop has not queued already.
|
|
16109
|
+
readNow = readFindingsForReport(P);
|
|
16110
|
+
findingsNow = readNow.findings;
|
|
16111
|
+
ordinalsNow = fullProseOrdinals(findingsNow);
|
|
16112
|
+
try {
|
|
16113
|
+
if (existsSync(P.caseLaw)) caseLawNow = joinCaseLawProfiles(parseCaseLawProfiles(readFileSync(P.caseLaw, "utf8")), findingsNow);
|
|
16114
|
+
} catch { /* no case-law layer */ }
|
|
16115
|
+
// After a correction, and after a rollback that rebuilt the record from a save, what reads the record is
|
|
16116
|
+
// rebuilt from it; a rollback that put the same bytes back leaves nothing stale, and queues nothing.
|
|
16117
|
+
if (pass.ok || pass.rollback) {
|
|
16118
|
+
// A label not queued yet is appended; one this loop already re-done is re-queued ONCE, so the
|
|
16119
|
+
// pass stays bounded and still ends with every surface rebuilt from the corrected record.
|
|
16120
|
+
const queued = new Set(staleStages.map((x) => x.label));
|
|
16121
|
+
// A finding the correction's save added is owed its one card call, as after any later save.
|
|
16122
|
+
for (const x of [...staleOnPath(run.runDir, deliveryPathStages(), deliveryInputsFor, { project: projectStageInput }),
|
|
16123
|
+
...cardsNeverBuilt(P, ctx.depth, cardOrdinals)]) {
|
|
16124
|
+
if (!(x.label === "report-overview" || x.label.startsWith("report-card:"))) continue;
|
|
16125
|
+
if (!queued.has(x.label)) { staleStages.push(x); queued.add(x.label); }
|
|
16126
|
+
else if (processedLabels.has(x.label) && !requeuedLabels.has(x.label)) { requeuedLabels.add(x.label); staleStages.push(x); }
|
|
16127
|
+
}
|
|
16128
|
+
}
|
|
16129
|
+
// THE WRITER'S DUTY, JUDGED AGAIN ONCE THE CORRECTION HAS SETTLED, rollback included. The terminal guards judged
|
|
16130
|
+
// it on the record before this correction. A correction that rolled back restores a record that the duty as it
|
|
16131
|
+
// now stands may not be met by: a save restored as accepted work, or records owed since the save was made. The
|
|
16132
|
+
// approved clause names what it leaves unaccounted, and a gap it closed withdraws its clause, as after the
|
|
16133
|
+
// writer's re-run in this loop; the label is saved again before anything is published.
|
|
16134
|
+
{
|
|
16135
|
+
const was = verdict, stood = JSON.stringify(clampReasons);
|
|
16136
|
+
const judged = clampOnTheWritersDuty({ again: true });
|
|
16137
|
+
if (restoredClear && verdict === "CLEAR") applyCoverageFloor();
|
|
16138
|
+
if (JSON.stringify(clampReasons) !== stood || verdict !== was) saveTheLabel("after the correction after a post-repair review");
|
|
16139
|
+
runLog(run.runDir, { event: "writer-duty-after-correction", was, now: verdict, rolledBack: Boolean(pass.rollback),
|
|
16140
|
+
owed: judged.duty?.computable === true ? judged.duty.totals?.owed ?? null : null,
|
|
16141
|
+
unaccounted: judged.duty?.computable === true ? judged.duty.unaccounted.length : null, recall: judged.recall });
|
|
16142
|
+
}
|
|
16143
|
+
}
|
|
16144
|
+
continue;
|
|
14824
16145
|
} else if (UPSTREAM_STALE_REPAIR[s2.label]) {
|
|
14825
16146
|
// The upstream arm (item 15a). Each of these re-runs the SAME stage the pipeline would have
|
|
14826
16147
|
// run, forced, with a `stale-repair` trigger so the telemetry says why. Dependency order put
|
|
14827
16148
|
// it ahead of anything that reads what it writes, so the tail arms below rebuild from the
|
|
14828
16149
|
// repaired artifact rather than from the one that moved.
|
|
16150
|
+
const reviewSeq0 = s2.label === "narrative-refutation" ? nextRefutationSeq(run.runDir) : null;
|
|
16151
|
+
const staleBefore = s2.label === "placement-inquiry" || chain.has(s2.label) ? staleNow() : null; // what was stale before this one
|
|
16152
|
+
if (s2.label === "placement-inquiry" && !rankingBeforeRepeat) rankingBeforeRepeat = rankingNow(P); // as the writer last read it
|
|
14829
16153
|
const r = await UPSTREAM_STALE_REPAIR[s2.label](ctx, { displayVerdict });
|
|
14830
|
-
|
|
16154
|
+
// NOT A PASS: a reviewer that ends with no accepted call reads ok whenever the earlier review's file
|
|
16155
|
+
// stands, and its stamp is refreshed over the change it never read. The record says so, the run delivers,
|
|
16156
|
+
// and a resume for any other cause reviews the change whole.
|
|
16157
|
+
if (r?.ok && s2.label === "narrative-refutation" && !readAcceptedReviewSince(run.runDir, reviewSeq0)) {
|
|
16158
|
+
writePostRepairRecord(run.runDir, { status: "unread", whole: true, findingOnly: queuedForTheRecordOnly(s2), by: "stale-repair", fail: "no-accepted-call" });
|
|
16159
|
+
runLog(run.runDir, { event: "post-repair-review", read: false, whole: true, fail: "no-accepted-call" });
|
|
16160
|
+
note(`stale-repair: the reviewer's re-run recorded no review — the change it was queued for is unreviewed; the run's record says so, and the run delivers`);
|
|
16161
|
+
} else if (r?.ok) {
|
|
14831
16162
|
staleRepaired.push(s2.label);
|
|
14832
16163
|
reassemble = true;
|
|
16164
|
+
// THE FLOOR, APPLIED AGAIN TO THE PLACEMENT THE REPORT IS NOW BUILT FROM. The delivery floor was judged on
|
|
16165
|
+
// the placement account before this repair; the repeated pass has recorded its own floor duty, and the
|
|
16166
|
+
// stages after it rebuild from it in this loop. The same rule is applied to that account, through the
|
|
16167
|
+
// same clamp, so the delivered verdict, its floor and its record describe one pass. It only tightens.
|
|
16168
|
+
if (s2.label === "placement-inquiry") {
|
|
16169
|
+
const was = verdict, stood = JSON.stringify(clampReasons);
|
|
16170
|
+
clampOnTheFloorDuty({ again: true });
|
|
16171
|
+
clampOnTheWritersDuty({ again: true }); // the recall gate leaves out the floor that now stands
|
|
16172
|
+
// A save that fails here is tried again, then owed, never the end of the run: see saveTheLabel.
|
|
16173
|
+
if (JSON.stringify(clampReasons) !== stood || verdict !== was) saveTheLabel("after the repeated placement pass");
|
|
16174
|
+
runLog(run.runDir, { event: "floor-duty-after-repair", was, now: verdict });
|
|
16175
|
+
}
|
|
16176
|
+
// A whole review that is read reviews the change an earlier unread one left: its record says so, and
|
|
16177
|
+
// its points stand under the late-review rule below, as any whole review's do. (The arm above has
|
|
16178
|
+
// already set aside a stage that read ok without an accepted review.)
|
|
16179
|
+
if (s2.label === "narrative-refutation" && unreadRecordReview(run.runDir))
|
|
16180
|
+
writePostRepairRecord(run.runDir, { status: "read", whole: true, by: "stale-repair" });
|
|
14833
16181
|
// its consumers rebuild from what it JUST wrote — re-read, do not carry the pre-repair copy
|
|
14834
16182
|
readNow = readFindingsForReport(P);
|
|
14835
16183
|
findingsNow = readNow.findings;
|
|
@@ -14837,6 +16185,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14837
16185
|
try {
|
|
14838
16186
|
if (existsSync(P.caseLaw)) caseLawNow = joinCaseLawProfiles(parseCaseLawProfiles(readFileSync(P.caseLaw, "utf8")), findingsNow);
|
|
14839
16187
|
} catch { /* no case-law layer */ }
|
|
16188
|
+
for (const owed of cardsNeverBuilt(P, ctx.depth, cardOrdinals)) if (!staleStages.some((x) => x.label === owed.label)) staleStages.push(owed);
|
|
14840
16189
|
// — AND THE REVIEW IS ALSO SOMETHING THIS PASS JUST WROTE.
|
|
14841
16190
|
//
|
|
14842
16191
|
// The re-reads above are the artifacts a repaired stage's CONSUMERS need. narrative-refutation
|
|
@@ -14854,56 +16203,28 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14854
16203
|
// stays where it is; this restores the input it was denied. `verdictHardenedTo` ratchets one
|
|
14855
16204
|
// way, so a review that softened during the repair cannot lift a clamp.
|
|
14856
16205
|
if (s2.label === "narrative-refutation") {
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
if (
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
// so a reviewer refusing to sign DURING this repair "is written to disk and never
|
|
14874
|
-
// heard". Measured on `bf21580e` — the narrative printed a registration date of
|
|
14875
|
-
// 1 March 2011 against a record reading 25 February 2011, the reviewer said so, and
|
|
14876
|
-
// the run delivered anyway.
|
|
14877
|
-
//
|
|
14878
|
-
// DELETING THE THROW WITHOUT A DESTINATION RE-OPENS EXACTLY THAT HOLE, and that is
|
|
14879
|
-
// why this branch is not the same edit as the gate's. The owner's ruling removes the
|
|
14880
|
-
// REFUSAL; it does not say the reviewer stops being heard. So the late verdict is
|
|
14881
|
-
// adopted as the run's own and everything derived from it is rebuilt:
|
|
14882
|
-
//
|
|
14883
|
-
// · `verdict` and status.json — the run's own record
|
|
14884
|
-
// · verdict.json — "the single label authority" (its words)
|
|
14885
|
-
// · `reassemble` — assembleReportMd re-runs, so the open-points
|
|
14886
|
-
// section is built from THIS review, and
|
|
14887
|
-
// applyVerdictFrontMatter re-reads the sidecar
|
|
14888
|
-
// and re-stamps overall_label / overall_badge
|
|
14889
|
-
//
|
|
14890
|
-
// Without the sidecar write the report would ship BADGED WITH THE VERDICT SETTLED
|
|
14891
|
-
// BEFORE THE REVIEWER HARDENED — refused by the reviewer, labelled CONDITIONAL. That
|
|
14892
|
-
// is worse than either behaviour this replaces, and it is `bf21580e`'s shape one
|
|
14893
|
-
// layer up: the reviewer's word on disk and the client's document disagreeing.
|
|
14894
|
-
//
|
|
14895
|
-
// `verdictHardenedTo` still ratchets ONE WAY, so a review that SOFTENED during the
|
|
14896
|
-
// repair still cannot lift a clamp. This branch only ever tightens.
|
|
14897
|
-
runLog(run.runDir, { event: "verdict-hardened-by-repair", was: verdict, now: hardened, stage: s2.label, adopted: true });
|
|
14898
|
-
note(`stale-repair: the reviewer returned ${hardened} — adopting it as the run's verdict (the one settled earlier was decided against a review this repair has since rewritten) and rebuilding the label from it`);
|
|
14899
|
-
verdict = hardened;
|
|
14900
|
-
writeRunStatus(ctx, signoffPatch(verdict));
|
|
14901
|
-
try { writeVerdictSidecar(); }
|
|
14902
|
-
catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
|
|
14903
|
-
reassemble = true;
|
|
14904
|
-
}
|
|
16206
|
+
adoptLateReview(existsSync(P.seniorEyeReview) ? readFileSync(P.seniorEyeReview, "utf8") : "", s2.label);
|
|
16207
|
+
}
|
|
16208
|
+
// THE WRITER, JUDGED AGAIN WHEN A CHANGED DECISION RE-RAN IT. Its duty and the recall gate were judged on the
|
|
16209
|
+
// writer before it; a gap this one opens is named through the same clamps, and one already named stays named.
|
|
16210
|
+
// The label is saved again as the floor's is above, through saveTheLabel.
|
|
16211
|
+
if (s2.label === "synthesis") keepRankingReadByWriter(P); // where each record stood as this writer read it
|
|
16212
|
+
if (s2.label === "synthesis" && chain.has(s2.label)) {
|
|
16213
|
+
const was = verdict, stood = JSON.stringify(clampReasons);
|
|
16214
|
+
const judged = clampOnTheWritersDuty({ again: true });
|
|
16215
|
+
// A CLEAR a closed gap gave back meets the floor again on what this writer now says: its forward actions
|
|
16216
|
+
// are a ground of their own.
|
|
16217
|
+
if (restoredClear && verdict === "CLEAR") applyCoverageFloor();
|
|
16218
|
+
if (JSON.stringify(clampReasons) !== stood || verdict !== was) saveTheLabel("after the writer's re-run");
|
|
16219
|
+
runLog(run.runDir, { event: "writer-duty-after-repair", was, now: verdict,
|
|
16220
|
+
owed: judged.duty?.computable === true ? judged.duty.totals?.owed ?? null : null,
|
|
16221
|
+
unaccounted: judged.duty?.computable === true ? judged.duty.unaccounted.length : null, recall: judged.recall });
|
|
14905
16222
|
}
|
|
14906
|
-
|
|
16223
|
+
if (staleBefore) queueWhatItLeftStale(s2, staleBefore);
|
|
16224
|
+
} else {
|
|
16225
|
+
repairFailed.set(s2.label, String(r?.fail ?? "failed").slice(0, 200));
|
|
16226
|
+
note(`stale-repair: ${s2.label} re-run failed (${r?.fail}) — stays stale, the guard below decides`);
|
|
16227
|
+
}
|
|
14907
16228
|
} else if (s2.label.startsWith("report-card:")) {
|
|
14908
16229
|
const ord = Number(s2.label.slice("report-card:".length));
|
|
14909
16230
|
const f = findingsNow.find((x) => x.ordinal === ord);
|
|
@@ -14923,12 +16244,19 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14923
16244
|
}
|
|
14924
16245
|
}
|
|
14925
16246
|
if (reassemble) {
|
|
14926
|
-
|
|
16247
|
+
reassembleFromTheRecord(P, findingsNow, ctx.depth); // the plan the first assembly used, made from the record as it stands
|
|
14927
16248
|
applyVerdictFrontMatter(); // the reassembly rewrote report.md — re-apply every driver-owned
|
|
14928
16249
|
// field, exactly as the lint-repair branch above does
|
|
14929
16250
|
writeScopeFacts();
|
|
14930
16251
|
applyScopeFrontMatter();
|
|
14931
16252
|
applyRatedUnderFrontMatter();
|
|
16253
|
+
// A repair that re-ran a stage the trace and the Audit Trail report has both written again, before the lint
|
|
16254
|
+
// below reads the trace. A rebuild that fails withdraws the audit written before the repair, as a failed first
|
|
16255
|
+
// build ships none.
|
|
16256
|
+
if (rebuildTheDeliveredRecord && staleRepaired.some((l) => REBUILDS_THE_DELIVERED_RECORD.has(l))) {
|
|
16257
|
+
const rebuilt = rebuildTheDeliveredRecord("stale-repair");
|
|
16258
|
+
if (!rebuilt.ok) note(`stale-repair: the audit could not be written again (${String(rebuilt.reason).slice(0, 100)}) — the one written before the repair is withdrawn and kept under _history; the report ships without an audit`);
|
|
16259
|
+
}
|
|
14932
16260
|
}
|
|
14933
16261
|
// The AD-2 in-pass stale-repair arm for client-summary is retired with the stage
|
|
14934
16262
|
// (2026-08-01): nothing generates that label, so it can never turn up stale here.
|
|
@@ -14936,7 +16264,65 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14936
16264
|
// freshness question — one bounded pass; anything still stale blocks below, exactly as before.
|
|
14937
16265
|
lint = lintNow();
|
|
14938
16266
|
autoCorrectRegistry();
|
|
14939
|
-
staleStages = staleOnPath(run.runDir, deliveryPathStages, deliveryInputsFor, { project: projectStageInput });
|
|
16267
|
+
staleStages = staleOnPath(run.runDir, deliveryPathStages(), deliveryInputsFor, { project: projectStageInput });
|
|
16268
|
+
// A REPAIR WHOSE REVIEW COULD NOT BE READ IS NOT REVIEWED, and its record says so (`unreadRecordReview`). The guard
|
|
16269
|
+
// is never handed that record: a run is not held on an unread review (owner's ruling, 2026-10-19). What the guard
|
|
16270
|
+
// reads here is staleness alone, as in the released engine.
|
|
16271
|
+
// A CHANGED DECISION STILL UNREAD AFTER ITS RE-RUN IS DELIVERED, NOT HELD (the owner's ruling of 2026-10-19 that a
|
|
16272
|
+
// run is never held). When every stage still stale is one the changed decision left stale and whose re-run in
|
|
16273
|
+
// this pass did not complete, the run delivers what it has. The writer's duty and the recall gate are judged
|
|
16274
|
+
// again on the writer delivered, and name in their approved sentences what they find. Staleness for any other
|
|
16275
|
+
// reason still falls to the guard.
|
|
16276
|
+
// LATE RECORDS NO PLACEMENT PASS COULD ASSESS ARE DELIVERED, NOT HELD (ruled 2026-10-19). The band gained records
|
|
16277
|
+
// after placement's last pass, and neither the pass that would have assessed them nor the delivery check's repeat
|
|
16278
|
+
// of placement completed. The report stands as the last accepted passes built it. An account from before the late
|
|
16279
|
+
// records cannot show they were assessed, so the floor and the recall gate are judged again on the band as it
|
|
16280
|
+
// stands, and the late records the report does not address are counted in the approved sentence for records
|
|
16281
|
+
// neither addressed nor set aside. The attempts and the records are kept in the run record.
|
|
16282
|
+
const unassessed = repairFailed.has("placement-inquiry") ? lateRecordIds(P) : [];
|
|
16283
|
+
if (unassessed.length && staleStages.some((x) => x.label === "placement-inquiry")
|
|
16284
|
+
&& staleStages.every((x) => x.label === "placement-inquiry" || chain.has(x.label))) {
|
|
16285
|
+
const was = verdict, stood = JSON.stringify(clampReasons);
|
|
16286
|
+
clampOnTheFloorDuty({ again: true });
|
|
16287
|
+
const judged = clampOnTheWritersDuty({ again: true });
|
|
16288
|
+
if ((JSON.stringify(clampReasons) !== stood || verdict !== was) && saveTheLabel("after late records went unassessed")) applyVerdictFrontMatter();
|
|
16289
|
+
runLog(run.runDir, { event: "late-evidence-unassessed-delivered", was, now: verdict, records: unassessed.length, ids: unassessed.slice(0, 20),
|
|
16290
|
+
stated: judged.recall, latePass: ctx.latePassFailed ?? null, repeat: repairFailed.get("placement-inquiry") });
|
|
16291
|
+
note(`late evidence: ${unassessed.length} record(s) reached the band after placement's last pass, and neither its pass for them nor the delivery check's repeat completed — the run delivers what the last accepted passes built, and the approved clauses state what is open`);
|
|
16292
|
+
staleStages = staleStages.filter((x) => x.label !== "placement-inquiry");
|
|
16293
|
+
}
|
|
16294
|
+
if (staleStages.length && staleStages.every((x) => chain.has(x.label))) {
|
|
16295
|
+
const was = verdict, named = clampReasons.length, stood = JSON.stringify(clampReasons);
|
|
16296
|
+
clampOnTheWritersDuty({ again: true });
|
|
16297
|
+
// THE RECORDS RE-RANKED SINCE THE WRITER LAST WROTE, in the owner's words of 2026-10-19. The report states the
|
|
16298
|
+
// ranking the writer last read; the audit trail, written again after the repeat, states the one that stands.
|
|
16299
|
+
const readByWriter = rankingFromJson(safeReadJson(rankingReadByWriterPath(P))) ?? rankingBeforeRepeat;
|
|
16300
|
+
if (readByWriter && staleStages.some((x) => x.label === "synthesis")) {
|
|
16301
|
+
const now = rankingNow(P), n = recordsReRanked(readByWriter, now);
|
|
16302
|
+
const m = Math.max(rankedRecords(readByWriter), rankedRecords(now), n);
|
|
16303
|
+
if (n > 0) deliverAndClamp({
|
|
16304
|
+
event: "ranking-changed-unread-delivered",
|
|
16305
|
+
defect: `ranking_changed_unread:${n}`,
|
|
16306
|
+
detail: { records: n, of: m },
|
|
16307
|
+
reason: `ranking_changed_unread:${n} of ${m} record(s) — the delivery check's repeat of placement changed where these records stand, and the writer's re-run for the change did not complete, so the report gives the ranking it last read and the audit trail the one that stands. The report ships with this named rather than being withheld.`,
|
|
16308
|
+
clause: clauseForDefect("ranking_changed_unread", n, m),
|
|
16309
|
+
});
|
|
16310
|
+
}
|
|
16311
|
+
if ((JSON.stringify(clampReasons) !== stood || verdict !== was) && saveTheLabel("after a changed decision went unread")) applyVerdictFrontMatter();
|
|
16312
|
+
runLog(run.runDir, { event: "changed-decision-unread-delivered", was, now: verdict, named: clampReasons.length - named,
|
|
16313
|
+
stages: staleStages.map((x) => ({ label: x.label, changed: (x.changed ?? []).map((c) => c.name) })) });
|
|
16314
|
+
note(`stale-repair: ${staleStages.map((x) => x.label).join(", ")} could not be re-run for the changed decision — the run delivers, and the approved clauses name what they find`);
|
|
16315
|
+
staleStages = [];
|
|
16316
|
+
}
|
|
16317
|
+
// THE LABEL OWED IS SAVED NOW, before anything is published from it, and the report's own label follows it. Only
|
|
16318
|
+
// a run directory that refuses every save of it ends the run here, and then nothing the run writes can be published.
|
|
16319
|
+
if (labelOwed) {
|
|
16320
|
+
const owed = labelOwed;
|
|
16321
|
+
labelOwed = null;
|
|
16322
|
+
if (!saveTheLabel(`${owed}, saved again before delivery`))
|
|
16323
|
+
throw new StageFailure("verdict", `the run directory refused every save of the result's label (${owed}) — no surface can be published with it`);
|
|
16324
|
+
applyVerdictFrontMatter();
|
|
16325
|
+
}
|
|
14940
16326
|
note(`stale-repair: re-did ${staleRepaired.length} stage(s) in-pass (${staleRepaired.join(", ") || "none"}) — ${staleStages.length ? `${staleStages.length} still stale, the delivery guard blocks` : "the delivery path is fresh"}`);
|
|
14941
16327
|
}
|
|
14942
16328
|
// P2 — record the delivery-BLOCKING stale labels the moment they are known (not only at the throw
|
|
@@ -16421,12 +17807,12 @@ async function runExperimentInner(job, opts) {
|
|
|
16421
17807
|
|
|
16422
17808
|
// — THE CORRECTIVE PASS, on a COLD turn, and it says which pass it reproduced.
|
|
16423
17809
|
//
|
|
16424
|
-
// Production's corrective pass
|
|
16425
|
-
//
|
|
16426
|
-
//
|
|
16427
|
-
//
|
|
16428
|
-
//
|
|
16429
|
-
// one another.
|
|
17810
|
+
// Production's corrective pass is a followup dispatch on the run's own synthesis key, and its message is
|
|
17811
|
+
// these blocks: its first attempt starts fresh, and it never resumes the synthesis session (no engine resumes
|
|
17812
|
+
// by key). An arm builds a fresh key in a sandbox, and dispatching on the real run's key was rejected because
|
|
17813
|
+
// "canonical run untouched" is the arm's whole contract. So the arm carries the same INPUTS on a cold turn,
|
|
17814
|
+
// composed by the same function production uses — reaching the seam, not reproducing production's dispatch.
|
|
17815
|
+
// The receipt says `dispatched-cold`, never `dispatched-warm`, so the two cannot be read as one another.
|
|
16430
17816
|
const P = shadowCtx.paths;
|
|
16431
17817
|
const correctiveReady = correctiveReadiness({
|
|
16432
17818
|
preCorrective: existsSync(driverDir(shadowDir, "findings-pre-corrective.json")),
|
|
@@ -17060,6 +18446,52 @@ async function afterLateSynthesisSave(name, ctx, r) {
|
|
|
17060
18446
|
if (r?.ok && !r.skipped && name === "synthesis" && typeof ctx?.afterSynthesisSave === "function") await ctx.afterSynthesisSave();
|
|
17061
18447
|
}
|
|
17062
18448
|
|
|
18449
|
+
/**
|
|
18450
|
+
* THE DRIVER'S WRITES FROM BEFORE THE SEAM, MADE AGAIN AFTER A SYNTHESIS SAVE THAT FOLLOWS IT.
|
|
18451
|
+
*
|
|
18452
|
+
* Two writes into the findings record run before the seam rather than at it: the basis derivation, which demotes a
|
|
18453
|
+
* `verified-from-record` stamp the reading log cannot back, and the deadline enrichment, which gives a finding with
|
|
18454
|
+
* no deadline the register's own opposition window. A save after the seam renders the record from the model's call.
|
|
18455
|
+
* A patch keeps both, because each also lands in the merge base the patch is merged onto; a whole re-send replaces
|
|
18456
|
+
* that base, and they went with it. The seam's own writes were made again after such a save, and these two were not,
|
|
18457
|
+
* so the delivered record claimed reads no step had made, and every card whose finding had carried a demoted stamp
|
|
18458
|
+
* read as stale and was rebuilt for a finding the save had not changed. They run again here, in the order the pass
|
|
18459
|
+
* ran them, before the seam's writes. Each leaves a record that already carries it as it is, and neither throws.
|
|
18460
|
+
*/
|
|
18461
|
+
export function rewriteFromBeforeTheSeam(ctx) { // @internal
|
|
18462
|
+
settleDerivedBases(ctx, "late-save");
|
|
18463
|
+
enrichFindingDeadlines(ctx.paths, ctx.paths.runDir, note, { nowMs: deadlineClockOf(ctx.paths.runDir) });
|
|
18464
|
+
}
|
|
18465
|
+
|
|
18466
|
+
/**
|
|
18467
|
+
* The clock the run's deadline enrichment judged its windows by, so a window is judged again exactly as it was
|
|
18468
|
+
* judged the first time, never moved across its edge by the minutes a repair took. Now, on a run whose enrichment
|
|
18469
|
+
* recorded none: it judged nothing, so there is nothing to keep it consistent with. Never throws.
|
|
18470
|
+
*/
|
|
18471
|
+
function deadlineClockOf(runDir) {
|
|
18472
|
+
try {
|
|
18473
|
+
const at = Date.parse(JSON.parse(readFileSync(driverDir(runDir, "deadline-enrich.json"), "utf8"))?.judged_at);
|
|
18474
|
+
return Number.isFinite(at) ? at : Date.now();
|
|
18475
|
+
} catch { return Date.now(); }
|
|
18476
|
+
}
|
|
18477
|
+
|
|
18478
|
+
/**
|
|
18479
|
+
* THE MERGE BASE TAKES THE ENRICHED DEADLINES. A patch after the enrichment is merged onto
|
|
18480
|
+
* `_driver/synthesis-calls/accepted.json` and re-renders every finding from there, so a deadline written to the
|
|
18481
|
+
* findings file alone was dropped by the next patch, and a card built with it was rebuilt without it. In the model's
|
|
18482
|
+
* numbering: under a fold, a deadline lands on the member the fold kept, whose object the folded finding is, and the
|
|
18483
|
+
* fold carries it again. Only a finding the base holds with no deadline of its own takes one, as in the file. The
|
|
18484
|
+
* edit is judged as a call is; a refusal leaves the base as it was, and the receipt says what happened. Never throws.
|
|
18485
|
+
*/
|
|
18486
|
+
function rebaseEnrichedDeadlines(runDir, enriched) {
|
|
18487
|
+
const fold = readFold(runDir);
|
|
18488
|
+
const byOrdinal = new Map(enriched.map((e) => [keptModelOrdinal(fold, e.ordinal), e.date]).filter(([o]) => o != null));
|
|
18489
|
+
if (!byOrdinal.size) return "unmapped";
|
|
18490
|
+
const r = rebaseSynthesis(runDir, (held) => ({ ...held, findings: (Array.isArray(held?.findings) ? held.findings : [])
|
|
18491
|
+
.map((f) => (byOrdinal.has(f?.ordinal) && !f?.deadline?.date ? { ...f, deadline: { kind: "opposition", date: byOrdinal.get(f.ordinal) } } : f)) }));
|
|
18492
|
+
return r.ok ? (r.changed ? "enriched" : "unchanged") : /^no_base:/.test(String(r.reason)) ? "no-base" : "refused";
|
|
18493
|
+
}
|
|
18494
|
+
|
|
17063
18495
|
/**
|
|
17064
18496
|
* The reader's name for a planned search that never ran and that no open ledger row on its axis claims —
|
|
17065
18497
|
* the orphan row of `coverageJudgmentRows`. Its area is the plan's qid, which a lawyer cannot read; the
|
|
@@ -17112,3 +18544,202 @@ export function foreignRecordLinksBlanked(r, ctx, name) { // exported for its
|
|
|
17112
18544
|
runLog(ctx.paths.runDir, { event: "foreign-record-links-recheck", stage: name, ok: false, fail: String(v?.reason ?? "no validator").slice(0, 200) });
|
|
17113
18545
|
return false;
|
|
17114
18546
|
}
|
|
18547
|
+
|
|
18548
|
+
/**
|
|
18549
|
+
* THE WRITE-UPS A RECORD OWES, PLANNED IN ONE PLACE. PURE.
|
|
18550
|
+
*
|
|
18551
|
+
* §9: a finding owed prose earns a full card where its form is `full`, and the rest render code-side, as a short
|
|
18552
|
+
* entry or as a line in the grouped section (§8). The first assembly made this plan inline, and the delivery check's
|
|
18553
|
+
* repair, which reassembles the report after a synthesis save has moved the record, made its own: every finding owed
|
|
18554
|
+
* prose and no grouped section. So a report reassembled on a graded product lost its "Also on the register" section,
|
|
18555
|
+
* where its grouped findings are read by territory or by right, each short entry kept the text composed before the
|
|
18556
|
+
* save, and the cards a run owed stayed the ones the first plan named whatever the record became. One plan, made here
|
|
18557
|
+
* from the record as it stands, now serves the first build, the delivery path and every reassembly.
|
|
18558
|
+
*
|
|
18559
|
+
* The floor tier joins on `mark_text` (floorTierByMark); an absent band shape yields an empty map, and every finding
|
|
18560
|
+
* then reads as holding no floor. On one country every form is `full`, so the plan is `fullProseOrdinals` itself.
|
|
18561
|
+
*/
|
|
18562
|
+
export function planWriteUps(findings, depth, bandShape) { // @internal
|
|
18563
|
+
const floorTiers = floorTierByMark(bandShape);
|
|
18564
|
+
const formOf = (f) => writeUpForm(depth, f, { floorTier: floorTiers.get(floorMarkKey(f?.mark)) ?? null });
|
|
18565
|
+
const byOrdinal = new Map((findings ?? []).filter((f) => f?.ordinal != null).map((f) => [f.ordinal, f]));
|
|
18566
|
+
const proseOrdinals = fullProseOrdinals(findings);
|
|
18567
|
+
const cardOrdinals = proseOrdinals.filter((ord) => formOf(byOrdinal.get(ord)) === "full");
|
|
18568
|
+
const entryOrdinals = proseOrdinals.filter((ord) => formOf(byOrdinal.get(ord)) !== "full");
|
|
18569
|
+
const entryFindings = entryOrdinals.map((ord) => byOrdinal.get(ord)).filter(Boolean);
|
|
18570
|
+
// Worldwide groups by the right, multi-country by territory: the depth row's prose rung says which.
|
|
18571
|
+
const byRight = String(depth?.narrativeProse ?? "") === "adversarial";
|
|
18572
|
+
const { groups, singles: entrySingles } = groupEntries(entryFindings, { byRight });
|
|
18573
|
+
return {
|
|
18574
|
+
cardOrdinals, entryOrdinals, entryFindings, byRight, entrySingles,
|
|
18575
|
+
// Assembly sees full cards AND short entries; a grouped finding is a line in its section, never a card.
|
|
18576
|
+
assembledOrdinals: [...cardOrdinals, ...entrySingles.map((f) => f.ordinal)],
|
|
18577
|
+
groupedMembers: groups.flatMap((g) => g.findings),
|
|
18578
|
+
};
|
|
18579
|
+
}
|
|
18580
|
+
|
|
18581
|
+
/** Each short entry's card file, composed by code from its finding as it stands. No model turn. */
|
|
18582
|
+
function writeShortEntries(P, entrySingles) {
|
|
18583
|
+
for (const f of entrySingles) {
|
|
18584
|
+
try { writeFileSync(P.reportCard(String(f.ordinal)), `${shortEntryBody(f)}\n`); }
|
|
18585
|
+
catch (e) { note(`short entry ${f.ordinal} not written (${String(e?.message ?? e).slice(0, 80)}) — it renders structured-only, as an absent card always has`); }
|
|
18586
|
+
}
|
|
18587
|
+
}
|
|
18588
|
+
|
|
18589
|
+
/** The full cards the record owes as it stands now, in its order. */
|
|
18590
|
+
function owedCardOrdinals(P, depth) {
|
|
18591
|
+
return planWriteUps(readFindingsForReport(P).findings, depth, safeReadJson(P.bandShape)).cardOrdinals;
|
|
18592
|
+
}
|
|
18593
|
+
|
|
18594
|
+
/**
|
|
18595
|
+
* THE CARD A FINDING ADDED AFTER THE CARDS IS OWED. A card with no stamp is never stale, so a finding that a
|
|
18596
|
+
* synthesis save after the cards made owed a full card, at an ordinal the first plan never dispatched one for (a
|
|
18597
|
+
* finding it added, or one it moved from a short entry to a full card), got no card call, and the report rendered it
|
|
18598
|
+
* structured-only or as the short entry. Each such card joins the delivery check's repair once, as owed and never
|
|
18599
|
+
* built; a card the first plan dispatched and that failed keeps that outcome, as a failed card always has. Returns
|
|
18600
|
+
* entries in the shape `staleOnPath` gives.
|
|
18601
|
+
*/
|
|
18602
|
+
function cardsNeverBuilt(P, depth, firstPlan) {
|
|
18603
|
+
const dispatched = new Set(firstPlan ?? []);
|
|
18604
|
+
return owedCardOrdinals(P, depth)
|
|
18605
|
+
.filter((o) => !dispatched.has(o) && !readStamp(P.runDir, `report-card:${o}`))
|
|
18606
|
+
.map((o) => ({ label: `report-card:${o}`, changed: [{ name: `${o}.md`, was: null, now: null, via: "never-built" }] }));
|
|
18607
|
+
}
|
|
18608
|
+
|
|
18609
|
+
/**
|
|
18610
|
+
* The repairs that fix only what they name: a delivery lint fault, or an object of the writer's record that failed the
|
|
18611
|
+
* strict parse. They are not told placement's set-asides with the order to check each ground (ruled 2026-10-19); every
|
|
18612
|
+
* other follow-up of the reading step and the writer is.
|
|
18613
|
+
*/
|
|
18614
|
+
export const NARROW_REPAIR_TRIGGERS = new Set(["lint-repair", "finding-reemit", "action-reemit", "ask-answer-reemit"]); // @internal — driven by its test
|
|
18615
|
+
|
|
18616
|
+
/** The repaired stages whose output the record-carry trace or the Audit Trail reports. */
|
|
18617
|
+
const REBUILDS_THE_DELIVERED_RECORD = new Set(["placement-inquiry", "register-digest", "synthesis", "narrative-refutation"]);
|
|
18618
|
+
|
|
18619
|
+
/**
|
|
18620
|
+
* Keep the Audit Trail, the record-carry trace and the doubt ledger as they stand, under _history/<ts>-audit-and-trace-
|
|
18621
|
+
* <reason>/, before any of them is written again, so a pass that writes them over a repair or a resume never loses what
|
|
18622
|
+
* the earlier state said. The pass that wrote them may have delivered nothing: a pass the delivery check held writes
|
|
18623
|
+
* them too. Returns the directory, or null when there was nothing to keep. Best-effort, like every snapshot.
|
|
18624
|
+
*/
|
|
18625
|
+
export function keepDeliveredRecordAsHistory(P, reason) { // @internal — driven by its test with a run's own files
|
|
18626
|
+
const files = [P.audit, P.recordCarry, driverDir(P.runDir, "doubts.json")].filter((f) => existsSync(f));
|
|
18627
|
+
if (!files.length) return null;
|
|
18628
|
+
try {
|
|
18629
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
18630
|
+
const dest = join(P.runDir, "_history", `${ts}-audit-and-trace-${reason}`);
|
|
18631
|
+
mkdirSync(dest, { recursive: true });
|
|
18632
|
+
for (const f of files) copyFileSync(f, join(dest, basename(f)));
|
|
18633
|
+
return dest;
|
|
18634
|
+
} catch { return null; }
|
|
18635
|
+
}
|
|
18636
|
+
|
|
18637
|
+
/**
|
|
18638
|
+
* The reading audit's lookups (band-server appends every lookup to _driver/reading-log.jsonl). Read on any run that
|
|
18639
|
+
* derived a band shape, an empty list included, because a band-consuming stage that read nothing is a fact the audit
|
|
18640
|
+
* must show; null on a run with no shape, which renders no section. A torn tail line never kills the audit.
|
|
18641
|
+
*/
|
|
18642
|
+
function readReadingLog(P) {
|
|
18643
|
+
if (!existsSync(P.bandShape)) return null;
|
|
18644
|
+
const log = [];
|
|
18645
|
+
try {
|
|
18646
|
+
for (const line of readFileSync(P.readingLog, "utf8").split("\n")) {
|
|
18647
|
+
if (!line.trim()) continue;
|
|
18648
|
+
try { log.push(JSON.parse(line)); } catch { /* a torn tail line never kills the audit */ }
|
|
18649
|
+
}
|
|
18650
|
+
} catch { /* no lookups recorded — the empty array IS the disclosure */ }
|
|
18651
|
+
return log;
|
|
18652
|
+
}
|
|
18653
|
+
|
|
18654
|
+
/**
|
|
18655
|
+
* Take an audit that no longer describes the run out of the place publishing reads it from, keeping it under _history
|
|
18656
|
+
* (unless the caller has just kept it). The report then ships without that Audit Trail, as it does when a first build
|
|
18657
|
+
* fails, never with an earlier one offered as current; the publisher still builds the workbook from the current
|
|
18658
|
+
* findings. Returns whether an audit was withdrawn.
|
|
18659
|
+
*/
|
|
18660
|
+
export function withdrawSupersededAudit(P, reason, { keep = true } = {}) { // @internal — driven by its test with a run's own files
|
|
18661
|
+
if (!existsSync(P.audit)) return false;
|
|
18662
|
+
if (keep) keepDeliveredRecordAsHistory(P, reason);
|
|
18663
|
+
try { rmSync(P.audit, { force: true }); } catch { return false; }
|
|
18664
|
+
runLog(P.runDir, { event: "audit-withdrawn", reason });
|
|
18665
|
+
return true;
|
|
18666
|
+
}
|
|
18667
|
+
|
|
18668
|
+
/** The doubt ledger on disk, written again with the doubts the rebuilt audit carries; what it said of the closure stays. */
|
|
18669
|
+
function writeDoubtLedgerAgain(P, doubts, truncated) {
|
|
18670
|
+
const path = driverDir(P.runDir, "doubts.json");
|
|
18671
|
+
const earlier = safeReadJson(path) ?? {};
|
|
18672
|
+
const settled = doubts.filter((d) => d?.status === "checked-and-settled").length;
|
|
18673
|
+
const { doubts: _d, minted: _m, settled: _s, open: _o, truncated: _t, ...closure } = earlier;
|
|
18674
|
+
writeFileSync(path, JSON.stringify({ minted: doubts.length, settled, open: doubts.length - settled,
|
|
18675
|
+
...(truncated?.length ? { truncated } : {}), ...closure, doubts }, null, 2));
|
|
18676
|
+
}
|
|
18677
|
+
|
|
18678
|
+
/**
|
|
18679
|
+
* THE TRACE AND THE AUDIT TRAIL, WRITTEN AGAIN OVER A REPAIR. Both describe a pass before the delivery check repairs
|
|
18680
|
+
* anything, and a stage that check re-runs can change what they report: a repeated placement pass can reverse a
|
|
18681
|
+
* set-aside or change its ground. This keeps the pair as it stands under _history, then writes the trace and the audit
|
|
18682
|
+
* again from the run's files, so what is delivered describes the decision that stands at delivery. `inputs` are what the
|
|
18683
|
+
* pass built its audit from. The doubts minted from the trace follow the trace written again (recordCarryDoubtsAgain);
|
|
18684
|
+
* every other doubt and ask stands as the pass raised and closed it, and nothing is dispatched again.
|
|
18685
|
+
* A rebuild that fails, the trace written and the audit not among the ways, withdraws the audit on disk: it describes
|
|
18686
|
+
* the state before the repair. Returns {ok, counts} or {ok:false, reason}. IMPURE (writes the run).
|
|
18687
|
+
*/
|
|
18688
|
+
export function rebuildDeliveredRecord(ctx, inputs = {}, trigger = "stale-repair") { // @internal — driven by its test with a run's own files
|
|
18689
|
+
const P = ctx.paths;
|
|
18690
|
+
keepDeliveredRecordAsHistory(P, trigger);
|
|
18691
|
+
try {
|
|
18692
|
+
let findingsNow = [], actionsNow = [];
|
|
18693
|
+
try {
|
|
18694
|
+
if (existsSync(P.findings)) {
|
|
18695
|
+
const parsed = parseFindingsJsonLenient(readFileSync(P.findings, "utf8"));
|
|
18696
|
+
findingsNow = parsed.findings ?? [];
|
|
18697
|
+
actionsNow = parsed.actions ?? [];
|
|
18698
|
+
}
|
|
18699
|
+
} catch { /* the audit still builds without them */ }
|
|
18700
|
+
deriveRecordCarry(ctx, trigger, { findings: findingsNow });
|
|
18701
|
+
const registerFindingsText = readFileSync(P.registerFindings, "utf8");
|
|
18702
|
+
// THE DOUBTS MINTED FROM THE TRACE FOLLOW THE TRACE. The record-carry family was minted from the trace before the
|
|
18703
|
+
// repair; read against the trace written just above, a record the repair answered loses its doubt and one it left
|
|
18704
|
+
// unreasoned gains one, keyed by record so no closure the closure step recorded moves onto another record. Every
|
|
18705
|
+
// other family stands as the pass raised and closed it.
|
|
18706
|
+
let doubts = inputs.doubts ?? null, doubtTruncations = inputs.doubtTruncations ?? null;
|
|
18707
|
+
if (Array.isArray(doubts)) {
|
|
18708
|
+
const coverageRows = (() => { try { const rows = JSON.parse(readFileSync(P.registerCoverageLedger, "utf8")); return Array.isArray(rows) ? rows : []; } catch { return []; } })();
|
|
18709
|
+
const re = recordCarryDoubtsAgain(doubts, safeReadJson(P.recordCarry),
|
|
18710
|
+
{ stitch: (ds) => stitchDoubts(ds, { findings: { findings: findingsNow, actions: actionsNow }, coverageRows, registerFindingsText }) });
|
|
18711
|
+
doubts = re.doubts;
|
|
18712
|
+
// A trace that could not be read says nothing about any record: the doubts stand as raised, and so does the count
|
|
18713
|
+
// of those the cap left out.
|
|
18714
|
+
if (!re.unavailable) doubtTruncations = [...(doubtTruncations ?? []).filter((t) => t?.source !== "record-carry"),
|
|
18715
|
+
...(re.omitted ? [{ source: "record-carry", minted: re.minted, omitted: re.omitted }] : [])];
|
|
18716
|
+
writeDoubtLedgerAgain(P, doubts, doubtTruncations);
|
|
18717
|
+
if (re.fresh.length || re.dropped.length || re.unavailable)
|
|
18718
|
+
runLog(P.runDir, { event: "record-carry-doubts-again", trigger, fresh: re.fresh.length, dropped: re.dropped.length, ...(re.unavailable ? { unavailable: true } : {}) });
|
|
18719
|
+
}
|
|
18720
|
+
const again = buildAuditMd(registerFindingsText, inputs.commonLawText ?? "",
|
|
18721
|
+
{ findings: findingsNow, doubts, asks: inputs.asks ?? null, readingLog: readReadingLog(P),
|
|
18722
|
+
commonLawGrid: inputs.commonLawGrid ?? null, doubtTruncations,
|
|
18723
|
+
registerPresence: inputs.registerPresence ?? null, placementRows: placementAuditRows(P) });
|
|
18724
|
+
if (!again.counts.findings) throw new Error("0 findings parsed from the spine tables");
|
|
18725
|
+
writeFileSync(P.audit, again.md);
|
|
18726
|
+
runLog(P.runDir, { event: "audit-rebuilt", trigger, ...again.counts });
|
|
18727
|
+
return { ok: true, counts: again.counts };
|
|
18728
|
+
} catch (e) {
|
|
18729
|
+
const reason = String(e?.message ?? e).slice(0, 160);
|
|
18730
|
+
runLog(P.runDir, { event: "audit-rebuild-failed", trigger, reason });
|
|
18731
|
+
withdrawSupersededAudit(P, `${trigger}-failed`, { keep: false }); // the pair as it stood was kept above
|
|
18732
|
+
return { ok: false, reason };
|
|
18733
|
+
}
|
|
18734
|
+
}
|
|
18735
|
+
|
|
18736
|
+
/**
|
|
18737
|
+
* Reassemble report.md from the record as it stands, as the first assembly did: the plan made again, each short entry
|
|
18738
|
+
* composed again from its finding, and the grouped section built. Returns the plan.
|
|
18739
|
+
*/
|
|
18740
|
+
export function reassembleFromTheRecord(P, findings, depth) { // @internal
|
|
18741
|
+
const plan = planWriteUps(findings, depth, safeReadJson(P.bandShape));
|
|
18742
|
+
writeShortEntries(P, plan.entrySingles);
|
|
18743
|
+
assembleReportMd(P, findings, plan.assembledOrdinals, { grouped: plan.groupedMembers, byRight: plan.byRight });
|
|
18744
|
+
return plan;
|
|
18745
|
+
}
|