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.
- package/dist/audit/cli/auditStep.d.ts +30 -0
- package/dist/audit/cli/auditStep.d.ts.map +1 -1
- package/dist/audit/cli/auditStep.js +50 -0
- package/dist/audit/cli/auditStep.js.map +1 -1
- package/dist/audit/cli/foldTransaction.d.ts +103 -0
- package/dist/audit/cli/foldTransaction.d.ts.map +1 -0
- package/dist/audit/cli/foldTransaction.js +243 -0
- package/dist/audit/cli/foldTransaction.js.map +1 -0
- package/dist/audit/cli/nextStepHelpers.d.ts +109 -28
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +661 -560
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/reviewRun.d.ts +15 -3
- package/dist/audit/cli/reviewRun.d.ts.map +1 -1
- package/dist/audit/cli/reviewRun.js +39 -13
- package/dist/audit/cli/reviewRun.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts +38 -36
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +126 -82
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts +23 -0
- package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts +8 -2
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewSnapshot.js +12 -6
- package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +1 -1
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/executors.d.ts.map +1 -1
- package/dist/audit/orchestrator/executors.js +1 -1
- package/dist/audit/orchestrator/executors.js.map +1 -1
- package/dist/audit/orchestrator/hostInputPause.d.ts +0 -33
- package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
- package/dist/audit/orchestrator/hostInputPause.js +6 -64
- package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
- package/dist/audit/orchestrator/obligationPolicy.d.ts +90 -0
- package/dist/audit/orchestrator/obligationPolicy.d.ts.map +1 -0
- package/dist/audit/orchestrator/obligationPolicy.js +228 -0
- package/dist/audit/orchestrator/obligationPolicy.js.map +1 -0
- package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts +1 -1
- package/dist/audit/orchestrator/systemicChallengeExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/systemicChallengeExecutor.js +23 -1
- package/dist/audit/orchestrator/systemicChallengeExecutor.js.map +1 -1
- package/dist/audit/types/systemicChallenge.d.ts +9 -0
- package/dist/audit/types/systemicChallenge.d.ts.map +1 -1
- package/dist/shared/engine/obligationEngine.d.ts +52 -11
- package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
- package/dist/shared/engine/obligationEngine.js +46 -9
- package/dist/shared/engine/obligationEngine.js.map +1 -1
- package/dist/shared/observability/runLog.d.ts +2 -0
- package/dist/shared/observability/runLog.d.ts.map +1 -1
- package/dist/shared/observability/runLog.js.map +1 -1
- 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,
|
|
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,
|
|
13
|
-
import {
|
|
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 {
|
|
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
|
|
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 {
|
|
28
|
-
import { writeHandoffOnly,
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 ??
|
|
268
|
-
// Fold-level pause detection is single-sourced in `hostInputPause` so the
|
|
269
|
-
//
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
352
|
-
//
|
|
353
|
-
|
|
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
|
-
|
|
384
|
+
}, bundle);
|
|
385
|
+
markSubmissionApplied(tx, edgeReasoningIncoming.path);
|
|
362
386
|
await clearEdgeReasoningRejection(params.artifactsDir);
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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 (
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
762
|
+
assessment =
|
|
763
|
+
withRejectedDesignReviewSubmission(assessment, "contract", contractResult) ??
|
|
764
|
+
assessment;
|
|
744
765
|
}
|
|
745
|
-
else if (contractResult.status === "ok" &&
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
779
|
+
assessment =
|
|
780
|
+
withRejectedDesignReviewSubmission(assessment, "conceptual", conceptualResult) ??
|
|
781
|
+
assessment;
|
|
756
782
|
}
|
|
757
|
-
else if (conceptualResult.status === "ok" &&
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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 &&
|
|
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.
|
|
772
|
-
// so
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
797
|
-
const
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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:
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
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:
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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:
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
shouldOmit:
|
|
1085
|
-
|
|
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:
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
|
1153
|
-
* `index` is the 0-based
|
|
1154
|
-
* 1-based `iteration` in the
|
|
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
|
|
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
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
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
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
|
|
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
|
|
1215
|
-
// the fold position stays reconstructable from the marker
|
|
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
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
1442
|
-
//
|
|
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
|
|
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
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
|
|
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
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
1678
|
-
{
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1686
|
-
deterministic
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1860
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|