@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,195 @@
1
+ /**
2
+ * The migration escape hatch.
3
+ *
4
+ * Cases float to the latest definitions: a deploy changes what the steps are
5
+ * and every in-flight case is immediately governed by the new ones, with no
6
+ * migration executed and no version pinned. That covers almost everything,
7
+ * because almost every change is *additive* — a new step, a new condition, a
8
+ * new optional field — and the `?? fallback` discipline makes old
9
+ * state readable by new code.
10
+ *
11
+ * What float cannot cover is a **restructure**: a field renamed, one field
12
+ * split into two, a scalar turned into a collection. No fallback expression
13
+ * makes `state.buyer` readable as `state.buyers[]`. For exactly that,
14
+ * this module.
15
+ *
16
+ * Two properties make it an escape hatch rather than a second way of working:
17
+ *
18
+ * 1. **A migration is an Execution.** Not a script with a database
19
+ * connection — the same claim → run → commit as any step, so the case row
20
+ * lock, the in-flight claim, state schema validation
21
+ * and the journal entry all apply unchanged. The transform runs
22
+ * as a synthetic step named `migrate:<name>`, with a migration actor.
23
+ * An auditor asking "why did this case's shape change in March" gets the
24
+ * same kind of answer as for anything else that ever happened to it.
25
+ * 2. **The journal is the marker.** A case has been migrated iff it carries a
26
+ * completed `migrate:<name>` entry. Nothing is written into app state to
27
+ * record it, so a migration leaves no residue in the document it
28
+ * restructured — and re-running is a no-op by construction, which is also
29
+ * what makes an interrupted run resumable.
30
+ *
31
+ * When *not* to reach for this is in `docs/migration.md`. The short version:
32
+ * if a total condition can read the old shape, write the total condition.
33
+ */
34
+ import { toError } from '../errors.js';
35
+ import { diffState, runAsSystem } from '../execution/index.js';
36
+ import { step } from '../model/index.js';
37
+ import { FRAMEWORK_SCHEMA, queryableOf, resolveStoredState, sqlWhere, } from '../store/index.js';
38
+ const CASES = `${FRAMEWORK_SCHEMA}.cases`;
39
+ const JOURNAL = `${FRAMEWORK_SCHEMA}.journal`;
40
+ /** The synthetic step name a migration executes under — its journal marker. */
41
+ export const migrationStepName = (name) => `migrate:${name}`;
42
+ /**
43
+ * Build the synthetic step the transform runs as. It declares no conditions:
44
+ * a migration is not a business affordance and must not be blockable by one
45
+ * — it is the deliberate exception float reserves, and the guard model
46
+ * would only ever be in its way. `maxAttempts: 1` because a transform that
47
+ * throws is a defect in the transform, and retrying it just throws again.
48
+ */
49
+ const migrationStep = (name, transform) => step({
50
+ name: migrationStepName(name),
51
+ retry: { maxAttempts: 1 },
52
+ handler: async (state) => transform(state),
53
+ });
54
+ /**
55
+ * A view of the case type with the migration step spliced in, so the
56
+ * ordinary lifecycle can address it by name. The real definition is left
57
+ * untouched: the migration step exists for the duration of the run and is
58
+ * never part of anything a client could see as an affordance.
59
+ */
60
+ const withMigrationStep = (definition, migration) => ({
61
+ ...definition,
62
+ steps: [...definition.steps, migration],
63
+ getStep: (stepName) => stepName === migration.name ? migration : definition.getStep(stepName),
64
+ });
65
+ /** Case ids of this type that do not yet carry the migration's marker, oldest first. */
66
+ const findCandidates = async (db, caseTypeName, marker, options, afterId, batchSize) => {
67
+ const { conditions, values, bind, where } = sqlWhere([
68
+ `c.case_type = $1`,
69
+ // The marker: a completed Execution of this migration on this case. The
70
+ // journal is the record of what has happened, so it is also the record of
71
+ // what has already been migrated — no bookkeeping table, no state flag.
72
+ `not exists (
73
+ select 1 from ${JOURNAL} j
74
+ where j.case_id = c.id and j.step = $2 and j.entry = 'completed'
75
+ )`,
76
+ ], [caseTypeName, marker]);
77
+ if (options.includeEnded !== true)
78
+ conditions.push(`c.ended_at is null`);
79
+ if (options.caseIds !== undefined)
80
+ conditions.push(`c.id = any(${bind(options.caseIds)}::text[])`);
81
+ if (afterId !== null)
82
+ conditions.push(`c.id > ${bind(afterId)}`);
83
+ const { rows } = await db.query(`select c.id, c.state from ${CASES} c
84
+ where ${where()}
85
+ order by c.id asc
86
+ limit ${bind(batchSize)}`, values);
87
+ return rows;
88
+ };
89
+ /**
90
+ * Run a state restructure over every case of a case type, as journaled
91
+ * system Executions.
92
+ *
93
+ * Idempotent: a case that already carries the migration's marker is never
94
+ * examined again, so re-running is a no-op and an interrupted run resumes
95
+ * where it stopped. Failures do not stop the run — a case that cannot be
96
+ * migrated (its claim is held, the transform threw, the result fails the
97
+ * state schema) is reported and the run moves on, and the next run will pick
98
+ * it up because it never got its marker.
99
+ *
100
+ * The cursor walks case ids ascending, and the "already migrated" filter is
101
+ * applied by the database on every batch — so a case migrated by a
102
+ * concurrently-running instance of the same migration simply is not returned.
103
+ */
104
+ export const migrate = async (env, caseTypeName, name, transform, options = {}) => {
105
+ if (typeof name !== 'string' || name.trim() === '') {
106
+ throw new TypeError('migrate: a migration must have a non-empty name — it is the journal marker');
107
+ }
108
+ if (typeof transform !== 'function') {
109
+ throw new TypeError(`migrate '${name}': the transform must be a function of case state`);
110
+ }
111
+ const definition = env.caseTypeFor(caseTypeName);
112
+ const marker = migrationStepName(name);
113
+ const migration = migrationStep(name, transform);
114
+ const augmented = withMigrationStep(definition, migration);
115
+ const actor = options.actor ?? { kind: 'migration', migration: name };
116
+ const batchSize = Math.max(1, options.batchSize ?? 100);
117
+ const dryRun = options.dryRun === true;
118
+ let scanned = 0;
119
+ let migrated = 0;
120
+ let unchanged = 0;
121
+ const failed = [];
122
+ let cursor = null;
123
+ /** One candidate's verdict, with early returns — no outcome threading. */
124
+ const runOne = async (candidate) => {
125
+ try {
126
+ if (dryRun) {
127
+ // The transform is pure, so "what would this become" needs no
128
+ // claim, no lock and no write — and leaves no marker behind.
129
+ const resolved = await resolveStoredState(definition, candidate.state);
130
+ const before = resolved === null ? candidate.state : resolved.state;
131
+ const delta = diffState(before, transform(before));
132
+ return {
133
+ outcome: delta.length === 0 ? 'unchanged' : 'migrated',
134
+ delta,
135
+ error: null,
136
+ };
137
+ }
138
+ // The system runner returns case-level failures as values rather than
139
+ // throwing, and the sweep filters on that outcome: only a commit
140
+ // advances this case. A settled run — a held claim, a throwing
141
+ // transform, a result its schema rejects — is reported with no marker
142
+ // written, so the next run picks the case up again.
143
+ const ran = await runAsSystem(env, candidate.id, marker, {
144
+ actor,
145
+ definition: augmented,
146
+ });
147
+ if (ran.outcome === 'settled')
148
+ return { outcome: 'failed', delta: [], error: ran.error };
149
+ const delta = ran.result.delta;
150
+ return {
151
+ outcome: delta.length === 0 ? 'unchanged' : 'migrated',
152
+ delta,
153
+ error: null,
154
+ };
155
+ }
156
+ catch (caught) {
157
+ // The dry-run path's own throws (a transform is app code).
158
+ return { outcome: 'failed', delta: [], error: toError(caught) };
159
+ }
160
+ };
161
+ for (;;) {
162
+ const remaining = options.limit === undefined ? batchSize : options.limit - scanned;
163
+ if (remaining <= 0)
164
+ break;
165
+ const candidates = await findCandidates(queryableOf(env.db), caseTypeName, marker, options, cursor, Math.min(batchSize, remaining));
166
+ if (candidates.length === 0)
167
+ break;
168
+ for (const candidate of candidates) {
169
+ cursor = candidate.id;
170
+ scanned += 1;
171
+ const { outcome, delta, error } = await runOne(candidate);
172
+ if (error !== null)
173
+ failed.push({ caseId: candidate.id, error });
174
+ else if (outcome === 'unchanged')
175
+ unchanged += 1;
176
+ else
177
+ migrated += 1;
178
+ options.onProgress?.({
179
+ caseId: candidate.id,
180
+ outcome,
181
+ delta,
182
+ error,
183
+ processed: scanned,
184
+ });
185
+ }
186
+ }
187
+ return { name, caseTypeName, dryRun, scanned, migrated, unchanged, failed };
188
+ };
189
+ /** Whether one case already carries a migration's marker. */
190
+ export const hasMigrated = async (db, caseId, name) => {
191
+ const { rows } = await db.query(`select 1 as one from ${JOURNAL}
192
+ where case_id = $1 and step = $2 and entry = 'completed' limit 1`, [caseId, migrationStepName(name)]);
193
+ return rows.length > 0;
194
+ };
195
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/migration/migrate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The migration escape hatch.\n *\n * Cases float to the latest definitions: a deploy changes what the steps are\n * and every in-flight case is immediately governed by the new ones, with no\n * migration executed and no version pinned. That covers almost everything,\n * because almost every change is *additive* — a new step, a new condition, a\n * new optional field — and the `?? fallback` discipline makes old\n * state readable by new code.\n *\n * What float cannot cover is a **restructure**: a field renamed, one field\n * split into two, a scalar turned into a collection. No fallback expression\n * makes `state.buyer` readable as `state.buyers[]`. For exactly that,\n * this module.\n *\n * Two properties make it an escape hatch rather than a second way of working:\n *\n * 1. **A migration is an Execution.** Not a script with a database\n * connection — the same claim → run → commit as any step, so the case row\n * lock, the in-flight claim, state schema validation\n * and the journal entry all apply unchanged. The transform runs\n * as a synthetic step named `migrate:<name>`, with a migration actor.\n * An auditor asking \"why did this case's shape change in March\" gets the\n * same kind of answer as for anything else that ever happened to it.\n * 2. **The journal is the marker.** A case has been migrated iff it carries a\n * completed `migrate:<name>` entry. Nothing is written into app state to\n * record it, so a migration leaves no residue in the document it\n * restructured — and re-running is a no-op by construction, which is also\n * what makes an interrupted run resumable.\n *\n * When *not* to reach for this is in `docs/migration.md`. The short version:\n * if a total condition can read the old shape, write the total condition.\n */\n\nimport { toError } from '../errors.js'\nimport type { ExecutionEnvironment, StateDelta } from '../execution/index.js'\nimport { diffState, runAsSystem } from '../execution/index.js'\nimport type { AnyCaseType, StepDefinition } from '../model/index.js'\nimport { step } from '../model/index.js'\nimport type { Queryable } from '../store/index.js'\nimport {\n FRAMEWORK_SCHEMA,\n queryableOf,\n resolveStoredState,\n sqlWhere,\n} from '../store/index.js'\n\nconst CASES = `${FRAMEWORK_SCHEMA}.cases`\nconst JOURNAL = `${FRAMEWORK_SCHEMA}.journal`\n\n/** The synthetic step name a migration executes under — its journal marker. */\nexport const migrationStepName = (name: string): string => `migrate:${name}`\n\n/** The Actor a migration runs as, when the app supplies none. */\nexport interface MigrationActor {\n readonly kind: 'migration'\n readonly migration: string\n}\n\n/**\n * The state transform. Pure and total, like a condition: it will\n * be handed state written by every definition the case has ever floated\n * through, and returning the input unchanged must be safe.\n */\nexport type MigrationTransform<TState = any> = (state: TState) => TState\n\n/** Options for {@link migrate}. */\nexport interface MigrationOptions {\n /** How many cases to claim per round trip when scanning (default 100). */\n readonly batchSize?: number\n /** Stop after this many cases — for a cautious first run. */\n readonly limit?: number\n /** Restrict the run to these cases; they are still skipped if already migrated. */\n readonly caseIds?: readonly string[]\n /** The Actor the Executions run as; defaults to a {@link MigrationActor}. */\n readonly actor?: unknown\n /** Include dormant (ended) cases. Off by default — a closed case is usually not worth restructuring. */\n readonly includeEnded?: boolean\n /**\n * Compute what each case *would* become and report the deltas without\n * writing anything. Nothing is journaled, so a dry run leaves no marker\n * and the real run still has every case to do.\n */\n readonly dryRun?: boolean\n /** Called after each case, for a progress bar or a log line. */\n readonly onProgress?: (progress: MigrationProgress) => void\n}\n\n/** Progress after one case. */\nexport interface MigrationProgress {\n readonly caseId: string\n readonly outcome: 'migrated' | 'unchanged' | 'failed'\n /** What the transform changed, as JSON Patch; empty when it changed nothing. */\n readonly delta: StateDelta\n readonly error: Error | null\n /** How many cases have been processed so far in this run. */\n readonly processed: number\n}\n\n/** One case a migration could not restructure. */\nexport interface MigrationFailure {\n readonly caseId: string\n readonly error: Error\n}\n\n/** What {@link migrate} resolves to. */\nexport interface MigrationReport {\n readonly name: string\n readonly caseTypeName: string\n readonly dryRun: boolean\n /** Cases examined — those not already bearing the marker. */\n readonly scanned: number\n /** Cases whose state the transform changed and which committed. */\n readonly migrated: number\n /**\n * Cases the transform left identical. Still journaled (and so still\n * marked), because \"this migration considered this case and had nothing to\n * do\" is a fact worth being able to prove.\n */\n readonly unchanged: number\n readonly failed: readonly MigrationFailure[]\n}\n\n/**\n * Build the synthetic step the transform runs as. It declares no conditions:\n * a migration is not a business affordance and must not be blockable by one\n * — it is the deliberate exception float reserves, and the guard model\n * would only ever be in its way. `maxAttempts: 1` because a transform that\n * throws is a defect in the transform, and retrying it just throws again.\n */\nconst migrationStep = (\n name: string,\n transform: MigrationTransform,\n): StepDefinition<unknown, unknown> =>\n step({\n name: migrationStepName(name),\n retry: { maxAttempts: 1 },\n handler: async (state: unknown) => transform(state),\n })\n\n/**\n * A view of the case type with the migration step spliced in, so the\n * ordinary lifecycle can address it by name. The real definition is left\n * untouched: the migration step exists for the duration of the run and is\n * never part of anything a client could see as an affordance.\n */\nconst withMigrationStep = (\n definition: AnyCaseType,\n migration: StepDefinition<unknown, unknown>,\n): AnyCaseType => ({\n ...definition,\n steps: [...definition.steps, migration],\n getStep: (stepName: string) =>\n stepName === migration.name ? migration : definition.getStep(stepName),\n})\n\n/** Case ids of this type that do not yet carry the migration's marker, oldest first. */\nconst findCandidates = async (\n db: Queryable,\n caseTypeName: string,\n marker: string,\n options: MigrationOptions,\n afterId: string | null,\n batchSize: number,\n): Promise<readonly { id: string; state: unknown }[]> => {\n const { conditions, values, bind, where } = sqlWhere(\n [\n `c.case_type = $1`,\n // The marker: a completed Execution of this migration on this case. The\n // journal is the record of what has happened, so it is also the record of\n // what has already been migrated — no bookkeeping table, no state flag.\n `not exists (\n select 1 from ${JOURNAL} j\n where j.case_id = c.id and j.step = $2 and j.entry = 'completed'\n )`,\n ],\n [caseTypeName, marker],\n )\n if (options.includeEnded !== true) conditions.push(`c.ended_at is null`)\n if (options.caseIds !== undefined)\n conditions.push(`c.id = any(${bind(options.caseIds)}::text[])`)\n if (afterId !== null) conditions.push(`c.id > ${bind(afterId)}`)\n\n const { rows } = await db.query<{ id: string; state: unknown }>(\n `select c.id, c.state from ${CASES} c\n where ${where()}\n order by c.id asc\n limit ${bind(batchSize)}`,\n values,\n )\n return rows\n}\n\n/**\n * Run a state restructure over every case of a case type, as journaled\n * system Executions.\n *\n * Idempotent: a case that already carries the migration's marker is never\n * examined again, so re-running is a no-op and an interrupted run resumes\n * where it stopped. Failures do not stop the run — a case that cannot be\n * migrated (its claim is held, the transform threw, the result fails the\n * state schema) is reported and the run moves on, and the next run will pick\n * it up because it never got its marker.\n *\n * The cursor walks case ids ascending, and the \"already migrated\" filter is\n * applied by the database on every batch — so a case migrated by a\n * concurrently-running instance of the same migration simply is not returned.\n */\nexport const migrate = async (\n env: ExecutionEnvironment,\n caseTypeName: string,\n name: string,\n transform: MigrationTransform,\n options: MigrationOptions = {},\n): Promise<MigrationReport> => {\n if (typeof name !== 'string' || name.trim() === '') {\n throw new TypeError(\n 'migrate: a migration must have a non-empty name — it is the journal marker',\n )\n }\n if (typeof transform !== 'function') {\n throw new TypeError(\n `migrate '${name}': the transform must be a function of case state`,\n )\n }\n\n const definition = env.caseTypeFor(caseTypeName)\n const marker = migrationStepName(name)\n const migration = migrationStep(name, transform)\n const augmented = withMigrationStep(definition, migration)\n const actor: unknown = options.actor ?? { kind: 'migration', migration: name }\n const batchSize = Math.max(1, options.batchSize ?? 100)\n const dryRun = options.dryRun === true\n\n let scanned = 0\n let migrated = 0\n let unchanged = 0\n const failed: MigrationFailure[] = []\n let cursor: string | null = null\n\n /** One candidate's verdict, with early returns — no outcome threading. */\n const runOne = async (candidate: {\n readonly id: string\n readonly state: unknown\n }): Promise<Pick<MigrationProgress, 'outcome' | 'delta' | 'error'>> => {\n try {\n if (dryRun) {\n // The transform is pure, so \"what would this become\" needs no\n // claim, no lock and no write — and leaves no marker behind.\n const resolved = await resolveStoredState(definition, candidate.state)\n const before = resolved === null ? candidate.state : resolved.state\n const delta = diffState(before, transform(before))\n return {\n outcome: delta.length === 0 ? 'unchanged' : 'migrated',\n delta,\n error: null,\n }\n }\n // The system runner returns case-level failures as values rather than\n // throwing, and the sweep filters on that outcome: only a commit\n // advances this case. A settled run — a held claim, a throwing\n // transform, a result its schema rejects — is reported with no marker\n // written, so the next run picks the case up again.\n const ran = await runAsSystem(env, candidate.id, marker, {\n actor,\n definition: augmented,\n })\n if (ran.outcome === 'settled')\n return { outcome: 'failed', delta: [], error: ran.error }\n const delta = ran.result.delta\n return {\n outcome: delta.length === 0 ? 'unchanged' : 'migrated',\n delta,\n error: null,\n }\n } catch (caught) {\n // The dry-run path's own throws (a transform is app code).\n return { outcome: 'failed', delta: [], error: toError(caught) }\n }\n }\n\n for (;;) {\n const remaining =\n options.limit === undefined ? batchSize : options.limit - scanned\n if (remaining <= 0) break\n const candidates = await findCandidates(\n queryableOf(env.db),\n caseTypeName,\n marker,\n options,\n cursor,\n Math.min(batchSize, remaining),\n )\n if (candidates.length === 0) break\n\n for (const candidate of candidates) {\n cursor = candidate.id\n scanned += 1\n const { outcome, delta, error } = await runOne(candidate)\n if (error !== null) failed.push({ caseId: candidate.id, error })\n else if (outcome === 'unchanged') unchanged += 1\n else migrated += 1\n\n options.onProgress?.({\n caseId: candidate.id,\n outcome,\n delta,\n error,\n processed: scanned,\n })\n }\n }\n\n return { name, caseTypeName, dryRun, scanned, migrated, unchanged, failed }\n}\n\n/** Whether one case already carries a migration's marker. */\nexport const hasMigrated = async (\n db: Queryable,\n caseId: string,\n name: string,\n): Promise<boolean> => {\n const { rows } = await db.query<{ one: number }>(\n `select 1 as one from ${JOURNAL}\n where case_id = $1 and step = $2 and entry = 'completed' limit 1`,\n [caseId, migrationStepName(name)],\n )\n return rows.length > 0\n}\n"]}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Case type definitions.
3
+ *
4
+ * A **case type** declares exactly two things: a typed state schema and a set
5
+ * of steps. Nothing else — no flow, stages, graph, or completion test; a
6
+ * case's only "position" is its state.
7
+ *
8
+ * There is deliberately no completion predicate. Whether a
9
+ * matter is finished is a fact about the matter, and outcomes are state: a
10
+ * closed purchase has `closedAt`. The framework's own answer to "is there
11
+ * anything to do here" is the affordance listing being empty for the actor
12
+ * asking — no declaration required, and correct again the moment state moves.
13
+ */
14
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
15
+ import type { StepDefinition } from './step.js';
16
+ /** Options for {@link caseType}. */
17
+ export interface CaseTypeOptions<S extends StandardSchemaV1, TActor = unknown> {
18
+ /**
19
+ * The case type's name — what `cases.case_type` records. Only the name is
20
+ * stored: the code definition floats, meaning existing cases always run
21
+ * against the latest deployed definition.
22
+ */
23
+ readonly name: string;
24
+ /** The Case State schema (any Standard Schema — zod v4 qualifies). */
25
+ readonly state: S;
26
+ /** The case type's steps, in declaration order (which affordance listings preserve). */
27
+ readonly steps: readonly StepDefinition<StandardSchemaV1.InferOutput<S>, TActor>[];
28
+ }
29
+ /** A validated case type definition — the unit the engine registers. */
30
+ export interface CaseTypeDefinition<S extends StandardSchemaV1 = StandardSchemaV1, TActor = unknown> {
31
+ readonly name: string;
32
+ readonly state: S;
33
+ readonly steps: readonly StepDefinition<StandardSchemaV1.InferOutput<S>, TActor>[];
34
+ /** Look up a step by name; `undefined` when the case type declares no such step. */
35
+ readonly getStep: (name: string) => StepDefinition<StandardSchemaV1.InferOutput<S>, TActor> | undefined;
36
+ }
37
+ /**
38
+ * A case type with its schema and actor generics erased — what heterogeneous
39
+ * registries (the engine's `caseTypes`) hold. `any` is deliberate: it is the
40
+ * only way a `CaseTypeDefinition<PurchaseSchema, Ops>` and a
41
+ * `CaseTypeDefinition<LoanSchema, Servicer>` fit one list; every use is
42
+ * re-anchored by the state schema validation the engine performs on load.
43
+ */
44
+ export type AnyCaseType = CaseTypeDefinition<any, any>;
45
+ /**
46
+ * Define a case type. Validates loudly at construction time:
47
+ *
48
+ * - `name` must be a non-empty string and `state` a Standard Schema
49
+ * - every element of `steps` must be a `step(...)` definition
50
+ * - step names must be unique within the case type — a duplicate would make
51
+ * affordance identity (step × scope key) ambiguous
52
+ */
53
+ export declare const caseType: <S extends StandardSchemaV1, TActor = unknown>(options: CaseTypeOptions<S, TActor>) => CaseTypeDefinition<S, TActor>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Case type definitions.
3
+ *
4
+ * A **case type** declares exactly two things: a typed state schema and a set
5
+ * of steps. Nothing else — no flow, stages, graph, or completion test; a
6
+ * case's only "position" is its state.
7
+ *
8
+ * There is deliberately no completion predicate. Whether a
9
+ * matter is finished is a fact about the matter, and outcomes are state: a
10
+ * closed purchase has `closedAt`. The framework's own answer to "is there
11
+ * anything to do here" is the affordance listing being empty for the actor
12
+ * asking — no declaration required, and correct again the moment state moves.
13
+ */
14
+ import { isStandardSchema, looksLikeStepDefinition } from './step.js';
15
+ /**
16
+ * Define a case type. Validates loudly at construction time:
17
+ *
18
+ * - `name` must be a non-empty string and `state` a Standard Schema
19
+ * - every element of `steps` must be a `step(...)` definition
20
+ * - step names must be unique within the case type — a duplicate would make
21
+ * affordance identity (step × scope key) ambiguous
22
+ */
23
+ export const caseType = (options) => {
24
+ const { name, state, steps } = options;
25
+ if (typeof name !== 'string' || name.trim() === '') {
26
+ throw new TypeError('caseType: name must be a non-empty string');
27
+ }
28
+ if (!isStandardSchema(state)) {
29
+ throw new TypeError(`caseType '${name}': state must be a Standard Schema (e.g. a zod schema)`);
30
+ }
31
+ if (!Array.isArray(steps)) {
32
+ throw new TypeError(`caseType '${name}': steps must be an array of step(...) definitions`);
33
+ }
34
+ const byName = new Map();
35
+ for (const definition of steps) {
36
+ if (!looksLikeStepDefinition(definition)) {
37
+ throw new TypeError(`caseType '${name}': every step must be built with step(...)`);
38
+ }
39
+ if (byName.has(definition.name)) {
40
+ throw new TypeError(`caseType '${name}': duplicate step name '${definition.name}'`);
41
+ }
42
+ byName.set(definition.name, definition);
43
+ }
44
+ return {
45
+ name,
46
+ state,
47
+ steps: [...steps],
48
+ getStep: (stepName) => byName.get(stepName),
49
+ };
50
+ };
51
+ //# sourceMappingURL=casetype.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"casetype.js","sourceRoot":"","sources":["../../src/model/casetype.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAmDrE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,OAAmC,EACJ,EAAE;IACjC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAA;IACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAA;IAClE,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CACjB,aAAa,IAAI,wDAAwD,CAC1E,CAAA;IACH,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,SAAS,CACjB,aAAa,IAAI,oDAAoD,CACtE,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAGnB,CAAA;IACH,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,SAAS,CACjB,aAAa,IAAI,4CAA4C,CAC9D,CAAA;QACH,CAAC;QACD,IAAI,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,SAAS,CACjB,aAAa,IAAI,2BAA2B,UAAU,CAAC,IAAI,GAAG,CAC/D,CAAA;QACH,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACzC,CAAC;IAED,OAAO;QACL,IAAI;QACJ,KAAK;QACL,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC;QACjB,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;KAC5C,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Case type definitions.\n *\n * A **case type** declares exactly two things: a typed state schema and a set\n * of steps. Nothing else — no flow, stages, graph, or completion test; a\n * case's only \"position\" is its state.\n *\n * There is deliberately no completion predicate. Whether a\n * matter is finished is a fact about the matter, and outcomes are state: a\n * closed purchase has `closedAt`. The framework's own answer to \"is there\n * anything to do here\" is the affordance listing being empty for the actor\n * asking — no declaration required, and correct again the moment state moves.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { StepDefinition } from './step.js'\nimport { isStandardSchema, looksLikeStepDefinition } from './step.js'\n\n/** Options for {@link caseType}. */\nexport interface CaseTypeOptions<S extends StandardSchemaV1, TActor = unknown> {\n /**\n * The case type's name — what `cases.case_type` records. Only the name is\n * stored: the code definition floats, meaning existing cases always run\n * against the latest deployed definition.\n */\n readonly name: string\n /** The Case State schema (any Standard Schema — zod v4 qualifies). */\n readonly state: S\n /** The case type's steps, in declaration order (which affordance listings preserve). */\n readonly steps: readonly StepDefinition<\n StandardSchemaV1.InferOutput<S>,\n TActor\n >[]\n}\n\n/** A validated case type definition — the unit the engine registers. */\nexport interface CaseTypeDefinition<\n S extends StandardSchemaV1 = StandardSchemaV1,\n TActor = unknown,\n> {\n readonly name: string\n readonly state: S\n readonly steps: readonly StepDefinition<\n StandardSchemaV1.InferOutput<S>,\n TActor\n >[]\n /** Look up a step by name; `undefined` when the case type declares no such step. */\n readonly getStep: (\n name: string,\n ) => StepDefinition<StandardSchemaV1.InferOutput<S>, TActor> | undefined\n}\n\n/**\n * A case type with its schema and actor generics erased — what heterogeneous\n * registries (the engine's `caseTypes`) hold. `any` is deliberate: it is the\n * only way a `CaseTypeDefinition<PurchaseSchema, Ops>` and a\n * `CaseTypeDefinition<LoanSchema, Servicer>` fit one list; every use is\n * re-anchored by the state schema validation the engine performs on load.\n */\n// deliberate `any`: the existential form of CaseTypeDefinition — \"some case\n// type\", its schema deliberately unstated (see doc above). The schema slot\n// must be bare `any` — CaseTypeDefinition is invariant in S, meaning no wider\n// or narrower schema type is assignable (S feeds both the state property and\n// condition/handler parameters), so any narrower existential would reject\n// every concrete schema.\nexport type AnyCaseType = CaseTypeDefinition<any, any>\n\n/**\n * Define a case type. Validates loudly at construction time:\n *\n * - `name` must be a non-empty string and `state` a Standard Schema\n * - every element of `steps` must be a `step(...)` definition\n * - step names must be unique within the case type — a duplicate would make\n * affordance identity (step × scope key) ambiguous\n */\nexport const caseType = <S extends StandardSchemaV1, TActor = unknown>(\n options: CaseTypeOptions<S, TActor>,\n): CaseTypeDefinition<S, TActor> => {\n const { name, state, steps } = options\n if (typeof name !== 'string' || name.trim() === '') {\n throw new TypeError('caseType: name must be a non-empty string')\n }\n if (!isStandardSchema(state)) {\n throw new TypeError(\n `caseType '${name}': state must be a Standard Schema (e.g. a zod schema)`,\n )\n }\n if (!Array.isArray(steps)) {\n throw new TypeError(\n `caseType '${name}': steps must be an array of step(...) definitions`,\n )\n }\n\n const byName = new Map<\n string,\n StepDefinition<StandardSchemaV1.InferOutput<S>, TActor>\n >()\n for (const definition of steps) {\n if (!looksLikeStepDefinition(definition)) {\n throw new TypeError(\n `caseType '${name}': every step must be built with step(...)`,\n )\n }\n if (byName.has(definition.name)) {\n throw new TypeError(\n `caseType '${name}': duplicate step name '${definition.name}'`,\n )\n }\n byName.set(definition.name, definition)\n }\n\n return {\n name,\n state,\n steps: [...steps],\n getStep: (stepName) => byName.get(stepName),\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Addressing failures: naming a step, or an element of one, that the
3
+ * definitions do not offer.
4
+ *
5
+ * These belong to the model rather than to any one consumer, because
6
+ * addressing is a fact about a step definition and a state — the claim,
7
+ * `explain` and audit replay all ask the same question
8
+ * and get the same two answers back.
9
+ */
10
+ import { AffordanceError } from '../errors.js';
11
+ /** The named step does not exist on the case's case type. */
12
+ export declare class UnknownStepError extends AffordanceError {
13
+ readonly caseTypeName: string;
14
+ readonly stepName: string;
15
+ constructor(caseTypeName: string, stepName: string, known: readonly string[]);
16
+ }
17
+ /**
18
+ * A scope-key problem: an element's derived key is invalid or duplicated
19
+ * (identity corruption — scope keys are half of an affordance's identity,
20
+ * so this fails loudly rather than degrading), or a caller addressed a
21
+ * scoped step without a key / with an unknown key / gave a key for an
22
+ * unscoped step.
23
+ */
24
+ export declare class ScopeKeyError extends AffordanceError {
25
+ readonly stepName: string;
26
+ readonly scopeKey: string | null;
27
+ constructor(stepName: string, scopeKey: string | null, message: string);
28
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Addressing failures: naming a step, or an element of one, that the
3
+ * definitions do not offer.
4
+ *
5
+ * These belong to the model rather than to any one consumer, because
6
+ * addressing is a fact about a step definition and a state — the claim,
7
+ * `explain` and audit replay all ask the same question
8
+ * and get the same two answers back.
9
+ */
10
+ import { AffordanceError } from '../errors.js';
11
+ /** The named step does not exist on the case's case type. */
12
+ export class UnknownStepError extends AffordanceError {
13
+ caseTypeName;
14
+ stepName;
15
+ constructor(caseTypeName, stepName, known) {
16
+ super('bad-request', `case type '${caseTypeName}' has no step '${stepName}' — steps: ${known.length > 0 ? known.join(', ') : '(none)'}`);
17
+ this.name = 'UnknownStepError';
18
+ this.caseTypeName = caseTypeName;
19
+ this.stepName = stepName;
20
+ }
21
+ }
22
+ /**
23
+ * A scope-key problem: an element's derived key is invalid or duplicated
24
+ * (identity corruption — scope keys are half of an affordance's identity,
25
+ * so this fails loudly rather than degrading), or a caller addressed a
26
+ * scoped step without a key / with an unknown key / gave a key for an
27
+ * unscoped step.
28
+ */
29
+ export class ScopeKeyError extends AffordanceError {
30
+ stepName;
31
+ scopeKey;
32
+ constructor(stepName, scopeKey, message) {
33
+ super('bad-request', `step '${stepName}': ${message}`);
34
+ this.name = 'ScopeKeyError';
35
+ this.stepName = stepName;
36
+ this.scopeKey = scopeKey;
37
+ }
38
+ }
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/model/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C,6DAA6D;AAC7D,MAAM,OAAO,gBAAiB,SAAQ,eAAe;IAC1C,YAAY,CAAQ;IACpB,QAAQ,CAAQ;IAEzB,YACE,YAAoB,EACpB,QAAgB,EAChB,KAAwB;QAExB,KAAK,CACH,aAAa,EACb,cAAc,YAAY,kBAAkB,QAAQ,cAClD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QACxC,EAAE,CACH,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAC9B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;QAChC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,eAAe;IACvC,QAAQ,CAAQ;IAChB,QAAQ,CAAe;IAEhC,YAAY,QAAgB,EAAE,QAAuB,EAAE,OAAe;QACpE,KAAK,CAAC,aAAa,EAAE,SAAS,QAAQ,MAAM,OAAO,EAAE,CAAC,CAAA;QACtD,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF","sourcesContent":["/**\n * Addressing failures: naming a step, or an element of one, that the\n * definitions do not offer.\n *\n * These belong to the model rather than to any one consumer, because\n * addressing is a fact about a step definition and a state — the claim,\n * `explain` and audit replay all ask the same question\n * and get the same two answers back.\n */\n\nimport { AffordanceError } from '../errors.js'\n\n/** The named step does not exist on the case's case type. */\nexport class UnknownStepError extends AffordanceError {\n readonly caseTypeName: string\n readonly stepName: string\n\n constructor(\n caseTypeName: string,\n stepName: string,\n known: readonly string[],\n ) {\n super(\n 'bad-request',\n `case type '${caseTypeName}' has no step '${stepName}' — steps: ${\n known.length > 0 ? known.join(', ') : '(none)'\n }`,\n )\n this.name = 'UnknownStepError'\n this.caseTypeName = caseTypeName\n this.stepName = stepName\n }\n}\n\n/**\n * A scope-key problem: an element's derived key is invalid or duplicated\n * (identity corruption — scope keys are half of an affordance's identity,\n * so this fails loudly rather than degrading), or a caller addressed a\n * scoped step without a key / with an unknown key / gave a key for an\n * unscoped step.\n */\nexport class ScopeKeyError extends AffordanceError {\n readonly stepName: string\n readonly scopeKey: string | null\n\n constructor(stepName: string, scopeKey: string | null, message: string) {\n super('bad-request', `step '${stepName}': ${message}`)\n this.name = 'ScopeKeyError'\n this.stepName = stepName\n this.scopeKey = scopeKey\n }\n}\n"]}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Handler types.
3
+ *
4
+ * A **handler** is a step's effect function — the only thing that mutates
5
+ * Case State (CONTEXT.md). It receives the current Case State and an
6
+ * execution context, and returns the **next** Case State document; the
7
+ * execution lifecycle derives the journaled delta from
8
+ * (previous, next).
9
+ *
10
+ * Handlers are short-lived, at-least-once and idempotent: the
11
+ * same handler may run more than once for the same Execution, so every
12
+ * external effect must be deduplicated on `ctx.executionId`. Nothing in the
13
+ * model layer ever invokes a handler — `packages/core/src/execution` does.
14
+ */
15
+ import type { Queryable } from '../store/index.js';
16
+ /**
17
+ * A write to run inside the framework's **commit** transaction, registered
18
+ * from a handler via `ctx.onCommit`. It receives the transaction handle
19
+ * the framework is committing Case State on, so app-table writes land
20
+ * atomically with the case row and the journal entry. This is the
21
+ * shared-transaction seam — the one point where app writes join the
22
+ * framework's transaction — and it avoids holding a transaction open across
23
+ * the handler's external calls.
24
+ *
25
+ * A callback that throws aborts the whole commit: nothing is written, the
26
+ * attempt is journaled as failed, and the retry policy applies.
27
+ */
28
+ export type CommitWrite = (tx: Queryable) => Promise<void>;
29
+ /**
30
+ * What a handler registers when it hands work to an external system
31
+ *: "this envelope id is how the answer will come back". The case
32
+ * is implicit — it is the one the handler is running on — and for a scoped
33
+ * step the scope element is too.
34
+ */
35
+ export interface CorrelationRequest {
36
+ /** The external system, as the app names it: `'esign'`, `'verify'`, `'escrow'`. */
37
+ readonly system: string;
38
+ /** The identifier that system will quote back. */
39
+ readonly externalId: string;
40
+ /** Defaults to the Execution's own scope key on a scoped step; pass `null` for case-level. */
41
+ readonly scopeKey?: string | null;
42
+ /** The step an event on this identifier should execute — usually the materializing step. */
43
+ readonly step?: string | null;
44
+ /** Anything the app wants to keep alongside the mapping. */
45
+ readonly metadata?: unknown;
46
+ }
47
+ /**
48
+ * The context a handler receives alongside Case State.
49
+ *
50
+ * `executionId` is the idempotency key: handlers run at-least-once, so any
51
+ * external effect must be deduplicated on it. `attempt` /
52
+ * `maxAttempts` let a handler tell a first try from a retry.
53
+ */
54
+ export interface HandlerContext<TActor = unknown, TInput = undefined> {
55
+ /** Unique id of this Execution — the handler's idempotency key. */
56
+ readonly executionId: string;
57
+ /** The case this Execution runs on. */
58
+ readonly caseId: string;
59
+ /** The Actor the step is being executed as (app-defined shape). */
60
+ readonly actor: TActor;
61
+ /**
62
+ * The step's input, validated against the step's `input` schema before the
63
+ * handler runs (`undefined` for steps that declare no input schema).
64
+ */
65
+ readonly input: TInput;
66
+ /** 1-based attempt number for this Execution; > 1 means a retry of the same `executionId`. */
67
+ readonly attempt: number;
68
+ /** Total attempts this Execution is allowed, from the step's retry policy. */
69
+ readonly maxAttempts: number;
70
+ /**
71
+ * Register an app-table write to run inside the framework's commit
72
+ * transaction, so it commits atomically with the new Case State.
73
+ * Callbacks run in registration order; registrations from a failed attempt
74
+ * are discarded before the next attempt.
75
+ */
76
+ onCommit(write: CommitWrite): void;
77
+ /**
78
+ * Register an external identifier against this case (and, on a scoped
79
+ * step, this element) so the eventual webhook can be routed back.
80
+ * Correlation is one half of integrating an external system; Ingestion —
81
+ * executing the routed event as an ordinary step — is the other.
82
+ *
83
+ * Written inside the commit transaction, like any `onCommit` write: a case
84
+ * cannot end up having sent an envelope whose answer it could not route,
85
+ * because the sending and the mapping are the same commit.
86
+ */
87
+ correlate(request: CorrelationRequest): void;
88
+ /**
89
+ * Mark the case dormant: a journaled terminal marker written with this
90
+ * Execution's commit. Dormancy means exclusion from
91
+ * default active listings — **never** a freeze: a dormant case still
92
+ * computes affordances, and a step guarded on ended state can still claim
93
+ * and {@link HandlerContext.reopen} it.
94
+ */
95
+ end(): void;
96
+ /** Clear the dormancy marker — un-ending is an ordinary step. */
97
+ reopen(): void;
98
+ }
99
+ /** The context a scoped step's handler receives: the base context plus the scope binding. */
100
+ export interface ScopedHandlerContext<TElement, TActor = unknown, TInput = undefined> extends HandlerContext<TActor, TInput> {
101
+ /** The bound scope element the Execution is about (e.g. one buyer). */
102
+ readonly scope: TElement;
103
+ /** The element's scope key — half of the affordance's identity. */
104
+ readonly scopeKey: string;
105
+ }
106
+ /**
107
+ * An unscoped step's handler: async, receives the current Case State and the
108
+ * execution context, and returns the **next** Case State document.
109
+ */
110
+ export type StepHandler<TState, TActor = unknown, TInput = undefined> = (state: TState, ctx: HandlerContext<TActor, TInput>) => Promise<TState>;
111
+ /** A scoped step's handler: as {@link StepHandler}, with the scope binding on ctx. */
112
+ export type ScopedStepHandler<TState, TElement, TActor = unknown, TInput = undefined> = (state: TState, ctx: ScopedHandlerContext<TElement, TActor, TInput>) => Promise<TState>;
113
+ /**
114
+ * A handler as held on a normalized {@link StepDefinition}: the authoring
115
+ * generics (input, scope element) erased. The execution lifecycle invokes
116
+ * through this type, constructing a context that satisfies the authored shape
117
+ * (the model layer guarantees input was validated and, for scoped steps, a
118
+ * scope is bound).
119
+ */
120
+ export type ErasedStepHandler<TState, TActor = unknown> = (state: TState, ctx: HandlerContext<TActor, unknown> | ScopedHandlerContext<unknown, TActor, unknown>) => Promise<TState>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Handler types.
3
+ *
4
+ * A **handler** is a step's effect function — the only thing that mutates
5
+ * Case State (CONTEXT.md). It receives the current Case State and an
6
+ * execution context, and returns the **next** Case State document; the
7
+ * execution lifecycle derives the journaled delta from
8
+ * (previous, next).
9
+ *
10
+ * Handlers are short-lived, at-least-once and idempotent: the
11
+ * same handler may run more than once for the same Execution, so every
12
+ * external effect must be deduplicated on `ctx.executionId`. Nothing in the
13
+ * model layer ever invokes a handler — `packages/core/src/execution` does.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/model/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG","sourcesContent":["/**\n * Handler types.\n *\n * A **handler** is a step's effect function — the only thing that mutates\n * Case State (CONTEXT.md). It receives the current Case State and an\n * execution context, and returns the **next** Case State document; the\n * execution lifecycle derives the journaled delta from\n * (previous, next).\n *\n * Handlers are short-lived, at-least-once and idempotent: the\n * same handler may run more than once for the same Execution, so every\n * external effect must be deduplicated on `ctx.executionId`. Nothing in the\n * model layer ever invokes a handler — `packages/core/src/execution` does.\n */\n\nimport type { Queryable } from '../store/index.js'\n\n/**\n * A write to run inside the framework's **commit** transaction, registered\n * from a handler via `ctx.onCommit`. It receives the transaction handle\n * the framework is committing Case State on, so app-table writes land\n * atomically with the case row and the journal entry. This is the\n * shared-transaction seam — the one point where app writes join the\n * framework's transaction — and it avoids holding a transaction open across\n * the handler's external calls.\n *\n * A callback that throws aborts the whole commit: nothing is written, the\n * attempt is journaled as failed, and the retry policy applies.\n */\nexport type CommitWrite = (tx: Queryable) => Promise<void>\n\n/**\n * What a handler registers when it hands work to an external system\n *: \"this envelope id is how the answer will come back\". The case\n * is implicit — it is the one the handler is running on — and for a scoped\n * step the scope element is too.\n */\nexport interface CorrelationRequest {\n /** The external system, as the app names it: `'esign'`, `'verify'`, `'escrow'`. */\n readonly system: string\n /** The identifier that system will quote back. */\n readonly externalId: string\n /** Defaults to the Execution's own scope key on a scoped step; pass `null` for case-level. */\n readonly scopeKey?: string | null\n /** The step an event on this identifier should execute — usually the materializing step. */\n readonly step?: string | null\n /** Anything the app wants to keep alongside the mapping. */\n readonly metadata?: unknown\n}\n\n/**\n * The context a handler receives alongside Case State.\n *\n * `executionId` is the idempotency key: handlers run at-least-once, so any\n * external effect must be deduplicated on it. `attempt` /\n * `maxAttempts` let a handler tell a first try from a retry.\n */\nexport interface HandlerContext<TActor = unknown, TInput = undefined> {\n /** Unique id of this Execution — the handler's idempotency key. */\n readonly executionId: string\n /** The case this Execution runs on. */\n readonly caseId: string\n /** The Actor the step is being executed as (app-defined shape). */\n readonly actor: TActor\n /**\n * The step's input, validated against the step's `input` schema before the\n * handler runs (`undefined` for steps that declare no input schema).\n */\n readonly input: TInput\n /** 1-based attempt number for this Execution; > 1 means a retry of the same `executionId`. */\n readonly attempt: number\n /** Total attempts this Execution is allowed, from the step's retry policy. */\n readonly maxAttempts: number\n\n /**\n * Register an app-table write to run inside the framework's commit\n * transaction, so it commits atomically with the new Case State.\n * Callbacks run in registration order; registrations from a failed attempt\n * are discarded before the next attempt.\n */\n onCommit(write: CommitWrite): void\n\n /**\n * Register an external identifier against this case (and, on a scoped\n * step, this element) so the eventual webhook can be routed back.\n * Correlation is one half of integrating an external system; Ingestion —\n * executing the routed event as an ordinary step — is the other.\n *\n * Written inside the commit transaction, like any `onCommit` write: a case\n * cannot end up having sent an envelope whose answer it could not route,\n * because the sending and the mapping are the same commit.\n */\n correlate(request: CorrelationRequest): void\n\n /**\n * Mark the case dormant: a journaled terminal marker written with this\n * Execution's commit. Dormancy means exclusion from\n * default active listings — **never** a freeze: a dormant case still\n * computes affordances, and a step guarded on ended state can still claim\n * and {@link HandlerContext.reopen} it.\n */\n end(): void\n\n /** Clear the dormancy marker — un-ending is an ordinary step. */\n reopen(): void\n}\n\n/** The context a scoped step's handler receives: the base context plus the scope binding. */\nexport interface ScopedHandlerContext<\n TElement,\n TActor = unknown,\n TInput = undefined,\n> extends HandlerContext<TActor, TInput> {\n /** The bound scope element the Execution is about (e.g. one buyer). */\n readonly scope: TElement\n /** The element's scope key — half of the affordance's identity. */\n readonly scopeKey: string\n}\n\n/**\n * An unscoped step's handler: async, receives the current Case State and the\n * execution context, and returns the **next** Case State document.\n */\nexport type StepHandler<TState, TActor = unknown, TInput = undefined> = (\n state: TState,\n ctx: HandlerContext<TActor, TInput>,\n) => Promise<TState>\n\n/** A scoped step's handler: as {@link StepHandler}, with the scope binding on ctx. */\nexport type ScopedStepHandler<\n TState,\n TElement,\n TActor = unknown,\n TInput = undefined,\n> = (\n state: TState,\n ctx: ScopedHandlerContext<TElement, TActor, TInput>,\n) => Promise<TState>\n\n/**\n * A handler as held on a normalized {@link StepDefinition}: the authoring\n * generics (input, scope element) erased. The execution lifecycle invokes\n * through this type, constructing a context that satisfies the authored shape\n * (the model layer guarantees input was validated and, for scoped steps, a\n * scope is bound).\n */\nexport type ErasedStepHandler<TState, TActor = unknown> = (\n state: TState,\n ctx:\n | HandlerContext<TActor, unknown>\n | ScopedHandlerContext<unknown, TActor, unknown>,\n) => Promise<TState>\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The definition API: case types and steps.
3
+ *
4
+ * A **case type** declares a typed state schema and a set of guarded
5
+ * **steps** — nothing else. A **step** is a
6
+ * guard plus a handler; a step may declare a **scope** over a state
7
+ * collection, yielding one affordance per selected element with identity
8
+ * (step × scope key). Handlers are declared here and executed by the
9
+ * execution lifecycle; this module never invokes one.
10
+ *
11
+ * See CONTEXT.md for the vocabulary and the guards module for the condition
12
+ * algebra these definitions are built from.
13
+ */
14
+ export type { AnyCaseType, CaseTypeDefinition, CaseTypeOptions, } from './casetype.js';
15
+ export { caseType } from './casetype.js';
16
+ export { ScopeKeyError, UnknownStepError } from './errors.js';
17
+ export type { CommitWrite, CorrelationRequest, ErasedStepHandler, HandlerContext, ScopedHandlerContext, ScopedStepHandler, StepHandler, } from './handler.js';
18
+ export type { RetryOptions, RetryPolicy } from './retry.js';
19
+ export { DEFAULT_RETRY, normalizeRetry } from './retry.js';
20
+ export type { ScopeDeclaration, ScopedCondition, ScopedConditionContext, ScopedConditionMap, ScopedConditionMapEntry, } from './scope.js';
21
+ export type { ActorMarker, BoundStep, ScopedStepOptions, StepDefinition, StepMetadata, StepOptions, } from './step.js';
22
+ export { actor, StepInputValidationError, step, stepsOf, validateStepInput, } from './step.js';
23
+ export type { ComputationContext, ScopeBinding, StepTarget, TargetAddress, TargetAddressFailure, TargetSelection, } from './target.js';
24
+ export { addressTarget, evaluateTarget, resolveTarget, SCOPE_FAILURE_CONDITION, scopeFailureEvaluation, selectTargets, } from './target.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The definition API: case types and steps.
3
+ *
4
+ * A **case type** declares a typed state schema and a set of guarded
5
+ * **steps** — nothing else. A **step** is a
6
+ * guard plus a handler; a step may declare a **scope** over a state
7
+ * collection, yielding one affordance per selected element with identity
8
+ * (step × scope key). Handlers are declared here and executed by the
9
+ * execution lifecycle; this module never invokes one.
10
+ *
11
+ * See CONTEXT.md for the vocabulary and the guards module for the condition
12
+ * algebra these definitions are built from.
13
+ */
14
+ export { caseType } from './casetype.js';
15
+ export { ScopeKeyError, UnknownStepError } from './errors.js';
16
+ export { DEFAULT_RETRY, normalizeRetry } from './retry.js';
17
+ export { actor, StepInputValidationError, step, stepsOf, validateStepInput, } from './step.js';
18
+ export { addressTarget, evaluateTarget, resolveTarget, SCOPE_FAILURE_CONDITION, scopeFailureEvaluation, selectTargets, } from './target.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/model/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAW7D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgB1D,OAAO,EACL,KAAK,EACL,wBAAwB,EACxB,IAAI,EACJ,OAAO,EACP,iBAAiB,GAClB,MAAM,WAAW,CAAA;AASlB,OAAO,EACL,aAAa,EACb,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,sBAAsB,EACtB,aAAa,GACd,MAAM,aAAa,CAAA","sourcesContent":["/**\n * The definition API: case types and steps.\n *\n * A **case type** declares a typed state schema and a set of guarded\n * **steps** — nothing else. A **step** is a\n * guard plus a handler; a step may declare a **scope** over a state\n * collection, yielding one affordance per selected element with identity\n * (step × scope key). Handlers are declared here and executed by the\n * execution lifecycle; this module never invokes one.\n *\n * See CONTEXT.md for the vocabulary and the guards module for the condition\n * algebra these definitions are built from.\n */\n\nexport type {\n AnyCaseType,\n CaseTypeDefinition,\n CaseTypeOptions,\n} from './casetype.js'\nexport { caseType } from './casetype.js'\nexport { ScopeKeyError, UnknownStepError } from './errors.js'\nexport type {\n CommitWrite,\n CorrelationRequest,\n ErasedStepHandler,\n HandlerContext,\n ScopedHandlerContext,\n ScopedStepHandler,\n StepHandler,\n} from './handler.js'\nexport type { RetryOptions, RetryPolicy } from './retry.js'\nexport { DEFAULT_RETRY, normalizeRetry } from './retry.js'\nexport type {\n ScopeDeclaration,\n ScopedCondition,\n ScopedConditionContext,\n ScopedConditionMap,\n ScopedConditionMapEntry,\n} from './scope.js'\nexport type {\n ActorMarker,\n BoundStep,\n ScopedStepOptions,\n StepDefinition,\n StepMetadata,\n StepOptions,\n} from './step.js'\nexport {\n actor,\n StepInputValidationError,\n step,\n stepsOf,\n validateStepInput,\n} from './step.js'\nexport type {\n ComputationContext,\n ScopeBinding,\n StepTarget,\n TargetAddress,\n TargetAddressFailure,\n TargetSelection,\n} from './target.js'\nexport {\n addressTarget,\n evaluateTarget,\n resolveTarget,\n SCOPE_FAILURE_CONDITION,\n scopeFailureEvaluation,\n selectTargets,\n} from './target.js'\n"]}