@sensigo/realm 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/abandon-run.d.ts.map +1 -1
- package/dist/engine/abandon-run.js +5 -7
- package/dist/engine/abandon-run.js.map +1 -1
- package/dist/engine/apply-resume.d.ts.map +1 -1
- package/dist/engine/apply-resume.js +13 -1
- package/dist/engine/apply-resume.js.map +1 -1
- package/dist/engine/eligibility.d.ts +85 -2
- package/dist/engine/eligibility.d.ts.map +1 -1
- package/dist/engine/eligibility.js +271 -14
- package/dist/engine/eligibility.js.map +1 -1
- package/dist/engine/execution-loop.d.ts.map +1 -1
- package/dist/engine/execution-loop.js +164 -34
- package/dist/engine/execution-loop.js.map +1 -1
- package/dist/engine/run-health.d.ts.map +1 -1
- package/dist/engine/run-health.js.map +1 -1
- package/dist/engine/settlement.d.ts +61 -6
- package/dist/engine/settlement.d.ts.map +1 -1
- package/dist/engine/settlement.js +190 -25
- package/dist/engine/settlement.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/store/json-file-store.d.ts +9 -2
- package/dist/store/json-file-store.d.ts.map +1 -1
- package/dist/store/json-file-store.js +76 -0
- package/dist/store/json-file-store.js.map +1 -1
- package/dist/store/seal-integrity.d.ts +9 -0
- package/dist/store/seal-integrity.d.ts.map +1 -0
- package/dist/store/seal-integrity.js +249 -0
- package/dist/store/seal-integrity.js.map +1 -0
- package/dist/store/store-interface.d.ts +47 -2
- package/dist/store/store-interface.d.ts.map +1 -1
- package/dist/types/run-record.d.ts +123 -6
- package/dist/types/run-record.d.ts.map +1 -1
- package/dist/types/run-record.js +25 -1
- package/dist/types/run-record.js.map +1 -1
- package/dist/types/workflow-error.d.ts +1 -1
- package/dist/types/workflow-error.d.ts.map +1 -1
- package/dist/types/workflow-error.js.map +1 -1
- package/dist/workflow/yaml-loader.js +109 -0
- package/dist/workflow/yaml-loader.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { storeDeclaresSeal, storeDeclaresNonceCarriage } from '../store/trace-bu
|
|
|
5
5
|
import { partitionBufferedEntries } from './trace-adoption.js';
|
|
6
6
|
import { deriveDefaultedSteps } from './defaulted-steps.js';
|
|
7
7
|
import { computeGateDueState } from './gate-timing.js';
|
|
8
|
-
import { selectFinalizers, deriveEffectiveTriggers, applySettlement } from './settlement.js';
|
|
8
|
+
import { selectFinalizers, deriveEffectiveTriggers, applySettlement, renderFailCause, failureMessagesFromEvidence, failureMessagesWithOverlay, } from './settlement.js';
|
|
9
9
|
import { captureEvidence } from '../evidence/snapshot.js';
|
|
10
10
|
import { validateInputSchema, validateOutputSchema, validateTraceSchema, } from '../validation/input-schema.js';
|
|
11
11
|
import { normalizeTrace } from './trace-normalizer.js';
|
|
@@ -18,7 +18,7 @@ import { createDefaultRegistry } from '../extensions/default-registry.js';
|
|
|
18
18
|
import { renderTemplate, resolvePath, UnknownFilterError } from './render-template.js';
|
|
19
19
|
import { generateSchemaSkeleton } from '../utils/schema-skeleton.js';
|
|
20
20
|
import { loadWorkflowContext } from './workflow-context-loader.js';
|
|
21
|
-
import { findEligibleSteps, findEligibleGuardSteps, isWorkflowComplete, buildEvidenceByStep, propagateSkips, deriveRunPhase, } from './eligibility.js';
|
|
21
|
+
import { findEligibleSteps, findEligibleGuardSteps, isWorkflowComplete, buildEvidenceByStep, propagateSkips, deriveRunPhase, armToOutcome, assertSealMarkersAgree, } from './eligibility.js';
|
|
22
22
|
import { requirementForStep } from './capability.js';
|
|
23
23
|
import { resolvePreExecutionAgentAction, resolvePostDispatchAgentAction, } from './error-resolution.js';
|
|
24
24
|
function delayMs(ms) {
|
|
@@ -1764,6 +1764,8 @@ export async function executeStep(store, definition, options) {
|
|
|
1764
1764
|
const abortDraft = {
|
|
1765
1765
|
...withAllSkipped,
|
|
1766
1766
|
terminal_state: true,
|
|
1767
|
+
// issue #367: the seal fields live in the SAME object literal as the terminal flip.
|
|
1768
|
+
sealed_by: { arm: 'handler_abort', step: options.command },
|
|
1767
1769
|
terminal_reason: `Handler '${options.command}' aborted the run: ${abortMessage}`,
|
|
1768
1770
|
aborted_at: {
|
|
1769
1771
|
step_id: options.command,
|
|
@@ -2394,14 +2396,34 @@ export async function executeStep(store, definition, options) {
|
|
|
2394
2396
|
(withSkippedFail.in_progress_steps.length === 0 &&
|
|
2395
2397
|
findEligibleSteps(definition, withSkippedFail).length === 0 &&
|
|
2396
2398
|
findEligibleGuardSteps(definition, withSkippedFail).length === 0);
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2399
|
+
// Hoisted so the #373 message walk below reads the SAME evidence the record will carry —
|
|
2400
|
+
// `withSkippedFail` does not yet include this attempt's snapshots.
|
|
2401
|
+
const failEvidence = [...pendingRun.evidence, ...allEvidence];
|
|
2402
|
+
// issue #373 — twin of settlement.ts's fail seal; both layers must emit byte-identical
|
|
2403
|
+
// sentences for identical inputs. Single-failure shape unchanged (this site has never had the
|
|
2404
|
+
// `?? 'unknown error'` fallback — `dispatchError.message` is always a string).
|
|
2405
|
+
const failCause = new Set(withSkippedFail.failed_steps).size > 1
|
|
2406
|
+
? renderFailCause(withSkippedFail.failed_steps, failureMessagesWithOverlay(failEvidence, options.command, dispatchError.message))
|
|
2407
|
+
: `Step '${options.command}' failed: ${dispatchError.message}`;
|
|
2408
|
+
// issue #367: seal fields staged FIRST in the SAME object literal, and a stale prior seal
|
|
2409
|
+
// never survives either fork. #373's `failCause` (computed above) is untouched — the arm sits
|
|
2410
|
+
// BESIDE the rendered sentence, never inside it. Validation exhaustion is deliberately NOT a
|
|
2411
|
+
// distinct arm: a VALIDATION_EXHAUSTED failure seals 'step_failure' like any other terminal
|
|
2412
|
+
// step failure, and the distinction lives in defaulted_steps/diagnostics.
|
|
2413
|
+
const { sealed_by: _priorFailSeal, ...withSkippedFailBase } = withSkippedFail;
|
|
2414
|
+
const failDraft = isComplete
|
|
2415
|
+
? {
|
|
2416
|
+
...withSkippedFailBase,
|
|
2417
|
+
terminal_state: true,
|
|
2418
|
+
sealed_by: { arm: 'step_failure', step: options.command },
|
|
2419
|
+
evidence: failEvidence,
|
|
2420
|
+
terminal_reason: failCause,
|
|
2421
|
+
}
|
|
2422
|
+
: {
|
|
2423
|
+
...withSkippedFailBase,
|
|
2424
|
+
terminal_state: false,
|
|
2425
|
+
evidence: failEvidence,
|
|
2426
|
+
};
|
|
2405
2427
|
// On the terminal transition, drain the fail/always finalizers before the single seal.
|
|
2406
2428
|
// Non-terminal failures (recovery steps remain) run no finalizers.
|
|
2407
2429
|
const failedRun = isComplete
|
|
@@ -2979,11 +3001,16 @@ export async function executeStep(store, definition, options) {
|
|
|
2979
3001
|
(withSkippedComplete.in_progress_steps.length === 0 &&
|
|
2980
3002
|
findEligibleSteps(definition, withSkippedComplete).length === 0 &&
|
|
2981
3003
|
findEligibleGuardSteps(definition, withSkippedComplete).length === 0);
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3004
|
+
// issue #367: seal fields staged first in the SAME object literal; stale prior seal stripped.
|
|
3005
|
+
const { sealed_by: _priorCompleteSeal, ...withSkippedCompleteBase } = withSkippedComplete;
|
|
3006
|
+
const completeDraft = isComplete
|
|
3007
|
+
? {
|
|
3008
|
+
...withSkippedCompleteBase,
|
|
3009
|
+
terminal_state: true,
|
|
3010
|
+
sealed_by: { arm: 'complete', step: options.command },
|
|
3011
|
+
terminal_reason: `Workflow completed.`,
|
|
3012
|
+
}
|
|
3013
|
+
: { ...withSkippedCompleteBase, terminal_state: false };
|
|
2987
3014
|
// On the terminal transition, drain the complete/always finalizers before the single seal.
|
|
2988
3015
|
// issue #220 PR-2 (D6): stamp defaulted_steps onto the SEALED terminal record only — the
|
|
2989
3016
|
// non-terminal branch (`completeDraft`) is never stamped (FM-5 guard: it must never leak onto a
|
|
@@ -3619,7 +3646,11 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3619
3646
|
? { gate_message: run.pending_gate.resolved_message }
|
|
3620
3647
|
: {}),
|
|
3621
3648
|
};
|
|
3622
|
-
|
|
3649
|
+
// issue #367: `sealed_by` joins the strip list — this write re-derives the run's liveness from
|
|
3650
|
+
// scratch, so a stale seal (a grandfathered/mixed-fleet record) must not survive it. The
|
|
3651
|
+
// explicit `terminal_state: false` below keeps the strip inside the store boundary's ORPHANED
|
|
3652
|
+
// exemption: a strip site flips non-terminal in the SAME write.
|
|
3653
|
+
const { pending_gate: _pg, terminal_reason: _tr, sealed_by: _sb, ...rest } = run;
|
|
3623
3654
|
const afterGate = {
|
|
3624
3655
|
...rest,
|
|
3625
3656
|
in_progress_steps: rest.in_progress_steps.filter((s) => s !== gateStepName),
|
|
@@ -3643,11 +3674,16 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3643
3674
|
(withSkippedGate.in_progress_steps.length === 0 &&
|
|
3644
3675
|
findEligibleSteps(definition, withSkippedGate).length === 0 &&
|
|
3645
3676
|
findEligibleGuardSteps(definition, withSkippedGate).length === 0);
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3677
|
+
// issue #367: seal fields staged first (the prior seal was already stripped at the `afterGate`
|
|
3678
|
+
// construction above, so this fork starts clean by construction).
|
|
3679
|
+
const gateDraft = isComplete
|
|
3680
|
+
? {
|
|
3681
|
+
...withSkippedGate,
|
|
3682
|
+
terminal_state: true,
|
|
3683
|
+
sealed_by: { arm: 'gate_resolution_complete', step: gateStepName },
|
|
3684
|
+
terminal_reason: `Workflow completed.`,
|
|
3685
|
+
}
|
|
3686
|
+
: { ...withSkippedGate, terminal_state: false };
|
|
3651
3687
|
// On the gate-completion terminal transition, drain complete/always finalizers before seal.
|
|
3652
3688
|
// issue #220 PR-2 (D6): stamp defaulted_steps onto the SEALED terminal record only (never the
|
|
3653
3689
|
// non-terminal `gateDraft` — the FM-5 guard).
|
|
@@ -3736,7 +3772,19 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3736
3772
|
completedAt: now,
|
|
3737
3773
|
input: {},
|
|
3738
3774
|
output: { error: `Unresolvable path: ${outcome.unresolvable_path}` },
|
|
3739
|
-
|
|
3775
|
+
// issue #373 correction: the path is the DIAGNOSTIC, and it used to live only in
|
|
3776
|
+
// `output_summary` + a transient seal-time overlay — so the post-drain re-render, which
|
|
3777
|
+
// rebuilds the cause from evidence alone, replaced it with the generic condition text.
|
|
3778
|
+
// Carrying it here makes every downstream read of this failure lossless. Sole production
|
|
3779
|
+
// mint: the settlement delta reuses this exact snapshot via `guardOwnEvidence`.
|
|
3780
|
+
//
|
|
3781
|
+
// The path goes FIRST because the per-message cap slices from the head: with the path last,
|
|
3782
|
+
// a long enough condition pushed it off the tail and the diagnostic vanished again. Honest
|
|
3783
|
+
// bound: a pathological PATH over ~230 chars still truncates itself, which is accepted —
|
|
3784
|
+
// head-first truncation keeps its prefix, and the prefix is the orienting part. ASCII
|
|
3785
|
+
// parenthetical, not an em dash, for the same reason the truncation marker is ASCII (logs,
|
|
3786
|
+
// terminals, a Postgres text column) — and a cut-off parenthetical reads as obviously partial.
|
|
3787
|
+
error: `Guard resolution error: unresolvable path '${outcome.unresolvable_path}' (condition: ${outcome.condition})`,
|
|
3740
3788
|
});
|
|
3741
3789
|
const withFailed = {
|
|
3742
3790
|
...run,
|
|
@@ -3749,10 +3797,18 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3749
3797
|
skipped_steps: resolutionErrorPropagated.skipped,
|
|
3750
3798
|
skip_details: resolutionErrorPropagated.details,
|
|
3751
3799
|
};
|
|
3800
|
+
// issue #373 — twin of settlement.ts's guard seal. The overlay is DEFENSIVE once evidence
|
|
3801
|
+
// carries the path (issue #373 correction, the `error` above); kept against caller-shaped
|
|
3802
|
+
// evidence that arrives without it.
|
|
3803
|
+
const guardPath = `unresolvable path '${outcome.unresolvable_path}'`;
|
|
3752
3804
|
return {
|
|
3753
3805
|
...withSkipped,
|
|
3754
3806
|
terminal_state: true,
|
|
3755
|
-
|
|
3807
|
+
// issue #367: the arm sits BESIDE #373's rendered sentence, never inside it.
|
|
3808
|
+
sealed_by: { arm: 'guard_resolution_error', step: stepName },
|
|
3809
|
+
terminal_reason: new Set(withSkipped.failed_steps).size > 1
|
|
3810
|
+
? renderFailCause(withSkipped.failed_steps, failureMessagesWithOverlay(withSkipped.evidence, stepName, guardPath))
|
|
3811
|
+
: `Guard step '${stepName}' failed: ${guardPath}`,
|
|
3756
3812
|
};
|
|
3757
3813
|
}
|
|
3758
3814
|
if (outcome.kind === 'pass') {
|
|
@@ -3779,11 +3835,16 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3779
3835
|
(withSkipped.in_progress_steps.length === 0 &&
|
|
3780
3836
|
findEligibleSteps(definition, withSkipped).length === 0 &&
|
|
3781
3837
|
findEligibleGuardSteps(definition, withSkipped).length === 0);
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3838
|
+
// issue #367: the stamp lives INSIDE the isComplete arm ONLY — a non-terminal guard pass must
|
|
3839
|
+
// never carry a seal.
|
|
3840
|
+
return isComplete
|
|
3841
|
+
? {
|
|
3842
|
+
...withSkipped,
|
|
3843
|
+
terminal_state: true,
|
|
3844
|
+
sealed_by: { arm: 'guard_pass_complete', step: stepName },
|
|
3845
|
+
terminal_reason: 'Workflow completed.',
|
|
3846
|
+
}
|
|
3847
|
+
: { ...withSkipped, terminal_state: false };
|
|
3787
3848
|
}
|
|
3788
3849
|
// Guard fired — one or more conditions false; abort the run.
|
|
3789
3850
|
const evidenceEntry = captureEvidence({
|
|
@@ -3816,6 +3877,10 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3816
3877
|
return {
|
|
3817
3878
|
...withAllSkipped,
|
|
3818
3879
|
terminal_state: true,
|
|
3880
|
+
// issue #367: same object literal as the terminal flip. terminal_reason stays ABSENT here (a
|
|
3881
|
+
// guard abort is the one reason-less seal) — the phase now derives from the arm, and
|
|
3882
|
+
// `aborted_at` is asserted congruent rather than consulted.
|
|
3883
|
+
sealed_by: { arm: 'guard_abort', step: stepName },
|
|
3819
3884
|
aborted_at: {
|
|
3820
3885
|
step_id: stepName,
|
|
3821
3886
|
conditions: outcome.conditions,
|
|
@@ -3841,8 +3906,10 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3841
3906
|
* `withTimeout` honoring `timeout_seconds` (default `DRAIN_CEILING_SECONDS`). Success →
|
|
3842
3907
|
* evidence + completed_steps; thrown error / STEP_TIMEOUT / handler `{ abort }` → evidence
|
|
3843
3908
|
* marked failed + failed_steps, NON-FATAL (the drain continues). A finalizer NEVER mutates
|
|
3844
|
-
* `aborted_at`, `
|
|
3845
|
-
* the terminal marks come from `sealDraft` and `deriveRunPhase` precedence keeps
|
|
3909
|
+
* `aborted_at`, `terminal_state`, `skipped_steps`, or emits next_actions, and never changes the
|
|
3910
|
+
* sealed OUTCOME — the terminal marks come from `sealDraft` and `deriveRunPhase` precedence keeps
|
|
3911
|
+
* the phase. issue #373: a finalizer whose OWN failure grows `failed_steps` on a fail-class seal
|
|
3912
|
+
* DOES re-render `terminal_reason`, so the one-line cause keeps agreeing with the record.
|
|
3846
3913
|
*/
|
|
3847
3914
|
async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
3848
3915
|
// Zero-finalizer fast path: no finalizer steps declared ⇒ return the seal draft
|
|
@@ -3858,6 +3925,13 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3858
3925
|
// legacy seal path for a non-declaring external store (the dormancy fallback).
|
|
3859
3926
|
const settled = new Set([...sealDraft.completed_steps, ...sealDraft.failed_steps]);
|
|
3860
3927
|
const selected = selectFinalizers(definition, settled, deriveEffectiveTriggers(outcome, sealDraft));
|
|
3928
|
+
// issue #367: the draft already carries sealed_by (every caller stamps at construction), so the
|
|
3929
|
+
// fast path above can never bypass the stamp.
|
|
3930
|
+
// issue #373: the post-drain re-render fires only if the drain ACTUALLY appended a failure.
|
|
3931
|
+
// The legacy seal is reached on every fail-class outcome, including one where every finalizer
|
|
3932
|
+
// succeeds — re-rendering there would rebuild the sentence without the seal site's in-hand
|
|
3933
|
+
// overlay (the guard sites' unresolvable-path text) and silently drop it.
|
|
3934
|
+
const failedBeforeDrain = sealDraft.failed_steps.length;
|
|
3861
3935
|
let record = sealDraft;
|
|
3862
3936
|
for (const name of selected) {
|
|
3863
3937
|
const step = definition.steps[name];
|
|
@@ -3876,8 +3950,10 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3876
3950
|
try {
|
|
3877
3951
|
const result = await withTimeout((signal) => callHandler(step, options, record, evidenceByStep, signal), timeoutMs, name);
|
|
3878
3952
|
if (result.kind === 'abort') {
|
|
3879
|
-
// A finalizer handler returning { abort } is a recorded NON-FATAL failure — it must
|
|
3880
|
-
//
|
|
3953
|
+
// A finalizer handler returning { abort } is a recorded NON-FATAL failure — it must never
|
|
3954
|
+
// mutate aborted_at or the sealed OUTCOME. (issue #373: it does join `failed_steps`, and
|
|
3955
|
+
// the post-drain re-render below folds it into the one-line cause — that reports the
|
|
3956
|
+
// outcome faithfully, it does not change it.)
|
|
3881
3957
|
record = {
|
|
3882
3958
|
...record,
|
|
3883
3959
|
evidence: [
|
|
@@ -3932,7 +4008,44 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3932
4008
|
};
|
|
3933
4009
|
}
|
|
3934
4010
|
}
|
|
3935
|
-
|
|
4011
|
+
// issue #367: `sealed_by` is INHERITED from sealDraft, never re-derived here — a finalizer never
|
|
4012
|
+
// changes which arm sealed the run, and re-deriving from `outcome` would re-couple the arm to
|
|
4013
|
+
// mintOutcome, the exact coupling the recorded fact exists to break. `sealDraft` is terminal at
|
|
4014
|
+
// every call site (all of them sit inside an isComplete/abort branch), so this narrow is total;
|
|
4015
|
+
// it throws rather than fabricating an arm if a future caller violates that.
|
|
4016
|
+
if (sealDraft.terminal_state !== true) {
|
|
4017
|
+
throw new Error('buildFinalizedSeal called with a non-terminal sealDraft');
|
|
4018
|
+
}
|
|
4019
|
+
// Conditional spread, NOT `sealed_by: sealDraft.sealed_by`: under the flat optional plus
|
|
4020
|
+
// exactOptionalPropertyTypes the unconditional form does not compile. An unstamped draft passes
|
|
4021
|
+
// through honest-absent, and refusing it is the store boundary's job.
|
|
4022
|
+
/* eslint-disable-next-line no-restricted-syntax --
|
|
4023
|
+
* issue #367 (part 2), AUTHORIZED: the stamp IS in this literal, as a conditional spread —
|
|
4024
|
+
* `exactOptionalPropertyTypes` forbids the unconditional `sealed_by: sealDraft.sealed_by` form,
|
|
4025
|
+
* and a spread is not a `sealed_by` Property the selector can see. An unstamped draft passes
|
|
4026
|
+
* through honest-absent on purpose; refusing it is the store boundary's job, per the comment
|
|
4027
|
+
* directly above.
|
|
4028
|
+
*/
|
|
4029
|
+
const drained = {
|
|
4030
|
+
...record,
|
|
4031
|
+
terminal_state: true,
|
|
4032
|
+
...(sealDraft.sealed_by !== undefined ? { sealed_by: sealDraft.sealed_by } : {}),
|
|
4033
|
+
};
|
|
4034
|
+
// issue #367: SEAL_MARKERS_AGREE — transform-scoped assertion on the record THIS function
|
|
4035
|
+
// produces (the second of the two transform homes; never universal).
|
|
4036
|
+
assertSealMarkersAgree(drained);
|
|
4037
|
+
// Twin of applyMarkFinalizer's failed-arm re-render. `deriveRunPhase`, not `outcome`: an aborted
|
|
4038
|
+
// run whose finalizer also failed keeps its abort sentence (aborted_at wins), and a
|
|
4039
|
+
// complete-outcome run keeps the complete-seal literal untouched.
|
|
4040
|
+
if (record.failed_steps.length > failedBeforeDrain &&
|
|
4041
|
+
deriveRunPhase(drained) === 'failed' &&
|
|
4042
|
+
new Set(record.failed_steps).size > 1) {
|
|
4043
|
+
return {
|
|
4044
|
+
...drained,
|
|
4045
|
+
terminal_reason: renderFailCause(record.failed_steps, failureMessagesFromEvidence(record.evidence)),
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
return drained;
|
|
3936
4049
|
}
|
|
3937
4050
|
/** Every currently-`'pending'` finalizer in `run`'s ledger, ascending by rank — the order the
|
|
3938
4051
|
* drain loop below consumes (design record §6). */
|
|
@@ -4412,13 +4525,30 @@ depth0Warnings) {
|
|
|
4412
4525
|
// (terminal_reason 'Workflow completed.', no aborted_at). `aborted_at ? 'abort' : 'fail'`
|
|
4413
4526
|
// would wrongly run the catch finalizers on that success. When terminal, drain the
|
|
4414
4527
|
// matching finalizers before the single seal write; non-terminal guard passes persist as-is.
|
|
4415
|
-
const
|
|
4528
|
+
const guardProse = guardResult.terminal_state
|
|
4416
4529
|
? guardResult.aborted_at !== undefined
|
|
4417
4530
|
? 'abort'
|
|
4418
4531
|
: guardResult.terminal_reason === 'Workflow completed.'
|
|
4419
4532
|
? 'complete'
|
|
4420
4533
|
: 'fail'
|
|
4421
4534
|
: undefined;
|
|
4535
|
+
// issue #367: this classifier stays the PRODUCER of guardOutcome (it feeds buildFinalizedSeal,
|
|
4536
|
+
// so a wrong answer here silently drains the wrong finalizers — the executed harm). It now
|
|
4537
|
+
// prefers the RECORDED arm, keeps the prose branch as the fallback for an unstamped record,
|
|
4538
|
+
// and throws if the two disagree: a future writer gap here is loud, never a silent loss.
|
|
4539
|
+
const guardArmOutcome = guardResult.terminal_state && guardResult.sealed_by !== undefined
|
|
4540
|
+
? armToOutcome(guardResult.sealed_by.arm)
|
|
4541
|
+
: undefined;
|
|
4542
|
+
if (guardArmOutcome === 'abandon') {
|
|
4543
|
+
throw new Error(`guard seal on '${guardName}' produced a non-guard arm`);
|
|
4544
|
+
}
|
|
4545
|
+
if (guardArmOutcome !== undefined &&
|
|
4546
|
+
guardProse !== undefined &&
|
|
4547
|
+
guardArmOutcome !== guardProse) {
|
|
4548
|
+
throw new Error(`sealed_by.arm (${guardArmOutcome}) disagrees with the prose classifier (${guardProse}) ` +
|
|
4549
|
+
`on guard '${guardName}'`);
|
|
4550
|
+
}
|
|
4551
|
+
const guardOutcome = guardArmOutcome ?? guardProse;
|
|
4422
4552
|
// issue #220 PR-2 (D6): stamp defaulted_steps ONLY on the 'complete' seal — a guard that FAILS
|
|
4423
4553
|
// or ABORTS the run does not get the qualifier (the FM-5 guard: never on a non-complete
|
|
4424
4554
|
// terminal, and never on the non-terminal `guardResult` passthrough).
|