audit-tools 0.50.4 → 0.50.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/audit/cli/auditStep.d.ts +30 -0
  2. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  3. package/dist/audit/cli/auditStep.js +50 -0
  4. package/dist/audit/cli/auditStep.js.map +1 -1
  5. package/dist/audit/cli/foldTransaction.d.ts +103 -0
  6. package/dist/audit/cli/foldTransaction.d.ts.map +1 -0
  7. package/dist/audit/cli/foldTransaction.js +243 -0
  8. package/dist/audit/cli/foldTransaction.js.map +1 -0
  9. package/dist/audit/cli/nextStepHelpers.d.ts +109 -28
  10. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  11. package/dist/audit/cli/nextStepHelpers.js +661 -560
  12. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  13. package/dist/audit/cli/reviewRun.d.ts +15 -3
  14. package/dist/audit/cli/reviewRun.d.ts.map +1 -1
  15. package/dist/audit/cli/reviewRun.js +39 -13
  16. package/dist/audit/cli/reviewRun.js.map +1 -1
  17. package/dist/audit/orchestrator/advance.d.ts +38 -36
  18. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  19. package/dist/audit/orchestrator/advance.js +126 -82
  20. package/dist/audit/orchestrator/advance.js.map +1 -1
  21. package/dist/audit/orchestrator/advanceTypes.d.ts +23 -0
  22. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  23. package/dist/audit/orchestrator/designReviewSnapshot.d.ts +8 -2
  24. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  25. package/dist/audit/orchestrator/designReviewSnapshot.js +12 -6
  26. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  27. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  28. package/dist/audit/orchestrator/executorRunners.js +1 -1
  29. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  30. package/dist/audit/orchestrator/executors.d.ts.map +1 -1
  31. package/dist/audit/orchestrator/executors.js +1 -1
  32. package/dist/audit/orchestrator/executors.js.map +1 -1
  33. package/dist/audit/orchestrator/hostInputPause.d.ts +0 -33
  34. package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
  35. package/dist/audit/orchestrator/hostInputPause.js +6 -64
  36. package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
  37. package/dist/audit/orchestrator/obligationPolicy.d.ts +90 -0
  38. package/dist/audit/orchestrator/obligationPolicy.d.ts.map +1 -0
  39. package/dist/audit/orchestrator/obligationPolicy.js +228 -0
  40. package/dist/audit/orchestrator/obligationPolicy.js.map +1 -0
  41. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts +1 -1
  42. package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts.map +1 -1
  43. package/dist/audit/orchestrator/systemicChallengeExecutor.js +23 -1
  44. package/dist/audit/orchestrator/systemicChallengeExecutor.js.map +1 -1
  45. package/dist/audit/types/systemicChallenge.d.ts +9 -0
  46. package/dist/audit/types/systemicChallenge.d.ts.map +1 -1
  47. package/dist/shared/engine/obligationEngine.d.ts +52 -11
  48. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  49. package/dist/shared/engine/obligationEngine.js +46 -9
  50. package/dist/shared/engine/obligationEngine.js.map +1 -1
  51. package/dist/shared/observability/runLog.d.ts +2 -0
  52. package/dist/shared/observability/runLog.d.ts.map +1 -1
  53. package/dist/shared/observability/runLog.js.map +1 -1
  54. package/package.json +1 -1
@@ -6,26 +6,28 @@
6
6
  * concern.
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
9
+ import { mkdir, readFile, unlink } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
- import { advance, describeStoppedFold, compareCodeUnits, isFileMissingError, isJsonParseError, isRecord, readJsonFile, persistAnalyzerConsent, persistAnalyzerSettings, writeJsonFile, } from "audit-tools/shared";
12
- import { loadArtifactBundle, promoteFinalAuditReport, writeCoreArtifacts, } from "../io/artifacts.js";
13
- import { artifactTreeLockPath, auditReportPath, groundDesignFindings, laneAssetsDir, promotedAuditReportPath, withFileLock, } from "audit-tools/shared";
11
+ import { advance, describeStoppedFold, RunLogger, compareCodeUnits, isFileMissingError, isJsonParseError, isRecord, readJsonFile, persistAnalyzerConsent, persistAnalyzerSettings, writeJsonFile, } from "audit-tools/shared";
12
+ import { loadArtifactBundle, promoteFinalAuditReport, } from "../io/artifacts.js";
13
+ import { auditReportPath, groundDesignFindings, laneAssetsDir, promotedAuditReportPath, } from "audit-tools/shared";
14
14
  import { charterExtractionKindsForCeiling } from "./charterExtractionPrompt.js";
15
15
  import { LANE_SUBMISSION_SCHEMAS, charterLaneSchema, describeSubmissionShapeMismatch, unwrapSubmissionArray, } from "./laneValidators.js";
16
- import { advanceAudit, findExecutorFailure, } from "../orchestrator/advance.js";
17
- import { captureDesignReviewSnapshot, isDesignReviewStale, } from "../orchestrator/designReviewSnapshot.js";
16
+ import { advanceAudit, engineMaxTransitions, findExecutorFailure, runSingleAdvanceStep, MAX_DRAIN_STEPS, } from "../orchestrator/advance.js";
17
+ import { buildDesignReviewSnapshot, isDesignReviewStale, } from "../orchestrator/designReviewSnapshot.js";
18
+ import { computeStaleArtifacts, emitStalenessRecord, isMetadataMigrationStaleness, resetStalenessDedup, } from "../orchestrator/staleness.js";
19
+ import { commitFold, createFoldTransaction, markSubmissionApplied, quarantineSubmissionFile, recoverStagedSubmissions, stageLaneSubmission, } from "./foldTransaction.js";
20
+ import { charterClarificationOmits, charterExtractionOmits, intentEquivalenceOmits, synthesisNarrativeOmits, systemicChallengeOmits, } from "../orchestrator/obligationPolicy.js";
18
21
  import { computeArtifactStateSignature } from "../orchestrator/artifactMetadata.js";
19
22
  import { decideNextStep, PRIORITY, AUDIT_FRICTION_RUN_ID, decideAuditFrictionCloseout, } from "../orchestrator/nextStep.js";
20
23
  import { isHostDelegationExecutor } from "../orchestrator/executors.js";
21
- import { resolveCharterCeiling, ceilingRequestsCharters, } from "../orchestrator/charterExtractionExecutor.js";
22
- import { resolveClarificationAttention } from "../orchestrator/charterClarificationExecutor.js";
24
+ import { resolveCharterCeiling } from "../orchestrator/charterExtractionExecutor.js";
23
25
  import { deriveAuditState } from "../orchestrator/state.js";
24
- import { deriveIntentEquivalenceStatus } from "../orchestrator/intentEquivalenceExecutor.js";
25
26
  import { checkFileIntegrity } from "../orchestrator/fileIntegrity.js";
26
27
  import { graphEnrichmentUnresolvedAnalyzers, graphEnrichmentLowConfidenceEdges, pendingAnalyzerConsent, } from "../orchestrator/hostInputPause.js";
27
- import { runAuditStep } from "./auditStep.js";
28
- import { writeHandoffOnly, ensureSemanticReviewRun, loadCurrentActiveReviewRun, } from "./reviewRun.js";
28
+ import { runAuditStepUnlocked, withArtifactTreeHold } from "./auditStep.js";
29
+ import { writeHandoffOnly, ensureSemanticReviewRunUnlocked, loadCurrentActiveReviewRun, } from "./reviewRun.js";
30
+ import { sizeIndexFromManifest } from "../orchestrator/reviewPackets.js";
29
31
  import { buildPendingAuditTasks } from "./dispatch.js";
30
32
  import { buildLineIndex } from "./lineIndex.js";
31
33
  import { ingestAuditHostResults, } from "./dispatch/hostHandoff.js";
@@ -36,8 +38,31 @@ import { CHARTER_EXTRACTION_MERGED_FILENAME, GATE_LANES, charterExtractionLane,
36
38
  * `absent` on ENOENT-family errors; `malformed` when the file exists but is not
37
39
  * JSON — submitted content is the CALLER's to quarantine, never an
38
40
  * infrastructure failure. All other IO errors re-throw unchanged.
41
+ *
42
+ * With a fold transaction the submission is STAGED first — renamed into
43
+ * `submission-staging/` before anything parses it (CX-02 landing 3), so a
44
+ * crash mid-consumption is recoverable and `path` names the STAGED file: the
45
+ * quarantine helpers move it from there, and `markSubmissionApplied` (never a
46
+ * direct unlink) schedules its commit-time deletion. Without a transaction the
47
+ * read is a pure probe of the bound path — nothing moves.
39
48
  */
40
- export async function tryConsumeSubmission(artifactsDir, lane) {
49
+ export async function tryConsumeSubmission(artifactsDir, lane, tx) {
50
+ if (tx) {
51
+ const stagedResult = await stageLaneSubmission(tx, artifactsDir, lane);
52
+ if (stagedResult.status === "absent")
53
+ return { status: "absent" };
54
+ const { stagingPath, contentHash } = stagedResult.staged;
55
+ try {
56
+ const value = await readJsonFile(stagingPath);
57
+ return { status: "ok", value, path: stagingPath, contentHash };
58
+ }
59
+ catch (error) {
60
+ if (isJsonParseError(error)) {
61
+ return { status: "malformed", path: stagingPath, reason: error.message };
62
+ }
63
+ throw error;
64
+ }
65
+ }
41
66
  const filePath = laneSubmissionPath(artifactsDir, lane);
42
67
  try {
43
68
  const value = await readJsonFile(filePath);
@@ -194,7 +219,7 @@ export async function buildTerminalStep(params, bundle, state, blockedReason) {
194
219
  * (`return`), so applicable consent-gated candidates are never silently
195
220
  * skipped (the silent-fail-closed defect this program exists to fix).
196
221
  */
197
- export async function handleAnalyzerConsentBranch(params, bundle, state, analyzersRef) {
222
+ export async function handleAnalyzerConsentBranch(params, bundle, state, analyzersRef, tx) {
198
223
  const pending = pendingAnalyzerConsent({
199
224
  root: params.root,
200
225
  analyzers: analyzersRef.value,
@@ -207,7 +232,7 @@ export async function handleAnalyzerConsentBranch(params, bundle, state, analyze
207
232
  });
208
233
  if (pending.length === 0)
209
234
  return { action: "fallthrough" };
210
- const incoming = await consumeEnumMapSubmission(params.artifactsDir, GATE_LANES.analyzer_consent, ANALYZER_CONSENT_VALUES);
235
+ const incoming = await consumeEnumMapSubmission(params.artifactsDir, GATE_LANES.analyzer_consent, ANALYZER_CONSENT_VALUES, tx);
211
236
  if (incoming.status === "quarantined") {
212
237
  return { action: "continue" };
213
238
  }
@@ -244,13 +269,12 @@ export async function handleAnalyzerConsentBranch(params, bundle, state, analyze
244
269
  };
245
270
  }
246
271
  }
247
- await unlink(incoming.path).catch(() => { });
248
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.analyzer_consent, {
249
- kind: "accepted",
250
- ...(incoming.ignored.length > 0
251
- ? { message: describeIgnoredKeys(incoming.ignored, ANALYZER_CONSENT_VALUES) }
252
- : {}),
253
- });
272
+ // Deletion + the accepted ledger event are COMMIT-phase (the consent
273
+ // persist above is durable-by-design and idempotent, so a crash-replay
274
+ // re-applies it harmlessly while the staged file is restored).
275
+ markSubmissionApplied(tx, incoming.path, incoming.ignored.length > 0
276
+ ? describeIgnoredKeys(incoming.ignored, ANALYZER_CONSENT_VALUES)
277
+ : undefined);
254
278
  return { action: "continue" };
255
279
  }
256
280
  return { action: "return", result: { kind: "analyzer_consent", state, bundle, pending } };
@@ -263,11 +287,11 @@ export async function handleAnalyzerConsentBranch(params, bundle, state, analyze
263
287
  * - `return` → caller should emit the embedded result to cmdNextStep.
264
288
  * - `fallthrough` → nothing submitted; run the deterministic executor.
265
289
  */
266
- export async function handleGraphEnrichmentBranch(params, bundle, state, analyzersRef, deps = {}) {
267
- const runStep = deps.runStep ?? runAuditStep;
268
- // Fold-level pause detection is single-sourced in `hostInputPause` so the drain
269
- // stop predicate (`nextStepPausesForHostInput`) and this fold agree EXACTLY on
270
- // when the analyzer-install consent / edge-reasoning turns are owed.
290
+ export async function handleGraphEnrichmentBranch(params, bundle, state, analyzersRef, tx, deps = {}) {
291
+ const runStep = deps.runStep ?? runAuditStepUnlocked;
292
+ // Fold-level pause detection is single-sourced in `hostInputPause` so the
293
+ // plan draw's classifier (`obligationPolicy.ts`) and this fold agree EXACTLY
294
+ // on when the analyzer-install consent / edge-reasoning turns are owed.
271
295
  const pauseInputs = {
272
296
  root: params.root,
273
297
  analyzers: analyzersRef.value,
@@ -275,24 +299,20 @@ export async function handleGraphEnrichmentBranch(params, bundle, state, analyze
275
299
  };
276
300
  const unresolved = (deps.unresolvedAnalyzers ?? graphEnrichmentUnresolvedAnalyzers)(bundle, pauseInputs);
277
301
  if (unresolved.length > 0) {
278
- const incoming = await consumeEnumMapSubmission(params.artifactsDir, GATE_LANES.analyzer_decisions, ANALYZER_SETTING_VALUES);
302
+ const incoming = await consumeEnumMapSubmission(params.artifactsDir, GATE_LANES.analyzer_decisions, ANALYZER_SETTING_VALUES, tx);
279
303
  if (incoming.status === "quarantined") {
280
304
  // A non-object top-level value used to be neither merged, deleted, nor
281
305
  // diagnosed — the file lingered at its bound path and the analyzer_install
282
306
  // step re-emitted silently forever. Quarantined + diagnosed instead.
283
- return { action: "continue" };
307
+ return { action: "continue", bundle };
284
308
  }
285
309
  if (incoming.status === "ok") {
286
310
  const merged = await persistAnalyzerSettings(params.root, incoming.values);
287
311
  analyzersRef.value = merged.analyzers;
288
- await unlink(incoming.path).catch(() => { });
289
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.analyzer_decisions, {
290
- kind: "accepted",
291
- ...(incoming.ignored.length > 0
292
- ? { message: describeIgnoredKeys(incoming.ignored, ANALYZER_SETTING_VALUES) }
293
- : {}),
294
- });
295
- return { action: "continue" };
312
+ markSubmissionApplied(tx, incoming.path, incoming.ignored.length > 0
313
+ ? describeIgnoredKeys(incoming.ignored, ANALYZER_SETTING_VALUES)
314
+ : undefined);
315
+ return { action: "continue", bundle };
296
316
  }
297
317
  return { action: "return", result: { kind: "analyzer_install", state, bundle, unresolved } };
298
318
  }
@@ -307,7 +327,7 @@ export async function handleGraphEnrichmentBranch(params, bundle, state, analyze
307
327
  {
308
328
  const candidates = graphEnrichmentLowConfidenceEdges(bundle, pauseInputs);
309
329
  if (candidates.length > 0) {
310
- const edgeReasoningIncoming = await tryConsumeSubmission(params.artifactsDir, GATE_LANES.edge_reasoning);
330
+ const edgeReasoningIncoming = await tryConsumeSubmission(params.artifactsDir, GATE_LANES.edge_reasoning, tx);
311
331
  if (edgeReasoningIncoming.status === "malformed") {
312
332
  const quarantinePath = await quarantineSubmissionFile(params.artifactsDir, edgeReasoningIncoming.path, GATE_LANES.edge_reasoning);
313
333
  await recordEdgeReasoningRejection(params.artifactsDir, {
@@ -321,7 +341,7 @@ export async function handleGraphEnrichmentBranch(params, bundle, state, analyze
321
341
  issueCode: "submission_malformed",
322
342
  message: edgeReasoningIncoming.reason,
323
343
  });
324
- return { action: "continue" };
344
+ return { action: "continue", bundle };
325
345
  }
326
346
  if (edgeReasoningIncoming.status === "ok") {
327
347
  // Same hazard class as the design-review quarantine fix: a malformed
@@ -346,24 +366,25 @@ export async function handleGraphEnrichmentBranch(params, bundle, state, analyze
346
366
  issueCode: "submission_contract_invalid",
347
367
  message: unwrapped.reason,
348
368
  });
349
- return { action: "continue" };
369
+ return { action: "continue", bundle };
350
370
  }
351
- // Apply BEFORE deleting the submission: if runStep throws (locks,
352
- // crash), the submission survives for the retry instead of being lost.
353
- await runStep({
371
+ // Apply BEFORE the deletion commits: if runStep throws, the staged
372
+ // submission is RESTORED for the retry instead of being lost. The
373
+ // executor is forced explicitly — the fold's engine selected this
374
+ // obligation, so the selection is graph enrichment by construction,
375
+ // and an unforced call would re-enter a drain.
376
+ const applied = await runStep({
354
377
  root: params.root,
355
378
  artifactsDir: params.artifactsDir,
356
379
  analyzers: analyzersRef.value,
357
380
  graphLlmEdgeReasoning: true,
381
+ preferredExecutor: "graph_enrichment_executor",
358
382
  edgeReasoningResults: { rewrites: unwrapped.array },
359
383
  since: params.since,
360
- });
361
- await unlink(edgeReasoningIncoming.path).catch(() => { });
384
+ }, bundle);
385
+ markSubmissionApplied(tx, edgeReasoningIncoming.path);
362
386
  await clearEdgeReasoningRejection(params.artifactsDir);
363
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.edge_reasoning, {
364
- kind: "accepted",
365
- });
366
- return { action: "continue" };
387
+ return { action: "continue", bundle: applied.updated_bundle };
367
388
  }
368
389
  return { action: "return", result: { kind: "edge_reasoning", state, bundle, candidates } };
369
390
  }
@@ -390,32 +411,8 @@ function passIsStale(bundle, pass) {
390
411
  const snapshot = bundle.design_review_snapshots?.[pass];
391
412
  return snapshot ? isDesignReviewStale(snapshot, bundle) : false;
392
413
  }
393
- /**
394
- * Move a refused submission to `<artifactsDir>/quarantine/` rather than
395
- * deleting it. Falls back to copy+unlink if `rename` fails (e.g. a cross-device
396
- * artifacts mount) so the content is never lost. The quarantined file is named
397
- * for its LANE — the bound path is a digest, which tells an operator nothing.
398
- */
399
- async function quarantineSubmissionFile(artifactsDir, filePath, lane) {
400
- const quarantineDir = join(artifactsDir, "quarantine");
401
- await mkdir(quarantineDir, { recursive: true });
402
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
403
- const quarantinePath = join(quarantineDir, `${lane}.${timestamp}.json`);
404
- try {
405
- await rename(filePath, quarantinePath);
406
- }
407
- catch {
408
- try {
409
- const content = await readFile(filePath, "utf8");
410
- await writeFile(quarantinePath, content, "utf8");
411
- }
412
- catch {
413
- // Best-effort: nothing left to quarantine if even the read failed.
414
- }
415
- await unlink(filePath).catch(() => { });
416
- }
417
- return quarantinePath;
418
- }
414
+ // `quarantineSubmissionFile` moved to `foldTransaction.ts` (the staging/commit
415
+ // module owns submission-file lifecycle mechanics); imported above.
419
416
  /**
420
417
  * Quarantine a submission that failed zod validation — or failed to parse as
421
418
  * JSON at all (a plain string reason): move it off its bound path (never
@@ -452,8 +449,8 @@ async function quarantineMisshapedSubmission(artifactsDir, filePath, lane, error
452
449
  * it has applied the value, so a submission is never destroyed before its
453
450
  * content has landed somewhere else.
454
451
  */
455
- export async function consumeArraySubmission(artifactsDir, lane) {
456
- const incoming = await tryConsumeSubmission(artifactsDir, lane);
452
+ export async function consumeArraySubmission(artifactsDir, lane, tx) {
453
+ const incoming = await tryConsumeSubmission(artifactsDir, lane, tx);
457
454
  if (incoming.status === "absent")
458
455
  return { status: "absent" };
459
456
  if (incoming.status === "malformed") {
@@ -487,8 +484,8 @@ export async function consumeArraySubmission(artifactsDir, lane) {
487
484
  * caller unlinks after applying, so a crash mid-apply retains the submission
488
485
  * for the retry.
489
486
  */
490
- export async function consumeObjectSubmission(artifactsDir, lane) {
491
- const incoming = await tryConsumeSubmission(artifactsDir, lane);
487
+ export async function consumeObjectSubmission(artifactsDir, lane, tx) {
488
+ const incoming = await tryConsumeSubmission(artifactsDir, lane, tx);
492
489
  if (incoming.status === "absent")
493
490
  return { status: "absent" };
494
491
  if (incoming.status === "malformed") {
@@ -546,8 +543,8 @@ function describeIgnoredKeys(ignored, allowed) {
546
543
  * on the accepting event so the omission is on the record rather than in a
547
544
  * stderr line nobody kept.
548
545
  */
549
- async function consumeEnumMapSubmission(artifactsDir, lane, allowed) {
550
- const incoming = await consumeObjectSubmission(artifactsDir, lane);
546
+ async function consumeEnumMapSubmission(artifactsDir, lane, allowed, tx) {
547
+ const incoming = await consumeObjectSubmission(artifactsDir, lane, tx);
551
548
  if (incoming.status === "absent")
552
549
  return { status: "absent" };
553
550
  if (incoming.status === "quarantined")
@@ -636,9 +633,9 @@ export async function renderEdgeReasoningRejectionNotice(artifactsDir) {
636
633
  * here. Two appends per refusal made a single rejection read as two on a record
637
634
  * whose whole value is counting them.
638
635
  */
639
- async function recordRejectedDesignReviewSubmission(artifactsDir, existing, pass, quarantined) {
636
+ function withRejectedDesignReviewSubmission(existing, pass, quarantined) {
640
637
  if (!existing)
641
- return;
638
+ return undefined;
642
639
  const entry = {
643
640
  pass,
644
641
  lane: quarantined.lane,
@@ -646,11 +643,17 @@ async function recordRejectedDesignReviewSubmission(artifactsDir, existing, pass
646
643
  reason: quarantined.reason,
647
644
  rejected_at: new Date().toISOString(),
648
645
  };
649
- existing.rejected_submissions = [
650
- ...(existing.rejected_submissions ?? []).filter((r) => r.pass !== pass),
651
- entry,
652
- ];
653
- await writeJsonFile(join(artifactsDir, "design_assessment.json"), existing);
646
+ // PURE rebuild — never a mutation of the carried assessment. Both derive
647
+ // memos key on bundle identity, so an in-place edit would let an earlier
648
+ // carry observe a later change (the aliasing hazard CX-02's record pins);
649
+ // the persisted write is the fold's single core commit, not this site.
650
+ return {
651
+ ...existing,
652
+ rejected_submissions: [
653
+ ...(existing.rejected_submissions ?? []).filter((r) => r.pass !== pass),
654
+ entry,
655
+ ],
656
+ };
654
657
  }
655
658
  /**
656
659
  * Render a host-facing notice for any pending quarantined design-review
@@ -677,8 +680,22 @@ export function renderDesignReviewRejectionNotice(bundle, passes) {
677
680
  'one array-valued property (e.g. `{"findings": [...]}`).');
678
681
  return lines.join("\n");
679
682
  }
680
- export async function handleDesignReviewBranch(params, bundle, state) {
681
- const existing = bundle.design_assessment;
683
+ export async function handleDesignReviewBranch(params, bundle, state, tx) {
684
+ // Working copies, replaced IMMUTABLY on every change — never a mutation of
685
+ // the carried bundle's nested objects (the aliasing hazard the CX-02 record
686
+ // pins: both derive memos key on bundle identity, and an in-place edit lets
687
+ // an earlier carry observe a later change). `carried()` assembles the fresh
688
+ // bundle every path hands back.
689
+ let assessment = bundle.design_assessment;
690
+ let snapshots = bundle.design_review_snapshots;
691
+ const carried = () => {
692
+ const next = { ...bundle };
693
+ if (assessment !== undefined)
694
+ next.design_assessment = assessment;
695
+ if (snapshots !== undefined)
696
+ next.design_review_snapshots = snapshots;
697
+ return next;
698
+ };
682
699
  /**
683
700
  * P25-f: a valid submission with nowhere to merge it is HELD and RECORDED,
684
701
  * never consumed-and-dropped. `design_assessment` is normally present by the
@@ -705,107 +722,121 @@ export async function handleDesignReviewBranch(params, bundle, state) {
705
722
  issueCode: "submission_rejected",
706
723
  message: `${pass} pass: ${reason}`,
707
724
  });
708
- await recordRejectedDesignReviewSubmission(params.artifactsDir, existing, pass, {
709
- status: "quarantined",
710
- quarantinePath,
711
- lane,
712
- reason,
713
- });
725
+ assessment =
726
+ withRejectedDesignReviewSubmission(assessment, pass, {
727
+ status: "quarantined",
728
+ quarantinePath,
729
+ lane,
730
+ reason,
731
+ }) ?? assessment;
714
732
  };
715
733
  // Legacy: consume the old combined findings submission. Tolerant-unwrap or
716
734
  // quarantine (never a bare unconditional delete) — see the block comment above.
717
- const legacyResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_legacy);
735
+ const legacyResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_legacy, tx);
718
736
  if (legacyResult.status === "quarantined") {
719
- await recordRejectedDesignReviewSubmission(params.artifactsDir, existing, "legacy", legacyResult);
720
- return { action: "continue" };
737
+ assessment =
738
+ withRejectedDesignReviewSubmission(assessment, "legacy", legacyResult) ??
739
+ assessment;
740
+ return { action: "continue", bundle: carried() };
721
741
  }
722
742
  if (legacyResult.status === "ok") {
723
- if (existing) {
724
- existing.review_findings = groundDesignFindings(legacyResult.value, bundle.repo_manifest);
725
- existing.reviewed = true;
726
- existing.rejected_submissions = (existing.rejected_submissions ?? []).filter((r) => r.pass !== "legacy");
727
- await writeJsonFile(join(params.artifactsDir, "design_assessment.json"), existing);
728
- await unlink(legacyResult.path).catch(() => { });
729
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.design_review_legacy, {
730
- kind: "accepted",
731
- });
732
- return { action: "continue" };
743
+ if (assessment) {
744
+ assessment = {
745
+ ...assessment,
746
+ review_findings: groundDesignFindings(legacyResult.value, bundle.repo_manifest),
747
+ reviewed: true,
748
+ rejected_submissions: (assessment.rejected_submissions ?? []).filter((r) => r.pass !== "legacy"),
749
+ };
750
+ markSubmissionApplied(tx, legacyResult.path);
751
+ return { action: "continue", bundle: carried() };
733
752
  }
734
753
  await holdWithoutTarget("legacy", GATE_LANES.design_review_legacy, legacyResult.path);
735
- return { action: "continue" };
754
+ return { action: "continue", bundle: carried() };
736
755
  }
737
756
  // absent: fall through to the contract/conceptual check.
738
757
  // New: consume contract-findings and/or conceptual-findings independently.
739
- const contractResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_contract);
740
- const conceptualResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_conceptual);
758
+ const contractResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_contract, tx);
759
+ const conceptualResult = await consumeArraySubmission(params.artifactsDir, GATE_LANES.design_review_conceptual, tx);
741
760
  let consumed = false;
742
761
  if (contractResult.status === "quarantined") {
743
- await recordRejectedDesignReviewSubmission(params.artifactsDir, existing, "contract", contractResult);
762
+ assessment =
763
+ withRejectedDesignReviewSubmission(assessment, "contract", contractResult) ??
764
+ assessment;
744
765
  }
745
- else if (contractResult.status === "ok" && existing) {
746
- existing.contract_findings = groundDesignFindings(contractResult.value, bundle.repo_manifest);
747
- existing.contract_reviewed = true;
748
- existing.rejected_submissions = (existing.rejected_submissions ?? []).filter((r) => r.pass !== "contract");
766
+ else if (contractResult.status === "ok" && assessment) {
767
+ assessment = {
768
+ ...assessment,
769
+ contract_findings: groundDesignFindings(contractResult.value, bundle.repo_manifest),
770
+ contract_reviewed: true,
771
+ rejected_submissions: (assessment.rejected_submissions ?? []).filter((r) => r.pass !== "contract"),
772
+ };
749
773
  consumed = true;
750
774
  }
751
775
  else if (contractResult.status === "ok") {
752
776
  await holdWithoutTarget("contract", GATE_LANES.design_review_contract, contractResult.path);
753
777
  }
754
778
  if (conceptualResult.status === "quarantined") {
755
- await recordRejectedDesignReviewSubmission(params.artifactsDir, existing, "conceptual", conceptualResult);
779
+ assessment =
780
+ withRejectedDesignReviewSubmission(assessment, "conceptual", conceptualResult) ??
781
+ assessment;
756
782
  }
757
- else if (conceptualResult.status === "ok" && existing) {
758
- existing.conceptual_findings = groundDesignFindings(conceptualResult.value, bundle.repo_manifest);
759
- existing.conceptual_reviewed = true;
760
- existing.rejected_submissions = (existing.rejected_submissions ?? []).filter((r) => r.pass !== "conceptual");
783
+ else if (conceptualResult.status === "ok" && assessment) {
784
+ assessment = {
785
+ ...assessment,
786
+ conceptual_findings: groundDesignFindings(conceptualResult.value, bundle.repo_manifest),
787
+ conceptual_reviewed: true,
788
+ rejected_submissions: (assessment.rejected_submissions ?? []).filter((r) => r.pass !== "conceptual"),
789
+ };
761
790
  consumed = true;
762
791
  }
763
792
  else if (conceptualResult.status === "ok") {
764
793
  await holdWithoutTarget("conceptual", GATE_LANES.design_review_conceptual, conceptualResult.path);
765
794
  }
766
- if (consumed && existing) {
767
- await writeJsonFile(join(params.artifactsDir, "design_assessment.json"), existing);
795
+ if (consumed && assessment) {
768
796
  // Snapshot each just-completed pass (B2 parity port): record the verdict +
769
797
  // the semantic projection of the structural inputs it reviewed, so a later
770
798
  // upstream change re-stales the pass and the re-emit can be diff-scoped
771
- // rather than a blind full re-run. Capture after the design_assessment write
772
- // so the projection reflects the persisted findings.
799
+ // rather than a blind full re-run. The snapshot VALUE rides the carried
800
+ // bundle (so this fold's own staleness derivation sees the pass fresh) and
801
+ // its WRITE is staged on the transaction, committing WITH the core — a
802
+ // snapshot lost between fold and commit would silently mark a completed
803
+ // pass satisfied (CX-02 landing 2).
773
804
  const reviewedAt = new Date().toISOString();
774
805
  if (contractResult.status === "ok") {
775
- await captureDesignReviewSnapshot(params.artifactsDir, "contract", existing.contract_findings ?? [], bundle, reviewedAt);
776
- // Unlink only now: the findings are persisted and snapshotted, so the
777
- // submission's content survives its own deletion.
778
- await unlink(contractResult.path).catch(() => { });
779
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.design_review_contract, {
780
- kind: "accepted",
781
- });
806
+ const snapshot = buildDesignReviewSnapshot("contract", assessment.contract_findings ?? [], carried(), reviewedAt);
807
+ tx.pendingSnapshots.push(snapshot);
808
+ snapshots = { ...(snapshots ?? {}), contract: snapshot };
809
+ markSubmissionApplied(tx, contractResult.path);
782
810
  }
783
811
  if (conceptualResult.status === "ok") {
784
- await captureDesignReviewSnapshot(params.artifactsDir, "conceptual", existing.conceptual_findings ?? [], bundle, reviewedAt);
785
- await unlink(conceptualResult.path).catch(() => { });
786
- await recordLaneOutcome(params.artifactsDir, GATE_LANES.design_review_conceptual, {
787
- kind: "accepted",
788
- });
812
+ const snapshot = buildDesignReviewSnapshot("conceptual", assessment.conceptual_findings ?? [], carried(), reviewedAt);
813
+ tx.pendingSnapshots.push(snapshot);
814
+ snapshots = { ...(snapshots ?? {}), conceptual: snapshot };
815
+ markSubmissionApplied(tx, conceptualResult.path);
789
816
  }
790
- return { action: "continue" };
817
+ return { action: "continue", bundle: carried() };
791
818
  }
792
819
  // Determine which passes still need to run. A completed pass whose snapshot has
793
820
  // gone stale (a structural input changed in projection) is NOT done — it must
794
821
  // re-run as a diff-based re-review. This mirrors the obligation staleness in
795
- // `designReviewPassState`.
796
- const contractDone = existing?.contract_reviewed === true && !passIsStale(bundle, "contract");
797
- const conceptualDone = existing?.conceptual_reviewed === true && !passIsStale(bundle, "conceptual");
822
+ // `designReviewPassState`. Checked against the CARRIED bundle so a rejection
823
+ // note recorded above reaches the re-emitted step's renderer.
824
+ const current = carried();
825
+ const contractDone = current.design_assessment?.contract_reviewed === true &&
826
+ !passIsStale(current, "contract");
827
+ const conceptualDone = current.design_assessment?.conceptual_reviewed === true &&
828
+ !passIsStale(current, "conceptual");
798
829
  if (!contractDone && !conceptualDone) {
799
- return { action: "return", result: { kind: "design_review_parallel", state, bundle } };
830
+ return { action: "return", result: { kind: "design_review_parallel", state, bundle: current } };
800
831
  }
801
832
  if (!contractDone) {
802
- return { action: "return", result: { kind: "design_review_contract", state, bundle } };
833
+ return { action: "return", result: { kind: "design_review_contract", state, bundle: current } };
803
834
  }
804
835
  if (!conceptualDone) {
805
- return { action: "return", result: { kind: "design_review_conceptual", state, bundle } };
836
+ return { action: "return", result: { kind: "design_review_conceptual", state, bundle: current } };
806
837
  }
807
838
  // Both done — should not normally reach here (obligations would be satisfied).
808
- return { action: "continue" };
839
+ return { action: "continue", bundle: current };
809
840
  }
810
841
  /**
811
842
  * Drive one "poll the lane → apply+continue, else omit-or-return" gate — the
@@ -813,8 +844,8 @@ export async function handleDesignReviewBranch(params, bundle, state) {
813
844
  * charter_clarification, and systemic_challenge. See the section comment
814
845
  * above for the two gates that deviate and are not run through this engine.
815
846
  */
816
- async function runOmittableGate(descriptor, params, bundle, state) {
817
- const incoming = await tryConsumeSubmission(params.artifactsDir, descriptor.lane);
847
+ async function runOmittableGate(descriptor, params, bundle, state, tx) {
848
+ const incoming = await tryConsumeSubmission(params.artifactsDir, descriptor.lane, tx);
818
849
  if (incoming.status === "malformed") {
819
850
  // Not-JSON submission: same quarantine-loudly lifecycle as a mis-shaped one.
820
851
  await quarantineMisshapedSubmission(params.artifactsDir, incoming.path, descriptor.lane, incoming.reason);
@@ -822,10 +853,11 @@ async function runOmittableGate(descriptor, params, bundle, state) {
822
853
  else if (incoming.status === "ok") {
823
854
  const parsed = descriptor.schema.safeParse(incoming.value);
824
855
  if (parsed.success) {
825
- await descriptor.apply(parsed.data, incoming.path, params);
826
- await unlink(incoming.path).catch(() => { });
827
- await recordLaneOutcome(params.artifactsDir, descriptor.lane, { kind: "accepted" });
828
- return { action: "continue" };
856
+ const applied = await descriptor.apply(parsed.data, incoming.path, params, bundle, { contentHash: incoming.contentHash });
857
+ // Deletion + the accepted event commit WITH the core write; a throw
858
+ // between here and the commit restores the staged submission instead.
859
+ markSubmissionApplied(tx, incoming.path);
860
+ return { action: "continue", bundle: applied.updated_bundle };
829
861
  }
830
862
  // Mis-shaped submission: quarantine loudly and fall through to
831
863
  // shouldOmit/return — never hand it to the executor to crash on or silently
@@ -849,22 +881,21 @@ async function runOmittableGate(descriptor, params, bundle, state) {
849
881
  * This MUST make progress, never a no-op reload — otherwise the obligation
850
882
  * stays actionable and the fold spins (the guards do not cover this branch).
851
883
  */
852
- export async function handleSynthesisNarrativeBranch(params, bundle, state) {
884
+ export async function handleSynthesisNarrativeBranch(params, bundle, state, tx) {
853
885
  return runOmittableGate({
854
886
  kind: "synthesis_narrative",
855
887
  lane: GATE_LANES.synthesis_narrative,
856
888
  schema: LANE_SUBMISSION_SCHEMAS[GATE_LANES.synthesis_narrative],
857
- apply: async (_value, path, p) => {
858
- await runAuditStep({
859
- root: p.root,
860
- artifactsDir: p.artifactsDir,
861
- preferredExecutor: "synthesis_narrative_executor",
862
- narrativeResultsPath: path,
863
- });
864
- },
889
+ apply: (_value, path, p, foldBundle) => runAuditStepUnlocked({
890
+ root: p.root,
891
+ artifactsDir: p.artifactsDir,
892
+ preferredExecutor: "synthesis_narrative_executor",
893
+ narrativeResultsPath: path,
894
+ }, foldBundle),
865
895
  // Narrative disabled: omit (run the deterministic omit executor below).
866
- shouldOmit: () => !params.narrativeEnabled,
867
- }, params, bundle, state);
896
+ // Single-sourced with the plan draw's classifier (obligationPolicy.ts).
897
+ shouldOmit: () => synthesisNarrativeOmits({ narrativeEnabled: params.narrativeEnabled }),
898
+ }, params, bundle, state, tx);
868
899
  }
869
900
  /**
870
901
  * Handle the `intent_equivalence_executor` polling block (DD-9). Deviates from
@@ -877,9 +908,9 @@ export async function handleSynthesisNarrativeBranch(params, bundle, state) {
877
908
  * gate-version-stale / structured delta) — run the executor, stay drainable.
878
909
  * - `return` → a prose-only delta awaits the host judge; emit the step.
879
910
  */
880
- export async function handleIntentEquivalenceBranch(params, bundle, state) {
911
+ export async function handleIntentEquivalenceBranch(params, bundle, state, tx) {
881
912
  const lane = GATE_LANES.intent_equivalence;
882
- const incoming = await tryConsumeSubmission(params.artifactsDir, lane);
913
+ const incoming = await tryConsumeSubmission(params.artifactsDir, lane, tx);
883
914
  if (incoming.status === "malformed") {
884
915
  await quarantineMisshapedSubmission(params.artifactsDir, incoming.path, lane, incoming.reason);
885
916
  // Fall through: no valid submission — re-emit or deterministically resolve.
@@ -887,20 +918,20 @@ export async function handleIntentEquivalenceBranch(params, bundle, state) {
887
918
  else if (incoming.status === "ok") {
888
919
  const parsed = LANE_SUBMISSION_SCHEMAS[lane].safeParse(incoming.value);
889
920
  if (parsed.success) {
890
- await runAuditStep({
921
+ const applied = await runAuditStepUnlocked({
891
922
  root: params.root,
892
923
  artifactsDir: params.artifactsDir,
893
924
  preferredExecutor: "intent_equivalence_executor",
894
925
  intentEquivalenceVerdictPath: incoming.path,
895
- });
896
- await unlink(incoming.path).catch(() => { });
897
- await recordLaneOutcome(params.artifactsDir, lane, { kind: "accepted" });
898
- return { action: "continue" };
926
+ }, bundle);
927
+ markSubmissionApplied(tx, incoming.path);
928
+ return { action: "continue", bundle: applied.updated_bundle };
899
929
  }
900
930
  await quarantineMisshapedSubmission(params.artifactsDir, incoming.path, lane, parsed.error);
901
931
  // Fall through: no valid submission — re-emit or deterministically resolve.
902
932
  }
903
- if (deriveIntentEquivalenceStatus(bundle).kind !== "prose_judgment_pending") {
933
+ // Single-sourced with the plan draw's classifier (obligationPolicy.ts).
934
+ if (intentEquivalenceOmits(bundle)) {
904
935
  return { action: "run_omit" };
905
936
  }
906
937
  return { action: "return", result: { kind: "intent_equivalence", state, bundle } };
@@ -917,23 +948,21 @@ export async function handleIntentEquivalenceBranch(params, bundle, state) {
917
948
  * - `return` → no submission yet; emit the critical_flow_fallback host step.
918
949
  * `run_omit` is never returned (shouldOmit is constant-false).
919
950
  */
920
- export async function handleCriticalFlowFallbackBranch(params, bundle, state) {
951
+ export async function handleCriticalFlowFallbackBranch(params, bundle, state, tx) {
921
952
  return runOmittableGate({
922
953
  kind: "critical_flow_fallback",
923
954
  lane: GATE_LANES.critical_flow_fallback,
924
955
  schema: LANE_SUBMISSION_SCHEMAS[GATE_LANES.critical_flow_fallback],
925
- apply: async (_value, path, p) => {
926
- await runAuditStep({
927
- root: p.root,
928
- artifactsDir: p.artifactsDir,
929
- preferredExecutor: "critical_flow_fallback_executor",
930
- criticalFlowFallbackResultsPath: path,
931
- });
932
- },
956
+ apply: (_value, path, p, foldBundle) => runAuditStepUnlocked({
957
+ root: p.root,
958
+ artifactsDir: p.artifactsDir,
959
+ preferredExecutor: "critical_flow_fallback_executor",
960
+ criticalFlowFallbackResultsPath: path,
961
+ }, foldBundle),
933
962
  // Never omit: the obligation is only reached when the deterministic bar
934
963
  // failed, and the host is always available to author the enrichment.
935
964
  shouldOmit: () => false,
936
- }, params, bundle, state);
965
+ }, params, bundle, state, tx);
937
966
  }
938
967
  /**
939
968
  * Handle the `charter_extraction_executor` submission-polling block
@@ -946,10 +975,11 @@ export async function handleCriticalFlowFallbackBranch(params, bundle, state) {
946
975
  * - a `deep`/`deepest` ceiling with no submission yet → `return` the host step
947
976
  * that renders the charter-extraction prompt.
948
977
  */
949
- export async function handleCharterExtractionBranch(params, bundle, state) {
978
+ export async function handleCharterExtractionBranch(params, bundle, state, tx) {
950
979
  const ceiling = resolveCharterCeiling(bundle.intent_checkpoint);
951
980
  // Shallow ceiling (default): omit deterministically, no host turn, no lanes.
952
- if (!ceilingRequestsCharters(ceiling)) {
981
+ // Single-sourced with the plan draw's classifier (obligationPolicy.ts).
982
+ if (charterExtractionOmits(bundle)) {
953
983
  return { action: "run_omit" };
954
984
  }
955
985
  // Per-kind blind lanes (design resolution 2): one submission file per kind,
@@ -968,7 +998,10 @@ export async function handleCharterExtractionBranch(params, bundle, state) {
968
998
  let quarantinedAny = false;
969
999
  for (const kind of kinds) {
970
1000
  const lane = charterExtractionLane(kind);
971
- const incoming = await tryConsumeSubmission(params.artifactsDir, lane);
1001
+ // Staged per lane; an INCOMPLETE set is restored to its bound paths at
1002
+ // commit (un-applied), which is exactly the K-of-N resume the design
1003
+ // wants — pending lanes survive on disk until every lane is present.
1004
+ const incoming = await tryConsumeSubmission(params.artifactsDir, lane, tx);
972
1005
  if (incoming.status === "absent")
973
1006
  continue;
974
1007
  if (incoming.status === "malformed") {
@@ -1001,25 +1034,24 @@ export async function handleCharterExtractionBranch(params, bundle, state) {
1001
1034
  // wrote). It is handed to the executor by path and deleted after ingest.
1002
1035
  const mergedPath = join(laneAssetsDir(params.artifactsDir), CHARTER_EXTRACTION_MERGED_FILENAME);
1003
1036
  await writeJsonFile(mergedPath, merged);
1004
- await runAuditStep({
1037
+ const applied = await runAuditStepUnlocked({
1005
1038
  root: params.root,
1006
1039
  artifactsDir: params.artifactsDir,
1007
1040
  preferredExecutor: "charter_extraction_executor",
1008
1041
  charterSubmissionPath: mergedPath,
1009
- });
1042
+ }, bundle);
1010
1043
  await unlink(mergedPath).catch(() => { });
1011
- for (const [kind, lane] of laneValues.entries()) {
1012
- await unlink(lane.path).catch(() => { });
1013
- await recordLaneOutcome(params.artifactsDir, charterExtractionLane(kind), {
1014
- kind: "accepted",
1015
- });
1044
+ for (const lane of laneValues.values()) {
1045
+ // Deletion + accepted events commit WITH the core write (lane.path is
1046
+ // the STAGED file — a throw before the commit restores every lane).
1047
+ markSubmissionApplied(tx, lane.path);
1016
1048
  }
1017
1049
  // Evidence packets are consumed inputs like the lane submissions — a stale
1018
1050
  // packet left behind would feed a later re-extraction yesterday's evidence.
1019
1051
  for (const kind of kinds) {
1020
1052
  await unlink(join(laneAssetsDir(params.artifactsDir), charterExtractionPacketFilename(kind))).catch(() => { });
1021
1053
  }
1022
- return { action: "continue" };
1054
+ return { action: "continue", bundle: applied.updated_bundle };
1023
1055
  }
1024
1056
  // Missing or quarantined lane(s): a host turn is still owed — the emitter
1025
1057
  // re-materializes only the missing lanes (completed lane results stay).
@@ -1036,23 +1068,21 @@ export async function handleCharterExtractionBranch(params, bundle, state) {
1036
1068
  * - a `deltas_pending` register with no submission yet → `return` the host step
1037
1069
  * that renders the charter-delta prompt for the independent miner.
1038
1070
  */
1039
- export async function handleCharterDeltaBranch(params, bundle, state) {
1071
+ export async function handleCharterDeltaBranch(params, bundle, state, tx) {
1040
1072
  return runOmittableGate({
1041
1073
  kind: "charter_delta",
1042
1074
  lane: GATE_LANES.charter_delta,
1043
1075
  schema: LANE_SUBMISSION_SCHEMAS[GATE_LANES.charter_delta],
1044
- apply: async (_value, path, p) => {
1045
- await runAuditStep({
1046
- root: p.root,
1047
- artifactsDir: p.artifactsDir,
1048
- preferredExecutor: "charter_delta_executor",
1049
- charterDeltaSubmissionPath: path,
1050
- });
1051
- },
1076
+ apply: (_value, path, p, foldBundle) => runAuditStepUnlocked({
1077
+ root: p.root,
1078
+ artifactsDir: p.artifactsDir,
1079
+ preferredExecutor: "charter_delta_executor",
1080
+ charterDeltaSubmissionPath: path,
1081
+ }, foldBundle),
1052
1082
  // Nothing to mine (extraction omitted or no subsystems): settle
1053
1083
  // deterministically, no host turn.
1054
1084
  shouldOmit: (b) => !(b.charter_register?.deltas_pending === true),
1055
- }, params, bundle, state);
1085
+ }, params, bundle, state, tx);
1056
1086
  }
1057
1087
  /**
1058
1088
  * Handle the `charter_clarification_executor` obligation (Phase D triangulation
@@ -1068,39 +1098,21 @@ export async function handleCharterDeltaBranch(params, bundle, state) {
1068
1098
  * - once the register exists with ≥1 interactive `asked` question and no answers
1069
1099
  * yet → `return` the host step that relays the VOI queue.
1070
1100
  */
1071
- export async function handleCharterClarificationBranch(params, bundle, state) {
1101
+ export async function handleCharterClarificationBranch(params, bundle, state, tx) {
1072
1102
  return runOmittableGate({
1073
1103
  kind: "charter_clarification",
1074
1104
  lane: GATE_LANES.charter_clarification,
1075
1105
  schema: LANE_SUBMISSION_SCHEMAS[GATE_LANES.charter_clarification],
1076
- apply: async (_value, path, p) => {
1077
- await runAuditStep({
1078
- root: p.root,
1079
- artifactsDir: p.artifactsDir,
1080
- preferredExecutor: "charter_clarification_executor",
1081
- clarificationAnswersPath: path,
1082
- });
1083
- },
1084
- shouldOmit: (b) => {
1085
- const ceiling = resolveCharterCeiling(b.intent_checkpoint);
1086
- const attention = resolveClarificationAttention(b.intent_checkpoint);
1087
- // Shallow ceiling or autonomous (zero-attention) mode: assemble the
1088
- // register deterministically, no host turn (every question banks as
1089
- // a finding).
1090
- if (!ceilingRequestsCharters(ceiling) || attention === 0)
1091
- return true;
1092
- // The loop must be COMPUTED before we can relay a queue: if no register
1093
- // exists yet, run the deterministic assembler this turn (it partitions/
1094
- // ranks/gates/splits from the charter_register), then re-scan.
1095
- if (!b.charter_clarification)
1096
- return true;
1097
- // Register exists: relay the interactive queue only when there is one
1098
- // to ask.
1099
- if ((b.charter_clarification.asked?.length ?? 0) === 0)
1100
- return true;
1101
- return false;
1102
- },
1103
- }, params, bundle, state);
1106
+ apply: (_value, path, p, foldBundle) => runAuditStepUnlocked({
1107
+ root: p.root,
1108
+ artifactsDir: p.artifactsDir,
1109
+ preferredExecutor: "charter_clarification_executor",
1110
+ clarificationAnswersPath: path,
1111
+ }, foldBundle),
1112
+ // The omit predicate is single-sourced with the plan draw's classifier
1113
+ // (obligationPolicy.ts) — the two draws must agree on the boundary.
1114
+ shouldOmit: charterClarificationOmits,
1115
+ }, params, bundle, state, tx);
1104
1116
  }
1105
1117
  /**
1106
1118
  * Handle the `systemic_challenge_executor` obligation (Phase E — the second-order
@@ -1116,45 +1128,43 @@ export async function handleCharterClarificationBranch(params, bundle, state) {
1116
1128
  * A converged register satisfies the obligation, so this branch is never reached for
1117
1129
  * it (the priority scan skips a satisfied obligation).
1118
1130
  */
1119
- export async function handleSystemicChallengeBranch(params, bundle, state) {
1131
+ export async function handleSystemicChallengeBranch(params, bundle, state, tx) {
1120
1132
  return runOmittableGate({
1121
1133
  kind: "systemic_challenge",
1122
1134
  lane: GATE_LANES.systemic_challenge,
1123
1135
  schema: LANE_SUBMISSION_SCHEMAS[GATE_LANES.systemic_challenge],
1124
- apply: async (_value, path, p) => {
1125
- await runAuditStep({
1126
- root: p.root,
1127
- artifactsDir: p.artifactsDir,
1128
- preferredExecutor: "systemic_challenge_executor",
1129
- systemicChallengePath: path,
1130
- });
1131
- },
1132
- shouldOmit: (b) => {
1133
- // Shallow ceiling (default): omit deterministically, no host turn.
1134
- if (!ceilingRequestsCharters(resolveCharterCeiling(b.intent_checkpoint)))
1135
- return true;
1136
- // The loop must be OPENED before we can dispatch the adversary: if no
1137
- // register exists yet, run the deterministic executor this turn (it
1138
- // computes the metrics digest + writes an open register), then re-scan.
1139
- if (!b.systemic_challenge)
1140
- return true;
1141
- // A converged register is already satisfied (never reaches this branch
1142
- // in practice). An open register → dispatch the next
1143
- // second-order-adversary round.
1144
- if (b.systemic_challenge.converged)
1145
- return true;
1146
- return false;
1147
- },
1148
- }, params, bundle, state);
1136
+ apply: (_value, path, p, foldBundle, staged) => runAuditStepUnlocked({
1137
+ root: p.root,
1138
+ artifactsDir: p.artifactsDir,
1139
+ preferredExecutor: "systemic_challenge_executor",
1140
+ systemicChallengePath: path,
1141
+ // The iterative-fold duplicate guard: the executor's register
1142
+ // records every folded submission's content hash and IGNORES a
1143
+ // duplicate, so a crash-restored already-folded round can never
1144
+ // read as a quiet round and converge the adversary loop falsely.
1145
+ systemicChallengeSubmissionHash: staged.contentHash,
1146
+ }, foldBundle),
1147
+ // Single-sourced with the plan draw's classifier (obligationPolicy.ts).
1148
+ shouldOmit: systemicChallengeOmits,
1149
+ }, params, bundle, state, tx);
1149
1150
  }
1150
1151
  /**
1151
1152
  * Execute one deterministic audit step and record its progress. Throws (with
1152
- * cause) if the executor fails, preserving the existing throw-with-cause pattern.
1153
- * `index` is the 0-based fold position (the transition counter), surfaced as the
1154
- * 1-based `iteration` in the `deterministic-progress.json` marker a
1155
- * filesystem-watching host reads.
1153
+ * cause) if the executor fails, preserving the existing throw-with-cause
1154
+ * pattern. `index` is the 0-based deterministic-dispatch ordinal of this fold
1155
+ * call, surfaced as the 1-based `iteration` in the
1156
+ * `deterministic-progress.json` marker a filesystem-watching host reads
1157
+ * (semantics stated in the marker-protocol note on the fold driver: under one
1158
+ * drain it counts DISPATCHES of this call, no longer outer transitions).
1159
+ *
1160
+ * The dispatch is `runSingleAdvanceStep` on the fold's CARRIED bundle — one
1161
+ * step, no lock (the fold holds the one lock), no persist (the fold commits
1162
+ * once), no reload (disk holds the fold's pre-state). Failure attribution is
1163
+ * dispatch-local: a single step's failing identity IS its selection, recorded
1164
+ * on `failureRef` so the fold's commit-on-throw persists it without a second
1165
+ * lock acquisition (the deleted O2 RMW).
1156
1166
  */
1157
- export async function executeAndRecord(params, analyzers, decision, index, lastSummary) {
1167
+ export async function executeAndRecord(params, analyzers, decision, index, lastSummary, bundle, ctx) {
1158
1168
  try {
1159
1169
  // Write a "started" marker before execution so a host watching the filesystem
1160
1170
  // can tell which executor is active during a long-running step (OBS-0d4c2311).
@@ -1166,13 +1176,16 @@ export async function executeAndRecord(params, analyzers, decision, index, lastS
1166
1176
  status: "running",
1167
1177
  started_at: startedAt,
1168
1178
  });
1169
- const result = await runAuditStep({
1179
+ const indexes = await manifestIndexes(params.root, bundle, ctx.manifestIndexCache);
1180
+ const result = await runSingleAdvanceStep(bundle, {
1170
1181
  root: params.root,
1171
1182
  artifactsDir: params.artifactsDir,
1172
1183
  analyzers,
1173
1184
  graphLlmEdgeReasoning: params.graphLlmEdgeReasoning,
1174
1185
  externalAcquisition: params.externalAcquisition,
1175
1186
  since: params.since,
1187
+ lineIndex: indexes.lineIndex,
1188
+ sizeIndex: indexes.sizeIndex,
1176
1189
  });
1177
1190
  await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
1178
1191
  iteration: index + 1,
@@ -1187,32 +1200,23 @@ export async function executeAndRecord(params, analyzers, decision, index, lastS
1187
1200
  return result;
1188
1201
  }
1189
1202
  catch (error) {
1190
- // `runAuditStep` → `advanceAudit` DRAINS: this one call folds through
1191
- // successive obligations, so `decision` names only the FIRST one. Reporting
1192
- // it would attribute a failure in any later fold step to an executor that
1193
- // already SUCCEEDED (observed: a synthesis_executor blowup recorded against
1194
- // runtime_validation_executor). The failing identity is read off the error
1195
- // that carries it; `decision` is only the fallback for a throw raised before
1196
- // any executor was dispatched.
1203
+ // Dispatch-local attribution: this call ran ONE step, so its selection is
1204
+ // the failing identity; `findExecutorFailure` still reads a wrapped
1205
+ // `ExecutorFailure` when the runner threw one (a forced nested dispatch's
1206
+ // structured error contract survives).
1197
1207
  const failure = findExecutorFailure(error);
1198
1208
  const failedExecutor = failure?.executor ?? decision.selected_executor;
1199
1209
  const failedObligation = failure?.obligation ?? decision.selected_obligation;
1200
- // O2: error-recovery is itself a load→modify→persist artifact-tree mutation
1201
- // (runAuditStep has already released its lock by the time we reach this
1202
- // catch), so hold the artifact-tree lock across the whole RMW.
1203
- await withFileLock(artifactTreeLockPath(params.artifactsDir), async () => {
1204
- const current = await loadArtifactBundle(params.artifactsDir);
1205
- const currentState = deriveAuditState(current);
1206
- currentState.last_executor = failedExecutor ?? undefined;
1207
- currentState.last_obligation = failedObligation ?? undefined;
1208
- await writeCoreArtifacts(params.artifactsDir, { ...current, audit_state: currentState });
1209
- });
1210
+ // The fold's commit-on-throw persists this attribution in the SINGLE core
1211
+ // commit the old second lock acquisition here would deadlock against the
1212
+ // fold's own hold (`withFileLock` is non-reentrant).
1213
+ ctx.failureRef.value = { executor: failedExecutor, obligation: failedObligation };
1210
1214
  await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
1211
1215
  iteration: index + 1,
1212
1216
  last_executor: failedExecutor,
1213
1217
  last_obligation: failedObligation,
1214
- // The obligation the drain STARTED from, kept alongside the failing one so
1215
- // the fold position stays reconstructable from the marker alone.
1218
+ // The obligation the fold selected for this dispatch, kept alongside the
1219
+ // failing one so the fold position stays reconstructable from the marker.
1216
1220
  selected_executor: decision.selected_executor,
1217
1221
  selected_obligation: decision.selected_obligation,
1218
1222
  prior_summary: lastSummary || null,
@@ -1223,6 +1227,27 @@ export async function executeAndRecord(params, analyzers, decision, index, lastS
1223
1227
  throw new Error(`Deterministic executor ${failedExecutor} failed on obligation ${failedObligation} (iteration ${index + 1}, prior progress: ${lastSummary || "none"}): ${detail}`, { cause: error instanceof Error ? error : undefined });
1224
1228
  }
1225
1229
  }
1230
+ /**
1231
+ * Line/size indexes for a dispatch, memoized on `repo_manifest` IDENTITY: the
1232
+ * fold dispatches many steps per call and `buildLineIndex` walks real files,
1233
+ * so recomputing per dispatch would regress the drain's cost profile; the
1234
+ * manifest object is replaced whenever intake re-derives it, which is exactly
1235
+ * when the indexes must be rebuilt.
1236
+ */
1237
+ async function manifestIndexes(root, bundle, cache) {
1238
+ const manifest = bundle.repo_manifest;
1239
+ if (!manifest)
1240
+ return {};
1241
+ const cached = cache.get(manifest);
1242
+ if (cached)
1243
+ return cached;
1244
+ const built = {
1245
+ lineIndex: await buildLineIndex(root, manifest),
1246
+ sizeIndex: sizeIndexFromManifest(manifest),
1247
+ };
1248
+ cache.set(manifest, built);
1249
+ return built;
1250
+ }
1226
1251
  // ── Cycle guards (kept in audit's Ctx; NOT routed through advance) ─────────────
1227
1252
  //
1228
1253
  // HANDOFF approach-B: the shared `advance` engine is inherently 0-tolerance (it
@@ -1282,11 +1307,19 @@ export async function checkNoProgressBeforeDispatch(ctx) {
1282
1307
  "dispatched this run without net progress; stopping instead of looping.",
1283
1308
  timestamp: new Date().toISOString(),
1284
1309
  });
1285
- return buildTerminalStep(ctx.params, ctx.bundle, ctx.state, "No-progress guard: a deterministic executor was about to re-run on an " +
1286
- "artifact state it already processed this run without changing it " +
1287
- `(obligation ${ctx.selectedObligation ?? "unknown"}, executor ` +
1288
- `${ctx.selectedExecutor ?? "unknown"}). Stopping to avoid an infinite ` +
1289
- "no-progress loop.");
1310
+ // A terminal INTENT, not the built terminal: `buildTerminalStep` can
1311
+ // PROMOTE (which deletes artifactsDir), so it must run after the fold's
1312
+ // commit and outside its hold. The marker above is the in-fold record.
1313
+ return {
1314
+ kind: "terminal_intent",
1315
+ bundle: ctx.bundle,
1316
+ state: ctx.state,
1317
+ reason: "No-progress guard: a deterministic executor was about to re-run on an " +
1318
+ "artifact state it already processed this run without changing it " +
1319
+ `(obligation ${ctx.selectedObligation ?? "unknown"}, executor ` +
1320
+ `${ctx.selectedExecutor ?? "unknown"}). Stopping to avoid an infinite ` +
1321
+ "no-progress loop.",
1322
+ };
1290
1323
  }
1291
1324
  ctx.dispatchedSignatures.add(dispatchKey);
1292
1325
  return undefined;
@@ -1312,9 +1345,15 @@ export async function checkFinalizationCycle(ctx) {
1312
1345
  `progress; stopping. Cycling obligations: ${cycle.join(" -> ")}.`,
1313
1346
  timestamp: new Date().toISOString(),
1314
1347
  });
1315
- return buildTerminalStep(ctx.params, ctx.result.updated_bundle, ctx.result.audit_state, "Finalization is not converging: deterministic executors kept revisiting " +
1316
- `prior artifact states (${cycle.join(" -> ")}). Review whether these ` +
1317
- "obligations are erroneously invalidating each other.");
1348
+ // Intent, not the built terminal see checkNoProgressBeforeDispatch.
1349
+ return {
1350
+ kind: "terminal_intent",
1351
+ bundle: ctx.result.updated_bundle,
1352
+ state: ctx.result.audit_state,
1353
+ reason: "Finalization is not converging: deterministic executors kept revisiting " +
1354
+ `prior artifact states (${cycle.join(" -> ")}). Review whether these ` +
1355
+ "obligations are erroneously invalidating each other.",
1356
+ };
1318
1357
  }
1319
1358
  const EMPTY_FOLD_ADVISORIES = {
1320
1359
  ingestIssues: [],
@@ -1373,6 +1412,9 @@ function blockedFromResult(result) {
1373
1412
  bundle: result.updated_bundle,
1374
1413
  reason: result.progress_summary,
1375
1414
  },
1415
+ // The emit carries the advanced bundle so the fold's single commit
1416
+ // persists what this dispatch produced (audit_state included).
1417
+ state: result.updated_bundle,
1376
1418
  };
1377
1419
  }
1378
1420
  /**
@@ -1388,9 +1430,11 @@ function blockedFromResult(result) {
1388
1430
  * see the cycle-guard section comment.
1389
1431
  */
1390
1432
  async function runDeterministicExecutor(bundle, ctx) {
1391
- const decision = decideNextStep(bundle);
1433
+ // Emit-off: the fold's driver emits ONE consolidated staleness record at its
1434
+ // boundary (the preserve-list contract), so no in-fold derivation may emit.
1435
+ const decision = decideNextStep(bundle, { emitStaleness: false });
1392
1436
  const noProgress = await checkNoProgressBeforeDispatch({
1393
- index: ctx.iterationRef.value,
1437
+ index: ctx.dispatchOrdinalRef.value,
1394
1438
  dispatchedSignatures: ctx.dispatchedSignatures,
1395
1439
  params: ctx.params,
1396
1440
  bundle,
@@ -1398,15 +1442,16 @@ async function runDeterministicExecutor(bundle, ctx) {
1398
1442
  selectedObligation: decision.selected_obligation,
1399
1443
  selectedExecutor: decision.selected_executor,
1400
1444
  });
1401
- if (noProgress !== undefined)
1402
- return { kind: "emit", step: noProgress };
1403
- const result = await executeAndRecord(ctx.params, ctx.analyzersRef.value, decision, ctx.iterationRef.value, ctx.lastSummaryRef.value);
1445
+ if (noProgress !== undefined) {
1446
+ return { kind: "emit", step: noProgress, state: bundle };
1447
+ }
1448
+ const result = await executeAndRecord(ctx.params, ctx.analyzersRef.value, decision, ctx.dispatchOrdinalRef.value, ctx.lastSummaryRef.value, bundle, ctx);
1404
1449
  ctx.lastSummaryRef.value = result.progress_summary;
1405
1450
  if (!result.progress_made) {
1406
1451
  return blockedFromResult(result);
1407
1452
  }
1408
1453
  const cycle = await checkFinalizationCycle({
1409
- index: ctx.iterationRef.value,
1454
+ index: ctx.dispatchOrdinalRef.value,
1410
1455
  obligationTrail: ctx.obligationTrail,
1411
1456
  seenStateSignatures: ctx.seenStateSignatures,
1412
1457
  tolerance: FINALIZATION_CYCLE_TOLERANCE,
@@ -1416,9 +1461,15 @@ async function runDeterministicExecutor(bundle, ctx) {
1416
1461
  result,
1417
1462
  selectedObligation: decision.selected_obligation,
1418
1463
  });
1419
- if (cycle !== undefined)
1420
- return { kind: "emit", step: cycle };
1421
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1464
+ if (cycle !== undefined) {
1465
+ return { kind: "emit", step: cycle, state: result.updated_bundle };
1466
+ }
1467
+ // One counted, signed dispatch — the guards' unit (CX-02 constraint 1).
1468
+ ctx.dispatchOrdinalRef.value += 1;
1469
+ // The IN-MEMORY carry: the dispatch's own updated bundle, fresh identity by
1470
+ // construction. A disk reload here would read the fold's pre-state and
1471
+ // silently roll back everything the fold has done (persist-once).
1472
+ return { kind: "transition", state: result.updated_bundle };
1422
1473
  }
1423
1474
  /**
1424
1475
  * `derive` for an audit obligation: look up its precomputed satisfaction state
@@ -1434,25 +1485,23 @@ function deriveObligationState(id, cache) {
1434
1485
  return "satisfied";
1435
1486
  let state = cache.get(bundle);
1436
1487
  if (!state) {
1437
- // MEMOIZED per bundle IDENTITY, exactly as the inner drain's namesake in
1488
+ // MEMOIZED per bundle IDENTITY, exactly as the plan draw's namesake in
1438
1489
  // `orchestrator/advance.ts` is — and for the same reason. `advance` scans
1439
1490
  // by calling EVERY registered def's `derive`, so without this the fold
1440
1491
  // ran the holistic `deriveAuditState` once PER OBLIGATION per scan: 25
1441
- // full staleness passes to answer one question. That is the same
1442
- // regression `6145a1a3` measured and memoized away on the inner side; it
1443
- // was simply never applied here.
1492
+ // full staleness passes to answer one question (the regression
1493
+ // `6145a1a3` measured and memoized away).
1444
1494
  //
1445
1495
  // The key is safe for the same reason it is safe there: identity changes
1446
- // at every transition (each one loads or builds a fresh bundle), and the
1496
+ // at every transition (each one carries a fresh bundle), and the
1447
1497
  // `complete` gate above can only flip via a transition. So an entry can
1448
1498
  // never outlive the state it was derived under.
1449
1499
  //
1450
- // Emission stays at the DEFAULT here, unlike the inner drain's
1451
- // `emitStaleness: false`. This layer IS the boundary the inner one defers
1452
- // its record to, so it must still emit and now it emits once per scan
1453
- // rather than relying on `emitStalenessRecord`'s last-key latch to
1454
- // swallow 24 duplicates.
1455
- state = deriveAuditState(bundle);
1500
+ // EMIT-OFF, like every in-fold derivation (CX-02): the fold's DRIVER
1501
+ // emits the ONE consolidated staleness record at its boundary the
1502
+ // preserve-list contract so a derive that emitted per scan miss would
1503
+ // turn one call's cascade back into a record per carried bundle.
1504
+ state = deriveAuditState(bundle, { emitStaleness: false });
1456
1505
  cache.set(bundle, state);
1457
1506
  }
1458
1507
  const found = state.obligations.find((o) => o.id === id);
@@ -1473,250 +1522,194 @@ function deriveObligationState(id, cache) {
1473
1522
  * priority scan it mirrors.
1474
1523
  */
1475
1524
  export function buildAuditObligations() {
1476
- // One memo per registry construction, shared by every def in it the same
1477
- // shape `buildDrainObligations` uses. Scoped to the call so it dies with the
1478
- // fold it was built for, and keyed on bundle identity so it cannot outlive a
1479
- // transition.
1525
+ // One memo per registry construction, shared by every def in it. Scoped to
1526
+ // the call so it dies with the fold it was built for, and keyed on bundle
1527
+ // identity so it cannot outlive a transition.
1480
1528
  const cache = new WeakMap();
1481
- const deterministic = (id) => ({
1482
- id,
1483
- derive: deriveObligationState(id, cache),
1484
- execute: (bundle, ctx) => runDeterministicExecutor(bundle, ctx),
1485
- });
1486
- return [
1487
- deterministic("repo_manifest"),
1488
- deterministic("file_disposition"),
1489
- deterministic("auto_fixes_applied"),
1490
- deterministic("syntax_resolved"),
1491
- {
1492
- // External analyzers: the Item B consent fold runs FIRST — applicable
1493
- // consent-gated candidates with no recorded decision surface ONE batched
1494
- // operator offer (or consume the arrived decisions file), and only then
1495
- // does the deterministic acquisition executor run.
1496
- id: "external_analyzers_current",
1497
- derive: deriveObligationState("external_analyzers_current", cache),
1498
- execute: async (bundle, ctx) => {
1499
- const state = deriveAuditState(bundle);
1500
- const branch = await handleAnalyzerConsentBranch(ctx.params, bundle, state, ctx.analyzersRef);
1501
- if (branch.action === "return") {
1502
- return { kind: "emit", step: branch.result };
1503
- }
1504
- if (branch.action === "continue") {
1505
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1506
- }
1507
- return runDeterministicExecutor(bundle, ctx);
1508
- },
1509
- },
1510
- deterministic("structure_artifacts"),
1511
- {
1512
- // Critical-flow fallback: when deterministic flow inference fell below the
1513
- // confidence bar, poll the host submission (persist it → structure re-merges
1514
- // on the next fold) or emit the host step. No autonomous omit — the host is
1515
- // always available to author the enrichment. Non-drainable (host_delegation),
1516
- // so the drain stops here when a submission is still owed.
1517
- id: "critical_flow_fallback_current",
1518
- derive: deriveObligationState("critical_flow_fallback_current", cache),
1519
- execute: async (bundle, ctx) => {
1520
- const state = deriveAuditState(bundle);
1521
- const branch = await handleCriticalFlowFallbackBranch(ctx.params, bundle, state);
1522
- if (branch.action === "return") {
1523
- return { kind: "emit", step: branch.result };
1524
- }
1525
- // continue: a submission was consumed + persisted — re-scan (structure
1526
- // then re-stales + rebuilds critical_flows off the merged flows).
1527
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1528
- },
1529
+ // The 13 BESPOKE per-obligation host-boundary policy bodies — the content
1530
+ // the one registry carries beyond its PRIORITY-derived skeleton. Their
1531
+ // branch predicates are single-sourced with the plan draw's classifier
1532
+ // (`obligationPolicy.ts`), so the two draws agree on WHERE the boundary is;
1533
+ // what only this draw may do is CONSUME (stage/apply/quarantine) and EMIT.
1534
+ //
1535
+ // Every `emit` carries `state:` — the engine persists the outcome's state at
1536
+ // the fold's single commit, so an emit that advanced the bundle and omitted
1537
+ // it would silently roll its own work back. Every `transition` carries the
1538
+ // handler's returned bundle (an in-memory carry, never a disk reload).
1539
+ const bespoke = {
1540
+ // External analyzers: the Item B consent fold runs FIRST — applicable
1541
+ // consent-gated candidates with no recorded decision surface ONE batched
1542
+ // operator offer (or consume the arrived decisions file), and only then
1543
+ // does the deterministic acquisition executor run.
1544
+ external_analyzers_current: async (bundle, ctx) => {
1545
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1546
+ const branch = await handleAnalyzerConsentBranch(ctx.params, bundle, state, ctx.analyzersRef, ctx.tx);
1547
+ if (branch.action === "return") {
1548
+ return { kind: "emit", step: branch.result, state: bundle };
1549
+ }
1550
+ if (branch.action === "continue") {
1551
+ return { kind: "transition", state: bundle };
1552
+ }
1553
+ return runDeterministicExecutor(bundle, ctx);
1529
1554
  },
1530
- {
1531
- // Graph enrichment: poll the analyzer-decision / edge-reasoning lane
1532
- // artifacts first (emit a host step when one is needed), otherwise run the
1533
- // deterministic enrichment executor.
1534
- id: "graph_enrichment_current",
1535
- derive: deriveObligationState("graph_enrichment_current", cache),
1536
- execute: async (bundle, ctx) => {
1537
- const state = deriveAuditState(bundle);
1538
- const branch = await handleGraphEnrichmentBranch(ctx.params, bundle, state, ctx.analyzersRef);
1539
- if (branch.action === "return") {
1540
- return { kind: "emit", step: branch.result };
1541
- }
1542
- if (branch.action === "continue") {
1543
- // A decisions/edge file was consumed (and possibly applied): re-scan on
1544
- // the reloaded bundle without running the executor this turn.
1545
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1546
- }
1547
- // fallthrough: run the deterministic enrichment executor.
1555
+ // Critical-flow fallback: when deterministic flow inference fell below the
1556
+ // confidence bar, apply the host submission against the carried bundle or
1557
+ // emit the host step. No autonomous omit — the host is always available to
1558
+ // author the enrichment.
1559
+ critical_flow_fallback_current: async (bundle, ctx) => {
1560
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1561
+ const branch = await handleCriticalFlowFallbackBranch(ctx.params, bundle, state, ctx.tx);
1562
+ if (branch.action === "return") {
1563
+ return { kind: "emit", step: branch.result, state: bundle };
1564
+ }
1565
+ if (branch.action === "run_omit") {
1548
1566
  return runDeterministicExecutor(bundle, ctx);
1549
- },
1550
- },
1551
- deterministic("design_assessment_current"),
1552
- deterministic("structure_decomposition_current"),
1553
- deterministic("docs_digest_current"),
1554
- {
1555
- // Confirm-intent host step: the host writes intent_checkpoint.json (read by
1556
- // deriveAuditState on re-invocation), so there is no submission to
1557
- // consume — emit the step directly.
1558
- id: "intent_checkpoint_current",
1559
- derive: deriveObligationState("intent_checkpoint_current", cache),
1560
- execute: async (bundle) => ({
1561
- kind: "emit",
1562
- step: { kind: "confirm_intent", state: deriveAuditState(bundle), bundle },
1563
- }),
1564
- },
1565
- {
1566
- // DD-9 intent-equivalence gate: consume a judge verdict (validated +
1567
- // quarantined-loudly), resolve the deterministic arms in-fold (baseline
1568
- // stamp / gate-version-stale / structured delta), or emit the bounded
1569
- // prose-equivalence judge step. Sits between the intent checkpoint and
1570
- // every consumer of it, so a pending judgment pauses the cascade instead
1571
- // of racing it.
1572
- id: "intent_equivalence_current",
1573
- derive: deriveObligationState("intent_equivalence_current", cache),
1574
- execute: async (bundle, ctx) => {
1575
- const state = deriveAuditState(bundle);
1576
- const branch = await handleIntentEquivalenceBranch(ctx.params, bundle, state);
1577
- if (branch.action === "return") {
1578
- return { kind: "emit", step: branch.result };
1579
- }
1580
- if (branch.action === "run_omit") {
1581
- return runDeterministicExecutor(bundle, ctx);
1582
- }
1583
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1584
- },
1567
+ }
1568
+ return { kind: "transition", state: branch.bundle };
1585
1569
  },
1586
- {
1587
- // Charter extraction (Phase C): poll the lane submissions (ingest+gate),
1588
- // omit at a shallow ceiling, or emit the host charter-extraction step at a
1589
- // deep+ ceiling. Mirrors the synthesis-narrative branch.
1590
- id: "charter_extraction_current",
1591
- derive: deriveObligationState("charter_extraction_current", cache),
1592
- execute: async (bundle, ctx) => {
1593
- const state = deriveAuditState(bundle);
1594
- const branch = await handleCharterExtractionBranch(ctx.params, bundle, state);
1595
- if (branch.action === "return") {
1596
- return { kind: "emit", step: branch.result };
1597
- }
1598
- if (branch.action === "run_omit") {
1599
- return runDeterministicExecutor(bundle, ctx);
1600
- }
1601
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1602
- },
1570
+ // Graph enrichment: poll the analyzer-decision / edge-reasoning lane
1571
+ // artifacts first (emit a host step when one is needed), otherwise run the
1572
+ // deterministic enrichment executor.
1573
+ graph_enrichment_current: async (bundle, ctx) => {
1574
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1575
+ const branch = await handleGraphEnrichmentBranch(ctx.params, bundle, state, ctx.analyzersRef, ctx.tx);
1576
+ if (branch.action === "return") {
1577
+ return { kind: "emit", step: branch.result, state: bundle };
1578
+ }
1579
+ if (branch.action === "continue") {
1580
+ return { kind: "transition", state: branch.bundle };
1581
+ }
1582
+ return runDeterministicExecutor(bundle, ctx);
1603
1583
  },
1604
- {
1605
- // Charter delta-mining (Phase C.2): poll the delta lane submission
1606
- // (route+gate it), settle deterministically when the register is not
1607
- // deltas_pending (extraction omitted / no subsystems), or emit the host step
1608
- // for the INDEPENDENT delta-miner when a deltas_pending register has no
1609
- // submission yet. Mirrors the charter-extraction branch.
1610
- id: "charter_delta_current",
1611
- derive: deriveObligationState("charter_delta_current", cache),
1612
- execute: async (bundle, ctx) => {
1613
- const state = deriveAuditState(bundle);
1614
- const branch = await handleCharterDeltaBranch(ctx.params, bundle, state);
1615
- if (branch.action === "return") {
1616
- return { kind: "emit", step: branch.result };
1617
- }
1618
- if (branch.action === "run_omit") {
1619
- return runDeterministicExecutor(bundle, ctx);
1620
- }
1621
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1584
+ // Confirm-intent host step: the host writes intent_checkpoint.json (read by
1585
+ // deriveAuditState on re-invocation), so there is no submission to
1586
+ // consume emit the step directly.
1587
+ intent_checkpoint_current: async (bundle) => ({
1588
+ kind: "emit",
1589
+ step: {
1590
+ kind: "confirm_intent",
1591
+ state: deriveAuditState(bundle, { emitStaleness: false }),
1592
+ bundle,
1622
1593
  },
1594
+ state: bundle,
1595
+ }),
1596
+ // DD-9 intent-equivalence gate: consume a judge verdict (validated +
1597
+ // quarantined-loudly), resolve the deterministic arms in-fold, or emit the
1598
+ // bounded prose-equivalence judge step.
1599
+ intent_equivalence_current: async (bundle, ctx) => {
1600
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1601
+ const branch = await handleIntentEquivalenceBranch(ctx.params, bundle, state, ctx.tx);
1602
+ if (branch.action === "return") {
1603
+ return { kind: "emit", step: branch.result, state: bundle };
1604
+ }
1605
+ if (branch.action === "run_omit") {
1606
+ return runDeterministicExecutor(bundle, ctx);
1607
+ }
1608
+ return { kind: "transition", state: branch.bundle };
1623
1609
  },
1624
- {
1625
- // Contract design-review pass: poll the contract/conceptual lanes;
1626
- // emit the dispatch step when a pass still needs to run.
1627
- id: "design_review_contract_completed",
1628
- derive: deriveObligationState("design_review_contract_completed", cache),
1629
- execute: (bundle, ctx) => runDesignReviewObligation(bundle, ctx),
1630
- },
1631
- {
1632
- // Conceptual design-review pass: same submission-poll handler (it resolves
1633
- // which pass remains).
1634
- id: "design_review_conceptual_completed",
1635
- derive: deriveObligationState("design_review_conceptual_completed", cache),
1636
- execute: (bundle, ctx) => runDesignReviewObligation(bundle, ctx),
1610
+ // Charter extraction (Phase C): consume the lane submissions (ingest+gate),
1611
+ // omit at a shallow ceiling, or emit the host charter-extraction step.
1612
+ charter_extraction_current: async (bundle, ctx) => {
1613
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1614
+ const branch = await handleCharterExtractionBranch(ctx.params, bundle, state, ctx.tx);
1615
+ if (branch.action === "return") {
1616
+ return { kind: "emit", step: branch.result, state: bundle };
1617
+ }
1618
+ if (branch.action === "run_omit") {
1619
+ return runDeterministicExecutor(bundle, ctx);
1620
+ }
1621
+ return { kind: "transition", state: branch.bundle };
1637
1622
  },
1638
- {
1639
- // Charter clarification (Phase D triangulation loop): poll the answers lane
1640
- // (apply + re-split), assemble the loop deterministically at a shallow ceiling
1641
- // / zero attention (autonomous), or emit the host step relaying the VOI-ranked
1642
- // interactive queue at a deep+ ceiling with attention > 0. Non-drainable
1643
- // (host_delegation), so the drain stops here.
1644
- id: "charter_clarification_current",
1645
- derive: deriveObligationState("charter_clarification_current", cache),
1646
- execute: async (bundle, ctx) => {
1647
- const state = deriveAuditState(bundle);
1648
- const branch = await handleCharterClarificationBranch(ctx.params, bundle, state);
1649
- if (branch.action === "return") {
1650
- return { kind: "emit", step: branch.result };
1651
- }
1652
- if (branch.action === "run_omit") {
1653
- return runDeterministicExecutor(bundle, ctx);
1654
- }
1655
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1656
- },
1623
+ // Charter delta-mining (Phase C.2): consume the delta lane submission,
1624
+ // settle deterministically when the register is not deltas_pending, or
1625
+ // emit the independent delta-miner's host step.
1626
+ charter_delta_current: async (bundle, ctx) => {
1627
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1628
+ const branch = await handleCharterDeltaBranch(ctx.params, bundle, state, ctx.tx);
1629
+ if (branch.action === "return") {
1630
+ return { kind: "emit", step: branch.result, state: bundle };
1631
+ }
1632
+ if (branch.action === "run_omit") {
1633
+ return runDeterministicExecutor(bundle, ctx);
1634
+ }
1635
+ return { kind: "transition", state: branch.bundle };
1657
1636
  },
1658
- {
1659
- // Systemic challenge (Phase E loop-until-dry): poll the adversary lane's
1660
- // round (fold it), omit at a shallow ceiling, or emit the second-order-adversary
1661
- // host step when the loop is open at a deep+ ceiling. Non-drainable
1662
- // (host_delegation), so the drain stops here.
1663
- id: "systemic_challenge_current",
1664
- derive: deriveObligationState("systemic_challenge_current", cache),
1665
- execute: async (bundle, ctx) => {
1666
- const state = deriveAuditState(bundle);
1667
- const branch = await handleSystemicChallengeBranch(ctx.params, bundle, state);
1668
- if (branch.action === "return") {
1669
- return { kind: "emit", step: branch.result };
1670
- }
1671
- if (branch.action === "run_omit") {
1672
- return runDeterministicExecutor(bundle, ctx);
1673
- }
1674
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1675
- },
1637
+ // The two design-review passes share one submission-poll handler (it
1638
+ // resolves which pass remains).
1639
+ design_review_contract_completed: (bundle, ctx) => runDesignReviewObligation(bundle, ctx),
1640
+ design_review_conceptual_completed: (bundle, ctx) => runDesignReviewObligation(bundle, ctx),
1641
+ // Charter clarification (Phase D triangulation loop).
1642
+ charter_clarification_current: async (bundle, ctx) => {
1643
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1644
+ const branch = await handleCharterClarificationBranch(ctx.params, bundle, state, ctx.tx);
1645
+ if (branch.action === "return") {
1646
+ return { kind: "emit", step: branch.result, state: bundle };
1647
+ }
1648
+ if (branch.action === "run_omit") {
1649
+ return runDeterministicExecutor(bundle, ctx);
1650
+ }
1651
+ return { kind: "transition", state: branch.bundle };
1676
1652
  },
1677
- deterministic("planning_artifacts"),
1678
- {
1679
- // The audit-task dispatch obligation maps to the host-delegation
1680
- // semantic_review_executor (no deterministic runner) host review.
1681
- id: "audit_tasks_completed",
1682
- derive: deriveObligationState("audit_tasks_completed", cache),
1683
- execute: (bundle, ctx) => runHostDelegationObligation(bundle, ctx),
1653
+ // Systemic challenge (Phase E loop-until-dry).
1654
+ systemic_challenge_current: async (bundle, ctx) => {
1655
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1656
+ const branch = await handleSystemicChallengeBranch(ctx.params, bundle, state, ctx.tx);
1657
+ if (branch.action === "return") {
1658
+ return { kind: "emit", step: branch.result, state: bundle };
1659
+ }
1660
+ if (branch.action === "run_omit") {
1661
+ return runDeterministicExecutor(bundle, ctx);
1662
+ }
1663
+ return { kind: "transition", state: branch.bundle };
1684
1664
  },
1685
- deterministic("audit_results_ingested"),
1686
- deterministic("runtime_validation_current"),
1687
- deterministic("synthesis_current"),
1688
- {
1689
- // Synthesis narrative: poll the narrative lane; emit the host step when
1690
- // narrative is enabled and not yet supplied, otherwise the deterministic
1691
- // omit runs (fold on).
1692
- id: "synthesis_narrative_current",
1693
- derive: deriveObligationState("synthesis_narrative_current", cache),
1694
- execute: async (bundle, ctx) => {
1695
- const state = deriveAuditState(bundle);
1696
- const branch = await handleSynthesisNarrativeBranch(ctx.params, bundle, state);
1697
- if (branch.action === "return") {
1698
- return { kind: "emit", step: branch.result };
1699
- }
1700
- if (branch.action === "run_omit") {
1701
- // Narrative disabled: run the deterministic omit executor so the
1702
- // status:omitted marker is written and the obligation is satisfied.
1703
- // (A bare reload here would leave it actionable and spin the fold.)
1704
- return runDeterministicExecutor(bundle, ctx);
1705
- }
1706
- // continue: a narrative submission was consumed + applied — re-scan.
1707
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1708
- },
1665
+ // The audit-task dispatch obligation maps to the host-delegation
1666
+ // semantic_review_executor (no deterministic runner) → host review.
1667
+ audit_tasks_completed: (bundle, ctx) => runHostDelegationObligation(bundle, ctx),
1668
+ // Synthesis narrative: consume the narrative lane; emit the host step when
1669
+ // narrative is enabled and not yet supplied, otherwise the deterministic
1670
+ // omit runs (fold on).
1671
+ synthesis_narrative_current: async (bundle, ctx) => {
1672
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1673
+ const branch = await handleSynthesisNarrativeBranch(ctx.params, bundle, state, ctx.tx);
1674
+ if (branch.action === "return") {
1675
+ return { kind: "emit", step: branch.result, state: bundle };
1676
+ }
1677
+ if (branch.action === "run_omit") {
1678
+ // Narrative disabled: run the deterministic omit executor so the
1679
+ // status:omitted marker is written and the obligation is satisfied.
1680
+ return runDeterministicExecutor(bundle, ctx);
1681
+ }
1682
+ // continue: a narrative submission was consumed + applied — re-scan.
1683
+ return { kind: "transition", state: branch.bundle };
1709
1684
  },
1710
- ];
1685
+ };
1686
+ // The registry's membership and order DERIVE from PRIORITY — never a second
1687
+ // hand-enumerated list, so an id cannot be in the scan and absent from the
1688
+ // registry (this derivation dissolved the fold-array⇄PRIORITY sync tests).
1689
+ // `friction_capture_current` gets a plain def and stays inert by absence:
1690
+ // `deriveAuditState` never emits it, so its derive is always satisfied.
1691
+ for (const id of Object.keys(bespoke)) {
1692
+ if (!PRIORITY.includes(id)) {
1693
+ throw new Error(`buildAuditObligations: bespoke policy body for "${id}" names an id absent from PRIORITY — the registry derives from PRIORITY, so this body could never run`);
1694
+ }
1695
+ }
1696
+ return PRIORITY.map((id) => ({
1697
+ id,
1698
+ derive: deriveObligationState(id, cache),
1699
+ execute: bespoke[id] ??
1700
+ ((bundle, ctx) => runDeterministicExecutor(bundle, ctx)),
1701
+ }));
1711
1702
  }
1712
1703
  /** Shared design-review-pass executor (both pass obligations route here). */
1713
1704
  async function runDesignReviewObligation(bundle, ctx) {
1714
- const state = deriveAuditState(bundle);
1715
- const branch = await handleDesignReviewBranch(ctx.params, bundle, state);
1705
+ const state = deriveAuditState(bundle, { emitStaleness: false });
1706
+ const branch = await handleDesignReviewBranch(ctx.params, bundle, state, ctx.tx);
1716
1707
  if (branch.action === "return") {
1717
- return { kind: "emit", step: branch.result };
1708
+ // The handler's carried bundle rides BOTH the step (the renderer reads the
1709
+ // rejection notes off it) and the emit's state (the commit persists them).
1710
+ return { kind: "emit", step: branch.result, state: branch.result.bundle };
1718
1711
  }
1719
- return { kind: "transition", state: await loadArtifactBundle(ctx.params.artifactsDir) };
1712
+ return { kind: "transition", state: branch.bundle };
1720
1713
  }
1721
1714
  /**
1722
1715
  * Host-delegation dispatch obligation (`audit_tasks_completed` →
@@ -1726,7 +1719,7 @@ async function runDesignReviewObligation(bundle, ctx) {
1726
1719
  * delegation executor emits the same blocked step the no-executor branch did.
1727
1720
  */
1728
1721
  async function runHostDelegationObligation(bundle, ctx) {
1729
- const decision = decideNextStep(bundle);
1722
+ const decision = decideNextStep(bundle, { emitStaleness: false });
1730
1723
  const state = decision.state;
1731
1724
  if (!decision.selected_executor) {
1732
1725
  return emitNoExecutorBlocked(bundle, ctx, decision);
@@ -1783,12 +1776,14 @@ async function runHostDelegationObligation(bundle, ctx) {
1783
1776
  const pendingIds = new Set(buildPendingAuditTasks(bundle).map((task) => task.task_id));
1784
1777
  const pendingAccepted = acceptedResults.filter((result) => pendingIds.has(result.task_id));
1785
1778
  if (pendingAccepted.length > 0) {
1786
- const ingested = await runAuditStep({
1779
+ // Forced, lock-free, against the CARRIED bundle: the fold owns the one
1780
+ // hold and the one persist.
1781
+ const ingested = await runAuditStepUnlocked({
1787
1782
  root: ctx.params.root,
1788
1783
  artifactsDir: ctx.params.artifactsDir,
1789
1784
  preferredExecutor: "result_ingestion_executor",
1790
1785
  auditResultsData: [...pendingAccepted],
1791
- });
1786
+ }, bundle);
1792
1787
  // The transition returns before any emission, and the NEXT ingest skips
1793
1788
  // already-accepted bindings — so this fold's advisories would be a
1794
1789
  // one-shot loss. Carry them in the ctx ref; the semantic-review emit
@@ -1801,7 +1796,13 @@ async function runHostDelegationObligation(bundle, ctx) {
1801
1796
  return { kind: "transition", state: ingested.updated_bundle };
1802
1797
  }
1803
1798
  }
1804
- const review = await ensureSemanticReviewRun({
1799
+ // The UNLOCKED variant: the fold already holds the artifact-tree lock, and
1800
+ // `withFileLock` is non-reentrant — the locking `ensureSemanticReviewRun`
1801
+ // here would be a deterministic FileLockTimeoutError on the audit loop's
1802
+ // most common exit path. Its blocked core state rides the emit's `state`
1803
+ // below, so the fold's single commit persists it (the pause's own persist
1804
+ // was the split's other half).
1805
+ const review = await ensureSemanticReviewRunUnlocked({
1805
1806
  root: ctx.params.root,
1806
1807
  artifactsDir: ctx.params.artifactsDir,
1807
1808
  bundle,
@@ -1836,6 +1837,9 @@ async function runHostDelegationObligation(bundle, ctx) {
1836
1837
  ],
1837
1838
  }
1838
1839
  : emitted,
1840
+ // The pause's blocked core state — the fold's single commit persists it
1841
+ // (the locking variant's own write was the other half of the split).
1842
+ state: review.bundle,
1839
1843
  };
1840
1844
  }
1841
1845
  /** Emit the no-executor blocked step (the hand loop's `!selected_executor` arm). */
@@ -1849,23 +1853,27 @@ async function emitNoExecutorBlocked(bundle, ctx, decision) {
1849
1853
  audit_state: state,
1850
1854
  progress_summary: reason,
1851
1855
  });
1852
- return { kind: "emit", step: { kind: "blocked", state, bundle, reason } };
1856
+ return { kind: "emit", step: { kind: "blocked", state, bundle, reason }, state: bundle };
1853
1857
  }
1854
1858
  /**
1855
- * Wrap each obligation so the transition counter advances exactly once per fold
1856
- * iteration the analog of the hand loop's `index++`. Incrementing AFTER a
1857
- * `transition` (and never on an `emit`, which exits the fold) means during any
1858
- * `execute` the counter holds the current iteration's 0-based index, which the
1859
- * two cycle guards read as `index`. Single point of truth so a new obligation
1860
- * cannot forget to count.
1859
+ * Wrap each obligation so the fold's CURRENT carried bundle is always
1860
+ * observable on the ctx the commit-on-throw path persists it, and a throw
1861
+ * unwinds the engine before it can return its state. Single point of truth so
1862
+ * a new obligation cannot forget to track. (The dispatch ordinal the guards
1863
+ * read is counted at the dispatch site, `runDeterministicExecutor` a policy
1864
+ * transition mints no new artifact state, so it does not count.)
1861
1865
  */
1862
- function countTransitions(obligations) {
1866
+ function trackFoldBundle(obligations) {
1863
1867
  return obligations.map((obligation) => ({
1864
1868
  ...obligation,
1865
1869
  execute: async (bundle, ctx) => {
1866
1870
  const outcome = await obligation.execute(bundle, ctx);
1867
- if (outcome.kind === "transition")
1868
- ctx.iterationRef.value += 1;
1871
+ if (outcome.kind === "transition") {
1872
+ ctx.currentBundleRef.value = outcome.state;
1873
+ }
1874
+ else if (outcome.state !== undefined) {
1875
+ ctx.currentBundleRef.value = outcome.state;
1876
+ }
1869
1877
  return outcome;
1870
1878
  },
1871
1879
  }));
@@ -1936,21 +1944,114 @@ export async function runDeterministicForNextStep(params) {
1936
1944
  validationWarnings: [...EMPTY_FOLD_ADVISORIES.validationWarnings],
1937
1945
  },
1938
1946
  },
1939
- iterationRef: { value: 0 },
1947
+ dispatchOrdinalRef: { value: 0 },
1940
1948
  dispatchedSignatures: new Set(),
1941
1949
  seenStateSignatures: new Set(),
1942
1950
  obligationTrail: [],
1951
+ tx: createFoldTransaction(),
1952
+ currentBundleRef: { value: {} },
1953
+ failureRef: { value: null },
1954
+ manifestIndexCache: new WeakMap(),
1943
1955
  };
1944
- const startBundle = await loadArtifactBundle(params.artifactsDir);
1945
- const outcome = await advance({
1946
- priority: PRIORITY,
1947
- obligations: countTransitions(buildAuditObligations()),
1948
- }, startBundle, ctx);
1949
- if (outcome.step)
1956
+ // ONE hold, ONE core commit (CX-02 constraint 3): the whole drain runs under
1957
+ // a single artifact-tree lock, carries its bundle in memory, and lands ONE
1958
+ // authoritative core write at the boundary — on EVERY exit, the throw path
1959
+ // included (a persist only on success would drop the failure attribution the
1960
+ // recovery path reads). Non-core writes (markers, handoff, quarantine, the
1961
+ // durable analyzer stores) stay mid-fold by design: the delivered property
1962
+ // is one CORE write boundary, not one persist boundary.
1963
+ resetStalenessDedup();
1964
+ const foldLogger = new RunLogger(join(params.artifactsDir, "run.log.jsonl"), {
1965
+ enabled: true,
1966
+ });
1967
+ const outcome = await withArtifactTreeHold(params.artifactsDir, foldLogger, async () => {
1968
+ // CX-02 hold measurement: time the IN-HOLD span (acquire→release, this
1969
+ // callback), and record the engine's own charged-execution count. The
1970
+ // `finally` below runs on the throw path too, still under the hold.
1971
+ const holdStartMs = Date.now();
1972
+ let chargedExecutions;
1973
+ await recoverStagedSubmissions(params.artifactsDir);
1974
+ const startBundle = await loadArtifactBundle(params.artifactsDir);
1975
+ ctx.currentBundleRef.value = startBundle;
1976
+ try {
1977
+ const engineOutcome = await advance({
1978
+ priority: PRIORITY,
1979
+ obligations: trackFoldBundle(buildAuditObligations()),
1980
+ }, startBundle, ctx, {
1981
+ maxTransitions: engineMaxTransitions(),
1982
+ maxExecutions: MAX_DRAIN_STEPS,
1983
+ });
1984
+ chargedExecutions = engineOutcome.executions;
1985
+ await commitFold(params.artifactsDir, engineOutcome.state, ctx.tx);
1986
+ return engineOutcome;
1987
+ }
1988
+ catch (error) {
1989
+ // Commit-on-throw: persist the carried bundle WITH dispatch-local
1990
+ // failure attribution, then rethrow. This replaces the old catch's
1991
+ // second lock acquisition (a deterministic timeout under one hold).
1992
+ const bundle = ctx.currentBundleRef.value;
1993
+ const failure = ctx.failureRef.value ??
1994
+ (() => {
1995
+ const found = findExecutorFailure(error);
1996
+ return found
1997
+ ? { executor: found.executor, obligation: found.obligation }
1998
+ : null;
1999
+ })();
2000
+ const failedState = deriveAuditState(bundle, { emitStaleness: false });
2001
+ failedState.last_executor = failure?.executor ?? undefined;
2002
+ failedState.last_obligation = failure?.obligation ?? undefined;
2003
+ await commitFold(params.artifactsDir, { ...bundle, audit_state: failedState }, ctx.tx);
2004
+ throw error;
2005
+ }
2006
+ finally {
2007
+ foldLogger.event({
2008
+ kind: "outcome",
2009
+ note: "fold_hold",
2010
+ duration_ms: Date.now() - holdStartMs,
2011
+ ...(chargedExecutions === undefined
2012
+ ? {}
2013
+ : { executions: chargedExecutions }),
2014
+ });
2015
+ }
2016
+ });
2017
+ // The ONE consolidated staleness record for the whole fold (preserve list):
2018
+ // every in-fold derivation ran emit-off, so this is the only scan-level
2019
+ // record this call emits (forced applies keep their own per-apply records,
2020
+ // exactly as each per-transition runAuditStep call did before the collapse).
2021
+ const finalStale = computeStaleArtifacts(outcome.state, { emit: false });
2022
+ emitStalenessRecord(finalStale, isMetadataMigrationStaleness(outcome.state)
2023
+ ? "metadata_schema_version_migration"
2024
+ : undefined);
2025
+ if (outcome.step) {
2026
+ if (outcome.step.kind === "terminal_intent") {
2027
+ // Guard terminals convert POST-commit, POST-hold: buildTerminalStep can
2028
+ // promote, and promotion deletes artifactsDir.
2029
+ return await buildTerminalStep(params, outcome.step.bundle, outcome.step.state, outcome.step.reason);
2030
+ }
1950
2031
  return outcome.step;
1951
- // This call supplies no `maxTransitions`, so the bound in force IS the engine
1952
- // default `describeStoppedFold` reads it from the same place rather than
1953
- // this site restating the constant.
2032
+ }
2033
+ if (outcome.stopped === "budget") {
2034
+ // The pacing cap (MAX_DRAIN_STEPS charged executions), spent gracefully:
2035
+ // the run is paused resumably — the committed state resumes on the next
2036
+ // call. Never non-convergence; never routed through the stalled branch.
2037
+ const bundle = outcome.state;
2038
+ const state = bundle.audit_state ?? deriveAuditState(bundle, { emitStaleness: false });
2039
+ const reason = `Pacing cap: this call spent its ${MAX_DRAIN_STEPS}-execution budget and paused. ` +
2040
+ "Nothing is wrong — the fold's work is committed; re-run next-step to resume.";
2041
+ await writeHandoffOnly({
2042
+ root: params.root,
2043
+ artifactsDir: params.artifactsDir,
2044
+ bundle,
2045
+ audit_state: state,
2046
+ progress_summary: reason,
2047
+ });
2048
+ return { kind: "blocked", state, bundle, reason };
2049
+ }
2050
+ // The engine stamps the limit that fired on the result (`stoppedBound`), so
2051
+ // `describeStoppedFold` reports the number in force with nothing restated
2052
+ // here. Budget stops were handled above; what reaches this branch is genuine
2053
+ // non-convergence (the derived transition backstop, which the execution
2054
+ // budget makes unreachable in practice — a backstop firing IS the anomaly).
1954
2055
  const stalled = describeStoppedFold(outcome);
1955
2056
  if (stalled) {
1956
2057
  // The engine's bound is its runaway backstop, and audit deliberately