@affordance/core 0.1.0 → 0.2.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 (82) hide show
  1. package/README.md +17 -6
  2. package/dist/engine/compute.d.ts +2 -2
  3. package/dist/engine/compute.js.map +1 -1
  4. package/dist/engine/engine.d.ts +14 -22
  5. package/dist/engine/engine.js +35 -17
  6. package/dist/engine/engine.js.map +1 -1
  7. package/dist/errors.d.ts +3 -9
  8. package/dist/errors.js +17 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/execution/delta.d.ts +1 -1
  11. package/dist/execution/delta.js +1 -1
  12. package/dist/execution/delta.js.map +1 -1
  13. package/dist/execution/execute.d.ts +19 -18
  14. package/dist/execution/execute.js +22 -25
  15. package/dist/execution/execute.js.map +1 -1
  16. package/dist/execution/index.d.ts +1 -3
  17. package/dist/execution/index.js +1 -3
  18. package/dist/execution/index.js.map +1 -1
  19. package/dist/execution/journal.d.ts +4 -12
  20. package/dist/execution/journal.js +20 -93
  21. package/dist/execution/journal.js.map +1 -1
  22. package/dist/execution/port.d.ts +15 -44
  23. package/dist/execution/port.js +1 -100
  24. package/dist/execution/port.js.map +1 -1
  25. package/dist/execution/replay.d.ts +1 -1
  26. package/dist/execution/replay.js.map +1 -1
  27. package/dist/index.d.ts +7 -5
  28. package/dist/index.js +4 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/ingestion/correlation.d.ts +0 -32
  31. package/dist/ingestion/correlation.js +1 -77
  32. package/dist/ingestion/correlation.js.map +1 -1
  33. package/dist/ingestion/index.d.ts +1 -2
  34. package/dist/ingestion/index.js +1 -2
  35. package/dist/ingestion/index.js.map +1 -1
  36. package/dist/ingestion/ingest.d.ts +10 -16
  37. package/dist/ingestion/ingest.js +12 -100
  38. package/dist/ingestion/ingest.js.map +1 -1
  39. package/dist/migration/migrate.d.ts +6 -7
  40. package/dist/migration/migrate.js +11 -40
  41. package/dist/migration/migrate.js.map +1 -1
  42. package/dist/model/casetype.d.ts +8 -8
  43. package/dist/model/casetype.js.map +1 -1
  44. package/dist/model/handler.d.ts +21 -15
  45. package/dist/model/handler.js.map +1 -1
  46. package/dist/model/index.d.ts +3 -3
  47. package/dist/model/index.js +1 -1
  48. package/dist/model/index.js.map +1 -1
  49. package/dist/model/step.d.ts +18 -13
  50. package/dist/model/step.js +2 -1
  51. package/dist/model/step.js.map +1 -1
  52. package/dist/model/target.d.ts +12 -15
  53. package/dist/model/target.js +2 -5
  54. package/dist/model/target.js.map +1 -1
  55. package/dist/storage.d.ts +93 -0
  56. package/dist/storage.js +4 -0
  57. package/dist/storage.js.map +1 -0
  58. package/dist/store/ids.d.ts +1 -2
  59. package/dist/store/ids.js +1 -2
  60. package/dist/store/ids.js.map +1 -1
  61. package/dist/store/index.d.ts +3 -8
  62. package/dist/store/index.js +2 -6
  63. package/dist/store/index.js.map +1 -1
  64. package/dist/store/resolve.d.ts +7 -14
  65. package/dist/store/resolve.js +4 -11
  66. package/dist/store/resolve.js.map +1 -1
  67. package/dist/store/store.d.ts +4 -41
  68. package/dist/store/store.js +1 -95
  69. package/dist/store/store.js.map +1 -1
  70. package/package.json +8 -8
  71. package/dist/execution/transaction.d.ts +0 -24
  72. package/dist/execution/transaction.js +0 -49
  73. package/dist/execution/transaction.js.map +0 -1
  74. package/dist/store/bootstrap.d.ts +0 -57
  75. package/dist/store/bootstrap.js +0 -268
  76. package/dist/store/bootstrap.js.map +0 -1
  77. package/dist/store/queryable.d.ts +0 -60
  78. package/dist/store/queryable.js +0 -7
  79. package/dist/store/queryable.js.map +0 -1
  80. package/dist/store/sql.d.ts +0 -26
  81. package/dist/store/sql.js +0 -21
  82. package/dist/store/sql.js.map +0 -1
package/README.md CHANGED
@@ -4,15 +4,16 @@ Compute what a case can do now, for a particular actor. A case is a persisted
4
4
  object with state and independently guarded steps. Steps become available
5
5
  through state changes, without a predefined ordering.
6
6
 
7
- Requires Node 22.12+ and Postgres. ESM JavaScript and TypeScript declarations
7
+ Requires Node 22.12+ and a storage adapter. The example uses Postgres through `@affordance/pg`. ESM JavaScript and TypeScript declarations
8
8
  are included. Zod is one option for the Standard Schema validation interface.
9
9
 
10
10
  ```bash
11
- npm install @affordance/core pg zod
11
+ npm install @affordance/core @affordance/pg pg zod
12
12
  ```
13
13
 
14
14
  ```ts
15
- import { actor, bootstrap, caseType, createEngine, stepsOf } from '@affordance/core'
15
+ import { actor, caseType, createEngine, stepsOf } from '@affordance/core'
16
+ import { bootstrap, createPgStorage } from '@affordance/pg'
16
17
  import { Pool } from 'pg'
17
18
  import { z } from 'zod'
18
19
 
@@ -32,7 +33,7 @@ const approval = caseType({
32
33
  const pool = new Pool({ connectionString: process.env.DATABASE_URL })
33
34
  try {
34
35
  await bootstrap(pool)
35
- const engine = createEngine({ db: { pool }, caseTypes: [approval] })
36
+ const engine = createEngine({ storage: createPgStorage({ db: { pool } }), caseTypes: [approval] })
36
37
  const current = await engine.createCase('approval', { ownerId: 'alice', approved: false })
37
38
  console.log(await engine.affordances(current.id, { id: 'alice' }))
38
39
  await engine.execute(current.id, 'approve', { actor: { id: 'alice' } })
@@ -48,5 +49,15 @@ external effects must tolerate retries.
48
49
 
49
50
  Read the [introduction](https://github.com/mochicodecom/affordance/blob/main/docs/tutorial/README.md)
50
51
  and [architecture](https://github.com/mochicodecom/affordance/blob/main/docs/architecture.md).
51
- The optional [HTTP adapter](https://github.com/mochicodecom/affordance/tree/main/packages/http)
52
- exposes available steps as links. Licensed under [MIT](./LICENSE).
52
+ The private [reference app](https://github.com/mochicodecom/affordance/tree/main/packages/reference-app)
53
+ shows how a host can expose available steps as HTTP links. Licensed under [MIT](./LICENSE).
54
+
55
+ `engine.listCases({ caseTypeName, includeEnded, limit, cursor })` returns
56
+ `{ cases, nextCursor }`. It lists registered types, validates stored state just
57
+ like `engine.case(id)`, and excludes dormant cases by default. The default page
58
+ size is 100 (maximum 1000); continue with the returned cursor and the same filters.
59
+ Validation failures are reported to the caller. Listing does not apply actor
60
+ permissions; the host controls access just as for addressed case reads.
61
+
62
+ See [storage adapters](https://github.com/mochicodecom/affordance/blob/main/docs/storage.md)
63
+ for the public interfaces, transaction guarantees, and custom commit contexts.
@@ -98,7 +98,7 @@ export interface AffordanceExplanation {
98
98
  * `ScopeKeyError` (identity corruption — `selectTargets` never absorbs it).
99
99
  * A scoped step selecting zero elements contributes nothing to either list.
100
100
  */
101
- export declare const computeAffordances: <S extends StandardSchemaV1, TActor>(definition: CaseTypeDefinition<S, TActor>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, ctx: ComputationContext<TActor>) => CaseAffordances;
101
+ export declare const computeAffordances: <S extends StandardSchemaV1, TActor, TCommit>(definition: CaseTypeDefinition<S, TActor, TCommit>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, ctx: ComputationContext<TActor>) => CaseAffordances;
102
102
  /** An `explain` request as a caller states it: everything optional. */
103
103
  export interface ExplainRequest {
104
104
  /** Required when the step is scoped; identifies the element. */
@@ -142,6 +142,6 @@ export declare const explainContext: <TActor = unknown>(request: ExplainRequest,
142
142
  * `actor` is whatever the caller supplies; to ask "why can't *this* actor",
143
143
  * pass that actor — `permits` conditions are evaluated against it verbatim.
144
144
  */
145
- export declare const computeExplanation: <S extends StandardSchemaV1, TActor>(definition: CaseTypeDefinition<S, TActor>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, stepName: string, ctx: ComputationContext<TActor> & {
145
+ export declare const computeExplanation: <S extends StandardSchemaV1, TActor, TCommit>(definition: CaseTypeDefinition<S, TActor, TCommit>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, stepName: string, ctx: ComputationContext<TActor> & {
146
146
  readonly scopeKey?: string;
147
147
  }) => AffordanceExplanation;
@@ -1 +1 @@
1
- {"version":3,"file":"compute.js","sourceRoot":"","sources":["../../src/engine/compute.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Affordance computation — the pure core of the engine.\n *\n * `computeAffordances` turns (case type, case snapshot, actor, asOf) into\n * the framework's HATEOAS answer: every step's guard evaluated (with scope\n * fan-out), split into available **affordances** and blocked steps with\n * their unmet named conditions, \"not possible\" mechanically distinct from\n * \"not permitted for this actor\". `computeExplanation` is the same\n * machinery pointed at one step, returning the full per-condition record.\n *\n * Both are pure: no I/O, no clock — `asOf` is explicit (the\n * *engine* defaults it to now at its boundary, never a condition). The\n * records are plain JSON-serializable objects, deterministic in their\n * inputs — the affordance JSON contract will serialize them\n * verbatim.\n *\n * Addressing a step and binding its scope element is `../model/target.js`'s\n * job rather than this module's: it is a fact about a step definition and a\n * state, and every other consumer of it — the claim,\n * audit replay — would otherwise have to import the engine to reach it.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type {\n ConditionResult,\n GuardEvaluation,\n Instant,\n} from '../guards/index.js'\nimport { NO_ACTOR, toIso, unmetConditions } from '../guards/index.js'\nimport type { CaseTypeDefinition, ComputationContext } from '../model/index.js'\nimport {\n addressTarget,\n evaluateTarget,\n scopeFailureEvaluation,\n selectTargets,\n} from '../model/index.js'\n\n/**\n * The slice of a case the pure computation needs: identity, state, dormancy.\n * The engine builds it from a store handle; tests may construct one directly.\n */\nexport interface CaseSnapshot<TState> {\n readonly id: string\n readonly state: TState\n /** Dormancy marker (`end()`): a dormant case still computes — dormancy is never a freeze. */\n readonly endedAt: Date | string | null\n}\n\n/**\n * One available affordance: a step (with its scope binding, if scoped) this\n * actor can take now. Deliberately silent about the step's input — how an\n * input is described to a caller is the adapter's translation\n * (`Engine.inputSchemaFor` is the registry read it serializes from), and a\n * second channel here could only drift from it.\n */\nexport interface Affordance {\n readonly step: string\n /** The bound element's scope key — present iff the step is scoped. */\n readonly scopeKey?: string\n}\n\n/**\n * A step (× scope element, if scoped) that is currently not available, with\n * the unmet named conditions saying why. `possible === false` means unmet\n * `requires` — not possible on this case, for anyone; `permitted === false`\n * means unmet `permits` — not permitted for this actor.\n */\nexport interface BlockedStep {\n readonly step: string\n readonly scopeKey?: string\n readonly possible: boolean\n readonly permitted: boolean\n /**\n * The failed condition results, verbatim from guard evaluation — named,\n * sectioned, with reasons and (for `after` conditions) basis/flip instants.\n */\n readonly unmet: readonly ConditionResult[]\n}\n\n/** The serializable affordances record for one case, one actor, one instant. */\nexport interface CaseAffordances {\n readonly caseId: string\n readonly caseTypeName: string\n /** The instant evaluated as of (ISO-8601 UTC). */\n readonly asOf: string\n /** Dormancy marker (ISO-8601 UTC), `null` while the case is active. A dormant case still computes. */\n readonly endedAt: string | null\n /** Available affordances, in step declaration order (scoped: selection order within a step). */\n readonly affordances: readonly Affordance[]\n /** Blocked steps (× scope element) with their unmet named conditions. */\n readonly blocked: readonly BlockedStep[]\n}\n\n/** The full per-condition breakdown for one step (× scope element, if scoped). */\nexport interface AffordanceExplanation {\n readonly caseId: string\n readonly caseTypeName: string\n readonly step: string\n /** The scope binding the explanation is about — present iff the step is scoped. */\n readonly scopeKey?: string\n readonly asOf: string\n readonly endedAt: string | null\n /** The guards module's full evaluation record: every condition, passed and failed. */\n readonly evaluation: GuardEvaluation\n}\n\nconst toIsoOrNull = (value: Instant | null): string | null =>\n value === null ? null : toIso(value)\n\n// The verdict lives with the model (`scopeFailureEvaluation`); this only\n// reshapes it into a blocked entry.\nconst scopeFailureEntry = (\n stepName: string,\n asOf: string,\n reason: string,\n): BlockedStep => {\n const evaluation = scopeFailureEvaluation(asOf, { reason })\n return {\n step: stepName,\n possible: evaluation.possible,\n permitted: evaluation.permitted,\n unmet: unmetConditions(evaluation),\n }\n}\n\nconst toAffordance = (stepName: string, scopeKey?: string): Affordance => ({\n step: stepName,\n ...(scopeKey !== undefined && { scopeKey }),\n})\n\nconst toBlocked = (\n stepName: string,\n evaluation: GuardEvaluation,\n scopeKey?: string,\n): BlockedStep => ({\n step: stepName,\n ...(scopeKey !== undefined && { scopeKey }),\n possible: evaluation.possible,\n permitted: evaluation.permitted,\n unmet: unmetConditions(evaluation),\n})\n\n/**\n * Compute the affordances record for one case snapshot: every step's guard\n * evaluated against (state, actor, asOf); scoped steps fan out to one\n * independent evaluation per selected element. Handlers are never\n * touched — this is a read (guards advise; enforcement is the claim's job).\n *\n * A scoped step whose selector is defective is absorbed into a blocked entry\n * under the synthetic `$scope` condition (one defective selector must not\n * take down the case's listing); scope-*key* integrity violations throw\n * `ScopeKeyError` (identity corruption — `selectTargets` never absorbs it).\n * A scoped step selecting zero elements contributes nothing to either list.\n */\nexport const computeAffordances = <S extends StandardSchemaV1, TActor>(\n definition: CaseTypeDefinition<S, TActor>,\n snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>,\n ctx: ComputationContext<TActor>,\n): CaseAffordances => {\n const asOf = toIso(ctx.asOf)\n const affordances: Affordance[] = []\n const blocked: BlockedStep[] = []\n\n for (const stepDefinition of definition.steps) {\n const { targets, failure } = selectTargets(stepDefinition, snapshot.state)\n if (failure !== null) {\n blocked.push(scopeFailureEntry(stepDefinition.name, asOf, failure.reason))\n continue\n }\n for (const target of targets) {\n const evaluation = evaluateTarget(target, { actor: ctx.actor, asOf })\n const scopeKey = target.binding?.key\n if (evaluation.available)\n affordances.push(toAffordance(stepDefinition.name, scopeKey))\n else blocked.push(toBlocked(stepDefinition.name, evaluation, scopeKey))\n }\n }\n\n return {\n caseId: snapshot.id,\n caseTypeName: definition.name,\n asOf,\n endedAt: toIsoOrNull(snapshot.endedAt),\n affordances,\n blocked,\n }\n}\n\n/** An `explain` request as a caller states it: everything optional. */\nexport interface ExplainRequest {\n /** Required when the step is scoped; identifies the element. */\n readonly scopeKey?: string\n /** The actor to evaluate `permits` against; omit to probe `requires` alone. */\n readonly actor?: unknown\n /** The instant to evaluate as of; defaults through the supplied clock. */\n readonly asOf?: Instant\n}\n\n/**\n * Normalize an {@link ExplainRequest} into the {@link ComputationContext}\n * the pure computation runs on — the engine boundary's one normalization,\n * stated here so the rule and {@link computeExplanation} share a test\n * surface:\n *\n * - An **absent** `actor` key means the requires-only probe ({@link NO_ACTOR}\n * — `permits` conditions are reported un-evaluated). A key that is\n * *present but `undefined`* is an actor like any other: the caller said\n * who is asking, and the answer is about them.\n * - `asOf` defaults through `now` — the clock stops here; everything below\n * is pure.\n * - `scopeKey` is carried only when given, so \"unscoped\" stays an absent\n * key rather than an `undefined` value.\n */\nexport const explainContext = <TActor = unknown>(\n request: ExplainRequest,\n now: () => Instant,\n): ComputationContext<TActor> & { readonly scopeKey?: string } => ({\n // The request's actor is caller-supplied and untyped, and NO_ACTOR is the\n // evaluator's own marker; the assertion papers over neither — `permits`\n // conditions must be total over whatever an actor turns out to be.\n actor: ('actor' in request ? request.actor : NO_ACTOR) as TActor,\n asOf: request.asOf ?? now(),\n ...(request.scopeKey !== undefined && { scopeKey: request.scopeKey }),\n})\n\n/**\n * The full per-condition breakdown for one step. Loud where the listing is\n * lenient — `explain` is a targeted probe, so a step name the case type\n * doesn't declare, a missing/unknown scope key on a scoped step, or a scope\n * key on an unscoped step all throw with precise messages (including the\n * currently-valid scope keys, where knowable).\n *\n * One deliberate exception: a scoped step whose selector is **defective**\n * answers rather than throws. The listing publishes exactly that condition\n * as a blocked `$scope` entry with an `explain` link, so the link must be\n * followable — the explanation *is* the `$scope` failure, in the same shape\n * the listing reported it.\n *\n * `actor` is whatever the caller supplies; to ask \"why can't *this* actor\",\n * pass that actor — `permits` conditions are evaluated against it verbatim.\n */\nexport const computeExplanation = <S extends StandardSchemaV1, TActor>(\n definition: CaseTypeDefinition<S, TActor>,\n snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>,\n stepName: string,\n ctx: ComputationContext<TActor> & { readonly scopeKey?: string },\n): AffordanceExplanation => {\n const asOf = toIso(ctx.asOf)\n const identity = {\n caseId: snapshot.id,\n caseTypeName: definition.name,\n step: stepName,\n asOf,\n endedAt: toIsoOrNull(snapshot.endedAt),\n }\n\n const address = addressTarget(\n definition,\n snapshot.state,\n stepName,\n ctx.scopeKey,\n )\n if (address.failure !== null) {\n if (address.failure.kind === 'defective-selector') {\n return {\n ...identity,\n evaluation: scopeFailureEvaluation(asOf, address.failure),\n }\n }\n throw address.failure.error\n }\n const target = address.target\n return {\n ...identity,\n ...(target.binding !== null && { scopeKey: target.binding.key }),\n evaluation: evaluateTarget(target, { actor: ctx.actor, asOf }),\n }\n}\n"]}
1
+ {"version":3,"file":"compute.js","sourceRoot":"","sources":["../../src/engine/compute.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Affordance computation — the pure core of the engine.\n *\n * `computeAffordances` turns (case type, case snapshot, actor, asOf) into\n * the framework's HATEOAS answer: every step's guard evaluated (with scope\n * fan-out), split into available **affordances** and blocked steps with\n * their unmet named conditions, \"not possible\" mechanically distinct from\n * \"not permitted for this actor\". `computeExplanation` is the same\n * machinery pointed at one step, returning the full per-condition record.\n *\n * Both are pure: no I/O, no clock — `asOf` is explicit (the\n * *engine* defaults it to now at its boundary, never a condition). The\n * records are plain JSON-serializable objects, deterministic in their\n * inputs — the affordance JSON contract will serialize them\n * verbatim.\n *\n * Addressing a step and binding its scope element is `../model/target.js`'s\n * job rather than this module's: it is a fact about a step definition and a\n * state, and every other consumer of it — the claim,\n * audit replay — would otherwise have to import the engine to reach it.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type {\n ConditionResult,\n GuardEvaluation,\n Instant,\n} from '../guards/index.js'\nimport { NO_ACTOR, toIso, unmetConditions } from '../guards/index.js'\nimport type { CaseTypeDefinition, ComputationContext } from '../model/index.js'\nimport {\n addressTarget,\n evaluateTarget,\n scopeFailureEvaluation,\n selectTargets,\n} from '../model/index.js'\n\n/**\n * The slice of a case the pure computation needs: identity, state, dormancy.\n * The engine builds it from a store handle; tests may construct one directly.\n */\nexport interface CaseSnapshot<TState> {\n readonly id: string\n readonly state: TState\n /** Dormancy marker (`end()`): a dormant case still computes — dormancy is never a freeze. */\n readonly endedAt: Date | string | null\n}\n\n/**\n * One available affordance: a step (with its scope binding, if scoped) this\n * actor can take now. Deliberately silent about the step's input — how an\n * input is described to a caller is the adapter's translation\n * (`Engine.inputSchemaFor` is the registry read it serializes from), and a\n * second channel here could only drift from it.\n */\nexport interface Affordance {\n readonly step: string\n /** The bound element's scope key — present iff the step is scoped. */\n readonly scopeKey?: string\n}\n\n/**\n * A step (× scope element, if scoped) that is currently not available, with\n * the unmet named conditions saying why. `possible === false` means unmet\n * `requires` — not possible on this case, for anyone; `permitted === false`\n * means unmet `permits` — not permitted for this actor.\n */\nexport interface BlockedStep {\n readonly step: string\n readonly scopeKey?: string\n readonly possible: boolean\n readonly permitted: boolean\n /**\n * The failed condition results, verbatim from guard evaluation — named,\n * sectioned, with reasons and (for `after` conditions) basis/flip instants.\n */\n readonly unmet: readonly ConditionResult[]\n}\n\n/** The serializable affordances record for one case, one actor, one instant. */\nexport interface CaseAffordances {\n readonly caseId: string\n readonly caseTypeName: string\n /** The instant evaluated as of (ISO-8601 UTC). */\n readonly asOf: string\n /** Dormancy marker (ISO-8601 UTC), `null` while the case is active. A dormant case still computes. */\n readonly endedAt: string | null\n /** Available affordances, in step declaration order (scoped: selection order within a step). */\n readonly affordances: readonly Affordance[]\n /** Blocked steps (× scope element) with their unmet named conditions. */\n readonly blocked: readonly BlockedStep[]\n}\n\n/** The full per-condition breakdown for one step (× scope element, if scoped). */\nexport interface AffordanceExplanation {\n readonly caseId: string\n readonly caseTypeName: string\n readonly step: string\n /** The scope binding the explanation is about — present iff the step is scoped. */\n readonly scopeKey?: string\n readonly asOf: string\n readonly endedAt: string | null\n /** The guards module's full evaluation record: every condition, passed and failed. */\n readonly evaluation: GuardEvaluation\n}\n\nconst toIsoOrNull = (value: Instant | null): string | null =>\n value === null ? null : toIso(value)\n\n// The verdict lives with the model (`scopeFailureEvaluation`); this only\n// reshapes it into a blocked entry.\nconst scopeFailureEntry = (\n stepName: string,\n asOf: string,\n reason: string,\n): BlockedStep => {\n const evaluation = scopeFailureEvaluation(asOf, { reason })\n return {\n step: stepName,\n possible: evaluation.possible,\n permitted: evaluation.permitted,\n unmet: unmetConditions(evaluation),\n }\n}\n\nconst toAffordance = (stepName: string, scopeKey?: string): Affordance => ({\n step: stepName,\n ...(scopeKey !== undefined && { scopeKey }),\n})\n\nconst toBlocked = (\n stepName: string,\n evaluation: GuardEvaluation,\n scopeKey?: string,\n): BlockedStep => ({\n step: stepName,\n ...(scopeKey !== undefined && { scopeKey }),\n possible: evaluation.possible,\n permitted: evaluation.permitted,\n unmet: unmetConditions(evaluation),\n})\n\n/**\n * Compute the affordances record for one case snapshot: every step's guard\n * evaluated against (state, actor, asOf); scoped steps fan out to one\n * independent evaluation per selected element. Handlers are never\n * touched — this is a read (guards advise; enforcement is the claim's job).\n *\n * A scoped step whose selector is defective is absorbed into a blocked entry\n * under the synthetic `$scope` condition (one defective selector must not\n * take down the case's listing); scope-*key* integrity violations throw\n * `ScopeKeyError` (identity corruption — `selectTargets` never absorbs it).\n * A scoped step selecting zero elements contributes nothing to either list.\n */\nexport const computeAffordances = <S extends StandardSchemaV1, TActor, TCommit>(\n definition: CaseTypeDefinition<S, TActor, TCommit>,\n snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>,\n ctx: ComputationContext<TActor>,\n): CaseAffordances => {\n const asOf = toIso(ctx.asOf)\n const affordances: Affordance[] = []\n const blocked: BlockedStep[] = []\n\n for (const stepDefinition of definition.steps) {\n const { targets, failure } = selectTargets(stepDefinition, snapshot.state)\n if (failure !== null) {\n blocked.push(scopeFailureEntry(stepDefinition.name, asOf, failure.reason))\n continue\n }\n for (const target of targets) {\n const evaluation = evaluateTarget(target, { actor: ctx.actor, asOf })\n const scopeKey = target.binding?.key\n if (evaluation.available)\n affordances.push(toAffordance(stepDefinition.name, scopeKey))\n else blocked.push(toBlocked(stepDefinition.name, evaluation, scopeKey))\n }\n }\n\n return {\n caseId: snapshot.id,\n caseTypeName: definition.name,\n asOf,\n endedAt: toIsoOrNull(snapshot.endedAt),\n affordances,\n blocked,\n }\n}\n\n/** An `explain` request as a caller states it: everything optional. */\nexport interface ExplainRequest {\n /** Required when the step is scoped; identifies the element. */\n readonly scopeKey?: string\n /** The actor to evaluate `permits` against; omit to probe `requires` alone. */\n readonly actor?: unknown\n /** The instant to evaluate as of; defaults through the supplied clock. */\n readonly asOf?: Instant\n}\n\n/**\n * Normalize an {@link ExplainRequest} into the {@link ComputationContext}\n * the pure computation runs on — the engine boundary's one normalization,\n * stated here so the rule and {@link computeExplanation} share a test\n * surface:\n *\n * - An **absent** `actor` key means the requires-only probe ({@link NO_ACTOR}\n * — `permits` conditions are reported un-evaluated). A key that is\n * *present but `undefined`* is an actor like any other: the caller said\n * who is asking, and the answer is about them.\n * - `asOf` defaults through `now` — the clock stops here; everything below\n * is pure.\n * - `scopeKey` is carried only when given, so \"unscoped\" stays an absent\n * key rather than an `undefined` value.\n */\nexport const explainContext = <TActor = unknown>(\n request: ExplainRequest,\n now: () => Instant,\n): ComputationContext<TActor> & { readonly scopeKey?: string } => ({\n // The request's actor is caller-supplied and untyped, and NO_ACTOR is the\n // evaluator's own marker; the assertion papers over neither — `permits`\n // conditions must be total over whatever an actor turns out to be.\n actor: ('actor' in request ? request.actor : NO_ACTOR) as TActor,\n asOf: request.asOf ?? now(),\n ...(request.scopeKey !== undefined && { scopeKey: request.scopeKey }),\n})\n\n/**\n * The full per-condition breakdown for one step. Loud where the listing is\n * lenient — `explain` is a targeted probe, so a step name the case type\n * doesn't declare, a missing/unknown scope key on a scoped step, or a scope\n * key on an unscoped step all throw with precise messages (including the\n * currently-valid scope keys, where knowable).\n *\n * One deliberate exception: a scoped step whose selector is **defective**\n * answers rather than throws. The listing publishes exactly that condition\n * as a blocked `$scope` entry with an `explain` link, so the link must be\n * followable — the explanation *is* the `$scope` failure, in the same shape\n * the listing reported it.\n *\n * `actor` is whatever the caller supplies; to ask \"why can't *this* actor\",\n * pass that actor — `permits` conditions are evaluated against it verbatim.\n */\nexport const computeExplanation = <S extends StandardSchemaV1, TActor, TCommit>(\n definition: CaseTypeDefinition<S, TActor, TCommit>,\n snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>,\n stepName: string,\n ctx: ComputationContext<TActor> & { readonly scopeKey?: string },\n): AffordanceExplanation => {\n const asOf = toIso(ctx.asOf)\n const identity = {\n caseId: snapshot.id,\n caseTypeName: definition.name,\n step: stepName,\n asOf,\n endedAt: toIsoOrNull(snapshot.endedAt),\n }\n\n const address = addressTarget(\n definition,\n snapshot.state,\n stepName,\n ctx.scopeKey,\n )\n if (address.failure !== null) {\n if (address.failure.kind === 'defective-selector') {\n return {\n ...identity,\n evaluation: scopeFailureEvaluation(asOf, address.failure),\n }\n }\n throw address.failure.error\n }\n const target = address.target\n return {\n ...identity,\n ...(target.binding !== null && { scopeKey: target.binding.key }),\n evaluation: evaluateTarget(target, { actor: ctx.actor, asOf }),\n }\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The engine: the case-type registry bound to the case store.
3
3
  *
4
- * `createEngine({ db, caseTypes })` wires the halves together — persistence
4
+ * `createEngine({ storage, caseTypes })` wires the halves together — persistence
5
5
  * (`../store`), guard evaluation (`../guards`), the definition API
6
6
  * (`../model`) and the execution lifecycle (`../execution`) — into the
7
7
  * framework's public face: `affordances`, `explain`, `execute`, `journal`.
@@ -17,21 +17,15 @@ import type { Instant } from '../guards/index.js';
17
17
  import type { Correlation, CorrelationRegistration, DeadLetter, DeadLetterFilter, ExternalEvent, IngestionOptions, IngestionResult } from '../ingestion/index.js';
18
18
  import type { MigrationOptions, MigrationReport, MigrationTransform } from '../migration/index.js';
19
19
  import type { AnyCaseType, StepMetadata } from '../model/index.js';
20
- import type { CaseHandle, DatabaseAccess } from '../store/index.js';
20
+ import type { CaseListOptions, CasePage, EngineStorage } from '../storage.js';
21
+ import type { CaseHandle } from '../store/index.js';
21
22
  import type { AffordanceExplanation, CaseAffordances, ExplainRequest } from './compute.js';
22
23
  /** Options for {@link createEngine}. */
23
- export interface EngineOptions {
24
- /**
25
- * The app brings its database, declaring which kind of handle it has:
26
- * `{ pool }` for anything that checks out connections (`pg.Pool`, or any
27
- * wrapper declaring `connect()`), `{ client }` for a single connection
28
- * dedicated to the engine. The declaration is what lets `execute` run its
29
- * transactions on one connection without guessing — see
30
- * {@link DatabaseAccess}.
31
- */
32
- readonly db: DatabaseAccess;
24
+ export interface EngineOptions<TCommit = unknown> {
25
+ /** One coordinated storage implementation for all engine persistence. */
26
+ readonly storage: EngineStorage<TCommit>;
33
27
  /** Every case type this engine serves; a loaded case's `case_type` must name one of them. */
34
- readonly caseTypes: readonly AnyCaseType[];
28
+ readonly caseTypes: readonly AnyCaseType<NoInfer<TCommit>>[];
35
29
  /**
36
30
  * How long an Execution's claim survives without a heartbeat (default 30s).
37
31
  * The floor on how long a crashed handler can hold a case.
@@ -45,13 +39,9 @@ export interface EngineOptions {
45
39
  */
46
40
  readonly ingestion?: IngestionOptions;
47
41
  /**
48
- * The clock every process-side "as of now" below the boundary reads
49
- * through it: guard evaluation instants, journal timestamps, ingestion
50
- * receipt times. Defaults to the wall clock. Two "nows" it deliberately
51
- * does not govern: lease expiry is judged by the storage adapter's own
52
- * clock (the one clock all competing processes share), and the retry
53
- * delay / heartbeat cadence run on process timers (an internal seam the
54
- * lifecycle's own tests drive virtually).
42
+ * Clock for guard evaluation instants, defaulting to the wall clock.
43
+ * Stored timestamps and claim expiry belong to the adapter's clock;
44
+ * heartbeat cadence and retry delays use process timers.
55
45
  */
56
46
  readonly now?: () => Date;
57
47
  }
@@ -64,6 +54,8 @@ export interface EngineOptions {
64
54
  */
65
55
  export type ExplainOptions = ExplainRequest;
66
56
  export interface Engine {
57
+ /** List validated cases of registered types, newest first. Throws on unreadable state. */
58
+ listCases(options?: CaseListOptions): Promise<CasePage>;
67
59
  /**
68
60
  * Create a case of a registered case type. The initial state is validated
69
61
  * against the type's schema.
@@ -154,9 +146,9 @@ export interface Engine {
154
146
  migrate(caseTypeName: string, name: string, transform: MigrationTransform, options?: MigrationOptions): Promise<MigrationReport>;
155
147
  }
156
148
  /**
157
- * Build an engine from the app's database and its case type definitions.
149
+ * Build an engine from the app's storage and its case type definitions.
158
150
  * Throws at construction on duplicate case type names — the registry is
159
151
  * keyed by name, which is all a case row records (definitions
160
152
  * float; only the name is persisted).
161
153
  */
162
- export declare const createEngine: (options: EngineOptions) => Engine;
154
+ export declare const createEngine: <TCommit>(options: EngineOptions<TCommit>) => Engine;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The engine: the case-type registry bound to the case store.
3
3
  *
4
- * `createEngine({ db, caseTypes })` wires the halves together — persistence
4
+ * `createEngine({ storage, caseTypes })` wires the halves together — persistence
5
5
  * (`../store`), guard evaluation (`../guards`), the definition API
6
6
  * (`../model`) and the execution lifecycle (`../execution`) — into the
7
7
  * framework's public face: `affordances`, `explain`, `execute`, `journal`.
@@ -11,14 +11,14 @@
11
11
  * threaded through the environment to everything below, and everything below
12
12
  * it is pure and reconstructable.
13
13
  */
14
- import { DEFAULT_CLAIM_TTL_MS, DEFAULT_HEARTBEAT_MS, executeStep, readJournal, realTimers, withTransaction, } from '../execution/index.js';
15
- import { correlationsFor, ingest, lookupCorrelation, normalizeIngestion, readDeadLetters, registerCorrelation, } from '../ingestion/index.js';
14
+ import { DEFAULT_CLAIM_TTL_MS, DEFAULT_HEARTBEAT_MS, executeStep, realTimers, } from '../execution/index.js';
15
+ import { ingest, normalizeIngestion } from '../ingestion/index.js';
16
16
  import { migrate } from '../migration/index.js';
17
- import { insertCase, queryableOf, resolveCase } from '../store/index.js';
17
+ import { resolveCase, validateAgainstSchema } from '../store/index.js';
18
18
  import { computeAffordances, computeExplanation, explainContext, } from './compute.js';
19
19
  import { UnknownCaseTypeError } from './errors.js';
20
20
  /**
21
- * Build an engine from the app's database and its case type definitions.
21
+ * Build an engine from the app's storage and its case type definitions.
22
22
  * Throws at construction on duplicate case type names — the registry is
23
23
  * keyed by name, which is all a case row records (definitions
24
24
  * float; only the name is persisted).
@@ -39,13 +39,11 @@ export const createEngine = (options) => {
39
39
  return definition;
40
40
  };
41
41
  const now = options.now ?? (() => new Date());
42
- // Single self-contained statements run against either arm alike; only
43
- // transactions (and the lifecycle behind them) need the declaration itself.
44
- const db = queryableOf(options.db);
42
+ const storage = options.storage;
45
43
  // The widest environment any subsystem asks for (IngestionEnvironment ⊇
46
44
  // ExecutionEnvironment), built once and handed to all of them.
47
45
  const environment = {
48
- db: options.db,
46
+ storage,
49
47
  caseTypeFor,
50
48
  claimTtlMs: options.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS,
51
49
  heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
@@ -55,16 +53,36 @@ export const createEngine = (options) => {
55
53
  };
56
54
  /** Load the case, resolve its type from the registry, validate state against the type's schema. */
57
55
  const load = async (caseId) => {
58
- const { definition, handle, state } = await resolveCase(db, caseTypeFor, caseId);
56
+ const { definition, handle, state } = await resolveCase(await storage.cases.get(caseId), caseTypeFor);
59
57
  return {
60
58
  definition,
61
59
  snapshot: { id: handle.id, state, endedAt: handle.endedAt },
62
60
  };
63
61
  };
64
62
  return {
63
+ listCases: async (options = {}) => {
64
+ const limit = options.limit ?? 100;
65
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
66
+ throw new TypeError('listCases: limit must be an integer between 1 and 1000');
67
+ }
68
+ const caseTypeNames = options.caseTypeName === undefined
69
+ ? [...registry.keys()]
70
+ : [caseTypeFor(options.caseTypeName).name];
71
+ const page = await storage.cases.list({
72
+ ...options,
73
+ caseTypeNames,
74
+ limit,
75
+ });
76
+ const cases = await Promise.all(page.cases.map(async (stored) => {
77
+ const { handle, state } = await resolveCase(stored, caseTypeFor);
78
+ return { ...handle, state };
79
+ }));
80
+ return { cases, nextCursor: page.nextCursor };
81
+ },
65
82
  createCase: async (caseTypeName, initialState) => {
66
83
  const definition = caseTypeFor(caseTypeName);
67
- return withTransaction(options.db, (tx) => insertCase(tx, caseTypeName, definition.state, initialState));
84
+ const state = await validateAgainstSchema(definition.state, initialState, 'initial state');
85
+ return storage.cases.create(caseTypeName, state);
68
86
  },
69
87
  affordances: async (caseId, actor, asOf) => {
70
88
  const { definition, snapshot } = await load(caseId);
@@ -92,12 +110,12 @@ export const createEngine = (options) => {
92
110
  heartbeatMs,
93
111
  retry,
94
112
  }),
95
- journal: (caseId, filter) => readJournal(db, caseId, filter),
113
+ journal: (caseId, filter) => storage.journal.read(caseId, filter),
96
114
  case: async (caseId) => {
97
- const { handle, state } = await resolveCase(db, caseTypeFor, caseId);
115
+ const { handle, state } = await resolveCase(await storage.cases.get(caseId), caseTypeFor);
98
116
  return { ...handle, state };
99
117
  },
100
- correlationOf: (system, externalId) => lookupCorrelation(db, system, externalId),
118
+ correlationOf: (system, externalId) => storage.correlations.lookup(system, externalId),
101
119
  inputSchemaFor: (caseTypeName, stepName) => caseTypeFor(caseTypeName).getStep(stepName)?.input ?? null,
102
120
  stepMetadataFor: (caseTypeName, stepName) => {
103
121
  const step = caseTypeFor(caseTypeName).getStep(stepName);
@@ -106,9 +124,9 @@ export const createEngine = (options) => {
106
124
  return { title: step.title, description: step.description };
107
125
  },
108
126
  ingest: (event) => ingest(environment, event),
109
- correlate: (registration) => registerCorrelation(db, registration),
110
- correlations: (caseId, scopeKey) => correlationsFor(db, caseId, scopeKey),
111
- deadLetters: (filter) => readDeadLetters(db, filter),
127
+ correlate: (registration) => storage.correlations.register(registration),
128
+ correlations: (caseId, scopeKey) => storage.correlations.list(caseId, scopeKey),
129
+ deadLetters: (filter) => storage.deliveries.deadLetters(filter),
112
130
  migrate: (caseTypeName, name, transform, migrationOptions) => migrate(environment, caseTypeName, name, transform, migrationOptions),
113
131
  };
114
132
  };
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The engine: the case-type registry bound to the case store.\n *\n * `createEngine({ db, caseTypes })` wires the halves together — persistence\n * (`../store`), guard evaluation (`../guards`), the definition API\n * (`../model`) and the execution lifecycle (`../execution`) — into the\n * framework's public face: `affordances`, `explain`, `execute`, `journal`.\n *\n * The engine is where `asOf` defaults to now: conditions never read the\n * clock, so `EngineOptions.now` — wall clock by default — is the one clock,\n * threaded through the environment to everything below, and everything below\n * it is pure and reconstructable.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type {\n ExecuteOptions,\n ExecutionResult,\n JournalEntry,\n JournalFilter,\n} from '../execution/index.js'\nimport {\n DEFAULT_CLAIM_TTL_MS,\n DEFAULT_HEARTBEAT_MS,\n executeStep,\n readJournal,\n realTimers,\n withTransaction,\n} from '../execution/index.js'\nimport type { Instant } from '../guards/index.js'\nimport type {\n Correlation,\n CorrelationRegistration,\n DeadLetter,\n DeadLetterFilter,\n ExternalEvent,\n IngestionEnvironment,\n IngestionOptions,\n IngestionResult,\n} from '../ingestion/index.js'\nimport {\n correlationsFor,\n ingest,\n lookupCorrelation,\n normalizeIngestion,\n readDeadLetters,\n registerCorrelation,\n} from '../ingestion/index.js'\nimport type {\n MigrationOptions,\n MigrationReport,\n MigrationTransform,\n} from '../migration/index.js'\nimport { migrate } from '../migration/index.js'\nimport type { AnyCaseType, StepMetadata } from '../model/index.js'\nimport type { CaseHandle, DatabaseAccess } from '../store/index.js'\nimport { insertCase, queryableOf, resolveCase } from '../store/index.js'\nimport type {\n AffordanceExplanation,\n CaseAffordances,\n CaseSnapshot,\n ExplainRequest,\n} from './compute.js'\nimport {\n computeAffordances,\n computeExplanation,\n explainContext,\n} from './compute.js'\nimport { UnknownCaseTypeError } from './errors.js'\n\n/** Options for {@link createEngine}. */\nexport interface EngineOptions {\n /**\n * The app brings its database, declaring which kind of handle it has:\n * `{ pool }` for anything that checks out connections (`pg.Pool`, or any\n * wrapper declaring `connect()`), `{ client }` for a single connection\n * dedicated to the engine. The declaration is what lets `execute` run its\n * transactions on one connection without guessing — see\n * {@link DatabaseAccess}.\n */\n readonly db: DatabaseAccess\n /** Every case type this engine serves; a loaded case's `case_type` must name one of them. */\n readonly caseTypes: readonly AnyCaseType[]\n /**\n * How long an Execution's claim survives without a heartbeat (default 30s).\n * The floor on how long a crashed handler can hold a case.\n */\n readonly claimTtlMs?: number\n /** How often a running handler refreshes its claim (default 5s). */\n readonly heartbeatMs?: number\n /**\n * Event ingestion: how an external event's Actor is derived.\n * Correlation needs no configuration — it is a registry, not a policy.\n */\n readonly ingestion?: IngestionOptions\n /**\n * The clock — every process-side \"as of now\" below the boundary reads\n * through it: guard evaluation instants, journal timestamps, ingestion\n * receipt times. Defaults to the wall clock. Two \"nows\" it deliberately\n * does not govern: lease expiry is judged by the storage adapter's own\n * clock (the one clock all competing processes share), and the retry\n * delay / heartbeat cadence run on process timers (an internal seam the\n * lifecycle's own tests drive virtually).\n */\n readonly now?: () => Date\n}\n\n/**\n * Options for {@link Engine.explain}. Omit `actor` to probe `requires`\n * alone: `permits` conditions are then reported un-evaluated (failed, with\n * the stated reason `'not evaluated: no actor supplied'`) rather than run\n * against nothing. The normalization rule is `explainContext`, stated and\n * tested beside the pure computation it feeds.\n */\nexport type ExplainOptions = ExplainRequest\n\nexport interface Engine {\n /**\n * Create a case of a registered case type. The initial state is validated\n * against the type's schema.\n */\n createCase(\n caseTypeName: string,\n initialState: unknown,\n ): Promise<CaseHandle<unknown>>\n\n /**\n * Compute the affordances record for a case: load it, evaluate every\n * step's guard for `actor` (with scope fan-out), and return the\n * serializable available + blocked answer. Dormant (ended) cases compute\n * like any other — dormancy is annotation, never a freeze.\n */\n affordances(\n caseId: string,\n actor: unknown,\n asOf?: Instant,\n ): Promise<CaseAffordances>\n\n /**\n * {@link Engine.affordances} for a case already in hand — no second load,\n * no re-validation. Synchronous: a registry read plus the pure\n * computation. The handle must be one this engine issued\n * ({@link Engine.createCase}, {@link Engine.case}) — their `state` is the\n * validated document; a hand-built handle carries no such guarantee.\n * This is how a create route answers with the fresh case's affordances\n * without re-reading what it just wrote.\n */\n affordancesOf(\n handle: CaseHandle<unknown>,\n actor: unknown,\n asOf?: Instant,\n ): CaseAffordances\n\n /** The full per-condition breakdown for one step (× scope element) of a case. */\n explain(\n caseId: string,\n stepName: string,\n options?: ExplainOptions,\n ): Promise<AffordanceExplanation>\n\n /**\n * Execute a step on a case: claim → run → commit. The claim\n * re-evaluates the guard transactionally — the enforcement moment — so an\n * affordance that has since gone away rejects with `StepNotAvailableError`\n * carrying the current unmet conditions, and a case with an Execution\n * already in flight rejects with `CaseBusyError`.\n */\n execute(\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n ): Promise<ExecutionResult>\n\n /**\n * Read a case's journal, oldest first. Filter by `scopeKey` for\n * a per-track audit — \"everything that happened on buyer #7\".\n */\n journal(\n caseId: string,\n filter?: JournalFilter,\n ): Promise<readonly JournalEntry[]>\n\n /**\n * Read one case as persisted: the row plus its Case State validated\n * against the registered schema. Loud — an addressed read owes an answer,\n * so an unknown case or a state its schema no longer accepts throws.\n * The read ops surfaces, tests and consoles would otherwise each\n * hand-write in SQL.\n */\n case(caseId: string): Promise<CaseHandle<unknown>>\n\n /** Where an external identifier routes — the reverse of {@link Engine.correlations}. */\n correlationOf(system: string, externalId: string): Promise<Correlation | null>\n\n /**\n * The declared input schema of one step of a registered case type, or\n * `null` when the step takes no input. Synchronous — a registry read. The\n * engine owns the registry and a case names its type, so an adapter never\n * needs to be handed the case types a second time to describe inputs.\n */\n inputSchemaFor(\n caseTypeName: string,\n stepName: string,\n ): StandardSchemaV1 | null\n\n /**\n * The declared human metadata of one step of a registered case type —\n * `title` and `description`, each `null` when undeclared — or `null` for\n * an unknown step. Synchronous, a registry read like\n * {@link Engine.inputSchemaFor}, and for the same reason: adapters\n * serialize step metadata from here rather than being handed the case\n * types a second time.\n */\n stepMetadataFor(caseTypeName: string, stepName: string): StepMetadata | null\n\n /**\n * Ingest one external event: dedup, correlate, then an ordinary Execution\n * with the external system as the actor. Never throws for an\n * event's own sake — an event that could not be applied is dead-lettered\n * with a reason, so a webhook endpoint can acknowledge and move on.\n */\n ingest(event: ExternalEvent): Promise<IngestionResult>\n\n /**\n * Register an external identifier against a case out of band. Handlers\n * should prefer `ctx.correlate(...)`, which rides the same commit as the\n * state recording that the interaction was started.\n */\n correlate(registration: CorrelationRegistration): Promise<Correlation>\n\n /** Every external identifier registered against a case (× scope element). */\n correlations(\n caseId: string,\n scopeKey?: string,\n ): Promise<readonly Correlation[]>\n\n /** The dead-letter surface: events that arrived and changed nothing, with why. */\n deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>\n\n /**\n * Restructure the state of every case of a case type, as journaled system\n * Executions (float's escape hatch). Idempotent: a case that\n * already carries the migration's marker is skipped, so re-running is a\n * no-op and an interrupted run resumes. Reach for it only when no total\n * condition can read the old shape — see `docs/migration.md`.\n */\n migrate(\n caseTypeName: string,\n name: string,\n transform: MigrationTransform,\n options?: MigrationOptions,\n ): Promise<MigrationReport>\n}\n\n/**\n * Build an engine from the app's database and its case type definitions.\n * Throws at construction on duplicate case type names — the registry is\n * keyed by name, which is all a case row records (definitions\n * float; only the name is persisted).\n */\nexport const createEngine = (options: EngineOptions): Engine => {\n const registry = new Map<string, AnyCaseType>()\n for (const definition of options.caseTypes) {\n if (registry.has(definition.name)) {\n throw new TypeError(\n `createEngine: duplicate case type name '${definition.name}'`,\n )\n }\n registry.set(definition.name, definition)\n }\n\n const caseTypeFor = (caseTypeName: string): AnyCaseType => {\n const definition = registry.get(caseTypeName)\n if (definition === undefined) {\n throw new UnknownCaseTypeError(caseTypeName, [...registry.keys()])\n }\n return definition\n }\n\n const now = options.now ?? (() => new Date())\n\n // Single self-contained statements run against either arm alike; only\n // transactions (and the lifecycle behind them) need the declaration itself.\n const db = queryableOf(options.db)\n\n // The widest environment any subsystem asks for (IngestionEnvironment ⊇\n // ExecutionEnvironment), built once and handed to all of them.\n const environment: IngestionEnvironment = {\n db: options.db,\n caseTypeFor,\n claimTtlMs: options.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS,\n heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,\n ingestion: normalizeIngestion(options.ingestion),\n now,\n timers: realTimers,\n }\n\n /** Load the case, resolve its type from the registry, validate state against the type's schema. */\n const load = async (\n caseId: string,\n ): Promise<{ definition: AnyCaseType; snapshot: CaseSnapshot<unknown> }> => {\n const { definition, handle, state } = await resolveCase(\n db,\n caseTypeFor,\n caseId,\n )\n return {\n definition,\n snapshot: { id: handle.id, state, endedAt: handle.endedAt },\n }\n }\n\n return {\n createCase: async (caseTypeName, initialState) => {\n const definition = caseTypeFor(caseTypeName)\n return withTransaction(options.db, (tx) =>\n insertCase(tx, caseTypeName, definition.state, initialState),\n )\n },\n affordances: async (caseId, actor, asOf) => {\n const { definition, snapshot } = await load(caseId)\n return computeAffordances(definition, snapshot, {\n actor,\n asOf: asOf ?? now(),\n })\n },\n affordancesOf: (handle, actor, asOf) =>\n computeAffordances(\n caseTypeFor(handle.caseTypeName),\n { id: handle.id, state: handle.state, endedAt: handle.endedAt },\n { actor, asOf: asOf ?? now() },\n ),\n explain: async (caseId, stepName, explainOptions = {}) => {\n const { definition, snapshot } = await load(caseId)\n // The boundary's one normalization (absent-vs-undefined actor, the\n // asOf default) lives with the pure computation — see explainContext.\n return computeExplanation(\n definition,\n snapshot,\n stepName,\n explainContext(explainOptions, now),\n )\n },\n // Rebuilt field by field, not spread: whatever extra properties a\n // caller's object drags along stop here, so the lifecycle only ever\n // sees the options the public interface declares.\n execute: (\n caseId,\n stepName,\n { actor, scopeKey, input, asOf, claimTtlMs, heartbeatMs, retry },\n ) =>\n executeStep(environment, caseId, stepName, {\n actor,\n scopeKey,\n input,\n asOf,\n claimTtlMs,\n heartbeatMs,\n retry,\n }),\n journal: (caseId, filter) => readJournal(db, caseId, filter),\n case: async (caseId) => {\n const { handle, state } = await resolveCase(db, caseTypeFor, caseId)\n return { ...handle, state }\n },\n correlationOf: (system, externalId) =>\n lookupCorrelation(db, system, externalId),\n inputSchemaFor: (caseTypeName, stepName) =>\n caseTypeFor(caseTypeName).getStep(stepName)?.input ?? null,\n stepMetadataFor: (caseTypeName, stepName) => {\n const step = caseTypeFor(caseTypeName).getStep(stepName)\n if (step === undefined) return null\n return { title: step.title, description: step.description }\n },\n ingest: (event) => ingest(environment, event),\n correlate: (registration) => registerCorrelation(db, registration),\n correlations: (caseId, scopeKey) => correlationsFor(db, caseId, scopeKey),\n deadLetters: (filter) => readDeadLetters(db, filter),\n migrate: (caseTypeName, name, transform, migrationOptions) =>\n migrate(environment, caseTypeName, name, transform, migrationOptions),\n }\n}\n"]}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The engine: the case-type registry bound to the case store.\n *\n * `createEngine({ storage, caseTypes })` wires the halves together — persistence\n * (`../store`), guard evaluation (`../guards`), the definition API\n * (`../model`) and the execution lifecycle (`../execution`) — into the\n * framework's public face: `affordances`, `explain`, `execute`, `journal`.\n *\n * The engine is where `asOf` defaults to now: conditions never read the\n * clock, so `EngineOptions.now` — wall clock by default — is the one clock,\n * threaded through the environment to everything below, and everything below\n * it is pure and reconstructable.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type {\n ExecuteOptions,\n ExecutionResult,\n JournalEntry,\n JournalFilter,\n} from '../execution/index.js'\nimport {\n DEFAULT_CLAIM_TTL_MS,\n DEFAULT_HEARTBEAT_MS,\n executeStep,\n realTimers,\n} from '../execution/index.js'\nimport type { Instant } from '../guards/index.js'\nimport type {\n Correlation,\n CorrelationRegistration,\n DeadLetter,\n DeadLetterFilter,\n ExternalEvent,\n IngestionEnvironment,\n IngestionOptions,\n IngestionResult,\n} from '../ingestion/index.js'\nimport { ingest, normalizeIngestion } from '../ingestion/index.js'\nimport type {\n MigrationOptions,\n MigrationReport,\n MigrationTransform,\n} from '../migration/index.js'\nimport { migrate } from '../migration/index.js'\nimport type { AnyCaseType, StepMetadata } from '../model/index.js'\nimport type { CaseListOptions, CasePage, EngineStorage } from '../storage.js'\nimport type { CaseHandle } from '../store/index.js'\nimport { resolveCase, validateAgainstSchema } from '../store/index.js'\nimport type {\n AffordanceExplanation,\n CaseAffordances,\n CaseSnapshot,\n ExplainRequest,\n} from './compute.js'\nimport {\n computeAffordances,\n computeExplanation,\n explainContext,\n} from './compute.js'\nimport { UnknownCaseTypeError } from './errors.js'\n\n/** Options for {@link createEngine}. */\nexport interface EngineOptions<TCommit = unknown> {\n /** One coordinated storage implementation for all engine persistence. */\n readonly storage: EngineStorage<TCommit>\n /** Every case type this engine serves; a loaded case's `case_type` must name one of them. */\n readonly caseTypes: readonly AnyCaseType<NoInfer<TCommit>>[]\n /**\n * How long an Execution's claim survives without a heartbeat (default 30s).\n * The floor on how long a crashed handler can hold a case.\n */\n readonly claimTtlMs?: number\n /** How often a running handler refreshes its claim (default 5s). */\n readonly heartbeatMs?: number\n /**\n * Event ingestion: how an external event's Actor is derived.\n * Correlation needs no configuration — it is a registry, not a policy.\n */\n readonly ingestion?: IngestionOptions\n /**\n * Clock for guard evaluation instants, defaulting to the wall clock.\n * Stored timestamps and claim expiry belong to the adapter's clock;\n * heartbeat cadence and retry delays use process timers.\n */\n readonly now?: () => Date\n}\n\n/**\n * Options for {@link Engine.explain}. Omit `actor` to probe `requires`\n * alone: `permits` conditions are then reported un-evaluated (failed, with\n * the stated reason `'not evaluated: no actor supplied'`) rather than run\n * against nothing. The normalization rule is `explainContext`, stated and\n * tested beside the pure computation it feeds.\n */\nexport type ExplainOptions = ExplainRequest\n\nexport interface Engine {\n /** List validated cases of registered types, newest first. Throws on unreadable state. */\n listCases(options?: CaseListOptions): Promise<CasePage>\n\n /**\n * Create a case of a registered case type. The initial state is validated\n * against the type's schema.\n */\n createCase(\n caseTypeName: string,\n initialState: unknown,\n ): Promise<CaseHandle<unknown>>\n\n /**\n * Compute the affordances record for a case: load it, evaluate every\n * step's guard for `actor` (with scope fan-out), and return the\n * serializable available + blocked answer. Dormant (ended) cases compute\n * like any other — dormancy is annotation, never a freeze.\n */\n affordances(\n caseId: string,\n actor: unknown,\n asOf?: Instant,\n ): Promise<CaseAffordances>\n\n /**\n * {@link Engine.affordances} for a case already in hand — no second load,\n * no re-validation. Synchronous: a registry read plus the pure\n * computation. The handle must be one this engine issued\n * ({@link Engine.createCase}, {@link Engine.case}) — their `state` is the\n * validated document; a hand-built handle carries no such guarantee.\n * This is how a create route answers with the fresh case's affordances\n * without re-reading what it just wrote.\n */\n affordancesOf(\n handle: CaseHandle<unknown>,\n actor: unknown,\n asOf?: Instant,\n ): CaseAffordances\n\n /** The full per-condition breakdown for one step (× scope element) of a case. */\n explain(\n caseId: string,\n stepName: string,\n options?: ExplainOptions,\n ): Promise<AffordanceExplanation>\n\n /**\n * Execute a step on a case: claim → run → commit. The claim\n * re-evaluates the guard transactionally — the enforcement moment — so an\n * affordance that has since gone away rejects with `StepNotAvailableError`\n * carrying the current unmet conditions, and a case with an Execution\n * already in flight rejects with `CaseBusyError`.\n */\n execute(\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n ): Promise<ExecutionResult>\n\n /**\n * Read a case's journal, oldest first. Filter by `scopeKey` for\n * a per-track audit — \"everything that happened on buyer #7\".\n */\n journal(\n caseId: string,\n filter?: JournalFilter,\n ): Promise<readonly JournalEntry[]>\n\n /**\n * Read one case as persisted: the row plus its Case State validated\n * against the registered schema. Loud — an addressed read owes an answer,\n * so an unknown case or a state its schema no longer accepts throws.\n * The read ops surfaces, tests and consoles would otherwise each\n * hand-write in SQL.\n */\n case(caseId: string): Promise<CaseHandle<unknown>>\n\n /** Where an external identifier routes — the reverse of {@link Engine.correlations}. */\n correlationOf(system: string, externalId: string): Promise<Correlation | null>\n\n /**\n * The declared input schema of one step of a registered case type, or\n * `null` when the step takes no input. Synchronous — a registry read. The\n * engine owns the registry and a case names its type, so an adapter never\n * needs to be handed the case types a second time to describe inputs.\n */\n inputSchemaFor(\n caseTypeName: string,\n stepName: string,\n ): StandardSchemaV1 | null\n\n /**\n * The declared human metadata of one step of a registered case type —\n * `title` and `description`, each `null` when undeclared — or `null` for\n * an unknown step. Synchronous, a registry read like\n * {@link Engine.inputSchemaFor}, and for the same reason: adapters\n * serialize step metadata from here rather than being handed the case\n * types a second time.\n */\n stepMetadataFor(caseTypeName: string, stepName: string): StepMetadata | null\n\n /**\n * Ingest one external event: dedup, correlate, then an ordinary Execution\n * with the external system as the actor. Never throws for an\n * event's own sake — an event that could not be applied is dead-lettered\n * with a reason, so a webhook endpoint can acknowledge and move on.\n */\n ingest(event: ExternalEvent): Promise<IngestionResult>\n\n /**\n * Register an external identifier against a case out of band. Handlers\n * should prefer `ctx.correlate(...)`, which rides the same commit as the\n * state recording that the interaction was started.\n */\n correlate(registration: CorrelationRegistration): Promise<Correlation>\n\n /** Every external identifier registered against a case (× scope element). */\n correlations(\n caseId: string,\n scopeKey?: string,\n ): Promise<readonly Correlation[]>\n\n /** The dead-letter surface: events that arrived and changed nothing, with why. */\n deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>\n\n /**\n * Restructure the state of every case of a case type, as journaled system\n * Executions (float's escape hatch). Idempotent: a case that\n * already carries the migration's marker is skipped, so re-running is a\n * no-op and an interrupted run resumes. Reach for it only when no total\n * condition can read the old shape — see `docs/migration.md`.\n */\n migrate(\n caseTypeName: string,\n name: string,\n transform: MigrationTransform,\n options?: MigrationOptions,\n ): Promise<MigrationReport>\n}\n\n/**\n * Build an engine from the app's storage and its case type definitions.\n * Throws at construction on duplicate case type names — the registry is\n * keyed by name, which is all a case row records (definitions\n * float; only the name is persisted).\n */\nexport const createEngine = <TCommit>(\n options: EngineOptions<TCommit>,\n): Engine => {\n const registry = new Map<string, AnyCaseType<TCommit>>()\n for (const definition of options.caseTypes) {\n if (registry.has(definition.name)) {\n throw new TypeError(\n `createEngine: duplicate case type name '${definition.name}'`,\n )\n }\n registry.set(definition.name, definition)\n }\n\n const caseTypeFor = (caseTypeName: string): AnyCaseType<TCommit> => {\n const definition = registry.get(caseTypeName)\n if (definition === undefined) {\n throw new UnknownCaseTypeError(caseTypeName, [...registry.keys()])\n }\n return definition\n }\n\n const now = options.now ?? (() => new Date())\n\n const storage = options.storage\n\n // The widest environment any subsystem asks for (IngestionEnvironment ⊇\n // ExecutionEnvironment), built once and handed to all of them.\n const environment: IngestionEnvironment<TCommit> = {\n storage,\n caseTypeFor,\n claimTtlMs: options.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS,\n heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,\n ingestion: normalizeIngestion(options.ingestion),\n now,\n timers: realTimers,\n }\n\n /** Load the case, resolve its type from the registry, validate state against the type's schema. */\n const load = async (\n caseId: string,\n ): Promise<{\n definition: AnyCaseType<TCommit>\n snapshot: CaseSnapshot<unknown>\n }> => {\n const { definition, handle, state } = await resolveCase(\n await storage.cases.get(caseId),\n caseTypeFor,\n )\n return {\n definition,\n snapshot: { id: handle.id, state, endedAt: handle.endedAt },\n }\n }\n\n return {\n listCases: async (options = {}) => {\n const limit = options.limit ?? 100\n if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {\n throw new TypeError(\n 'listCases: limit must be an integer between 1 and 1000',\n )\n }\n const caseTypeNames =\n options.caseTypeName === undefined\n ? [...registry.keys()]\n : [caseTypeFor(options.caseTypeName).name]\n const page = await storage.cases.list({\n ...options,\n caseTypeNames,\n limit,\n })\n const cases = await Promise.all(\n page.cases.map(async (stored) => {\n const { handle, state } = await resolveCase(stored, caseTypeFor)\n return { ...handle, state }\n }),\n )\n return { cases, nextCursor: page.nextCursor }\n },\n createCase: async (caseTypeName, initialState) => {\n const definition = caseTypeFor(caseTypeName)\n const state = await validateAgainstSchema(\n definition.state,\n initialState,\n 'initial state',\n )\n return storage.cases.create(caseTypeName, state)\n },\n affordances: async (caseId, actor, asOf) => {\n const { definition, snapshot } = await load(caseId)\n return computeAffordances(definition, snapshot, {\n actor,\n asOf: asOf ?? now(),\n })\n },\n affordancesOf: (handle, actor, asOf) =>\n computeAffordances(\n caseTypeFor(handle.caseTypeName),\n { id: handle.id, state: handle.state, endedAt: handle.endedAt },\n { actor, asOf: asOf ?? now() },\n ),\n explain: async (caseId, stepName, explainOptions = {}) => {\n const { definition, snapshot } = await load(caseId)\n // The boundary's one normalization (absent-vs-undefined actor, the\n // asOf default) lives with the pure computation — see explainContext.\n return computeExplanation(\n definition,\n snapshot,\n stepName,\n explainContext(explainOptions, now),\n )\n },\n // Rebuilt field by field, not spread: whatever extra properties a\n // caller's object drags along stop here, so the lifecycle only ever\n // sees the options the public interface declares.\n execute: (\n caseId,\n stepName,\n { actor, scopeKey, input, asOf, claimTtlMs, heartbeatMs, retry },\n ) =>\n executeStep(environment, caseId, stepName, {\n actor,\n scopeKey,\n input,\n asOf,\n claimTtlMs,\n heartbeatMs,\n retry,\n }),\n journal: (caseId, filter) => storage.journal.read(caseId, filter),\n case: async (caseId) => {\n const { handle, state } = await resolveCase(\n await storage.cases.get(caseId),\n caseTypeFor,\n )\n return { ...handle, state }\n },\n correlationOf: (system, externalId) =>\n storage.correlations.lookup(system, externalId),\n inputSchemaFor: (caseTypeName, stepName) =>\n caseTypeFor(caseTypeName).getStep(stepName)?.input ?? null,\n stepMetadataFor: (caseTypeName, stepName) => {\n const step = caseTypeFor(caseTypeName).getStep(stepName)\n if (step === undefined) return null\n return { title: step.title, description: step.description }\n },\n ingest: (event) => ingest(environment, event),\n correlate: (registration) => storage.correlations.register(registration),\n correlations: (caseId, scopeKey) =>\n storage.correlations.list(caseId, scopeKey),\n deadLetters: (filter) => storage.deliveries.deadLetters(filter),\n migrate: (caseTypeName, name, transform, migrationOptions) =>\n migrate(environment, caseTypeName, name, transform, migrationOptions),\n }\n}\n"]}
package/dist/errors.d.ts CHANGED
@@ -16,15 +16,9 @@
16
16
  * it is a bug or an infrastructure failure, and edges should let it through
17
17
  * rather than dress it up as an answer.
18
18
  */
19
- import type { RefusalCode } from '@affordance/contract';
20
- /**
21
- * What kind of refusal or failure this is. Deliberately small and closed:
22
- * these are the answers the affordance contract is specified in terms of, so
23
- * a new code *is* a contract change — and it reads as one, because the set
24
- * has exactly one declaration, in `@affordance/contract` (`REFUSAL_CODES`),
25
- * from which this type derives.
26
- */
27
- export type AffordanceErrorCode = RefusalCode;
19
+ /** The closed set of deliberate engine refusal codes. */
20
+ export declare const REFUSAL_CODES: readonly ["step-not-available", "case-busy", "invalid-input", "not-found", "bad-request", "execution-failed", "invalid-state"];
21
+ export type AffordanceErrorCode = (typeof REFUSAL_CODES)[number];
28
22
  /**
29
23
  * The base of every error the framework raises deliberately. Subclasses
30
24
  * declare their {@link AffordanceErrorCode} at construction — there is no
package/dist/errors.js CHANGED
@@ -16,6 +16,23 @@
16
16
  * it is a bug or an infrastructure failure, and edges should let it through
17
17
  * rather than dress it up as an answer.
18
18
  */
19
+ /** The closed set of deliberate engine refusal codes. */
20
+ export const REFUSAL_CODES = [
21
+ /** A guard said no. Carries the unmet conditions. */
22
+ 'step-not-available',
23
+ /** Another Execution holds the case — "not now", not "never". */
24
+ 'case-busy',
25
+ /** A step's input failed its declared schema. */
26
+ 'invalid-input',
27
+ /** No such case, or no such case type. */
28
+ 'not-found',
29
+ /** The caller addressed something that cannot be addressed: unknown step, bad scope key. */
30
+ 'bad-request',
31
+ /** A handler ran and failed. */
32
+ 'execution-failed',
33
+ /** A stored Case State no longer satisfies its schema. */
34
+ 'invalid-state',
35
+ ];
19
36
  /**
20
37
  * The base of every error the framework raises deliberately. Subclasses
21
38
  * declare their {@link AffordanceErrorCode} at construction — there is no
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH;;;;GAIG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAqB;IAElC,YACE,IAAyB,EACzB,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAA4B,EAAE,CAC5E,KAAK,YAAY,eAAe,CAAA;AAElC;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CACtD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAExD,gFAAgF;AAChF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAc,EAAS,EAAE,CAC/C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA","sourcesContent":["/**\n * The framework's error taxonomy.\n *\n * Every way the framework refuses or fails carries a **code**: a stable,\n * wire-safe name for the *kind* of refusal, distinct from the message (which\n * is prose, and which an adapter may have to rewrite for the audience it is\n * answering — see the HTTP contract's visibility rule).\n *\n * The code lives here, on the error, rather than in a mapping table at each\n * edge. An adapter that turns framework errors into responses then translates\n * a closed set of codes instead of enumerating error classes it has to be\n * told about: adding an error class cannot silently produce a 500, because\n * the class cannot be constructed without declaring what it is.\n *\n * Anything not an {@link AffordanceError} is not the framework's refusal —\n * it is a bug or an infrastructure failure, and edges should let it through\n * rather than dress it up as an answer.\n */\n\nimport type { RefusalCode } from '@affordance/contract'\n\n/**\n * What kind of refusal or failure this is. Deliberately small and closed:\n * these are the answers the affordance contract is specified in terms of, so\n * a new code *is* a contract change and it reads as one, because the set\n * has exactly one declaration, in `@affordance/contract` (`REFUSAL_CODES`),\n * from which this type derives.\n */\nexport type AffordanceErrorCode = RefusalCode\n\n/**\n * The base of every error the framework raises deliberately. Subclasses\n * declare their {@link AffordanceErrorCode} at construction — there is no\n * default, so the taxonomy cannot be extended by accident.\n */\nexport class AffordanceError extends Error {\n readonly code: AffordanceErrorCode\n\n constructor(\n code: AffordanceErrorCode,\n message: string,\n options?: ErrorOptions,\n ) {\n super(message, options)\n this.name = 'AffordanceError'\n this.code = code\n }\n}\n\n/** Narrow an unknown throw to a deliberate framework refusal. */\nexport const isAffordanceError = (error: unknown): error is AffordanceError =>\n error instanceof AffordanceError\n\n/**\n * The human-readable message of whatever was thrown — Error or not. The one\n * spelling of a conversion that guards, targets, and the lifecycle all need,\n * because app code can throw anything.\n */\nexport const thrownMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/** Convert an unknown throw into an `Error`, preserving one that already is. */\nexport const toError = (error: unknown): Error =>\n error instanceof Error ? error : new Error(thrownMessage(error))\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,qDAAqD;IACrD,oBAAoB;IACpB,iEAAiE;IACjE,WAAW;IACX,iDAAiD;IACjD,eAAe;IACf,0CAA0C;IAC1C,WAAW;IACX,4FAA4F;IAC5F,aAAa;IACb,gCAAgC;IAChC,kBAAkB;IAClB,0DAA0D;IAC1D,eAAe;CACP,CAAA;AAIV;;;;GAIG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAqB;IAElC,YACE,IAAyB,EACzB,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAA4B,EAAE,CAC5E,KAAK,YAAY,eAAe,CAAA;AAElC;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CACtD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAExD,gFAAgF;AAChF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAc,EAAS,EAAE,CAC/C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA","sourcesContent":["/**\n * The framework's error taxonomy.\n *\n * Every way the framework refuses or fails carries a **code**: a stable,\n * wire-safe name for the *kind* of refusal, distinct from the message (which\n * is prose, and which an adapter may have to rewrite for the audience it is\n * answering — see the HTTP contract's visibility rule).\n *\n * The code lives here, on the error, rather than in a mapping table at each\n * edge. An adapter that turns framework errors into responses then translates\n * a closed set of codes instead of enumerating error classes it has to be\n * told about: adding an error class cannot silently produce a 500, because\n * the class cannot be constructed without declaring what it is.\n *\n * Anything not an {@link AffordanceError} is not the framework's refusal —\n * it is a bug or an infrastructure failure, and edges should let it through\n * rather than dress it up as an answer.\n */\n\n/** The closed set of deliberate engine refusal codes. */\nexport const REFUSAL_CODES = [\n /** A guard said no. Carries the unmet conditions. */\n 'step-not-available',\n /** Another Execution holds the case \"not now\", not \"never\". */\n 'case-busy',\n /** A step's input failed its declared schema. */\n 'invalid-input',\n /** No such case, or no such case type. */\n 'not-found',\n /** The caller addressed something that cannot be addressed: unknown step, bad scope key. */\n 'bad-request',\n /** A handler ran and failed. */\n 'execution-failed',\n /** A stored Case State no longer satisfies its schema. */\n 'invalid-state',\n] as const\n\nexport type AffordanceErrorCode = (typeof REFUSAL_CODES)[number]\n\n/**\n * The base of every error the framework raises deliberately. Subclasses\n * declare their {@link AffordanceErrorCode} at construction — there is no\n * default, so the taxonomy cannot be extended by accident.\n */\nexport class AffordanceError extends Error {\n readonly code: AffordanceErrorCode\n\n constructor(\n code: AffordanceErrorCode,\n message: string,\n options?: ErrorOptions,\n ) {\n super(message, options)\n this.name = 'AffordanceError'\n this.code = code\n }\n}\n\n/** Narrow an unknown throw to a deliberate framework refusal. */\nexport const isAffordanceError = (error: unknown): error is AffordanceError =>\n error instanceof AffordanceError\n\n/**\n * The human-readable message of whatever was thrown — Error or not. The one\n * spelling of a conversion that guards, targets, and the lifecycle all need,\n * because app code can throw anything.\n */\nexport const thrownMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/** Convert an unknown throw into an `Error`, preserving one that already is. */\nexport const toError = (error: unknown): Error =>\n error instanceof Error ? error : new Error(thrownMessage(error))\n"]}
@@ -25,7 +25,7 @@ export type PatchOp = {
25
25
  export type StateDelta = readonly PatchOp[];
26
26
  /**
27
27
  * Structural equality over JSON values. `undefined` never appears in a
28
- * document that round-tripped through jsonb, but a handler's return value has
28
+ * document that round-tripped through JSON storage, but a handler's return value has
29
29
  * not round-tripped yet, so it is compared as-is.
30
30
  */
31
31
  export declare const jsonEqual: (a: unknown, b: unknown) => boolean;
@@ -13,7 +13,7 @@ const escapeToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
13
13
  const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
14
14
  /**
15
15
  * Structural equality over JSON values. `undefined` never appears in a
16
- * document that round-tripped through jsonb, but a handler's return value has
16
+ * document that round-tripped through JSON storage, but a handler's return value has
17
17
  * not round-tripped yet, so it is compared as-is.
18
18
  */
19
19
  export const jsonEqual = (a, b) => {
@@ -1 +1 @@
1
- {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/execution/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * State deltas — what an Execution changed, as RFC 6902 JSON Patch.\n *\n * Every committed Execution journals the delta from the previous Case State\n * to the next. A standard patch format is deliberate: the delta is\n * an audit artifact read by people and machines that are not this library, so\n * it should not need a bespoke decoder. Paths are RFC 6901 JSON Pointers.\n *\n * Pure and total over JSON values — no clock, no I/O, no schema knowledge.\n */\n\n/** One JSON Patch operation. */\nexport type PatchOp =\n | { readonly op: 'add'; readonly path: string; readonly value: unknown }\n | { readonly op: 'remove'; readonly path: string }\n | { readonly op: 'replace'; readonly path: string; readonly value: unknown }\n\n/** An Execution's state delta: the ops taking the previous Case State to the next. */\nexport type StateDelta = readonly PatchOp[]\n\n/** RFC 6901 escaping: `~` → `~0`, `/` → `~1`. */\nconst escapeToken = (token: string): string =>\n token.replace(/~/g, '~0').replace(/\\//g, '~1')\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Structural equality over JSON values. `undefined` never appears in a\n * document that round-tripped through jsonb, but a handler's return value has\n * not round-tripped yet, so it is compared as-is.\n */\nexport const jsonEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n return (\n a.length === b.length &&\n a.every((item, index) => jsonEqual(item, b[index]))\n )\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const keys = Object.keys(a)\n if (keys.length !== Object.keys(b).length) return false\n return keys.every(\n (key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]),\n )\n }\n return false\n}\n\nconst diffInto = (\n ops: PatchOp[],\n path: string,\n previous: unknown,\n next: unknown,\n): void => {\n if (previous === next) return\n\n if (Array.isArray(previous) && Array.isArray(next)) {\n const shared = Math.min(previous.length, next.length)\n for (let index = 0; index < shared; index += 1) {\n diffInto(ops, `${path}/${index}`, previous[index], next[index])\n }\n // Appends first, then trailing removals from the end backwards, so every\n // index a `remove` names is still valid when that op is applied.\n for (let index = previous.length; index < next.length; index += 1) {\n ops.push({ op: 'add', path: `${path}/-`, value: next[index] })\n }\n for (let index = previous.length - 1; index >= next.length; index -= 1) {\n ops.push({ op: 'remove', path: `${path}/${index}` })\n }\n return\n }\n\n if (isPlainObject(previous) && isPlainObject(next)) {\n for (const key of Object.keys(previous)) {\n const child = `${path}/${escapeToken(key)}`\n if (!Object.hasOwn(next, key)) ops.push({ op: 'remove', path: child })\n else diffInto(ops, child, previous[key], next[key])\n }\n for (const key of Object.keys(next)) {\n if (!Object.hasOwn(previous, key)) {\n ops.push({\n op: 'add',\n path: `${path}/${escapeToken(key)}`,\n value: next[key],\n })\n }\n }\n return\n }\n\n // Differing types, or two differing scalars: the whole node is replaced.\n // An equal pair never lands here — identical references return at the top,\n // equal scalars are identical, and matching containers recurse above\n // (a deeply-equal subtree just emits no ops) — so no deep comparison is\n // needed on the way down.\n ops.push({ op: 'replace', path, value: next })\n}\n\n/**\n * The delta from one Case State document to the next. An Execution that\n * changed nothing yields an empty delta — a real and unremarkable outcome\n * (a handler whose only effect was external, or a no-op retry landing).\n *\n * Arrays are diffed positionally: element *i* against element *i*, then\n * appends and trailing removals. Case State collections are keyed by the\n * app's own identifiers (scope keys), so a positional diff of a reordered\n * collection is verbose but never wrong.\n */\nexport const diffState = (previous: unknown, next: unknown): StateDelta => {\n const ops: PatchOp[] = []\n diffInto(ops, '', previous, next)\n return ops\n}\n"]}
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/execution/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * State deltas — what an Execution changed, as RFC 6902 JSON Patch.\n *\n * Every committed Execution journals the delta from the previous Case State\n * to the next. A standard patch format is deliberate: the delta is\n * an audit artifact read by people and machines that are not this library, so\n * it should not need a bespoke decoder. Paths are RFC 6901 JSON Pointers.\n *\n * Pure and total over JSON values — no clock, no I/O, no schema knowledge.\n */\n\n/** One JSON Patch operation. */\nexport type PatchOp =\n | { readonly op: 'add'; readonly path: string; readonly value: unknown }\n | { readonly op: 'remove'; readonly path: string }\n | { readonly op: 'replace'; readonly path: string; readonly value: unknown }\n\n/** An Execution's state delta: the ops taking the previous Case State to the next. */\nexport type StateDelta = readonly PatchOp[]\n\n/** RFC 6901 escaping: `~` → `~0`, `/` → `~1`. */\nconst escapeToken = (token: string): string =>\n token.replace(/~/g, '~0').replace(/\\//g, '~1')\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Structural equality over JSON values. `undefined` never appears in a\n * document that round-tripped through JSON storage, but a handler's return value has\n * not round-tripped yet, so it is compared as-is.\n */\nexport const jsonEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n return (\n a.length === b.length &&\n a.every((item, index) => jsonEqual(item, b[index]))\n )\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const keys = Object.keys(a)\n if (keys.length !== Object.keys(b).length) return false\n return keys.every(\n (key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]),\n )\n }\n return false\n}\n\nconst diffInto = (\n ops: PatchOp[],\n path: string,\n previous: unknown,\n next: unknown,\n): void => {\n if (previous === next) return\n\n if (Array.isArray(previous) && Array.isArray(next)) {\n const shared = Math.min(previous.length, next.length)\n for (let index = 0; index < shared; index += 1) {\n diffInto(ops, `${path}/${index}`, previous[index], next[index])\n }\n // Appends first, then trailing removals from the end backwards, so every\n // index a `remove` names is still valid when that op is applied.\n for (let index = previous.length; index < next.length; index += 1) {\n ops.push({ op: 'add', path: `${path}/-`, value: next[index] })\n }\n for (let index = previous.length - 1; index >= next.length; index -= 1) {\n ops.push({ op: 'remove', path: `${path}/${index}` })\n }\n return\n }\n\n if (isPlainObject(previous) && isPlainObject(next)) {\n for (const key of Object.keys(previous)) {\n const child = `${path}/${escapeToken(key)}`\n if (!Object.hasOwn(next, key)) ops.push({ op: 'remove', path: child })\n else diffInto(ops, child, previous[key], next[key])\n }\n for (const key of Object.keys(next)) {\n if (!Object.hasOwn(previous, key)) {\n ops.push({\n op: 'add',\n path: `${path}/${escapeToken(key)}`,\n value: next[key],\n })\n }\n }\n return\n }\n\n // Differing types, or two differing scalars: the whole node is replaced.\n // An equal pair never lands here — identical references return at the top,\n // equal scalars are identical, and matching containers recurse above\n // (a deeply-equal subtree just emits no ops) — so no deep comparison is\n // needed on the way down.\n ops.push({ op: 'replace', path, value: next })\n}\n\n/**\n * The delta from one Case State document to the next. An Execution that\n * changed nothing yields an empty delta — a real and unremarkable outcome\n * (a handler whose only effect was external, or a no-op retry landing).\n *\n * Arrays are diffed positionally: element *i* against element *i*, then\n * appends and trailing removals. Case State collections are keyed by the\n * app's own identifiers (scope keys), so a positional diff of a reordered\n * collection is verbose but never wrong.\n */\nexport const diffState = (previous: unknown, next: unknown): StateDelta => {\n const ops: PatchOp[] = []\n diffInto(ops, '', previous, next)\n return ops\n}\n"]}