@sensigo/realm 0.37.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.
Files changed (64) hide show
  1. package/dist/adapters/adapter-utils.d.ts +31 -0
  2. package/dist/adapters/adapter-utils.d.ts.map +1 -1
  3. package/dist/adapters/adapter-utils.js +16 -0
  4. package/dist/adapters/adapter-utils.js.map +1 -1
  5. package/dist/adapters/airtable-adapter.d.ts.map +1 -1
  6. package/dist/adapters/airtable-adapter.js +25 -14
  7. package/dist/adapters/airtable-adapter.js.map +1 -1
  8. package/dist/adapters/gorgias-adapter.d.ts.map +1 -1
  9. package/dist/adapters/gorgias-adapter.js +7 -3
  10. package/dist/adapters/gorgias-adapter.js.map +1 -1
  11. package/dist/adapters/notion-adapter.d.ts.map +1 -1
  12. package/dist/adapters/notion-adapter.js +10 -6
  13. package/dist/adapters/notion-adapter.js.map +1 -1
  14. package/dist/adapters/slack-adapter.d.ts.map +1 -1
  15. package/dist/adapters/slack-adapter.js +8 -2
  16. package/dist/adapters/slack-adapter.js.map +1 -1
  17. package/dist/engine/abandon-run.d.ts.map +1 -1
  18. package/dist/engine/abandon-run.js +5 -7
  19. package/dist/engine/abandon-run.js.map +1 -1
  20. package/dist/engine/apply-resume.d.ts.map +1 -1
  21. package/dist/engine/apply-resume.js +13 -1
  22. package/dist/engine/apply-resume.js.map +1 -1
  23. package/dist/engine/eligibility.d.ts +109 -2
  24. package/dist/engine/eligibility.d.ts.map +1 -1
  25. package/dist/engine/eligibility.js +306 -16
  26. package/dist/engine/eligibility.js.map +1 -1
  27. package/dist/engine/execution-loop.d.ts.map +1 -1
  28. package/dist/engine/execution-loop.js +199 -34
  29. package/dist/engine/execution-loop.js.map +1 -1
  30. package/dist/engine/run-health.d.ts.map +1 -1
  31. package/dist/engine/run-health.js.map +1 -1
  32. package/dist/engine/settlement.d.ts +61 -6
  33. package/dist/engine/settlement.d.ts.map +1 -1
  34. package/dist/engine/settlement.js +190 -25
  35. package/dist/engine/settlement.js.map +1 -1
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +9 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/store/json-file-store.d.ts +9 -2
  41. package/dist/store/json-file-store.d.ts.map +1 -1
  42. package/dist/store/json-file-store.js +76 -0
  43. package/dist/store/json-file-store.js.map +1 -1
  44. package/dist/store/seal-integrity.d.ts +9 -0
  45. package/dist/store/seal-integrity.d.ts.map +1 -0
  46. package/dist/store/seal-integrity.js +249 -0
  47. package/dist/store/seal-integrity.js.map +1 -0
  48. package/dist/store/store-interface.d.ts +47 -2
  49. package/dist/store/store-interface.d.ts.map +1 -1
  50. package/dist/types/run-record.d.ts +123 -6
  51. package/dist/types/run-record.d.ts.map +1 -1
  52. package/dist/types/run-record.js +25 -1
  53. package/dist/types/run-record.js.map +1 -1
  54. package/dist/types/workflow-error.d.ts +1 -1
  55. package/dist/types/workflow-error.d.ts.map +1 -1
  56. package/dist/types/workflow-error.js.map +1 -1
  57. package/dist/workflow/diagnostics.d.ts +10 -0
  58. package/dist/workflow/diagnostics.d.ts.map +1 -1
  59. package/dist/workflow/diagnostics.js +1 -1
  60. package/dist/workflow/diagnostics.js.map +1 -1
  61. package/dist/workflow/yaml-loader.d.ts.map +1 -1
  62. package/dist/workflow/yaml-loader.js +141 -1
  63. package/dist/workflow/yaml-loader.js.map +1 -1
  64. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { WorkflowError } from '../types/workflow-error.js';
2
- import { deriveRunPhase } from './eligibility.js';
2
+ import { deriveRunPhase, sealRunLevel } from './eligibility.js';
3
3
  /**
4
4
  * Explicitly abandon a run by stamping the authoritative `abandoned_at` marker. The run's phase
5
5
  * derives to `abandoned` (via {@link deriveRunPhase}) regardless of `failed_steps`/`terminal_reason`.
@@ -51,12 +51,10 @@ export async function abandonRun(store, runId, reason) {
51
51
  }
52
52
  const abandonedReason = reason ?? 'Abandoned via abandon_run';
53
53
  try {
54
- return await store.update({
55
- ...run,
56
- abandoned_at: new Date().toISOString(),
57
- terminal_state: true,
58
- terminal_reason: abandonedReason,
59
- });
54
+ // issue #367: run-level seal through the ONE bypass-writer chokepoint — it stamps
55
+ // sealed_by {arm: 'abandon_requested'} alongside abandoned_at and terminal_state in the same
56
+ // object literal.
57
+ return await store.update(sealRunLevel(run, 'abandon_requested', abandonedReason));
60
58
  }
61
59
  catch (err) {
62
60
  if (err instanceof WorkflowError && err.code === 'STATE_SNAPSHOT_MISMATCH') {
@@ -1 +1 @@
1
- {"version":3,"file":"abandon-run.js","sourceRoot":"","sources":["../../src/engine/abandon-run.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAe,EACf,KAAa,EACb,MAAe;IAEf,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEnC,wCAAwC;IACxC,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC;IACb,CAAC;IAED,+FAA+F;IAC/F,8FAA8F;IAC9F,4FAA4F;IAC5F,8FAA8F;IAC9F,yCAAyC;IACzC,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;QACvB,+FAA+F;QAC/F,MAAM,YAAY,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,IAAI,aAAa,CACrB,QAAQ,KAAK,0BAA0B,YAAY,mCAAmC,EACtF;YACE,IAAI,EAAE,oBAAoB;YAC1B,QAAQ,EAAE,OAAO;YACjB,WAAW,EAAE,gBAAgB;YAC7B,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,CAAC,SAAS,EAAE;SAChF,CACF,CAAC;IACJ,CAAC;IAED,6FAA6F;IAC7F,uEAAuE;IACvE,IAAI,GAAG,CAAC,SAAS,KAAK,cAAc,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,IAAI,aAAa,CACrB,QAAQ,KAAK,uGAAuG,EACpH;YACE,IAAI,EAAE,yBAAyB;YAC/B,QAAQ,EAAE,OAAO;YACjB,WAAW,EAAE,gBAAgB;YAC7B,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE;SAC7C,CACF,CAAC;IACJ,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,IAAI,2BAA2B,CAAC;IAC9D,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,MAAM,CAAC;YACxB,GAAG,GAAG;YACN,YAAY,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACtC,cAAc,EAAE,IAAI;YACpB,eAAe,EAAE,eAAe;SACjC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,aAAa,IAAI,GAAG,CAAC,IAAI,KAAK,yBAAyB,EAAE,CAAC;YAC3E,0CAA0C;YAC1C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxC,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;gBACxC,OAAO,QAAQ,CAAC,CAAC,8DAA8D;YACjF,CAAC;YACD,MAAM,GAAG,CAAC,CAAC,+DAA+D;QAC5E,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"abandon-run.js","sourceRoot":"","sources":["../../src/engine/abandon-run.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAe,EACf,KAAa,EACb,MAAe;IAEf,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEnC,wCAAwC;IACxC,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC;IACb,CAAC;IAED,+FAA+F;IAC/F,8FAA8F;IAC9F,4FAA4F;IAC5F,8FAA8F;IAC9F,yCAAyC;IACzC,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;QACvB,+FAA+F;QAC/F,MAAM,YAAY,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,IAAI,aAAa,CACrB,QAAQ,KAAK,0BAA0B,YAAY,mCAAmC,EACtF;YACE,IAAI,EAAE,oBAAoB;YAC1B,QAAQ,EAAE,OAAO;YACjB,WAAW,EAAE,gBAAgB;YAC7B,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,CAAC,SAAS,EAAE;SAChF,CACF,CAAC;IACJ,CAAC;IAED,6FAA6F;IAC7F,uEAAuE;IACvE,IAAI,GAAG,CAAC,SAAS,KAAK,cAAc,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvE,MAAM,IAAI,aAAa,CACrB,QAAQ,KAAK,uGAAuG,EACpH;YACE,IAAI,EAAE,yBAAyB;YAC/B,QAAQ,EAAE,OAAO;YACjB,WAAW,EAAE,gBAAgB;YAC7B,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE;SAC7C,CACF,CAAC;IACJ,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,IAAI,2BAA2B,CAAC;IAC9D,IAAI,CAAC;QACH,kFAAkF;QAClF,6FAA6F;QAC7F,kBAAkB;QAClB,OAAO,MAAM,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,mBAAmB,EAAE,eAAe,CAAC,CAAC,CAAC;IACrF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,aAAa,IAAI,GAAG,CAAC,IAAI,KAAK,yBAAyB,EAAE,CAAC;YAC3E,0CAA0C;YAC1C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxC,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;gBACxC,OAAO,QAAQ,CAAC,CAAC,8DAA8D;YACjF,CAAC;YACD,MAAM,GAAG,CAAC,CAAC,+DAA+D;QAC5E,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"apply-resume.d.ts","sourceRoot":"","sources":["../../src/engine/apply-resume.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG1E;;yDAEyD;AACzD,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,GAAG,EAAE,SAAS,CAAC;IACf;wEACoE;IACpE,MAAM,EAAE,0BAA0B,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,SAAS,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,kBAAkB,GAC7B,iBAAiB,CAuEnB"}
1
+ {"version":3,"file":"apply-resume.d.ts","sourceRoot":"","sources":["../../src/engine/apply-resume.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG1E;;yDAEyD;AACzD,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,GAAG,EAAE,SAAS,CAAC;IACf;wEACoE;IACpE,MAAM,EAAE,0BAA0B,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,SAAS,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,kBAAkB,GAC7B,iBAAiB,CA2FnB"}
@@ -58,10 +58,22 @@ export function applyResume(snapshot, stepName, definition) {
58
58
  // grandfathered/mixed-fleet record could still carry one), so this is defensive: it can never
59
59
  // silently strip a live decision out from under a human, only a stale, zombie one.
60
60
  const disclosures = [];
61
- const { terminal_reason: _tr, abandoned_at: _aa, pending_gate: strippedGate, ...rest } = snapshot;
61
+ // issue #367: `sealed_by` joins the strip list — the resumed run is live again, so the seal fact
62
+ // must leave in the SAME write that flips terminal_state:false. The store boundary's ORPHANED
63
+ // clause is what catches a future regression of this line.
64
+ const { terminal_reason: _tr, abandoned_at: _aa, sealed_by: _sb, pending_gate: strippedGate, ...rest } = snapshot;
62
65
  if (strippedGate !== undefined) {
63
66
  disclosures.push(`zombie gate '${strippedGate.gate_id}' on '${strippedGate.step_name}' cleared by resume — step will re-drive`);
64
67
  }
68
+ // issue #367 (part 4): the STRIP above is lawful and stays exactly as it is — a live run may not
69
+ // carry a seal. The silence was the defect: an operator's attributed ruling on this run's
70
+ // outcome vanished without a line, in a system whose whole argument is evidence honesty, while
71
+ // every voided finalizer and every stripped zombie gate got one.
72
+ const strippedRuling = snapshot.sealed_by?.adjudicated;
73
+ if (strippedRuling !== undefined) {
74
+ disclosures.push(`operator ruling by '${strippedRuling.by}' (at ${strippedRuling.at}) discarded by resume — ` +
75
+ `the sealed state it ruled on no longer exists`);
76
+ }
65
77
  // Strip terminal_reason + abandoned_at (issue #281); release ALL remaining claims
66
78
  // unconditionally (the CLI refusals guarantee none are healthy by the time this runs).
67
79
  const run = {
@@ -1 +1 @@
1
- {"version":3,"file":"apply-resume.js","sourceRoot":"","sources":["../../src/engine/apply-resume.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apply-resume.js","sourceRoot":"","sources":["../../src/engine/apply-resume.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,88 @@
1
1
  import type { WorkflowDefinition, StepDefinition } from '../types/workflow-definition.js';
2
- import type { RunRecord, SkipDetail } from '../types/run-record.js';
2
+ import type { RunRecord, SealArm, SkipDetail } from '../types/run-record.js';
3
3
  import type { RunPhase } from '../types/run-record.js';
4
+ /** `arm -> RunPhase`. Total, exhaustive, no default. */
5
+ export declare function armToPhase(arm: SealArm): RunPhase;
6
+ /**
7
+ * `arm -> outcome class`. Total, exhaustive, no default. NOT read by `deriveEffectiveTriggers`
8
+ * (which keeps its own `mintOutcome` — the two may diverge by design); this exists for readers
9
+ * that need the class from a STORED record.
10
+ */
11
+ export declare function armToOutcome(arm: SealArm): 'complete' | 'fail' | 'abort' | 'abandon';
12
+ /** The marker each arm is REQUIRED to co-write (`SEAL_MARKERS_AGREE`'s expectation). */
13
+ export declare function armMarker(arm: SealArm): 'aborted_at' | 'abandoned_at' | undefined;
14
+ /** The record shape both legacy classifiers read. */
15
+ type ClassifiableRun = Pick<RunRecord, 'terminal_state' | 'failed_steps' | 'terminal_reason' | 'aborted_at' | 'abandoned_at'>;
16
+ /**
17
+ * Classify a LEGACY terminal record (one written before `sealed_by` existed) into the arm it would
18
+ * have carried. Returns `undefined` when the record cannot be placed — callers must then refuse to
19
+ * fabricate (`deriveRunPhase` falls back to the pre-#367 ladder; the future migrate vehicle buckets
20
+ * it `unclassifiable` and exits 1).
21
+ *
22
+ * A READ-PATH function, not only a migration function: an unhealed record reached through
23
+ * `store.get()` never passes through any sweep, so this is a PERMANENT oracle and correctness never
24
+ * depends on a migration having run.
25
+ *
26
+ * Structure (design rev 3 §4 + Amendment 1 defect 4): MARKERS AS A HINT, PROSE REACHABLE WITHOUT
27
+ * THEM. Markers are checked first when present (they are the strongest legacy signal, and the
28
+ * legacy ladder gives them precedence), but every prose-identifiable arm is classifiable from
29
+ * `terminal_reason` alone, because the only external store has no marker columns. That leaves just
30
+ * the two genuinely prose-less arms — `guard_abort` (reason-less by construction) and
31
+ * `abandon_requested` (free-text reason) — marker-dependent.
32
+ */
33
+ export declare function classifyLegacySeal(run: ClassifiableRun): SealArm | undefined;
34
+ /**
35
+ * The comparator `SEAL_COHERENT` uses at the store boundary (Amendment 2 F2). It is
36
+ * {@link classifyLegacySeal} with EXACTLY ONE branch removed — the abandoned-marker branch, the
37
+ * sole measured false-positive source: a record legitimately carrying `abandoned_at` from an
38
+ * earlier life would classify `abandon_requested` and contradict an honest arm.
39
+ *
40
+ * The aborted-marker branch STAYS LIVE here on purpose. A prose-only comparator was the earlier,
41
+ * superseded bend, and it was refuted because it discards real coverage: the old-binary
42
+ * `guard_abort` re-seal is reason-less, so it is marker-only-visible, and the markers-first
43
+ * comparator is what catches it.
44
+ *
45
+ * Named blindness, on record: a stale arm on a record carrying `abandoned_at` is invisible here —
46
+ * that is BOTH abandon-class arms, `abandon_requested` AND `cleanup_sweep`. Their observer is the
47
+ * migrate sweep's `incoherent` bucket, which uses the FULL classifier.
48
+ *
49
+ * SKIPPING MEANS ABSTAINING, and that distinction is load-bearing — measured, not reasoned. An
50
+ * abandoned-marker record that merely FALLS THROUGH to the prose battery does not go unjudged: an
51
+ * abandon reason is free text, so the battery reaches its `failed_steps.length > 0` fallback and
52
+ * returns `step_failure`. `abandonRun` on a run that already has a failed step — an ordinary
53
+ * production path — would then be refused by the boundary as incoherent. Executed: the full
54
+ * classifier says `abandon_requested`, the fall-through form says `step_failure`. So the presence
55
+ * of the marker ends the comparison here rather than redirecting it.
56
+ */
57
+ export declare function classifyForCoherence(run: ClassifiableRun): SealArm | undefined;
58
+ /**
59
+ * issue #367 — `SEAL_MARKERS_AGREE`, TRANSFORM-SCOPED: asserted on the records
60
+ * `applyTerminalPostconditions` / `buildFinalizedSeal` THEMSELVES produce, where congruence is
61
+ * theirs to guarantee. Never universal, and ONE-DIRECTIONAL (`armMarker(arm)` present ⇒ assert;
62
+ * foreign markers are NOT forbidden): a transform's INPUT may legitimately carry a marker it did
63
+ * not write — the published `TERMINAL_STATE_ONLY` law seeds `abandoned_at`/`aborted_at` on a live
64
+ * run and settles it complete, in contract. A forbids-direction clause makes that record
65
+ * engine-unconstructible and reds the published law (measured).
66
+ *
67
+ * A violation is a transform contract bug, thrown as a plain Error (the `applySettlement`
68
+ * contract-violation precedent), never a predicate outcome.
69
+ */
70
+ export declare function assertSealMarkersAgree(run: Pick<RunRecord, 'id' | 'terminal_state' | 'sealed_by' | 'aborted_at' | 'abandoned_at'>): void;
71
+ /**
72
+ * issue #367 — `SEAL_OUTCOME_COHERENT` (design §10.7), transform-scoped: a fail-class arm that
73
+ * names a STEP must have that failure on the record. The two startup arms
74
+ * (`spawn_failure`/`extensions_load_failure`) map to `failed` with an empty `failed_steps` and are
75
+ * exempt BY the predicate, honestly — no step ever ran.
76
+ */
77
+ export declare function assertSealOutcomeCoherent(run: Pick<RunRecord, 'id' | 'terminal_state' | 'sealed_by' | 'failed_steps'>): void;
78
+ /**
79
+ * issue #367 — the ONE chokepoint for the four run-level BYPASS writers (`abandon-run.ts`,
80
+ * `cleanup.ts`, `listen.ts`, `run-attach.ts`): seals `run` with a run-scoped arm. It also retires
81
+ * those writers' fossil `run_phase` hand-writes — phase is derived at every store write tail, never
82
+ * hand-stamped (the #282 class). `step` is deliberately NOT accepted: every run-level arm is
83
+ * step-less by definition, so `SealedBy.step` stays ABSENT rather than becoming an `''` sentinel.
84
+ */
85
+ export declare function sealRunLevel(run: RunRecord, arm: SealArm, reason: string, now?: Date): RunRecord;
4
86
  /**
5
87
  * Derives the run_phase from the run record fields. Called after every store write to keep
6
88
  * run_phase consistent — this function's return value is ALWAYS what gets persisted (issue #279,
@@ -23,7 +105,7 @@ import type { RunPhase } from '../types/run-record.js';
23
105
  * never had a gate) derives IDENTICALLY either way; only the one previously-mis-derived class
24
106
  * changes, and it changes to the CORRECT phase.
25
107
  */
26
- export declare function deriveRunPhase(run: Pick<RunRecord, 'pending_gate' | 'terminal_state' | 'failed_steps' | 'terminal_reason' | 'aborted_at' | 'abandoned_at'>): RunPhase;
108
+ export declare function deriveRunPhase(run: Pick<RunRecord, 'pending_gate' | 'terminal_state' | 'failed_steps' | 'terminal_reason' | 'aborted_at' | 'abandoned_at' | 'sealed_by'>): RunPhase;
27
109
  /**
28
110
  * Evaluates whether a step's trigger_rule is satisfied given the current run state.
29
111
  * Empty or omitted depends_on means the step has no dependencies and is always eligible
@@ -101,10 +183,34 @@ export declare function buildEvidenceByStep(run: RunRecord): Record<string, Reco
101
183
  * namespace from the live run record — the shared helper is what makes replay's verdict
102
184
  * structurally unable to diverge from the live engine's for a `$settlement`-referencing
103
185
  * precondition (issue #220 §4c-M7).
186
+ *
187
+ * **`failed` (issue #305) — STATUS ONLY, deliberately.** The entry says THAT a step failed, never
188
+ * why. Failure detail (messages, causes) stays on the run record, which is where every surveyed
189
+ * orchestrator keeps it: status enumeration is the field's in-band floor (GitHub Actions'
190
+ * `needs.<job>.result`; Step Functions and Airflow make it query-reachable), while detail in a
191
+ * declarative cleanup payload is shipped nowhere. Detail here would also fork the truth away from
192
+ * the append-only record, and these payloads feed AGENT PROMPTS — provider-generated text of
193
+ * unbounded size is not something to inject by default.
194
+ *
195
+ * **`failed` is a POSITION fact.** It is derived from WHICH half of the domain walk produced the
196
+ * entry (`completed_steps` ⇒ false, `failed_steps` ⇒ true), not from a lookup. Any future
197
+ * refactor that flattens those two loops back into one spread MUST re-derive the flag explicitly
198
+ * — a flattened walk silently marks every entry with whichever literal it inherits.
199
+ *
200
+ * **Reading the three settlement outcomes.** A consumer distinguishes them with two fields plus
201
+ * ABSENCE: a failed step has `failed: true`; a step that completed on its declared default has
202
+ * `failed: false, settled_by_default: true`; a SKIPPED step has NO ENTRY AT ALL (skipped steps are
203
+ * deliberately outside this namespace, pinned three ways). In a `when`/precondition, absence is
204
+ * testable with the load-legal bare presence spelling `$settlement.<dep>.failed == null`, which is
205
+ * true exactly when the step is absent — so authors are not left unable to discriminate.
206
+ *
207
+ * **`failed` is live, not a fossil.** `applyResume` strips a resumed step out of `failed_steps`,
208
+ * so the marker clears on resume rather than permanently branding a step that has since re-run.
104
209
  */
105
210
  export declare function buildSettlementNamespace(run: RunRecord): Record<string, {
106
211
  settled_by_default: boolean;
107
212
  validation_rejections: number;
213
+ failed: boolean;
108
214
  }>;
109
215
  /**
110
216
  * True iff `stepId` is a member of `completed_steps ∪ failed_steps ∪ skipped_steps ∪
@@ -158,4 +264,5 @@ export interface PropagateSkipsResult {
158
264
  * touch `details`. Does not mutate the run record — callers apply the result before writing.
159
265
  */
160
266
  export declare function propagateSkips(run: RunRecord, definition: WorkflowDefinition): PropagateSkipsResult;
267
+ export {};
161
268
  //# sourceMappingURL=eligibility.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/engine/eligibility.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EAEf,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAmB,MAAM,wBAAwB,CAAC;AACrF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAKvD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,IAAI,CACP,SAAS,EACP,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,iBAAiB,GACjB,YAAY,GACZ,cAAc,CACjB,GACA,QAAQ,CA2BV;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CA0ClF;AAgDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvD,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,CA+CT;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EACzB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvD,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,CAGT;AAoDD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAwB3F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,SAAS,GACb,MAAM,CAAC,MAAM,EAAE;IAAE,kBAAkB,EAAE,OAAO,CAAC;IAAC,qBAAqB,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBhF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAO/E;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,kBAAkB,EAAE,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,CAiC1F;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,kBAAkB,EAAE,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,CA6B/F;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAe1F;AAkHD,sGAAsG;AACtG,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,UAAU,EAAE,kBAAkB,GAC7B,oBAAoB,CAgEtB"}
1
+ {"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/engine/eligibility.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,233 @@
1
+ import { SEAL_ARMS } from '../types/run-record.js';
1
2
  import { resolvePath } from './render-template.js';
2
3
  import { splitComparison } from './comparison-expr.js';
3
4
  import { boundResolvedValue } from '../utils/redaction.js';
5
+ // ---------------------------------------------------------------------------
6
+ // issue #367 — the recorded seal fact: mappings, the permanent legacy read oracle, the run-level
7
+ // writer chokepoint, and the transform-scoped marker assertion.
8
+ //
9
+ // These mappings are the engine-owned TOTAL functions over the recorded value. Changing one
10
+ // changes a JUDGEMENT; it never moves a stored record. The exhaustive no-default switches are the
11
+ // one real compile guarantee this design claims: a 14th arm added to SEAL_ARMS without mappings
12
+ // does not build.
13
+ // ---------------------------------------------------------------------------
14
+ /** `arm -> RunPhase`. Total, exhaustive, no default. */
15
+ export function armToPhase(arm) {
16
+ switch (arm) {
17
+ case 'complete':
18
+ case 'gate_resolution_complete':
19
+ case 'guard_pass_complete':
20
+ case 'gate_expiry_default':
21
+ return 'completed';
22
+ case 'step_failure':
23
+ case 'guard_resolution_error':
24
+ case 'spawn_failure':
25
+ case 'extensions_load_failure':
26
+ return 'failed';
27
+ case 'handler_abort':
28
+ case 'guard_abort':
29
+ case 'gate_expiry_abort':
30
+ return 'aborted';
31
+ case 'abandon_requested':
32
+ case 'cleanup_sweep':
33
+ return 'abandoned';
34
+ }
35
+ }
36
+ /**
37
+ * `arm -> outcome class`. Total, exhaustive, no default. NOT read by `deriveEffectiveTriggers`
38
+ * (which keeps its own `mintOutcome` — the two may diverge by design); this exists for readers
39
+ * that need the class from a STORED record.
40
+ */
41
+ export function armToOutcome(arm) {
42
+ switch (arm) {
43
+ case 'complete':
44
+ case 'gate_resolution_complete':
45
+ case 'guard_pass_complete':
46
+ case 'gate_expiry_default':
47
+ return 'complete';
48
+ case 'step_failure':
49
+ case 'guard_resolution_error':
50
+ case 'spawn_failure':
51
+ case 'extensions_load_failure':
52
+ return 'fail';
53
+ case 'handler_abort':
54
+ case 'guard_abort':
55
+ case 'gate_expiry_abort':
56
+ return 'abort';
57
+ case 'abandon_requested':
58
+ case 'cleanup_sweep':
59
+ return 'abandon';
60
+ }
61
+ }
62
+ /** The marker each arm is REQUIRED to co-write (`SEAL_MARKERS_AGREE`'s expectation). */
63
+ export function armMarker(arm) {
64
+ const phase = armToPhase(arm);
65
+ if (phase === 'aborted')
66
+ return 'aborted_at';
67
+ if (phase === 'abandoned')
68
+ return 'abandoned_at';
69
+ return undefined;
70
+ }
71
+ const GUARD_RESOLUTION_PROSE = /^Guard step '.+' failed: unresolvable path/;
72
+ /** issue #373's multi-failure shape rides here too — `N steps failed:` is a step-failure seal. */
73
+ const FAIL_PROSE = [/^Step '.+' failed:/, /^\d+ steps? failed:/, /^Guard step '.+' failed:/];
74
+ const CLEANUP_SWEEP_PROSE = 'Marked abandoned by realm cleanup';
75
+ const HANDLER_ABORT_PROSE = /^Handler '.+' aborted the run:/;
76
+ const GATE_EXPIRY_ABORT_PROSE = /^Gate '.+' expired and the run aborted/;
77
+ /** The prose battery, reachable with NO markers — the marker-column-less store class. */
78
+ function classifyByProse(run) {
79
+ const reason = run.terminal_reason;
80
+ if (reason === 'Workflow completed.')
81
+ return 'complete'; // gate/guard variants NOT recoverable
82
+ if (reason === 'spawn_failed')
83
+ return 'spawn_failure';
84
+ if (reason === 'extensions_load_failed')
85
+ return 'extensions_load_failure';
86
+ if (reason === CLEANUP_SWEEP_PROSE)
87
+ return 'cleanup_sweep';
88
+ if (reason !== undefined) {
89
+ if (HANDLER_ABORT_PROSE.test(reason))
90
+ return 'handler_abort';
91
+ if (GATE_EXPIRY_ABORT_PROSE.test(reason))
92
+ return 'gate_expiry_abort';
93
+ // Order load-bearing: the resolution-error regex MUST run before the generic guard-failure
94
+ // member of FAIL_PROSE, which also matches it.
95
+ if (GUARD_RESOLUTION_PROSE.test(reason))
96
+ return 'guard_resolution_error';
97
+ if (FAIL_PROSE.some((re) => re.test(reason)))
98
+ return 'step_failure';
99
+ }
100
+ if (run.failed_steps.length > 0)
101
+ return 'step_failure';
102
+ return undefined;
103
+ }
104
+ /** The aborted-marker branch — shared by both classifier entry points (it stays live in both). */
105
+ function classifyByAbortedMarker(run) {
106
+ const reason = run.terminal_reason;
107
+ if (reason === undefined)
108
+ return 'guard_abort'; // the ONLY reason-less abort
109
+ if (HANDLER_ABORT_PROSE.test(reason))
110
+ return 'handler_abort';
111
+ if (GATE_EXPIRY_ABORT_PROSE.test(reason))
112
+ return 'gate_expiry_abort';
113
+ return undefined; // an abort we do not recognise — never guess
114
+ }
115
+ /**
116
+ * Classify a LEGACY terminal record (one written before `sealed_by` existed) into the arm it would
117
+ * have carried. Returns `undefined` when the record cannot be placed — callers must then refuse to
118
+ * fabricate (`deriveRunPhase` falls back to the pre-#367 ladder; the future migrate vehicle buckets
119
+ * it `unclassifiable` and exits 1).
120
+ *
121
+ * A READ-PATH function, not only a migration function: an unhealed record reached through
122
+ * `store.get()` never passes through any sweep, so this is a PERMANENT oracle and correctness never
123
+ * depends on a migration having run.
124
+ *
125
+ * Structure (design rev 3 §4 + Amendment 1 defect 4): MARKERS AS A HINT, PROSE REACHABLE WITHOUT
126
+ * THEM. Markers are checked first when present (they are the strongest legacy signal, and the
127
+ * legacy ladder gives them precedence), but every prose-identifiable arm is classifiable from
128
+ * `terminal_reason` alone, because the only external store has no marker columns. That leaves just
129
+ * the two genuinely prose-less arms — `guard_abort` (reason-less by construction) and
130
+ * `abandon_requested` (free-text reason) — marker-dependent.
131
+ */
132
+ export function classifyLegacySeal(run) {
133
+ if (run.terminal_state !== true)
134
+ return undefined;
135
+ if (run.abandoned_at !== undefined) {
136
+ return run.terminal_reason === CLEANUP_SWEEP_PROSE ? 'cleanup_sweep' : 'abandon_requested';
137
+ }
138
+ if (run.aborted_at !== undefined)
139
+ return classifyByAbortedMarker(run);
140
+ return classifyByProse(run);
141
+ }
142
+ /**
143
+ * The comparator `SEAL_COHERENT` uses at the store boundary (Amendment 2 F2). It is
144
+ * {@link classifyLegacySeal} with EXACTLY ONE branch removed — the abandoned-marker branch, the
145
+ * sole measured false-positive source: a record legitimately carrying `abandoned_at` from an
146
+ * earlier life would classify `abandon_requested` and contradict an honest arm.
147
+ *
148
+ * The aborted-marker branch STAYS LIVE here on purpose. A prose-only comparator was the earlier,
149
+ * superseded bend, and it was refuted because it discards real coverage: the old-binary
150
+ * `guard_abort` re-seal is reason-less, so it is marker-only-visible, and the markers-first
151
+ * comparator is what catches it.
152
+ *
153
+ * Named blindness, on record: a stale arm on a record carrying `abandoned_at` is invisible here —
154
+ * that is BOTH abandon-class arms, `abandon_requested` AND `cleanup_sweep`. Their observer is the
155
+ * migrate sweep's `incoherent` bucket, which uses the FULL classifier.
156
+ *
157
+ * SKIPPING MEANS ABSTAINING, and that distinction is load-bearing — measured, not reasoned. An
158
+ * abandoned-marker record that merely FALLS THROUGH to the prose battery does not go unjudged: an
159
+ * abandon reason is free text, so the battery reaches its `failed_steps.length > 0` fallback and
160
+ * returns `step_failure`. `abandonRun` on a run that already has a failed step — an ordinary
161
+ * production path — would then be refused by the boundary as incoherent. Executed: the full
162
+ * classifier says `abandon_requested`, the fall-through form says `step_failure`. So the presence
163
+ * of the marker ends the comparison here rather than redirecting it.
164
+ */
165
+ export function classifyForCoherence(run) {
166
+ if (run.terminal_state !== true)
167
+ return undefined;
168
+ if (run.abandoned_at !== undefined)
169
+ return undefined; // abstain — see above
170
+ if (run.aborted_at !== undefined)
171
+ return classifyByAbortedMarker(run);
172
+ return classifyByProse(run);
173
+ }
174
+ /**
175
+ * issue #367 — `SEAL_MARKERS_AGREE`, TRANSFORM-SCOPED: asserted on the records
176
+ * `applyTerminalPostconditions` / `buildFinalizedSeal` THEMSELVES produce, where congruence is
177
+ * theirs to guarantee. Never universal, and ONE-DIRECTIONAL (`armMarker(arm)` present ⇒ assert;
178
+ * foreign markers are NOT forbidden): a transform's INPUT may legitimately carry a marker it did
179
+ * not write — the published `TERMINAL_STATE_ONLY` law seeds `abandoned_at`/`aborted_at` on a live
180
+ * run and settles it complete, in contract. A forbids-direction clause makes that record
181
+ * engine-unconstructible and reds the published law (measured).
182
+ *
183
+ * A violation is a transform contract bug, thrown as a plain Error (the `applySettlement`
184
+ * contract-violation precedent), never a predicate outcome.
185
+ */
186
+ export function assertSealMarkersAgree(run) {
187
+ if (run.terminal_state !== true || run.sealed_by === undefined)
188
+ return;
189
+ const marker = armMarker(run.sealed_by.arm);
190
+ if (marker === 'aborted_at' && run.aborted_at === undefined) {
191
+ throw new Error(`SEAL_MARKERS_AGREE violated on run '${run.id}': arm '${run.sealed_by.arm}' requires aborted_at`);
192
+ }
193
+ if (marker === 'abandoned_at' && run.abandoned_at === undefined) {
194
+ throw new Error(`SEAL_MARKERS_AGREE violated on run '${run.id}': arm '${run.sealed_by.arm}' requires abandoned_at`);
195
+ }
196
+ }
197
+ /**
198
+ * issue #367 — `SEAL_OUTCOME_COHERENT` (design §10.7), transform-scoped: a fail-class arm that
199
+ * names a STEP must have that failure on the record. The two startup arms
200
+ * (`spawn_failure`/`extensions_load_failure`) map to `failed` with an empty `failed_steps` and are
201
+ * exempt BY the predicate, honestly — no step ever ran.
202
+ */
203
+ export function assertSealOutcomeCoherent(run) {
204
+ if (run.terminal_state !== true || run.sealed_by === undefined)
205
+ return;
206
+ const arm = run.sealed_by.arm;
207
+ if ((arm === 'step_failure' || arm === 'guard_resolution_error') &&
208
+ run.failed_steps.length === 0) {
209
+ throw new Error(`SEAL_OUTCOME_COHERENT violated on run '${run.id}': arm '${arm}' with empty failed_steps`);
210
+ }
211
+ }
212
+ /**
213
+ * issue #367 — the ONE chokepoint for the four run-level BYPASS writers (`abandon-run.ts`,
214
+ * `cleanup.ts`, `listen.ts`, `run-attach.ts`): seals `run` with a run-scoped arm. It also retires
215
+ * those writers' fossil `run_phase` hand-writes — phase is derived at every store write tail, never
216
+ * hand-stamped (the #282 class). `step` is deliberately NOT accepted: every run-level arm is
217
+ * step-less by definition, so `SealedBy.step` stays ABSENT rather than becoming an `''` sentinel.
218
+ */
219
+ export function sealRunLevel(run, arm, reason, now = new Date()) {
220
+ const { sealed_by: _priorSeal, ...base } = run;
221
+ // The carried run_phase may be stale here — every store write tail re-derives it
222
+ // (PHASE_IS_GENERATED), which is exactly why the callers' fossil hand-writes were deletable.
223
+ return {
224
+ ...base,
225
+ terminal_state: true,
226
+ sealed_by: { arm },
227
+ terminal_reason: reason,
228
+ ...(armMarker(arm) === 'abandoned_at' ? { abandoned_at: now.toISOString() } : {}),
229
+ };
230
+ }
4
231
  /**
5
232
  * Derives the run_phase from the run record fields. Called after every store write to keep
6
233
  * run_phase consistent — this function's return value is ALWAYS what gets persisted (issue #279,
@@ -24,25 +251,55 @@ import { boundResolvedValue } from '../utils/redaction.js';
24
251
  * changes, and it changes to the CORRECT phase.
25
252
  */
26
253
  export function deriveRunPhase(run) {
27
- // abandoned_at is authoritative explicit operator/cleanup abandonment wins over any field
28
- // (failed_steps, pending_gate, terminal_reason, and now checked before the whole terminal
29
- // cluster below too). It is only ever set on a running run (gate_waiting abandonment is refused
30
- // by abandonRun), so it never co-occurs with pending_gate in practice; top placement is
31
- // future-proof and mirrors the aborted_at promotion rationale below.
254
+ // issue #367: the RECORDED fact wins over every marker and over the prose. Markers are asserted
255
+ // congruent at the transforms (SEAL_MARKERS_AGREE) and at the store boundary (SEAL_COHERENT);
256
+ // they are no longer a second oracle once a stamp exists. Three conjuncts, each load-bearing:
257
+ // - `terminal_state === true`: an ORPHAN (an old binary's resume keeps `sealed_by` while
258
+ // flipping terminal_state:false executed against the real v0.38 dist) must derive via the
259
+ // LIVE ladder below, or both purge gates pass on a live run and destroy it.
260
+ // - `sealed_by !== undefined`: the legacy population derives via the classifier + ladder.
261
+ // - membership in SEAL_ARMS: an unknown arm (a future binary's record read by this one) falls
262
+ // through to the classifier — never an `undefined` phase. Write-side loudness for that case
263
+ // is the store boundary's STATE_SEAL_UNKNOWN_ARM.
264
+ if (run.terminal_state === true &&
265
+ run.sealed_by !== undefined &&
266
+ SEAL_ARMS.includes(run.sealed_by.arm)) {
267
+ return armToPhase(run.sealed_by.arm);
268
+ }
269
+ if (run.terminal_state === true) {
270
+ // INV-CORPUS: every terminal record's cause is recoverable-or-honestly-unknown at read time.
271
+ // `classifyLegacySeal` is a PERMANENT read oracle — correctness never depends on migration.
272
+ const legacyArm = classifyLegacySeal(run);
273
+ if (legacyArm !== undefined)
274
+ return armToPhase(legacyArm);
275
+ }
276
+ // ---- The pre-#367 ladder, UNCHANGED, and now the LEGACY-ONLY path ----
277
+ // What follows runs only for records with no usable arm: everything written before #367, plus
278
+ // the honestly-unclassifiable tail. These comments used to say the markers were "authoritative"
279
+ // for the whole derivation; since #367 that is false as stated — a stamped record never reaches
280
+ // here, and on the records that do, the markers are the strongest signal available rather than
281
+ // the primary oracle.
282
+ //
283
+ // abandoned_at: explicit operator/cleanup abandonment wins over any other legacy field
284
+ // (failed_steps, pending_gate, terminal_reason, and the whole terminal cluster below). It is
285
+ // only ever set on a running run (gate_waiting abandonment is refused by abandonRun), so it
286
+ // never co-occurs with pending_gate in practice; top placement is future-proof and mirrors the
287
+ // aborted_at rationale below.
32
288
  if (run.abandoned_at !== undefined)
33
289
  return 'abandoned';
34
- // aborted_at is authoritative and is checked before pending_gate AND before the terminal_state
35
- // split below. The only records that carry aborted_at are guard/handler-aborted runs (always
36
- // written terminal_state:true); legitimately resumable runs (failed/abandoned) never carry it,
37
- // and an aborted run's cancel-gate write (design record §3 applyAbortEdge) clears any OTHER
38
- // step's pending_gate in the same write — but a grandfathered/legacy record could still carry
39
- // both, and aborted_at must win regardless.
290
+ // aborted_at is checked before pending_gate AND before the terminal_state split below. The only
291
+ // records that carry aborted_at are guard/handler-aborted runs (always written
292
+ // terminal_state:true); legitimately resumable runs (failed/abandoned) never carry it, and an
293
+ // aborted run's cancel-gate write (design record §3 applyAbortEdge) clears any OTHER step's
294
+ // pending_gate in the same write — but a grandfathered/legacy record could still carry both,
295
+ // and aborted_at must win regardless.
40
296
  if (run.aborted_at !== undefined)
41
297
  return 'aborted';
42
298
  if (run.terminal_state) {
43
- // A terminal run that completed successfully sets terminal_reason to 'Workflow completed.'.
44
- // Recovery workflows end with failed_steps non-empty but are still considered completed
45
- // when the final recovery step succeeds, so terminal_reason takes precedence over failed_steps.
299
+ // A legacy terminal run that completed successfully sets terminal_reason to
300
+ // 'Workflow completed.'. Recovery workflows end with failed_steps non-empty but are still
301
+ // considered completed when the final recovery step succeeds, so terminal_reason takes
302
+ // precedence over failed_steps. (For stamped records this distinction is the arm's job.)
46
303
  if (run.terminal_reason === 'Workflow completed.')
47
304
  return 'completed';
48
305
  if (run.failed_steps.length > 0)
@@ -319,6 +576,29 @@ export function buildEvidenceByStep(run) {
319
576
  * namespace from the live run record — the shared helper is what makes replay's verdict
320
577
  * structurally unable to diverge from the live engine's for a `$settlement`-referencing
321
578
  * precondition (issue #220 §4c-M7).
579
+ *
580
+ * **`failed` (issue #305) — STATUS ONLY, deliberately.** The entry says THAT a step failed, never
581
+ * why. Failure detail (messages, causes) stays on the run record, which is where every surveyed
582
+ * orchestrator keeps it: status enumeration is the field's in-band floor (GitHub Actions'
583
+ * `needs.<job>.result`; Step Functions and Airflow make it query-reachable), while detail in a
584
+ * declarative cleanup payload is shipped nowhere. Detail here would also fork the truth away from
585
+ * the append-only record, and these payloads feed AGENT PROMPTS — provider-generated text of
586
+ * unbounded size is not something to inject by default.
587
+ *
588
+ * **`failed` is a POSITION fact.** It is derived from WHICH half of the domain walk produced the
589
+ * entry (`completed_steps` ⇒ false, `failed_steps` ⇒ true), not from a lookup. Any future
590
+ * refactor that flattens those two loops back into one spread MUST re-derive the flag explicitly
591
+ * — a flattened walk silently marks every entry with whichever literal it inherits.
592
+ *
593
+ * **Reading the three settlement outcomes.** A consumer distinguishes them with two fields plus
594
+ * ABSENCE: a failed step has `failed: true`; a step that completed on its declared default has
595
+ * `failed: false, settled_by_default: true`; a SKIPPED step has NO ENTRY AT ALL (skipped steps are
596
+ * deliberately outside this namespace, pinned three ways). In a `when`/precondition, absence is
597
+ * testable with the load-legal bare presence spelling `$settlement.<dep>.failed == null`, which is
598
+ * true exactly when the step is absent — so authors are not left unable to discriminate.
599
+ *
600
+ * **`failed` is live, not a fossil.** `applyResume` strips a resumed step out of `failed_steps`,
601
+ * so the marker clears on resume rather than permanently branding a step that has since re-run.
322
602
  */
323
603
  export function buildSettlementNamespace(run) {
324
604
  const lastDiag = {};
@@ -328,13 +608,23 @@ export function buildSettlementNamespace(run) {
328
608
  }
329
609
  }
330
610
  const result = {};
331
- for (const step of [...run.completed_steps, ...run.failed_steps]) {
611
+ // issue #305: `failed` is derived from LOOP POSITION, not from a membership set built beside
612
+ // the walk. The domain here IS `completed_steps` then `failed_steps`, so splitting the single
613
+ // spread into its two halves makes the flag and the membership the same fact — they cannot
614
+ // drift, and no second structure has to be kept in sync. It also keeps the O(1)-per-step
615
+ // contract above intact (no set construction, no lookup).
616
+ const write = (step, failed) => {
332
617
  const diag = lastDiag[step];
333
618
  result[step] = {
334
619
  settled_by_default: diag?.settled_by_default ?? false,
335
620
  validation_rejections: diag?.validation_rejections ?? 0,
621
+ failed,
336
622
  };
337
- }
623
+ };
624
+ for (const step of run.completed_steps)
625
+ write(step, false);
626
+ for (const step of run.failed_steps)
627
+ write(step, true);
338
628
  return result;
339
629
  }
340
630
  /**