@affordance/core 0.1.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/engine/compute.d.ts +147 -0
  4. package/dist/engine/compute.js +152 -0
  5. package/dist/engine/compute.js.map +1 -0
  6. package/dist/engine/engine.d.ts +162 -0
  7. package/dist/engine/engine.js +115 -0
  8. package/dist/engine/engine.js.map +1 -0
  9. package/dist/engine/errors.d.ts +6 -0
  10. package/dist/engine/errors.js +11 -0
  11. package/dist/engine/errors.js.map +1 -0
  12. package/dist/engine/index.d.ts +16 -0
  13. package/dist/engine/index.js +15 -0
  14. package/dist/engine/index.js.map +1 -0
  15. package/dist/errors.d.ts +46 -0
  16. package/dist/errors.js +42 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/execution/delta.d.ts +42 -0
  19. package/dist/execution/delta.js +93 -0
  20. package/dist/execution/delta.js.map +1 -0
  21. package/dist/execution/errors.d.ts +94 -0
  22. package/dist/execution/errors.js +132 -0
  23. package/dist/execution/errors.js.map +1 -0
  24. package/dist/execution/execute.d.ts +179 -0
  25. package/dist/execution/execute.js +356 -0
  26. package/dist/execution/execute.js.map +1 -0
  27. package/dist/execution/index.d.ts +25 -0
  28. package/dist/execution/index.js +20 -0
  29. package/dist/execution/index.js.map +1 -0
  30. package/dist/execution/journal.d.ts +190 -0
  31. package/dist/execution/journal.js +192 -0
  32. package/dist/execution/journal.js.map +1 -0
  33. package/dist/execution/port.d.ts +81 -0
  34. package/dist/execution/port.js +101 -0
  35. package/dist/execution/port.js.map +1 -0
  36. package/dist/execution/replay.d.ts +58 -0
  37. package/dist/execution/replay.js +59 -0
  38. package/dist/execution/replay.js.map +1 -0
  39. package/dist/execution/timers.d.ts +21 -0
  40. package/dist/execution/timers.js +26 -0
  41. package/dist/execution/timers.js.map +1 -0
  42. package/dist/execution/transaction.d.ts +24 -0
  43. package/dist/execution/transaction.js +49 -0
  44. package/dist/execution/transaction.js.map +1 -0
  45. package/dist/guards/condition.d.ts +86 -0
  46. package/dist/guards/condition.js +11 -0
  47. package/dist/guards/condition.js.map +1 -0
  48. package/dist/guards/evaluate.d.ts +143 -0
  49. package/dist/guards/evaluate.js +167 -0
  50. package/dist/guards/evaluate.js.map +1 -0
  51. package/dist/guards/guard.d.ts +136 -0
  52. package/dist/guards/guard.js +118 -0
  53. package/dist/guards/guard.js.map +1 -0
  54. package/dist/guards/index.d.ts +20 -0
  55. package/dist/guards/index.js +17 -0
  56. package/dist/guards/index.js.map +1 -0
  57. package/dist/guards/time.d.ts +32 -0
  58. package/dist/guards/time.js +45 -0
  59. package/dist/guards/time.js.map +1 -0
  60. package/dist/index.d.ts +31 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/ingestion/correlation.d.ts +54 -0
  64. package/dist/ingestion/correlation.js +78 -0
  65. package/dist/ingestion/correlation.js.map +1 -0
  66. package/dist/ingestion/index.d.ts +16 -0
  67. package/dist/ingestion/index.js +15 -0
  68. package/dist/ingestion/index.js.map +1 -0
  69. package/dist/ingestion/ingest.d.ts +196 -0
  70. package/dist/ingestion/ingest.js +275 -0
  71. package/dist/ingestion/ingest.js.map +1 -0
  72. package/dist/migration/index.d.ts +10 -0
  73. package/dist/migration/index.js +10 -0
  74. package/dist/migration/index.js.map +1 -0
  75. package/dist/migration/migrate.d.ts +119 -0
  76. package/dist/migration/migrate.js +195 -0
  77. package/dist/migration/migrate.js.map +1 -0
  78. package/dist/model/casetype.d.ts +53 -0
  79. package/dist/model/casetype.js +51 -0
  80. package/dist/model/casetype.js.map +1 -0
  81. package/dist/model/errors.d.ts +28 -0
  82. package/dist/model/errors.js +39 -0
  83. package/dist/model/errors.js.map +1 -0
  84. package/dist/model/handler.d.ts +120 -0
  85. package/dist/model/handler.js +16 -0
  86. package/dist/model/handler.js.map +1 -0
  87. package/dist/model/index.d.ts +24 -0
  88. package/dist/model/index.js +19 -0
  89. package/dist/model/index.js.map +1 -0
  90. package/dist/model/retry.d.ts +40 -0
  91. package/dist/model/retry.js +51 -0
  92. package/dist/model/retry.js.map +1 -0
  93. package/dist/model/scope.d.ts +67 -0
  94. package/dist/model/scope.js +19 -0
  95. package/dist/model/scope.js.map +1 -0
  96. package/dist/model/step.d.ts +232 -0
  97. package/dist/model/step.js +210 -0
  98. package/dist/model/step.js.map +1 -0
  99. package/dist/model/target.d.ts +175 -0
  100. package/dist/model/target.js +236 -0
  101. package/dist/model/target.js.map +1 -0
  102. package/dist/store/bootstrap.d.ts +57 -0
  103. package/dist/store/bootstrap.js +268 -0
  104. package/dist/store/bootstrap.js.map +1 -0
  105. package/dist/store/errors.d.ts +18 -0
  106. package/dist/store/errors.js +26 -0
  107. package/dist/store/errors.js.map +1 -0
  108. package/dist/store/ids.d.ts +15 -0
  109. package/dist/store/ids.js +12 -0
  110. package/dist/store/ids.js.map +1 -0
  111. package/dist/store/index.d.ts +12 -0
  112. package/dist/store/index.js +9 -0
  113. package/dist/store/index.js.map +1 -0
  114. package/dist/store/queryable.d.ts +60 -0
  115. package/dist/store/queryable.js +7 -0
  116. package/dist/store/queryable.js.map +1 -0
  117. package/dist/store/resolve.d.ts +66 -0
  118. package/dist/store/resolve.js +71 -0
  119. package/dist/store/resolve.js.map +1 -0
  120. package/dist/store/sql.d.ts +26 -0
  121. package/dist/store/sql.js +21 -0
  122. package/dist/store/sql.js.map +1 -0
  123. package/dist/store/store.d.ts +66 -0
  124. package/dist/store/store.js +108 -0
  125. package/dist/store/store.js.map +1 -0
  126. package/package.json +61 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The ways an Execution can be refused or fail.
3
+ *
4
+ * Each is a distinct answer to "why didn't this run", and callers are
5
+ * expected to branch on them: {@link StepNotAvailableError} is a client-facing
6
+ * *no* with reasons attached; {@link CaseBusyError} is "not now, try again";
7
+ * {@link ClaimLostError} and {@link StepExecutionError} are failures of a run
8
+ * that did start.
9
+ */
10
+ import { AffordanceError, thrownMessage } from '../errors.js';
11
+ import { describeUnmet, unmetConditions } from '../guards/index.js';
12
+ /**
13
+ * How a step (× scope element) is named in prose: `'close-purchase'`, or
14
+ * `'request-re-sign' (buyer_7)` when scoped. One spelling, because refusal
15
+ * messages and adapter renderings must name the same affordance identically.
16
+ */
17
+ export const stepLabel = (stepName, scopeKey) => scopeKey === null ? `'${stepName}'` : `'${stepName}' (${scopeKey})`;
18
+ /**
19
+ * The claim's transactional guard re-evaluation said no — the enforcement
20
+ * moment. Guards advise; handlers enforce: an affordance computed
21
+ * for a render is advice, and by the time the execute request arrives, state
22
+ * may have moved (another Execution committed) or the definitions may have
23
+ * (a deploy tightened the guard — definition drift is handled by the
24
+ * same mechanism as state races).
25
+ *
26
+ * The unmet conditions carried here are the *current* ones, evaluated inside
27
+ * the claim transaction, so a rejection is self-explaining: hand `unmet`
28
+ * straight back to the caller.
29
+ */
30
+ export class StepNotAvailableError extends AffordanceError {
31
+ caseId;
32
+ stepName;
33
+ scopeKey;
34
+ /** False when a `requires` condition is unmet: not possible on this case, for anyone. */
35
+ possible;
36
+ /** False when a `permits` condition is unmet: possible, but not for this actor. */
37
+ permitted;
38
+ /** The failed condition results, verbatim from the claim-time evaluation. */
39
+ unmet;
40
+ /** The full evaluation record, for journaling or `explain`-style rendering. */
41
+ evaluation;
42
+ constructor(caseId, stepName, scopeKey, evaluation) {
43
+ const unmet = unmetConditions(evaluation);
44
+ const target = stepLabel(stepName, scopeKey);
45
+ super('step-not-available', `step ${target} is not available on case ${caseId}: ${describeUnmet(evaluation) || '(no unmet conditions reported)'}`);
46
+ this.name = 'StepNotAvailableError';
47
+ this.caseId = caseId;
48
+ this.stepName = stepName;
49
+ this.scopeKey = scopeKey;
50
+ this.possible = evaluation.possible;
51
+ this.permitted = evaluation.permitted;
52
+ this.unmet = unmet;
53
+ this.evaluation = evaluation;
54
+ }
55
+ }
56
+ /**
57
+ * Another Execution is already in flight on this case and its claim has not
58
+ * expired. Executions are serialized per case in v1 — cases advance at human
59
+ * pace, so one Execution at a time costs no real throughput — which makes
60
+ * this "not now", not "never": retry after
61
+ * {@link CaseBusyError.expiresAt} at the latest.
62
+ */
63
+ export class CaseBusyError extends AffordanceError {
64
+ caseId;
65
+ /** The in-flight Execution holding the case. */
66
+ executionId;
67
+ stepName;
68
+ scopeKey;
69
+ /** When the in-flight claim lapses if its handler stops heartbeating (ISO-8601 UTC). */
70
+ expiresAt;
71
+ constructor(caseId, holder) {
72
+ super('case-busy', `case ${caseId} is busy: execution ${holder.executionId} is running step ${stepLabel(holder.stepName, holder.scopeKey)}, claim expires ${holder.expiresAt}`);
73
+ this.name = 'CaseBusyError';
74
+ this.caseId = caseId;
75
+ this.executionId = holder.executionId;
76
+ this.stepName = holder.stepName;
77
+ this.scopeKey = holder.scopeKey;
78
+ this.expiresAt = holder.expiresAt;
79
+ }
80
+ }
81
+ /**
82
+ * The claim was gone (or belonged to someone else) when this Execution tried
83
+ * to commit: its lease expired mid-handler and another claimant took the case
84
+ * over. The handler's effects on the outside world already happened — they are
85
+ * at-least-once by contract and deduplicated on `executionId` — but
86
+ * its Case State write is refused, because the state it computed from is stale.
87
+ */
88
+ export class ClaimLostError extends AffordanceError {
89
+ caseId;
90
+ executionId;
91
+ /** The Execution now holding the case, if any. */
92
+ heldBy;
93
+ constructor(caseId, executionId, heldBy) {
94
+ super(
95
+ // The case moved on under this Execution: somebody else holds it now,
96
+ // and the state this Execution computed from is stale. That is the
97
+ // same "not now" answer a busy case gives, so it reuses 'case-busy' —
98
+ // a code an adapter already knows how to render.
99
+ 'case-busy', `execution ${executionId} lost its claim on case ${caseId}${heldBy === null
100
+ ? ' (claim expired and was released)'
101
+ : ` (now held by ${heldBy})`} — its state write was refused`);
102
+ this.name = 'ClaimLostError';
103
+ this.caseId = caseId;
104
+ this.executionId = executionId;
105
+ this.heldBy = heldBy;
106
+ }
107
+ }
108
+ /**
109
+ * The Execution ran and failed: the handler threw on every allowed attempt,
110
+ * or it returned a Case State the case type's schema rejects (a deterministic
111
+ * defect, failed without retry). A `failed` journal entry records it and the
112
+ * case is released.
113
+ */
114
+ export class StepExecutionError extends AffordanceError {
115
+ caseId;
116
+ executionId;
117
+ stepName;
118
+ scopeKey;
119
+ /** How many attempts ran before the Execution was given up on. */
120
+ attempts;
121
+ constructor(caseId, executionId, stepName, scopeKey, attempts, cause) {
122
+ const reason = thrownMessage(cause);
123
+ super('execution-failed', `step ${stepLabel(stepName, scopeKey)} failed on case ${caseId} after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${reason}`, { cause });
124
+ this.name = 'StepExecutionError';
125
+ this.caseId = caseId;
126
+ this.executionId = executionId;
127
+ this.stepName = stepName;
128
+ this.scopeKey = scopeKey;
129
+ this.attempts = attempts;
130
+ }
131
+ }
132
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/execution/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ways an Execution can be refused or fail.\n *\n * Each is a distinct answer to \"why didn't this run\", and callers are\n * expected to branch on them: {@link StepNotAvailableError} is a client-facing\n * *no* with reasons attached; {@link CaseBusyError} is \"not now, try again\";\n * {@link ClaimLostError} and {@link StepExecutionError} are failures of a run\n * that did start.\n */\n\nimport { AffordanceError, thrownMessage } from '../errors.js'\nimport type { ConditionResult, GuardEvaluation } from '../guards/index.js'\nimport { describeUnmet, unmetConditions } from '../guards/index.js'\n\n/**\n * How a step (× scope element) is named in prose: `'close-purchase'`, or\n * `'request-re-sign' (buyer_7)` when scoped. One spelling, because refusal\n * messages and adapter renderings must name the same affordance identically.\n */\nexport const stepLabel = (stepName: string, scopeKey: string | null): string =>\n scopeKey === null ? `'${stepName}'` : `'${stepName}' (${scopeKey})`\n\n/**\n * The claim's transactional guard re-evaluation said no — the enforcement\n * moment. Guards advise; handlers enforce: an affordance computed\n * for a render is advice, and by the time the execute request arrives, state\n * may have moved (another Execution committed) or the definitions may have\n * (a deploy tightened the guard — definition drift is handled by the\n * same mechanism as state races).\n *\n * The unmet conditions carried here are the *current* ones, evaluated inside\n * the claim transaction, so a rejection is self-explaining: hand `unmet`\n * straight back to the caller.\n */\nexport class StepNotAvailableError extends AffordanceError {\n readonly caseId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** False when a `requires` condition is unmet: not possible on this case, for anyone. */\n readonly possible: boolean\n /** False when a `permits` condition is unmet: possible, but not for this actor. */\n readonly permitted: boolean\n /** The failed condition results, verbatim from the claim-time evaluation. */\n readonly unmet: readonly ConditionResult[]\n /** The full evaluation record, for journaling or `explain`-style rendering. */\n readonly evaluation: GuardEvaluation\n\n constructor(\n caseId: string,\n stepName: string,\n scopeKey: string | null,\n evaluation: GuardEvaluation,\n ) {\n const unmet = unmetConditions(evaluation)\n const target = stepLabel(stepName, scopeKey)\n super(\n 'step-not-available',\n `step ${target} is not available on case ${caseId}: ${\n describeUnmet(evaluation) || '(no unmet conditions reported)'\n }`,\n )\n this.name = 'StepNotAvailableError'\n this.caseId = caseId\n this.stepName = stepName\n this.scopeKey = scopeKey\n this.possible = evaluation.possible\n this.permitted = evaluation.permitted\n this.unmet = unmet\n this.evaluation = evaluation\n }\n}\n\n/**\n * Another Execution is already in flight on this case and its claim has not\n * expired. Executions are serialized per case in v1 — cases advance at human\n * pace, so one Execution at a time costs no real throughput — which makes\n * this \"not now\", not \"never\": retry after\n * {@link CaseBusyError.expiresAt} at the latest.\n */\nexport class CaseBusyError extends AffordanceError {\n readonly caseId: string\n /** The in-flight Execution holding the case. */\n readonly executionId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** When the in-flight claim lapses if its handler stops heartbeating (ISO-8601 UTC). */\n readonly expiresAt: string\n\n constructor(\n caseId: string,\n holder: {\n executionId: string\n stepName: string\n scopeKey: string | null\n expiresAt: string\n },\n ) {\n super(\n 'case-busy',\n `case ${caseId} is busy: execution ${holder.executionId} is running step ${stepLabel(\n holder.stepName,\n holder.scopeKey,\n )}, claim expires ${holder.expiresAt}`,\n )\n this.name = 'CaseBusyError'\n this.caseId = caseId\n this.executionId = holder.executionId\n this.stepName = holder.stepName\n this.scopeKey = holder.scopeKey\n this.expiresAt = holder.expiresAt\n }\n}\n\n/**\n * The claim was gone (or belonged to someone else) when this Execution tried\n * to commit: its lease expired mid-handler and another claimant took the case\n * over. The handler's effects on the outside world already happened — they are\n * at-least-once by contract and deduplicated on `executionId` — but\n * its Case State write is refused, because the state it computed from is stale.\n */\nexport class ClaimLostError extends AffordanceError {\n readonly caseId: string\n readonly executionId: string\n /** The Execution now holding the case, if any. */\n readonly heldBy: string | null\n\n constructor(caseId: string, executionId: string, heldBy: string | null) {\n super(\n // The case moved on under this Execution: somebody else holds it now,\n // and the state this Execution computed from is stale. That is the\n // same \"not now\" answer a busy case gives, so it reuses 'case-busy' —\n // a code an adapter already knows how to render.\n 'case-busy',\n `execution ${executionId} lost its claim on case ${caseId}${\n heldBy === null\n ? ' (claim expired and was released)'\n : ` (now held by ${heldBy})`\n } — its state write was refused`,\n )\n this.name = 'ClaimLostError'\n this.caseId = caseId\n this.executionId = executionId\n this.heldBy = heldBy\n }\n}\n\n/**\n * The Execution ran and failed: the handler threw on every allowed attempt,\n * or it returned a Case State the case type's schema rejects (a deterministic\n * defect, failed without retry). A `failed` journal entry records it and the\n * case is released.\n */\nexport class StepExecutionError extends AffordanceError {\n readonly caseId: string\n readonly executionId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** How many attempts ran before the Execution was given up on. */\n readonly attempts: number\n\n constructor(\n caseId: string,\n executionId: string,\n stepName: string,\n scopeKey: string | null,\n attempts: number,\n cause: unknown,\n ) {\n const reason = thrownMessage(cause)\n super(\n 'execution-failed',\n `step ${stepLabel(stepName, scopeKey)} failed on case ${caseId} after ${attempts} attempt${\n attempts === 1 ? '' : 's'\n }: ${reason}`,\n { cause },\n )\n this.name = 'StepExecutionError'\n this.caseId = caseId\n this.executionId = executionId\n this.stepName = stepName\n this.scopeKey = scopeKey\n this.attempts = attempts\n }\n}\n"]}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The execution lifecycle: **claim → run → commit**.
3
+ *
4
+ * ```
5
+ * ┌── transaction ──────────────┐ ┌── transaction ──────────────┐
6
+ * │ lock the case row │ │ lock the case row │
7
+ * │ take over an expired claim │ handler runs │ verify the claim is ours │
8
+ * │ re-evaluate the guard ←────┼── here, outside ───┼→ write state, bump seq │
9
+ * │ insert the claim (lease) │ any transaction │ run ctx.onCommit writes │
10
+ * │ journal `claimed` │ (heartbeating) │ journal `completed` │
11
+ * └─────────────────────────────┘ └─────────────────────────────┘
12
+ * ```
13
+ *
14
+ * Three things this shape buys, each load-bearing:
15
+ *
16
+ * 1. **Guards advise, handlers enforce.** The affordance a client rendered is
17
+ * advice; the claim's transactional re-evaluation is the enforcement
18
+ * moment. State may have moved (another Execution committed) or the
19
+ * definitions may have (a deploy — definition drift is the
20
+ * same race, handled the same way). A claim that fails rejects with the
21
+ * *current* unmet conditions and writes nothing at all.
22
+ * 2. **No transaction spans a handler.** Handlers call the outside world;
23
+ * a transaction held across an escrow API call would tie up a pooled
24
+ * connection and the case row's lock for as long as the external service
25
+ * takes to answer. The lease (a `claims` row keyed by case id) carries
26
+ * the exclusivity instead, and a heartbeat keeps it alive.
27
+ * 3. **A crashed handler cannot deadlock a case.** The lease expires. The
28
+ * next claimant takes the case over, journaling the abandonment; if the
29
+ * zombie ever comes back to commit, it is refused ({@link ClaimLostError})
30
+ * — at-least-once effects are the handler's problem to deduplicate on
31
+ * `ctx.executionId`, but a stale state write is the framework's to refuse.
32
+ */
33
+ import type { GuardEvaluation, Instant } from '../guards/index.js';
34
+ import type { AnyCaseType, RetryOptions } from '../model/index.js';
35
+ import type { DatabaseAccess, Dormancy } from '../store/index.js';
36
+ import type { StateDelta } from './delta.js';
37
+ import type { LifecyclePort } from './port.js';
38
+ import type { Timers } from './timers.js';
39
+ /** How long a claim survives without a heartbeat, and how often the heartbeat beats. */
40
+ export declare const DEFAULT_CLAIM_TTL_MS = 30000;
41
+ export declare const DEFAULT_HEARTBEAT_MS = 5000;
42
+ /** What {@link executeStep} needs from its caller (the engine supplies all of it). */
43
+ export interface ExecutionEnvironment {
44
+ readonly db: DatabaseAccess;
45
+ /** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */
46
+ readonly caseTypeFor: (caseTypeName: string) => AnyCaseType;
47
+ readonly claimTtlMs: number;
48
+ readonly heartbeatMs: number;
49
+ /**
50
+ * The clock. Conditions never read it; every process-side "as of now"
51
+ * comes through here, so a test can hand the lifecycle a deterministic
52
+ * instant instead of building fixtures around far-future dates. (Lease
53
+ * expiry is judged by the storage adapter's own clock — see
54
+ * {@link LifecyclePort}.)
55
+ */
56
+ readonly now: () => Date;
57
+ /** The process timers — retry delays and the heartbeat. See {@link Timers}. */
58
+ readonly timers: Timers;
59
+ }
60
+ /** Options for one execute call. */
61
+ export interface ExecuteOptions<TActor = unknown> {
62
+ /** The Actor executing the step; `permits` conditions are evaluated against it. */
63
+ readonly actor: TActor;
64
+ /** Required for a scoped step, forbidden otherwise — the element's scope key. */
65
+ readonly scopeKey?: string;
66
+ /** The step's input, validated against its declared input schema before the handler runs. */
67
+ readonly input?: unknown;
68
+ /** The instant to re-evaluate the guard as of; defaults to now. */
69
+ readonly asOf?: Instant;
70
+ /** Override the claim lease for this Execution (long-running handler). */
71
+ readonly claimTtlMs?: number;
72
+ /** Override the heartbeat interval for this Execution. */
73
+ readonly heartbeatMs?: number;
74
+ /** Override the step's declared retry policy for this Execution. */
75
+ readonly retry?: RetryOptions;
76
+ }
77
+ /** A committed Execution — what `execute` resolves to. */
78
+ export interface ExecutionResult<TState = unknown> {
79
+ readonly executionId: string;
80
+ readonly caseId: string;
81
+ readonly caseTypeName: string;
82
+ readonly step: string;
83
+ /** Present iff the step is scoped. */
84
+ readonly scopeKey?: string;
85
+ /** How many attempts ran, including the one that succeeded. */
86
+ readonly attempts: number;
87
+ /** The claim-time guard evaluation — the enforcement moment, as journaled. */
88
+ readonly guard: GuardEvaluation;
89
+ /** The committed Case State. */
90
+ readonly state: TState;
91
+ /** What changed, as JSON Patch. */
92
+ readonly delta: StateDelta;
93
+ /** The case's sequence counter after this Execution. */
94
+ readonly seq: number;
95
+ /** `end()` / `reopen()` called by the handler, if either was. */
96
+ readonly dormancy: Dormancy | null;
97
+ /** The case's dormancy marker after this Execution (ISO-8601 UTC), `null` while active. */
98
+ readonly endedAt: string | null;
99
+ readonly claimedAt: string;
100
+ readonly committedAt: string;
101
+ }
102
+ /**
103
+ * What the lifecycle core needs besides storage: the clock, the timers, and
104
+ * the lease timings. Definition resolution is the port's own (its `loadCase`
105
+ * returns the case resolved whole), so it is no part of this interface.
106
+ */
107
+ export interface LifecycleDeps {
108
+ readonly now: () => Date;
109
+ readonly timers: Timers;
110
+ readonly claimTtlMs: number;
111
+ readonly heartbeatMs: number;
112
+ }
113
+ /**
114
+ * The lifecycle core: claim → run → commit against a {@link LifecyclePort}.
115
+ *
116
+ * {@link executeStep} binds this to the pg port; the claim state machine's
117
+ * own tests bind it to an in-memory port instead. Same body either way — the
118
+ * port is the only storage the lifecycle knows.
119
+ */
120
+ export declare const runLifecycle: (port: LifecyclePort, deps: LifecycleDeps, caseId: string, stepName: string, options: ExecuteOptions) => Promise<ExecutionResult>;
121
+ /**
122
+ * Execute one step on one case: claim it, run its handler, commit the result.
123
+ *
124
+ * Rejections before anything runs — {@link StepNotAvailableError} (the guard
125
+ * said no), {@link CaseBusyError} (another Execution holds the case),
126
+ * `UnknownStepError` / `ScopeKeyError` (bad address) — write nothing, not
127
+ * even a journal entry: the journal records Executions, and a refused claim
128
+ * never became one.
129
+ *
130
+ * A handler that throws is retried per the step's retry policy (same
131
+ * `executionId`, same claim, same starting state — nothing else can have
132
+ * moved it), each failure journaled as `attempt-failed`. When the attempts
133
+ * run out, a `failed` entry is journaled, the case is released, and
134
+ * {@link StepExecutionError} is thrown.
135
+ *
136
+ * This is {@link runLifecycle} bound to the pg port.
137
+ */
138
+ export declare const executeStep: (env: ExecutionEnvironment, caseId: string, stepName: string, options: ExecuteOptions) => Promise<ExecutionResult>;
139
+ /** A committed system run. */
140
+ export interface SystemCommit {
141
+ readonly outcome: 'committed';
142
+ readonly result: ExecutionResult;
143
+ }
144
+ /**
145
+ * A system run that did not commit, carrying whatever the lifecycle threw —
146
+ * a Refusal keeps its identity (its code is the answer, projectable by
147
+ * `isAffordanceError`, never re-derived from a class), a bug or an
148
+ * infrastructure failure passes through as itself. One settled variant, not
149
+ * a refused/failed pair: no sweep ever treated the halves differently, and a
150
+ * discriminant nobody branches on is interface without behaviour.
151
+ */
152
+ export interface SystemSettled {
153
+ readonly outcome: 'settled';
154
+ readonly error: Error;
155
+ }
156
+ /** How a step the system ran ended — total over every way the lifecycle can answer. */
157
+ export type SystemRunOutcome = SystemCommit | SystemSettled;
158
+ /** What a sweep may ask for beyond the lifecycle's own execute options. */
159
+ export interface SystemRunOptions extends ExecuteOptions {
160
+ /**
161
+ * Run against this definition instead of the registry's — how a migration
162
+ * executes its synthetic `migrate:<name>` step. First-class here so no
163
+ * sweep has to smuggle a definition in by rewriting `caseTypeFor`.
164
+ */
165
+ readonly definition?: AnyCaseType;
166
+ }
167
+ /**
168
+ * Turn a throw into the settled value. No classification happens here —
169
+ * "is this a Refusal" is asked exactly once, where a consumer needs the
170
+ * distinction (ingestion's `classifyDeadLetter`), via `isAffordanceError`.
171
+ */
172
+ export declare const settleSystemRun: (error: unknown) => SystemSettled;
173
+ /**
174
+ * Run one step on behalf of the system and say how it ended. Never throws:
175
+ * a sweep ranges over many cases with no caller waiting on any single one,
176
+ * so the answer is a value — committed (with the result) or settled (with
177
+ * the error) — and each sweep decides what its kind of sweep does with it.
178
+ */
179
+ export declare const runAsSystem: (env: ExecutionEnvironment, caseId: string, stepName: string, options: SystemRunOptions) => Promise<SystemRunOutcome>;