@sensigo/realm 0.38.0 → 0.40.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 +17 -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 +172 -37
- package/dist/engine/execution-loop.js.map +1 -1
- package/dist/engine/run-health.d.ts +1 -1
- package/dist/engine/run-health.d.ts.map +1 -1
- package/dist/engine/run-health.js +89 -0
- 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 +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/store/failed-attempt-store.d.ts +9 -1
- package/dist/store/failed-attempt-store.d.ts.map +1 -1
- package/dist/store/failed-attempt-store.js +12 -1
- package/dist/store/failed-attempt-store.js.map +1 -1
- package/dist/store/json-file-store.d.ts +28 -4
- package/dist/store/json-file-store.d.ts.map +1 -1
- package/dist/store/json-file-store.js +133 -5
- package/dist/store/json-file-store.js.map +1 -1
- package/dist/store/per-run-artifact-store.d.ts +42 -1
- package/dist/store/per-run-artifact-store.d.ts.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/store/trace-buffer-store.d.ts +32 -6
- package/dist/store/trace-buffer-store.d.ts.map +1 -1
- package/dist/store/trace-buffer-store.js +39 -6
- package/dist/store/trace-buffer-store.js.map +1 -1
- package/dist/types/response-envelope.d.ts +0 -1
- package/dist/types/response-envelope.d.ts.map +1 -1
- package/dist/types/run-record.d.ts +201 -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-definition.d.ts +11 -1
- package/dist/types/workflow-definition.d.ts.map +1 -1
- package/dist/types/workflow-definition.js +1 -0
- package/dist/types/workflow-definition.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/diagnostics.d.ts +39 -3
- package/dist/workflow/diagnostics.d.ts.map +1 -1
- package/dist/workflow/diagnostics.js +37 -10
- package/dist/workflow/diagnostics.js.map +1 -1
- package/dist/workflow/source-positions.d.ts +39 -0
- package/dist/workflow/source-positions.d.ts.map +1 -0
- package/dist/workflow/source-positions.js +107 -0
- package/dist/workflow/source-positions.js.map +1 -0
- package/dist/workflow/yaml-loader.d.ts.map +1 -1
- package/dist/workflow/yaml-loader.js +436 -117
- 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) {
|
|
@@ -370,7 +370,12 @@ function stepToNextAction(stepName, step, context) {
|
|
|
370
370
|
: {}),
|
|
371
371
|
human_readable: `Execute step '${stepName}': ${step.description}`,
|
|
372
372
|
orientation: `Run is active. Next step ready: '${stepName}'.`,
|
|
373
|
-
|
|
373
|
+
// issue #412: the `expected_timeout` display is GONE, not scoped. It rendered wherever a
|
|
374
|
+
// NextAction was built — agent steps, and handler-bearing steps of any kind — and on both
|
|
375
|
+
// the instructed action is outside anything that enforced the number: an agent step's
|
|
376
|
+
// dispatch is never wrapped in a timeout, and a handler NextAction tells the reader to call
|
|
377
|
+
// the handler themselves, which no withTimeout covers. A displayed bound that never governs
|
|
378
|
+
// the action it is displayed beside is worse than no display.
|
|
374
379
|
...(resolvedPrompt !== undefined ? { prompt: resolvedPrompt } : {}),
|
|
375
380
|
};
|
|
376
381
|
}
|
|
@@ -1764,6 +1769,8 @@ export async function executeStep(store, definition, options) {
|
|
|
1764
1769
|
const abortDraft = {
|
|
1765
1770
|
...withAllSkipped,
|
|
1766
1771
|
terminal_state: true,
|
|
1772
|
+
// issue #367: the seal fields live in the SAME object literal as the terminal flip.
|
|
1773
|
+
sealed_by: { arm: 'handler_abort', step: options.command },
|
|
1767
1774
|
terminal_reason: `Handler '${options.command}' aborted the run: ${abortMessage}`,
|
|
1768
1775
|
aborted_at: {
|
|
1769
1776
|
step_id: options.command,
|
|
@@ -2394,14 +2401,34 @@ export async function executeStep(store, definition, options) {
|
|
|
2394
2401
|
(withSkippedFail.in_progress_steps.length === 0 &&
|
|
2395
2402
|
findEligibleSteps(definition, withSkippedFail).length === 0 &&
|
|
2396
2403
|
findEligibleGuardSteps(definition, withSkippedFail).length === 0);
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2404
|
+
// Hoisted so the #373 message walk below reads the SAME evidence the record will carry —
|
|
2405
|
+
// `withSkippedFail` does not yet include this attempt's snapshots.
|
|
2406
|
+
const failEvidence = [...pendingRun.evidence, ...allEvidence];
|
|
2407
|
+
// issue #373 — twin of settlement.ts's fail seal; both layers must emit byte-identical
|
|
2408
|
+
// sentences for identical inputs. Single-failure shape unchanged (this site has never had the
|
|
2409
|
+
// `?? 'unknown error'` fallback — `dispatchError.message` is always a string).
|
|
2410
|
+
const failCause = new Set(withSkippedFail.failed_steps).size > 1
|
|
2411
|
+
? renderFailCause(withSkippedFail.failed_steps, failureMessagesWithOverlay(failEvidence, options.command, dispatchError.message))
|
|
2412
|
+
: `Step '${options.command}' failed: ${dispatchError.message}`;
|
|
2413
|
+
// issue #367: seal fields staged FIRST in the SAME object literal, and a stale prior seal
|
|
2414
|
+
// never survives either fork. #373's `failCause` (computed above) is untouched — the arm sits
|
|
2415
|
+
// BESIDE the rendered sentence, never inside it. Validation exhaustion is deliberately NOT a
|
|
2416
|
+
// distinct arm: a VALIDATION_EXHAUSTED failure seals 'step_failure' like any other terminal
|
|
2417
|
+
// step failure, and the distinction lives in defaulted_steps/diagnostics.
|
|
2418
|
+
const { sealed_by: _priorFailSeal, ...withSkippedFailBase } = withSkippedFail;
|
|
2419
|
+
const failDraft = isComplete
|
|
2420
|
+
? {
|
|
2421
|
+
...withSkippedFailBase,
|
|
2422
|
+
terminal_state: true,
|
|
2423
|
+
sealed_by: { arm: 'step_failure', step: options.command },
|
|
2424
|
+
evidence: failEvidence,
|
|
2425
|
+
terminal_reason: failCause,
|
|
2426
|
+
}
|
|
2427
|
+
: {
|
|
2428
|
+
...withSkippedFailBase,
|
|
2429
|
+
terminal_state: false,
|
|
2430
|
+
evidence: failEvidence,
|
|
2431
|
+
};
|
|
2405
2432
|
// On the terminal transition, drain the fail/always finalizers before the single seal.
|
|
2406
2433
|
// Non-terminal failures (recovery steps remain) run no finalizers.
|
|
2407
2434
|
const failedRun = isComplete
|
|
@@ -2979,11 +3006,16 @@ export async function executeStep(store, definition, options) {
|
|
|
2979
3006
|
(withSkippedComplete.in_progress_steps.length === 0 &&
|
|
2980
3007
|
findEligibleSteps(definition, withSkippedComplete).length === 0 &&
|
|
2981
3008
|
findEligibleGuardSteps(definition, withSkippedComplete).length === 0);
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3009
|
+
// issue #367: seal fields staged first in the SAME object literal; stale prior seal stripped.
|
|
3010
|
+
const { sealed_by: _priorCompleteSeal, ...withSkippedCompleteBase } = withSkippedComplete;
|
|
3011
|
+
const completeDraft = isComplete
|
|
3012
|
+
? {
|
|
3013
|
+
...withSkippedCompleteBase,
|
|
3014
|
+
terminal_state: true,
|
|
3015
|
+
sealed_by: { arm: 'complete', step: options.command },
|
|
3016
|
+
terminal_reason: `Workflow completed.`,
|
|
3017
|
+
}
|
|
3018
|
+
: { ...withSkippedCompleteBase, terminal_state: false };
|
|
2987
3019
|
// On the terminal transition, drain the complete/always finalizers before the single seal.
|
|
2988
3020
|
// issue #220 PR-2 (D6): stamp defaulted_steps onto the SEALED terminal record only — the
|
|
2989
3021
|
// non-terminal branch (`completeDraft`) is never stamped (FM-5 guard: it must never leak onto a
|
|
@@ -3244,7 +3276,7 @@ async function composeExpiredGateEnvelope(store, definition, registry, originalG
|
|
|
3244
3276
|
// — never attribute the outcome to THIS gate's expiry (no matched entry exists to attribute it
|
|
3245
3277
|
// to), and never claim "expired" at all, since nothing here witnessed this gate expiring.
|
|
3246
3278
|
const err = new WorkflowError(`Gate '${originalGateId}': the run reached a terminal outcome concurrently — your choice ` +
|
|
3247
|
-
`was NOT recorded. 'realm resume' clears a stale pending gate on a resumable run, or ` +
|
|
3279
|
+
`was NOT recorded. 'realm run resume' clears a stale pending gate on a resumable run, or ` +
|
|
3248
3280
|
`'realm run purge' removes the record entirely.`, {
|
|
3249
3281
|
code: 'STATE_RUN_TERMINAL',
|
|
3250
3282
|
category: 'STATE',
|
|
@@ -3454,7 +3486,7 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3454
3486
|
// Zombie/grandfathered variant — the #282 class: a terminal record may still carry a
|
|
3455
3487
|
// stale pending_gate (never cleared), which is the best-effort step label here.
|
|
3456
3488
|
const zombieStep = result.run.pending_gate?.step_name ?? 'submit_gate';
|
|
3457
|
-
const err = new WorkflowError(`Run '${options.runId}' is terminal; cannot submit a gate response — 'realm resume' ` +
|
|
3489
|
+
const err = new WorkflowError(`Run '${options.runId}' is terminal; cannot submit a gate response — 'realm run resume' ` +
|
|
3458
3490
|
`clears a stale pending gate on a resumable run, or 'realm run purge' removes the ` +
|
|
3459
3491
|
`record entirely.`, {
|
|
3460
3492
|
code: 'STATE_RUN_TERMINAL',
|
|
@@ -3619,7 +3651,11 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3619
3651
|
? { gate_message: run.pending_gate.resolved_message }
|
|
3620
3652
|
: {}),
|
|
3621
3653
|
};
|
|
3622
|
-
|
|
3654
|
+
// issue #367: `sealed_by` joins the strip list — this write re-derives the run's liveness from
|
|
3655
|
+
// scratch, so a stale seal (a grandfathered/mixed-fleet record) must not survive it. The
|
|
3656
|
+
// explicit `terminal_state: false` below keeps the strip inside the store boundary's ORPHANED
|
|
3657
|
+
// exemption: a strip site flips non-terminal in the SAME write.
|
|
3658
|
+
const { pending_gate: _pg, terminal_reason: _tr, sealed_by: _sb, ...rest } = run;
|
|
3623
3659
|
const afterGate = {
|
|
3624
3660
|
...rest,
|
|
3625
3661
|
in_progress_steps: rest.in_progress_steps.filter((s) => s !== gateStepName),
|
|
@@ -3643,11 +3679,16 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3643
3679
|
(withSkippedGate.in_progress_steps.length === 0 &&
|
|
3644
3680
|
findEligibleSteps(definition, withSkippedGate).length === 0 &&
|
|
3645
3681
|
findEligibleGuardSteps(definition, withSkippedGate).length === 0);
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3682
|
+
// issue #367: seal fields staged first (the prior seal was already stripped at the `afterGate`
|
|
3683
|
+
// construction above, so this fork starts clean by construction).
|
|
3684
|
+
const gateDraft = isComplete
|
|
3685
|
+
? {
|
|
3686
|
+
...withSkippedGate,
|
|
3687
|
+
terminal_state: true,
|
|
3688
|
+
sealed_by: { arm: 'gate_resolution_complete', step: gateStepName },
|
|
3689
|
+
terminal_reason: `Workflow completed.`,
|
|
3690
|
+
}
|
|
3691
|
+
: { ...withSkippedGate, terminal_state: false };
|
|
3651
3692
|
// On the gate-completion terminal transition, drain complete/always finalizers before seal.
|
|
3652
3693
|
// issue #220 PR-2 (D6): stamp defaulted_steps onto the SEALED terminal record only (never the
|
|
3653
3694
|
// non-terminal `gateDraft` — the FM-5 guard).
|
|
@@ -3736,7 +3777,19 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3736
3777
|
completedAt: now,
|
|
3737
3778
|
input: {},
|
|
3738
3779
|
output: { error: `Unresolvable path: ${outcome.unresolvable_path}` },
|
|
3739
|
-
|
|
3780
|
+
// issue #373 correction: the path is the DIAGNOSTIC, and it used to live only in
|
|
3781
|
+
// `output_summary` + a transient seal-time overlay — so the post-drain re-render, which
|
|
3782
|
+
// rebuilds the cause from evidence alone, replaced it with the generic condition text.
|
|
3783
|
+
// Carrying it here makes every downstream read of this failure lossless. Sole production
|
|
3784
|
+
// mint: the settlement delta reuses this exact snapshot via `guardOwnEvidence`.
|
|
3785
|
+
//
|
|
3786
|
+
// The path goes FIRST because the per-message cap slices from the head: with the path last,
|
|
3787
|
+
// a long enough condition pushed it off the tail and the diagnostic vanished again. Honest
|
|
3788
|
+
// bound: a pathological PATH over ~230 chars still truncates itself, which is accepted —
|
|
3789
|
+
// head-first truncation keeps its prefix, and the prefix is the orienting part. ASCII
|
|
3790
|
+
// parenthetical, not an em dash, for the same reason the truncation marker is ASCII (logs,
|
|
3791
|
+
// terminals, a Postgres text column) — and a cut-off parenthetical reads as obviously partial.
|
|
3792
|
+
error: `Guard resolution error: unresolvable path '${outcome.unresolvable_path}' (condition: ${outcome.condition})`,
|
|
3740
3793
|
});
|
|
3741
3794
|
const withFailed = {
|
|
3742
3795
|
...run,
|
|
@@ -3749,10 +3802,18 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3749
3802
|
skipped_steps: resolutionErrorPropagated.skipped,
|
|
3750
3803
|
skip_details: resolutionErrorPropagated.details,
|
|
3751
3804
|
};
|
|
3805
|
+
// issue #373 — twin of settlement.ts's guard seal. The overlay is DEFENSIVE once evidence
|
|
3806
|
+
// carries the path (issue #373 correction, the `error` above); kept against caller-shaped
|
|
3807
|
+
// evidence that arrives without it.
|
|
3808
|
+
const guardPath = `unresolvable path '${outcome.unresolvable_path}'`;
|
|
3752
3809
|
return {
|
|
3753
3810
|
...withSkipped,
|
|
3754
3811
|
terminal_state: true,
|
|
3755
|
-
|
|
3812
|
+
// issue #367: the arm sits BESIDE #373's rendered sentence, never inside it.
|
|
3813
|
+
sealed_by: { arm: 'guard_resolution_error', step: stepName },
|
|
3814
|
+
terminal_reason: new Set(withSkipped.failed_steps).size > 1
|
|
3815
|
+
? renderFailCause(withSkipped.failed_steps, failureMessagesWithOverlay(withSkipped.evidence, stepName, guardPath))
|
|
3816
|
+
: `Guard step '${stepName}' failed: ${guardPath}`,
|
|
3756
3817
|
};
|
|
3757
3818
|
}
|
|
3758
3819
|
if (outcome.kind === 'pass') {
|
|
@@ -3779,11 +3840,16 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3779
3840
|
(withSkipped.in_progress_steps.length === 0 &&
|
|
3780
3841
|
findEligibleSteps(definition, withSkipped).length === 0 &&
|
|
3781
3842
|
findEligibleGuardSteps(definition, withSkipped).length === 0);
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3843
|
+
// issue #367: the stamp lives INSIDE the isComplete arm ONLY — a non-terminal guard pass must
|
|
3844
|
+
// never carry a seal.
|
|
3845
|
+
return isComplete
|
|
3846
|
+
? {
|
|
3847
|
+
...withSkipped,
|
|
3848
|
+
terminal_state: true,
|
|
3849
|
+
sealed_by: { arm: 'guard_pass_complete', step: stepName },
|
|
3850
|
+
terminal_reason: 'Workflow completed.',
|
|
3851
|
+
}
|
|
3852
|
+
: { ...withSkipped, terminal_state: false };
|
|
3787
3853
|
}
|
|
3788
3854
|
// Guard fired — one or more conditions false; abort the run.
|
|
3789
3855
|
const evidenceEntry = captureEvidence({
|
|
@@ -3816,6 +3882,10 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3816
3882
|
return {
|
|
3817
3883
|
...withAllSkipped,
|
|
3818
3884
|
terminal_state: true,
|
|
3885
|
+
// issue #367: same object literal as the terminal flip. terminal_reason stays ABSENT here (a
|
|
3886
|
+
// guard abort is the one reason-less seal) — the phase now derives from the arm, and
|
|
3887
|
+
// `aborted_at` is asserted congruent rather than consulted.
|
|
3888
|
+
sealed_by: { arm: 'guard_abort', step: stepName },
|
|
3819
3889
|
aborted_at: {
|
|
3820
3890
|
step_id: stepName,
|
|
3821
3891
|
conditions: outcome.conditions,
|
|
@@ -3841,8 +3911,10 @@ async function executeGuardStep(stepName, stepDef, definition, run) {
|
|
|
3841
3911
|
* `withTimeout` honoring `timeout_seconds` (default `DRAIN_CEILING_SECONDS`). Success →
|
|
3842
3912
|
* evidence + completed_steps; thrown error / STEP_TIMEOUT / handler `{ abort }` → evidence
|
|
3843
3913
|
* 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
|
|
3914
|
+
* `aborted_at`, `terminal_state`, `skipped_steps`, or emits next_actions, and never changes the
|
|
3915
|
+
* sealed OUTCOME — the terminal marks come from `sealDraft` and `deriveRunPhase` precedence keeps
|
|
3916
|
+
* the phase. issue #373: a finalizer whose OWN failure grows `failed_steps` on a fail-class seal
|
|
3917
|
+
* DOES re-render `terminal_reason`, so the one-line cause keeps agreeing with the record.
|
|
3846
3918
|
*/
|
|
3847
3919
|
async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
3848
3920
|
// Zero-finalizer fast path: no finalizer steps declared ⇒ return the seal draft
|
|
@@ -3858,6 +3930,13 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3858
3930
|
// legacy seal path for a non-declaring external store (the dormancy fallback).
|
|
3859
3931
|
const settled = new Set([...sealDraft.completed_steps, ...sealDraft.failed_steps]);
|
|
3860
3932
|
const selected = selectFinalizers(definition, settled, deriveEffectiveTriggers(outcome, sealDraft));
|
|
3933
|
+
// issue #367: the draft already carries sealed_by (every caller stamps at construction), so the
|
|
3934
|
+
// fast path above can never bypass the stamp.
|
|
3935
|
+
// issue #373: the post-drain re-render fires only if the drain ACTUALLY appended a failure.
|
|
3936
|
+
// The legacy seal is reached on every fail-class outcome, including one where every finalizer
|
|
3937
|
+
// succeeds — re-rendering there would rebuild the sentence without the seal site's in-hand
|
|
3938
|
+
// overlay (the guard sites' unresolvable-path text) and silently drop it.
|
|
3939
|
+
const failedBeforeDrain = sealDraft.failed_steps.length;
|
|
3861
3940
|
let record = sealDraft;
|
|
3862
3941
|
for (const name of selected) {
|
|
3863
3942
|
const step = definition.steps[name];
|
|
@@ -3876,8 +3955,10 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3876
3955
|
try {
|
|
3877
3956
|
const result = await withTimeout((signal) => callHandler(step, options, record, evidenceByStep, signal), timeoutMs, name);
|
|
3878
3957
|
if (result.kind === 'abort') {
|
|
3879
|
-
// A finalizer handler returning { abort } is a recorded NON-FATAL failure — it must
|
|
3880
|
-
//
|
|
3958
|
+
// A finalizer handler returning { abort } is a recorded NON-FATAL failure — it must never
|
|
3959
|
+
// mutate aborted_at or the sealed OUTCOME. (issue #373: it does join `failed_steps`, and
|
|
3960
|
+
// the post-drain re-render below folds it into the one-line cause — that reports the
|
|
3961
|
+
// outcome faithfully, it does not change it.)
|
|
3881
3962
|
record = {
|
|
3882
3963
|
...record,
|
|
3883
3964
|
evidence: [
|
|
@@ -3932,7 +4013,44 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3932
4013
|
};
|
|
3933
4014
|
}
|
|
3934
4015
|
}
|
|
3935
|
-
|
|
4016
|
+
// issue #367: `sealed_by` is INHERITED from sealDraft, never re-derived here — a finalizer never
|
|
4017
|
+
// changes which arm sealed the run, and re-deriving from `outcome` would re-couple the arm to
|
|
4018
|
+
// mintOutcome, the exact coupling the recorded fact exists to break. `sealDraft` is terminal at
|
|
4019
|
+
// every call site (all of them sit inside an isComplete/abort branch), so this narrow is total;
|
|
4020
|
+
// it throws rather than fabricating an arm if a future caller violates that.
|
|
4021
|
+
if (sealDraft.terminal_state !== true) {
|
|
4022
|
+
throw new Error('buildFinalizedSeal called with a non-terminal sealDraft');
|
|
4023
|
+
}
|
|
4024
|
+
// Conditional spread, NOT `sealed_by: sealDraft.sealed_by`: under the flat optional plus
|
|
4025
|
+
// exactOptionalPropertyTypes the unconditional form does not compile. An unstamped draft passes
|
|
4026
|
+
// through honest-absent, and refusing it is the store boundary's job.
|
|
4027
|
+
/* eslint-disable-next-line no-restricted-syntax --
|
|
4028
|
+
* issue #367 (part 2), AUTHORIZED: the stamp IS in this literal, as a conditional spread —
|
|
4029
|
+
* `exactOptionalPropertyTypes` forbids the unconditional `sealed_by: sealDraft.sealed_by` form,
|
|
4030
|
+
* and a spread is not a `sealed_by` Property the selector can see. An unstamped draft passes
|
|
4031
|
+
* through honest-absent on purpose; refusing it is the store boundary's job, per the comment
|
|
4032
|
+
* directly above.
|
|
4033
|
+
*/
|
|
4034
|
+
const drained = {
|
|
4035
|
+
...record,
|
|
4036
|
+
terminal_state: true,
|
|
4037
|
+
...(sealDraft.sealed_by !== undefined ? { sealed_by: sealDraft.sealed_by } : {}),
|
|
4038
|
+
};
|
|
4039
|
+
// issue #367: SEAL_MARKERS_AGREE — transform-scoped assertion on the record THIS function
|
|
4040
|
+
// produces (the second of the two transform homes; never universal).
|
|
4041
|
+
assertSealMarkersAgree(drained);
|
|
4042
|
+
// Twin of applyMarkFinalizer's failed-arm re-render. `deriveRunPhase`, not `outcome`: an aborted
|
|
4043
|
+
// run whose finalizer also failed keeps its abort sentence (aborted_at wins), and a
|
|
4044
|
+
// complete-outcome run keeps the complete-seal literal untouched.
|
|
4045
|
+
if (record.failed_steps.length > failedBeforeDrain &&
|
|
4046
|
+
deriveRunPhase(drained) === 'failed' &&
|
|
4047
|
+
new Set(record.failed_steps).size > 1) {
|
|
4048
|
+
return {
|
|
4049
|
+
...drained,
|
|
4050
|
+
terminal_reason: renderFailCause(record.failed_steps, failureMessagesFromEvidence(record.evidence)),
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
return drained;
|
|
3936
4054
|
}
|
|
3937
4055
|
/** Every currently-`'pending'` finalizer in `run`'s ledger, ascending by rank — the order the
|
|
3938
4056
|
* drain loop below consumes (design record §6). */
|
|
@@ -4412,13 +4530,30 @@ depth0Warnings) {
|
|
|
4412
4530
|
// (terminal_reason 'Workflow completed.', no aborted_at). `aborted_at ? 'abort' : 'fail'`
|
|
4413
4531
|
// would wrongly run the catch finalizers on that success. When terminal, drain the
|
|
4414
4532
|
// matching finalizers before the single seal write; non-terminal guard passes persist as-is.
|
|
4415
|
-
const
|
|
4533
|
+
const guardProse = guardResult.terminal_state
|
|
4416
4534
|
? guardResult.aborted_at !== undefined
|
|
4417
4535
|
? 'abort'
|
|
4418
4536
|
: guardResult.terminal_reason === 'Workflow completed.'
|
|
4419
4537
|
? 'complete'
|
|
4420
4538
|
: 'fail'
|
|
4421
4539
|
: undefined;
|
|
4540
|
+
// issue #367: this classifier stays the PRODUCER of guardOutcome (it feeds buildFinalizedSeal,
|
|
4541
|
+
// so a wrong answer here silently drains the wrong finalizers — the executed harm). It now
|
|
4542
|
+
// prefers the RECORDED arm, keeps the prose branch as the fallback for an unstamped record,
|
|
4543
|
+
// and throws if the two disagree: a future writer gap here is loud, never a silent loss.
|
|
4544
|
+
const guardArmOutcome = guardResult.terminal_state && guardResult.sealed_by !== undefined
|
|
4545
|
+
? armToOutcome(guardResult.sealed_by.arm)
|
|
4546
|
+
: undefined;
|
|
4547
|
+
if (guardArmOutcome === 'abandon') {
|
|
4548
|
+
throw new Error(`guard seal on '${guardName}' produced a non-guard arm`);
|
|
4549
|
+
}
|
|
4550
|
+
if (guardArmOutcome !== undefined &&
|
|
4551
|
+
guardProse !== undefined &&
|
|
4552
|
+
guardArmOutcome !== guardProse) {
|
|
4553
|
+
throw new Error(`sealed_by.arm (${guardArmOutcome}) disagrees with the prose classifier (${guardProse}) ` +
|
|
4554
|
+
`on guard '${guardName}'`);
|
|
4555
|
+
}
|
|
4556
|
+
const guardOutcome = guardArmOutcome ?? guardProse;
|
|
4422
4557
|
// issue #220 PR-2 (D6): stamp defaulted_steps ONLY on the 'complete' seal — a guard that FAILS
|
|
4423
4558
|
// or ABORTS the run does not get the qualifier (the FM-5 guard: never on a non-complete
|
|
4424
4559
|
// terminal, and never on the non-terminal `guardResult` passthrough).
|