@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
@@ -1 +1 @@
1
- {"version":3,"file":"target.js","sourceRoot":"","sources":["../../src/model/target.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Step targeting: resolving \"step X, of element K, on this state\".\n *\n * A **step target** is a step definition plus its scope binding — the pair\n * that an affordance's identity (step × scope key) names. Everything\n * that acts on a step goes through here first: the execution lifecycle's\n * claim, `explain`, audit replay, and the affordance listing itself.\n *\n * It lives in the model because it is a fact about a {@link StepDefinition}\n * and a Case State, and nothing more — no store, no registry, no clock. Put\n * anywhere higher it would drag every consumer's imports upward toward the\n * engine, which is a facade none of them should need.\n *\n * ## One fan-out, filtered\n *\n * {@link selectTargets} is the only implementation of fan-out — expanding\n * one scoped step into its per-element targets — and it makes the one\n * distinction every consumer needs:\n *\n * - A **defective selector** — one that throws over historical state, or\n * returns something other than an array — is a *selection failure*,\n * reported as `failure` with no targets. Each caller decides what its\n * audience deserves: the affordance listing renders it as a blocked\n * `$scope` entry, a sweep skips the step, {@link resolveTarget} throws.\n * - A **key integrity violation** — duplicate or malformed scope keys —\n * corrupts affordance identity itself, so {@link ScopeKeyError} is loud\n * through every path. No caller may absorb it into an empty selection.\n *\n * Addressing — \"step X (of element K) on this state\" — is likewise one\n * implementation, {@link addressTarget}, answering with the target or a\n * {@link TargetAddressFailure} that names its kind. Its filters:\n * {@link resolveTarget} (loud: an addressed caller is owed the precise\n * failure, so it throws), audit replay (lenient: a sweep over the Journal\n * reports the failure as a value and keeps going), and `explain` (loud,\n * with one deliberate exception — a `defective-selector` failure is\n * *answered*, because the listing published that exact link).\n */\n\nimport { SCOPE_FAILURE_CONDITION } from '@affordance/contract'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport { thrownMessage } from '../errors.js'\nimport type {\n GuardEvaluation,\n Instant,\n SingleConditionResult,\n} from '../guards/index.js'\nimport { evaluateGuard } from '../guards/index.js'\nimport type { CaseTypeDefinition } from './casetype.js'\nimport { ScopeKeyError, UnknownStepError } from './errors.js'\nimport type { StepDefinition } from './step.js'\n\n/**\n * The synthetic condition name under which a throwing or malformed scope\n * selector is reported. `$`-prefixed so it can never collide with an\n * author's condition names. Declared by `@affordance/contract` — it reaches\n * clients through `blocked[].unmet[].name`, so it is wire vocabulary, and\n * the wire owns it; re-exported here for the engine's own consumers.\n */\nexport { SCOPE_FAILURE_CONDITION }\n\n/**\n * The synthetic `$scope` entry as a condition result — the one spelling of\n * how a failed scope selection reports into an evaluation-shaped record: a\n * failed condition carrying the selector's reason.\n */\nconst scopeConditionResult = (failure: {\n readonly reason: string\n}): SingleConditionResult => ({\n name: SCOPE_FAILURE_CONDITION,\n section: 'requires',\n kind: 'condition',\n passed: false,\n reason: failure.reason,\n})\n\n/**\n * A defective selection as a full evaluation record — the\n * possible/permitted/available verdict stated once. The selection failed, so\n * nothing about the step is possible\n * on this case; `permits` were never reachable (they may read the element),\n * reported vacuously satisfied — the requires-level `$scope` failure is the\n * answer. The listing's blocked entry and `explain`'s answer for the same\n * link both derive from this.\n */\nexport const scopeFailureEvaluation = (\n asOf: string,\n failure: { readonly reason: string },\n): GuardEvaluation => ({\n asOf,\n possible: false,\n permitted: true,\n available: false,\n conditions: [scopeConditionResult(failure)],\n})\n\n/** What a guard is evaluated against, beyond state: the actor and the instant. */\nexport interface ComputationContext<TActor = unknown> {\n readonly actor: TActor\n /**\n * The instant to evaluate as of — always explicit. Conditions cannot read\n * the clock, so defaulting to now is the engine's job, done once at its\n * boundary; everything below it is pure and reconstructable.\n */\n readonly asOf: Instant\n}\n\n/** One element of a scoped step's selection, with the key that identifies it. */\nexport interface ScopeBinding {\n readonly element: unknown\n readonly key: string\n}\n\n/**\n * A step addressed by name (× scope key, if scoped): the step definition and\n * its scope binding, resolved against a given Case State.\n */\nexport interface StepTarget<TState, TActor = unknown> {\n readonly step: StepDefinition<TState, TActor>\n /**\n * The Case State the target was resolved against — the document its guard\n * is evaluated over. Carried on the target so a binding can never be\n * evaluated against a different document than the one that produced it.\n */\n readonly state: TState\n /** The bound element and its key, or `null` for an unscoped step. */\n readonly binding: ScopeBinding | null\n}\n\n/** The outcome of scope fan-out: the step's targets, or why selection produced none. */\nexport interface TargetSelection<TState, TActor = unknown> {\n readonly targets: readonly StepTarget<TState, TActor>[]\n /** The selection failure — a defective selector — or `null` when selection succeeded. */\n readonly failure: { readonly reason: string } | null\n}\n\n/**\n * Select a scoped step's elements and derive their keys, enforcing key\n * integrity: every key a non-empty string, unique within the selection —\n * scope keys are affordance identity, so violations throw\n * {@link ScopeKeyError} instead of degrading. A `select` that throws\n * (totality bug) is left to the caller to absorb or report.\n */\nconst selectScope = <TState, TActor>(\n definition: StepDefinition<TState, TActor>,\n scope: NonNullable<StepDefinition<TState, TActor>['scope']>,\n state: TState,\n): readonly ScopeBinding[] => {\n const selected = scope.select(state)\n if (!Array.isArray(selected)) {\n throw new ScopeKeyError(\n definition.name,\n null,\n 'scope.select must return an array of elements',\n )\n }\n const seen = new Set<string>()\n return selected.map((element) => {\n let key: unknown\n try {\n key = scope.key(element)\n } catch (err) {\n throw new ScopeKeyError(\n definition.name,\n null,\n `scope.key threw: ${thrownMessage(err)}`,\n )\n }\n if (typeof key !== 'string' || key === '') {\n throw new ScopeKeyError(\n definition.name,\n null,\n 'scope.key must return a non-empty string for every element',\n )\n }\n if (seen.has(key)) {\n throw new ScopeKeyError(\n definition.name,\n key,\n `duplicate scope key '${key}' — scope keys are affordance identity and must be unique`,\n )\n }\n seen.add(key)\n return { element, key }\n })\n}\n\n/**\n * The one implementation of scope fan-out — every consumer (the affordance\n * listing, read tracing, addressing)\n * is a filter over this function.\n *\n * An unscoped step yields exactly one target; a scoped step yields one per\n * selected element, or none with a `failure` naming why when the selector is\n * defective. {@link ScopeKeyError} — identity corruption — propagates: it is\n * never a selection failure, and no caller may absorb it into \"no targets\".\n */\nexport const selectTargets = <TState, TActor>(\n step: StepDefinition<TState, TActor>,\n state: TState,\n): TargetSelection<TState, TActor> => {\n if (step.scope === null)\n return { targets: [{ step, state, binding: null }], failure: null }\n try {\n return {\n targets: selectScope(step, step.scope, state).map((binding) => ({\n step,\n state,\n binding,\n })),\n failure: null,\n }\n } catch (err) {\n if (err instanceof ScopeKeyError) throw err\n return {\n targets: [],\n failure: { reason: `scope selector threw: ${thrownMessage(err)}` },\n }\n }\n}\n\n/**\n * Why an address does not resolve, by kind. Each failure carries the error\n * the loud filter would throw, so the diagnosis (including the\n * currently-valid scope keys, where knowable) is constructed exactly once\n * and reads identically whether it is thrown at an addressed caller or\n * reported by a sweep. The kind is what lets a filter treat one failure\n * differently without re-deriving how the address failed — `explain`\n * *answers* a defective selector (the listing published that exact link)\n * and throws everything else.\n */\nexport type TargetAddressFailure =\n | {\n /** The named step is not declared on the case type. */\n readonly kind: 'unknown-step'\n readonly error: UnknownStepError\n }\n | {\n /** The selector threw or returned a non-array over this Case State. */\n readonly kind: 'defective-selector'\n /** The selection failure's own words — what the listing's `$scope` entry reports. */\n readonly reason: string\n readonly error: ScopeKeyError\n }\n | {\n /**\n * A scope-key problem on an otherwise healthy step: a key given for an\n * unscoped step, a missing key on a scoped one, or a key no selected\n * element carries.\n */\n readonly kind: 'unscoped-key' | 'missing-key' | 'unknown-key'\n readonly error: ScopeKeyError\n }\n\n/** The outcome of addressing a step: the target, or the precise failure. */\nexport type TargetAddress<TState, TActor = unknown> =\n | { readonly target: StepTarget<TState, TActor>; readonly failure: null }\n | { readonly target: null; readonly failure: TargetAddressFailure }\n\n/**\n * The one implementation of addressing — \"step X (of element K) on this\n * state\". Total over everything except key integrity: an undeclared step\n * name, a missing/unknown scope key on a scoped step, a scope key on an\n * unscoped step, and a defective scope selector all come back as `failure`.\n * {@link ScopeKeyError} raised for duplicate or malformed keys (identity\n * corruption, from {@link selectScope}) still propagates — no filter may\n * absorb it.\n */\nexport const addressTarget = <S extends StandardSchemaV1, TActor>(\n definition: CaseTypeDefinition<S, TActor>,\n state: StandardSchemaV1.InferOutput<S>,\n stepName: string,\n scopeKey?: string,\n): TargetAddress<StandardSchemaV1.InferOutput<S>, TActor> => {\n const stepDefinition = definition.getStep(stepName)\n if (stepDefinition === undefined) {\n return {\n target: null,\n failure: {\n kind: 'unknown-step',\n error: new UnknownStepError(\n definition.name,\n stepName,\n definition.steps.map((declared) => declared.name),\n ),\n },\n }\n }\n\n if (stepDefinition.scope === null) {\n if (scopeKey !== undefined) {\n return {\n target: null,\n failure: {\n kind: 'unscoped-key',\n error: new ScopeKeyError(\n stepName,\n scopeKey,\n `scope key '${scopeKey}' given, but the step is not scoped`,\n ),\n },\n }\n }\n return {\n target: { step: stepDefinition, state, binding: null },\n failure: null,\n }\n }\n\n const selection = selectTargets(stepDefinition, state)\n if (selection.failure !== null) {\n return {\n target: null,\n failure: {\n kind: 'defective-selector',\n reason: selection.failure.reason,\n error: new ScopeKeyError(\n stepName,\n scopeKey ?? null,\n selection.failure.reason,\n ),\n },\n }\n }\n const known = selection.targets.map((target) => target.binding?.key ?? '')\n const selected =\n known.length > 0 ? known.join(', ') : '(no elements in scope)'\n if (scopeKey === undefined) {\n return {\n target: null,\n failure: {\n kind: 'missing-key',\n error: new ScopeKeyError(\n stepName,\n null,\n `scoped step: a scopeKey is required — currently selected: ${selected}`,\n ),\n },\n }\n }\n const bound = selection.targets.find(\n (target) => target.binding?.key === scopeKey,\n )\n if (bound === undefined) {\n return {\n target: null,\n failure: {\n kind: 'unknown-key',\n error: new ScopeKeyError(\n stepName,\n scopeKey,\n `no element in scope has key '${scopeKey}' — currently selected: ${selected}`,\n ),\n },\n }\n }\n return { target: bound, failure: null }\n}\n\n/**\n * Resolve \"step X (of element K) on this state\" — the addressing shared by\n * `explain` (a targeted probe) and the execution lifecycle's claim.\n *\n * The loud filter over {@link addressTarget}: an addressed caller named a\n * case and a step and is owed an answer about *those*, so every way of\n * failing to address throws with its precise message. Addressing a step you\n * cannot name is a caller bug, not a blocked affordance.\n */\nexport const resolveTarget = <S extends StandardSchemaV1, TActor>(\n definition: CaseTypeDefinition<S, TActor>,\n state: StandardSchemaV1.InferOutput<S>,\n stepName: string,\n scopeKey?: string,\n): StepTarget<StandardSchemaV1.InferOutput<S>, TActor> => {\n const address = addressTarget(definition, state, stepName, scopeKey)\n if (address.failure !== null) throw address.failure.error\n return address.target\n}\n\n/**\n * Evaluate one addressed step's guard against the state it was resolved on —\n * the single evaluation shared by `explain` and the claim, so the enforcement\n * moment and the explanation of it can never drift apart.\n */\nexport const evaluateTarget = <TState, TActor>(\n target: StepTarget<TState, TActor>,\n ctx: ComputationContext<TActor>,\n): GuardEvaluation =>\n evaluateGuard(target.step.guard, {\n state: target.state,\n actor: ctx.actor,\n asOf: ctx.asOf,\n ...(target.binding !== null && { scope: target.binding.element }),\n })\n"]}
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../src/model/target.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Step targeting: resolving \"step X, of element K, on this state\".\n *\n * A **step target** is a step definition plus its scope binding — the pair\n * that an affordance's identity (step × scope key) names. Everything\n * that acts on a step goes through here first: the execution lifecycle's\n * claim, `explain`, audit replay, and the affordance listing itself.\n *\n * It lives in the model because it is a fact about a {@link StepDefinition}\n * and a Case State, and nothing more — no store, no registry, no clock. Put\n * anywhere higher it would drag every consumer's imports upward toward the\n * engine, which is a facade none of them should need.\n *\n * ## One fan-out, filtered\n *\n * {@link selectTargets} is the only implementation of fan-out — expanding\n * one scoped step into its per-element targets — and it makes the one\n * distinction every consumer needs:\n *\n * - A **defective selector** — one that throws over historical state, or\n * returns something other than an array — is a *selection failure*,\n * reported as `failure` with no targets. Each caller decides what its\n * audience deserves: the affordance listing renders it as a blocked\n * `$scope` entry, a sweep skips the step, {@link resolveTarget} throws.\n * - A **key integrity violation** — duplicate or malformed scope keys —\n * corrupts affordance identity itself, so {@link ScopeKeyError} is loud\n * through every path. No caller may absorb it into an empty selection.\n *\n * Addressing — \"step X (of element K) on this state\" — is likewise one\n * implementation, {@link addressTarget}, answering with the target or a\n * {@link TargetAddressFailure} that names its kind. Its filters:\n * {@link resolveTarget} (loud: an addressed caller is owed the precise\n * failure, so it throws), audit replay (lenient: a sweep over the Journal\n * reports the failure as a value and keeps going), and `explain` (loud,\n * with one deliberate exception — a `defective-selector` failure is\n * *answered*, because the listing published that exact link).\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport { thrownMessage } from '../errors.js'\nimport type {\n GuardEvaluation,\n Instant,\n SingleConditionResult,\n} from '../guards/index.js'\nimport { evaluateGuard } from '../guards/index.js'\nimport type { CaseTypeDefinition } from './casetype.js'\nimport { ScopeKeyError, UnknownStepError } from './errors.js'\nimport type { StepDefinition } from './step.js'\n\n/**\n * The synthetic condition name under which a throwing or malformed scope\n * selector is reported. `$`-prefixed so it can never collide with an\n * author's condition names. This is engine guard vocabulary.\n */\nexport const SCOPE_FAILURE_CONDITION = '$scope'\n\n/**\n * The synthetic `$scope` entry as a condition result — the one spelling of\n * how a failed scope selection reports into an evaluation-shaped record: a\n * failed condition carrying the selector's reason.\n */\nconst scopeConditionResult = (failure: {\n readonly reason: string\n}): SingleConditionResult => ({\n name: SCOPE_FAILURE_CONDITION,\n section: 'requires',\n kind: 'condition',\n passed: false,\n reason: failure.reason,\n})\n\n/**\n * A defective selection as a full evaluation record — the\n * possible/permitted/available verdict stated once. The selection failed, so\n * nothing about the step is possible\n * on this case; `permits` were never reachable (they may read the element),\n * reported vacuously satisfied — the requires-level `$scope` failure is the\n * answer. The listing's blocked entry and `explain`'s answer for the same\n * link both derive from this.\n */\nexport const scopeFailureEvaluation = (\n asOf: string,\n failure: { readonly reason: string },\n): GuardEvaluation => ({\n asOf,\n possible: false,\n permitted: true,\n available: false,\n conditions: [scopeConditionResult(failure)],\n})\n\n/** What a guard is evaluated against, beyond state: the actor and the instant. */\nexport interface ComputationContext<TActor = unknown> {\n readonly actor: TActor\n /**\n * The instant to evaluate as of — always explicit. Conditions cannot read\n * the clock, so defaulting to now is the engine's job, done once at its\n * boundary; everything below it is pure and reconstructable.\n */\n readonly asOf: Instant\n}\n\n/** One element of a scoped step's selection, with the key that identifies it. */\nexport interface ScopeBinding {\n readonly element: unknown\n readonly key: string\n}\n\n/**\n * A step addressed by name (× scope key, if scoped): the step definition and\n * its scope binding, resolved against a given Case State.\n */\nexport interface StepTarget<TState, TActor = unknown, TCommit = unknown> {\n readonly step: StepDefinition<TState, TActor, TCommit>\n /**\n * The Case State the target was resolved against — the document its guard\n * is evaluated over. Carried on the target so a binding can never be\n * evaluated against a different document than the one that produced it.\n */\n readonly state: TState\n /** The bound element and its key, or `null` for an unscoped step. */\n readonly binding: ScopeBinding | null\n}\n\n/** The outcome of scope fan-out: the step's targets, or why selection produced none. */\nexport interface TargetSelection<TState, TActor = unknown, TCommit = unknown> {\n readonly targets: readonly StepTarget<TState, TActor, TCommit>[]\n /** The selection failure — a defective selector — or `null` when selection succeeded. */\n readonly failure: { readonly reason: string } | null\n}\n\n/**\n * Select a scoped step's elements and derive their keys, enforcing key\n * integrity: every key a non-empty string, unique within the selection —\n * scope keys are affordance identity, so violations throw\n * {@link ScopeKeyError} instead of degrading. A `select` that throws\n * (totality bug) is left to the caller to absorb or report.\n */\nconst selectScope = <TState, TActor, TCommit>(\n definition: StepDefinition<TState, TActor, TCommit>,\n scope: NonNullable<StepDefinition<TState, TActor, TCommit>['scope']>,\n state: TState,\n): readonly ScopeBinding[] => {\n const selected = scope.select(state)\n if (!Array.isArray(selected)) {\n throw new ScopeKeyError(\n definition.name,\n null,\n 'scope.select must return an array of elements',\n )\n }\n const seen = new Set<string>()\n return selected.map((element) => {\n let key: unknown\n try {\n key = scope.key(element)\n } catch (err) {\n throw new ScopeKeyError(\n definition.name,\n null,\n `scope.key threw: ${thrownMessage(err)}`,\n )\n }\n if (typeof key !== 'string' || key === '') {\n throw new ScopeKeyError(\n definition.name,\n null,\n 'scope.key must return a non-empty string for every element',\n )\n }\n if (seen.has(key)) {\n throw new ScopeKeyError(\n definition.name,\n key,\n `duplicate scope key '${key}' — scope keys are affordance identity and must be unique`,\n )\n }\n seen.add(key)\n return { element, key }\n })\n}\n\n/**\n * The one implementation of scope fan-out — every consumer (the affordance\n * listing, read tracing, addressing)\n * is a filter over this function.\n *\n * An unscoped step yields exactly one target; a scoped step yields one per\n * selected element, or none with a `failure` naming why when the selector is\n * defective. {@link ScopeKeyError} — identity corruption — propagates: it is\n * never a selection failure, and no caller may absorb it into \"no targets\".\n */\nexport const selectTargets = <TState, TActor, TCommit>(\n step: StepDefinition<TState, TActor, TCommit>,\n state: TState,\n): TargetSelection<TState, TActor, TCommit> => {\n if (step.scope === null)\n return { targets: [{ step, state, binding: null }], failure: null }\n try {\n return {\n targets: selectScope(step, step.scope, state).map((binding) => ({\n step,\n state,\n binding,\n })),\n failure: null,\n }\n } catch (err) {\n if (err instanceof ScopeKeyError) throw err\n return {\n targets: [],\n failure: { reason: `scope selector threw: ${thrownMessage(err)}` },\n }\n }\n}\n\n/**\n * Why an address does not resolve, by kind. Each failure carries the error\n * the loud filter would throw, so the diagnosis (including the\n * currently-valid scope keys, where knowable) is constructed exactly once\n * and reads identically whether it is thrown at an addressed caller or\n * reported by a sweep. The kind is what lets a filter treat one failure\n * differently without re-deriving how the address failed — `explain`\n * *answers* a defective selector (the listing published that exact link)\n * and throws everything else.\n */\nexport type TargetAddressFailure =\n | {\n /** The named step is not declared on the case type. */\n readonly kind: 'unknown-step'\n readonly error: UnknownStepError\n }\n | {\n /** The selector threw or returned a non-array over this Case State. */\n readonly kind: 'defective-selector'\n /** The selection failure's own words — what the listing's `$scope` entry reports. */\n readonly reason: string\n readonly error: ScopeKeyError\n }\n | {\n /**\n * A scope-key problem on an otherwise healthy step: a key given for an\n * unscoped step, a missing key on a scoped one, or a key no selected\n * element carries.\n */\n readonly kind: 'unscoped-key' | 'missing-key' | 'unknown-key'\n readonly error: ScopeKeyError\n }\n\n/** The outcome of addressing a step: the target, or the precise failure. */\nexport type TargetAddress<TState, TActor = unknown, TCommit = unknown> =\n | {\n readonly target: StepTarget<TState, TActor, TCommit>\n readonly failure: null\n }\n | { readonly target: null; readonly failure: TargetAddressFailure }\n\n/**\n * The one implementation of addressing — \"step X (of element K) on this\n * state\". Total over everything except key integrity: an undeclared step\n * name, a missing/unknown scope key on a scoped step, a scope key on an\n * unscoped step, and a defective scope selector all come back as `failure`.\n * {@link ScopeKeyError} raised for duplicate or malformed keys (identity\n * corruption, from {@link selectScope}) still propagates — no filter may\n * absorb it.\n */\nexport const addressTarget = <S extends StandardSchemaV1, TActor, TCommit>(\n definition: CaseTypeDefinition<S, TActor, TCommit>,\n state: StandardSchemaV1.InferOutput<S>,\n stepName: string,\n scopeKey?: string,\n): TargetAddress<StandardSchemaV1.InferOutput<S>, TActor, TCommit> => {\n const stepDefinition = definition.getStep(stepName)\n if (stepDefinition === undefined) {\n return {\n target: null,\n failure: {\n kind: 'unknown-step',\n error: new UnknownStepError(\n definition.name,\n stepName,\n definition.steps.map((declared) => declared.name),\n ),\n },\n }\n }\n\n if (stepDefinition.scope === null) {\n if (scopeKey !== undefined) {\n return {\n target: null,\n failure: {\n kind: 'unscoped-key',\n error: new ScopeKeyError(\n stepName,\n scopeKey,\n `scope key '${scopeKey}' given, but the step is not scoped`,\n ),\n },\n }\n }\n return {\n target: { step: stepDefinition, state, binding: null },\n failure: null,\n }\n }\n\n const selection = selectTargets(stepDefinition, state)\n if (selection.failure !== null) {\n return {\n target: null,\n failure: {\n kind: 'defective-selector',\n reason: selection.failure.reason,\n error: new ScopeKeyError(\n stepName,\n scopeKey ?? null,\n selection.failure.reason,\n ),\n },\n }\n }\n const known = selection.targets.map((target) => target.binding?.key ?? '')\n const selected =\n known.length > 0 ? known.join(', ') : '(no elements in scope)'\n if (scopeKey === undefined) {\n return {\n target: null,\n failure: {\n kind: 'missing-key',\n error: new ScopeKeyError(\n stepName,\n null,\n `scoped step: a scopeKey is required — currently selected: ${selected}`,\n ),\n },\n }\n }\n const bound = selection.targets.find(\n (target) => target.binding?.key === scopeKey,\n )\n if (bound === undefined) {\n return {\n target: null,\n failure: {\n kind: 'unknown-key',\n error: new ScopeKeyError(\n stepName,\n scopeKey,\n `no element in scope has key '${scopeKey}' — currently selected: ${selected}`,\n ),\n },\n }\n }\n return { target: bound, failure: null }\n}\n\n/**\n * Resolve \"step X (of element K) on this state\" — the addressing shared by\n * `explain` (a targeted probe) and the execution lifecycle's claim.\n *\n * The loud filter over {@link addressTarget}: an addressed caller named a\n * case and a step and is owed an answer about *those*, so every way of\n * failing to address throws with its precise message. Addressing a step you\n * cannot name is a caller bug, not a blocked affordance.\n */\nexport const resolveTarget = <S extends StandardSchemaV1, TActor, TCommit>(\n definition: CaseTypeDefinition<S, TActor, TCommit>,\n state: StandardSchemaV1.InferOutput<S>,\n stepName: string,\n scopeKey?: string,\n): StepTarget<StandardSchemaV1.InferOutput<S>, TActor, TCommit> => {\n const address = addressTarget(definition, state, stepName, scopeKey)\n if (address.failure !== null) throw address.failure.error\n return address.target\n}\n\n/**\n * Evaluate one addressed step's guard against the state it was resolved on —\n * the single evaluation shared by `explain` and the claim, so the enforcement\n * moment and the explanation of it can never drift apart.\n */\nexport const evaluateTarget = <TState, TActor, TCommit>(\n target: StepTarget<TState, TActor, TCommit>,\n ctx: ComputationContext<TActor>,\n): GuardEvaluation =>\n evaluateGuard(target.step.guard, {\n state: target.state,\n actor: ctx.actor,\n asOf: ctx.asOf,\n ...(target.binding !== null && { scope: target.binding.element }),\n })\n"]}
@@ -0,0 +1,93 @@
1
+ /** Public interface for storage adapters. Every member belongs to one coordinated store. */
2
+ import type { JournalEntry, JournalFilter } from './execution/journal.js';
3
+ import type { LifecyclePort } from './execution/port.js';
4
+ import type { Correlation, CorrelationRegistration } from './ingestion/correlation.js';
5
+ import type { DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalEvent } from './ingestion/ingest.js';
6
+ import type { MigrationOptions } from './migration/migrate.js';
7
+ import type { StoredCase } from './store/store.js';
8
+ export interface CaseListOptions {
9
+ readonly caseTypeName?: string;
10
+ /** Active cases by default. */
11
+ readonly includeEnded?: boolean;
12
+ /** Default 100; integer between 1 and 1000. */
13
+ readonly limit?: number;
14
+ /** Opaque continuation returned by this adapter for the same filters. */
15
+ readonly cursor?: string;
16
+ }
17
+ export interface CasePage {
18
+ readonly cases: readonly StoredCase[];
19
+ readonly nextCursor: string | null;
20
+ }
21
+ export interface CaseRepository {
22
+ /** State is already schema-validated by core. */
23
+ create(caseTypeName: string, state: unknown): Promise<StoredCase>;
24
+ /** Throws CaseNotFoundError for an unknown id; state is unvalidated. */
25
+ get(caseId: string): Promise<StoredCase>;
26
+ /** Newest first, with a stable tie breaker. No duplicate records across pages. */
27
+ list(options: CaseListOptions & {
28
+ readonly caseTypeNames: readonly string[];
29
+ readonly limit: number;
30
+ }): Promise<CasePage>;
31
+ }
32
+ export interface CorrelationRepository {
33
+ /** Register or replace by (system, externalId), preserving id and createdAt. */
34
+ register(registration: CorrelationRegistration): Promise<Correlation>;
35
+ lookup(system: string, externalId: string): Promise<Correlation | null>;
36
+ list(caseId: string, scopeKey?: string): Promise<readonly Correlation[]>;
37
+ }
38
+ export interface DeliveryRecord {
39
+ readonly id: string;
40
+ readonly system: string;
41
+ readonly externalId: string;
42
+ readonly idempotencyKey: string;
43
+ readonly status: 'pending' | 'executed' | 'dead-lettered';
44
+ readonly reason: DeadLetterReason | null;
45
+ readonly receivedAt: string;
46
+ }
47
+ export interface DeliverySettlement {
48
+ readonly status: 'executed' | 'dead-lettered';
49
+ readonly caseId?: string | null;
50
+ readonly scopeKey?: string | null;
51
+ readonly step?: string | null;
52
+ readonly reason?: DeadLetterReason | null;
53
+ readonly detail?: string | null;
54
+ readonly executionId?: string | null;
55
+ }
56
+ export interface DeliveryRepository {
57
+ /** Exactly one concurrent caller acquires a new or eligible dead-lettered delivery. */
58
+ acquire(event: ExternalEvent, key: string, reopenable: readonly DeadLetterReason[]): Promise<{
59
+ row: DeliveryRecord;
60
+ fresh: boolean;
61
+ }>;
62
+ /** Delivery bookkeeping is separate from the case execution's atomic commit. */
63
+ settle(id: string, outcome: DeliverySettlement): Promise<void>;
64
+ deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>;
65
+ }
66
+ export interface MigrationPage {
67
+ readonly cases: readonly {
68
+ readonly id: string;
69
+ readonly state: unknown;
70
+ }[];
71
+ readonly nextCursor: string | null;
72
+ }
73
+ export interface MigrationRepository {
74
+ /** Excludes cases with a completed marker; pagination order is adapter-owned. */
75
+ candidates(caseTypeName: string, marker: string, options: MigrationOptions, cursor: string | null, limit: number): Promise<MigrationPage>;
76
+ hasCompleted(caseId: string, marker: string): Promise<boolean>;
77
+ }
78
+ export interface EngineStorage<TCommit = unknown> {
79
+ readonly cases: CaseRepository;
80
+ readonly execution: LifecyclePort<TCommit>;
81
+ readonly journal: {
82
+ read(caseId: string, filter?: JournalFilter): Promise<readonly JournalEntry[]>;
83
+ };
84
+ readonly correlations: CorrelationRepository;
85
+ readonly deliveries: DeliveryRepository;
86
+ readonly migrations: MigrationRepository;
87
+ }
88
+ export { projectEntry } from './execution/journal.js';
89
+ export type { HeldClaim, LifecyclePort, LifecycleTx } from './execution/port.js';
90
+ export type { CommitEffect } from './model/handler.js';
91
+ export { mintId } from './store/ids.js';
92
+ export type { StoredCase } from './store/store.js';
93
+ export { validateAgainstSchema } from './store/store.js';
@@ -0,0 +1,4 @@
1
+ export { projectEntry } from './execution/journal.js';
2
+ export { mintId } from './store/ids.js';
3
+ export { validateAgainstSchema } from './store/store.js';
4
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAmHA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAGrD,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA","sourcesContent":["/** Public interface for storage adapters. Every member belongs to one coordinated store. */\nimport type { JournalEntry, JournalFilter } from './execution/journal.js'\nimport type { LifecyclePort } from './execution/port.js'\nimport type {\n Correlation,\n CorrelationRegistration,\n} from './ingestion/correlation.js'\nimport type {\n DeadLetter,\n DeadLetterFilter,\n DeadLetterReason,\n ExternalEvent,\n} from './ingestion/ingest.js'\nimport type { MigrationOptions } from './migration/migrate.js'\nimport type { StoredCase } from './store/store.js'\n\nexport interface CaseListOptions {\n readonly caseTypeName?: string\n /** Active cases by default. */\n readonly includeEnded?: boolean\n /** Default 100; integer between 1 and 1000. */\n readonly limit?: number\n /** Opaque continuation returned by this adapter for the same filters. */\n readonly cursor?: string\n}\n\nexport interface CasePage {\n readonly cases: readonly StoredCase[]\n readonly nextCursor: string | null\n}\n\nexport interface CaseRepository {\n /** State is already schema-validated by core. */\n create(caseTypeName: string, state: unknown): Promise<StoredCase>\n /** Throws CaseNotFoundError for an unknown id; state is unvalidated. */\n get(caseId: string): Promise<StoredCase>\n /** Newest first, with a stable tie breaker. No duplicate records across pages. */\n list(\n options: CaseListOptions & {\n readonly caseTypeNames: readonly string[]\n readonly limit: number\n },\n ): Promise<CasePage>\n}\n\nexport interface CorrelationRepository {\n /** Register or replace by (system, externalId), preserving id and createdAt. */\n register(registration: CorrelationRegistration): Promise<Correlation>\n lookup(system: string, externalId: string): Promise<Correlation | null>\n list(caseId: string, scopeKey?: string): Promise<readonly Correlation[]>\n}\n\nexport interface DeliveryRecord {\n readonly id: string\n readonly system: string\n readonly externalId: string\n readonly idempotencyKey: string\n readonly status: 'pending' | 'executed' | 'dead-lettered'\n readonly reason: DeadLetterReason | null\n readonly receivedAt: string\n}\n\nexport interface DeliverySettlement {\n readonly status: 'executed' | 'dead-lettered'\n readonly caseId?: string | null\n readonly scopeKey?: string | null\n readonly step?: string | null\n readonly reason?: DeadLetterReason | null\n readonly detail?: string | null\n readonly executionId?: string | null\n}\n\nexport interface DeliveryRepository {\n /** Exactly one concurrent caller acquires a new or eligible dead-lettered delivery. */\n acquire(\n event: ExternalEvent,\n key: string,\n reopenable: readonly DeadLetterReason[],\n ): Promise<{ row: DeliveryRecord; fresh: boolean }>\n /** Delivery bookkeeping is separate from the case execution's atomic commit. */\n settle(id: string, outcome: DeliverySettlement): Promise<void>\n deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>\n}\n\nexport interface MigrationPage {\n readonly cases: readonly { readonly id: string; readonly state: unknown }[]\n readonly nextCursor: string | null\n}\n\nexport interface MigrationRepository {\n /** Excludes cases with a completed marker; pagination order is adapter-owned. */\n candidates(\n caseTypeName: string,\n marker: string,\n options: MigrationOptions,\n cursor: string | null,\n limit: number,\n ): Promise<MigrationPage>\n hasCompleted(caseId: string, marker: string): Promise<boolean>\n}\n\nexport interface EngineStorage<TCommit = unknown> {\n readonly cases: CaseRepository\n readonly execution: LifecyclePort<TCommit>\n readonly journal: {\n read(\n caseId: string,\n filter?: JournalFilter,\n ): Promise<readonly JournalEntry[]>\n }\n readonly correlations: CorrelationRepository\n readonly deliveries: DeliveryRepository\n readonly migrations: MigrationRepository\n}\n\nexport { projectEntry } from './execution/journal.js'\nexport type { HeldClaim, LifecyclePort, LifecycleTx } from './execution/port.js'\nexport type { CommitEffect } from './model/handler.js'\nexport { mintId } from './store/ids.js'\nexport type { StoredCase } from './store/store.js'\nexport { validateAgainstSchema } from './store/store.js'\n"]}
@@ -8,8 +8,7 @@ export type IdKind = 'case' | 'execution' | 'journal' | 'correlation' | 'event';
8
8
  *
9
9
  * Every framework-generated id carries its kind, so an id is
10
10
  * self-describing wherever it travels — a log line, a journal row's
11
- * `cause`, a correlation, a support ticket. The columns holding them are
12
- * `text`; nothing anywhere parses the id back apart — the prefix is for
11
+ * `cause`, a correlation, a support ticket. Storage keeps them as strings; nothing parses the id back apart — the prefix is for
13
12
  * humans, and equality is the only operation ids support.
14
13
  */
15
14
  export declare const mintId: (kind: IdKind) => string;
package/dist/store/ids.js CHANGED
@@ -4,8 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  *
5
5
  * Every framework-generated id carries its kind, so an id is
6
6
  * self-describing wherever it travels — a log line, a journal row's
7
- * `cause`, a correlation, a support ticket. The columns holding them are
8
- * `text`; nothing anywhere parses the id back apart — the prefix is for
7
+ * `cause`, a correlation, a support ticket. Storage keeps them as strings; nothing parses the id back apart — the prefix is for
9
8
  * humans, and equality is the only operation ids support.
10
9
  */
11
10
  export const mintId = (kind) => `${kind}:${randomUUID()}`;
@@ -1 +1 @@
1
- {"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/store/ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAQxC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,EAAE,CAAA","sourcesContent":["import { randomUUID } from 'node:crypto'\n\n/**\n * The kinds of id the framework mints. One entry per entity that gets an\n * id at runtime.\n */\nexport type IdKind = 'case' | 'execution' | 'journal' | 'correlation' | 'event'\n\n/**\n * Mint a typed id: `kind:uuid`.\n *\n * Every framework-generated id carries its kind, so an id is\n * self-describing wherever it travels — a log line, a journal row's\n * `cause`, a correlation, a support ticket. The columns holding them are\n * `text`; nothing anywhere parses the id back apart — the prefix is for\n * humans, and equality is the only operation ids support.\n */\nexport const mintId = (kind: IdKind): string => `${kind}:${randomUUID()}`\n"]}
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/store/ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAQxC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,EAAE,CAAA","sourcesContent":["import { randomUUID } from 'node:crypto'\n\n/**\n * The kinds of id the framework mints. One entry per entity that gets an\n * id at runtime.\n */\nexport type IdKind = 'case' | 'execution' | 'journal' | 'correlation' | 'event'\n\n/**\n * Mint a typed id: `kind:uuid`.\n *\n * Every framework-generated id carries its kind, so an id is\n * self-describing wherever it travels — a log line, a journal row's\n * `cause`, a correlation, a support ticket. Storage keeps them as strings; nothing parses the id back apart — the prefix is for\n * humans, and equality is the only operation ids support.\n */\nexport const mintId = (kind: IdKind): string => `${kind}:${randomUUID()}`\n"]}
@@ -1,12 +1,7 @@
1
- export { bootstrap, CASE_TABLES, FRAMEWORK_SCHEMA } from './bootstrap.js';
2
1
  export { CaseNotFoundError, CaseStateValidationError } from './errors.js';
3
2
  export type { IdKind } from './ids.js';
4
3
  export { mintId } from './ids.js';
5
- export type { DatabaseAccess, PoolLike, Queryable, Transaction, } from './queryable.js';
6
- export { queryableOf } from './queryable.js';
7
4
  export type { CaseTypeLookup, ResolvedCase } from './resolve.js';
8
- export { resolveCase, resolveCaseForUpdate, resolveStoredState, validateCaseState, } from './resolve.js';
9
- export type { SqlWhere } from './sql.js';
10
- export { sqlWhere } from './sql.js';
11
- export type { CaseHandle, Dormancy } from './store.js';
12
- export { insertCase, selectCase, selectCaseForUpdate, selectCaseUntyped, updateCaseState, validateAgainstSchema, } from './store.js';
5
+ export { resolveCase, resolveStoredState, validateCaseState, } from './resolve.js';
6
+ export type { CaseHandle, Dormancy, StoredCase } from './store.js';
7
+ export { validateAgainstSchema } from './store.js';
@@ -1,9 +1,5 @@
1
- // Postgres persistence + case store.
2
- export { bootstrap, CASE_TABLES, FRAMEWORK_SCHEMA } from './bootstrap.js';
3
1
  export { CaseNotFoundError, CaseStateValidationError } from './errors.js';
4
2
  export { mintId } from './ids.js';
5
- export { queryableOf } from './queryable.js';
6
- export { resolveCase, resolveCaseForUpdate, resolveStoredState, validateCaseState, } from './resolve.js';
7
- export { sqlWhere } from './sql.js';
8
- export { insertCase, selectCase, selectCaseForUpdate, selectCaseUntyped, updateCaseState, validateAgainstSchema, } from './store.js';
3
+ export { resolveCase, resolveStoredState, validateCaseState, } from './resolve.js';
4
+ export { validateAgainstSchema } from './store.js';
9
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAOjC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEnC,OAAO,EACL,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,GACtB,MAAM,YAAY,CAAA","sourcesContent":["// Postgres persistence + case store.\nexport { bootstrap, CASE_TABLES, FRAMEWORK_SCHEMA } from './bootstrap.js'\nexport { CaseNotFoundError, CaseStateValidationError } from './errors.js'\nexport type { IdKind } from './ids.js'\nexport { mintId } from './ids.js'\nexport type {\n DatabaseAccess,\n PoolLike,\n Queryable,\n Transaction,\n} from './queryable.js'\nexport { queryableOf } from './queryable.js'\nexport type { CaseTypeLookup, ResolvedCase } from './resolve.js'\nexport {\n resolveCase,\n resolveCaseForUpdate,\n resolveStoredState,\n validateCaseState,\n} from './resolve.js'\nexport type { SqlWhere } from './sql.js'\nexport { sqlWhere } from './sql.js'\nexport type { CaseHandle, Dormancy } from './store.js'\nexport {\n insertCase,\n selectCase,\n selectCaseForUpdate,\n selectCaseUntyped,\n updateCaseState,\n validateAgainstSchema,\n} from './store.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEjC,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA","sourcesContent":["export { CaseNotFoundError, CaseStateValidationError } from './errors.js'\nexport type { IdKind } from './ids.js'\nexport { mintId } from './ids.js'\nexport type { CaseTypeLookup, ResolvedCase } from './resolve.js'\nexport {\n resolveCase,\n resolveStoredState,\n validateCaseState,\n} from './resolve.js'\nexport type { CaseHandle, Dormancy, StoredCase } from './store.js'\nexport { validateAgainstSchema } from './store.js'\n"]}
@@ -9,7 +9,7 @@
9
9
  * document that fails validation is a real decision, so it is expressed
10
10
  * here as the interface rather than left to each caller:
11
11
  *
12
- * - {@link resolveCase} / {@link resolveCaseForUpdate} are **loud**. Their
12
+ * - {@link resolveCase} are **loud**. Their
13
13
  * callers were handed a case id by somebody and owe them an answer about
14
14
  * *that* case; a document that no longer validates is an app bug and says
15
15
  * so ({@link CaseStateValidationError}).
@@ -21,13 +21,12 @@
21
21
  * (`addressTarget`'s value vs `resolveTarget`'s throw), for the same reason.
22
22
  */
23
23
  import type { AnyCaseType } from '../model/index.js';
24
- import type { Queryable, Transaction } from './queryable.js';
25
24
  import type { CaseHandle } from './store.js';
26
25
  /** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */
27
- export type CaseTypeLookup = (caseTypeName: string) => AnyCaseType;
26
+ export type CaseTypeLookup<TCommit = unknown> = (caseTypeName: string) => AnyCaseType<TCommit>;
28
27
  /** A case row, its Case Type definition, and its validated Case State. */
29
- export interface ResolvedCase {
30
- readonly definition: AnyCaseType;
28
+ export interface ResolvedCase<TCommit = unknown> {
29
+ readonly definition: AnyCaseType<TCommit>;
31
30
  /** The row as persisted. Its `state` is the raw document; prefer {@link ResolvedCase.state}. */
32
31
  readonly handle: CaseHandle<unknown>;
33
32
  /** The stored Case State, validated against the definition's schema (defaults applied). */
@@ -41,7 +40,7 @@ export interface ResolvedCase {
41
40
  * for a handler's return. One function, because "does this document satisfy
42
41
  * the case type" is one question however the document was obtained.
43
42
  */
44
- export declare const validateCaseState: (definition: AnyCaseType, value: unknown, context?: string) => Promise<unknown>;
43
+ export declare const validateCaseState: <TCommit>(definition: AnyCaseType<TCommit>, value: unknown, context?: string) => Promise<unknown>;
45
44
  /**
46
45
  * Validate a Case State document already in hand, leniently: `null` when it
47
46
  * no longer satisfies its Case Type's schema.
@@ -54,13 +53,7 @@ export declare const validateCaseState: (definition: AnyCaseType, value: unknown
54
53
  * Wrapped in an object rather than returned bare, because a valid Case State
55
54
  * may legitimately *be* `null` and a sweep must not confuse the two.
56
55
  */
57
- export declare const resolveStoredState: (definition: AnyCaseType, value: unknown) => Promise<{
56
+ export declare const resolveStoredState: <TCommit>(definition: AnyCaseType<TCommit>, value: unknown) => Promise<{
58
57
  readonly state: unknown;
59
58
  } | null>;
60
- /** Load a case and resolve it against the registered definitions. Loud — see this module's note. */
61
- export declare const resolveCase: (db: Queryable, caseTypeFor: CaseTypeLookup, caseId: string) => Promise<ResolvedCase>;
62
- /**
63
- * {@link resolveCase} taking the case row's lock — the execution lifecycle's
64
- * serialization point.
65
- */
66
- export declare const resolveCaseForUpdate: (tx: Transaction, caseTypeFor: CaseTypeLookup, caseId: string) => Promise<ResolvedCase>;
59
+ export declare const resolveCase: <TCommit>(handle: CaseHandle<unknown>, caseTypeFor: CaseTypeLookup<TCommit>) => Promise<ResolvedCase<TCommit>>;
@@ -9,7 +9,7 @@
9
9
  * document that fails validation is a real decision, so it is expressed
10
10
  * here as the interface rather than left to each caller:
11
11
  *
12
- * - {@link resolveCase} / {@link resolveCaseForUpdate} are **loud**. Their
12
+ * - {@link resolveCase} are **loud**. Their
13
13
  * callers were handed a case id by somebody and owe them an answer about
14
14
  * *that* case; a document that no longer validates is an app bug and says
15
15
  * so ({@link CaseStateValidationError}).
@@ -21,7 +21,7 @@
21
21
  * (`addressTarget`'s value vs `resolveTarget`'s throw), for the same reason.
22
22
  */
23
23
  import { CaseStateValidationError } from './errors.js';
24
- import { selectCaseForUpdate, selectCaseUntyped, validateAgainstSchema, } from './store.js';
24
+ import { validateAgainstSchema } from './store.js';
25
25
  /**
26
26
  * Validate a Case State document against a Case Type's schema, loudly.
27
27
  *
@@ -53,19 +53,12 @@ export const resolveStoredState = async (definition, value) => {
53
53
  throw error;
54
54
  }
55
55
  };
56
- const resolved = async (handle, caseTypeFor) => {
56
+ export const resolveCase = async (handle, caseTypeFor) => {
57
57
  const definition = caseTypeFor(handle.caseTypeName);
58
58
  return {
59
59
  definition,
60
60
  handle,
61
- state: await validateCaseState(definition, handle.state),
61
+ state: await validateCaseState(definition, handle.state, `stored state for case '${handle.id}'`),
62
62
  };
63
63
  };
64
- /** Load a case and resolve it against the registered definitions. Loud — see this module's note. */
65
- export const resolveCase = async (db, caseTypeFor, caseId) => resolved(await selectCaseUntyped(db, caseId), caseTypeFor);
66
- /**
67
- * {@link resolveCase} taking the case row's lock — the execution lifecycle's
68
- * serialization point.
69
- */
70
- export const resolveCaseForUpdate = async (tx, caseTypeFor, caseId) => resolved(await selectCaseForUpdate(tx, caseId), caseTypeFor);
71
64
  //# sourceMappingURL=resolve.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/store/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAGtD,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,YAAY,CAAA;AAcnB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,UAAuB,EACvB,KAAc,EACd,OAAO,GAAG,cAAc,EACN,EAAE,CAAC,qBAAqB,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,UAAuB,EACvB,KAAc,EAC+B,EAAE;IAC/C,IAAI,CAAC;QACH,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAA;IAC9D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,wBAAwB;YAAE,OAAO,IAAI,CAAA;QAC1D,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,KAAK,EACpB,MAA2B,EAC3B,WAA2B,EACJ,EAAE;IACzB,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACnD,OAAO;QACL,UAAU;QACV,MAAM;QACN,KAAK,EAAE,MAAM,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC;KACzD,CAAA;AACH,CAAC,CAAA;AAED,oGAAoG;AACpG,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,EAAa,EACb,WAA2B,EAC3B,MAAc,EACS,EAAE,CACzB,QAAQ,CAAC,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,CAAA;AAE5D;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,EAAe,EACf,WAA2B,EAC3B,MAAc,EACS,EAAE,CACzB,QAAQ,CAAC,MAAM,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,CAAA","sourcesContent":["/**\n * Where the registry and the store meet: turning a case row into a Case Type\n * definition and a Case State that can be trusted.\n *\n * Every read path needs the same three moves, in the same order — load the\n * row, resolve `case_type` against the registered definitions, validate the\n * stored document against that definition's schema — because the schema to\n * validate against is only knowable *from* the row. What to do with a\n * document that fails validation is a real decision, so it is expressed\n * here as the interface rather than left to each caller:\n *\n * - {@link resolveCase} / {@link resolveCaseForUpdate} are **loud**. Their\n * callers were handed a case id by somebody and owe them an answer about\n * *that* case; a document that no longer validates is an app bug and says\n * so ({@link CaseStateValidationError}).\n * - {@link resolveStoredState} is **lenient**. Its callers sweep — a\n * migration scanning a case type — and one\n * unreadable case must not take the sweep down.\n *\n * The same lenient/loud pair the model draws around scope selection\n * (`addressTarget`'s value vs `resolveTarget`'s throw), for the same reason.\n */\n\nimport type { AnyCaseType } from '../model/index.js'\nimport { CaseStateValidationError } from './errors.js'\nimport type { Queryable, Transaction } from './queryable.js'\nimport type { CaseHandle } from './store.js'\nimport {\n selectCaseForUpdate,\n selectCaseUntyped,\n validateAgainstSchema,\n} from './store.js'\n\n/** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */\nexport type CaseTypeLookup = (caseTypeName: string) => AnyCaseType\n\n/** A case row, its Case Type definition, and its validated Case State. */\nexport interface ResolvedCase {\n readonly definition: AnyCaseType\n /** The row as persisted. Its `state` is the raw document; prefer {@link ResolvedCase.state}. */\n readonly handle: CaseHandle<unknown>\n /** The stored Case State, validated against the definition's schema (defaults applied). */\n readonly state: unknown\n}\n\n/**\n * Validate a Case State document against a Case Type's schema, loudly.\n *\n * `context` names what is being validated, and lands in the error message:\n * `'stored state'` for a document read back, `\"state returned by step 'x'\"`\n * for a handler's return. One function, because \"does this document satisfy\n * the case type\" is one question however the document was obtained.\n */\nexport const validateCaseState = async (\n definition: AnyCaseType,\n value: unknown,\n context = 'stored state',\n): Promise<unknown> => validateAgainstSchema(definition.state, value, context)\n\n/**\n * Validate a Case State document already in hand, leniently: `null` when it\n * no longer satisfies its Case Type's schema.\n *\n * The lenient twin of {@link validateCaseState} — literally: the same single\n * Standard-Schema invocation (`validateAgainstSchema`), with the loud\n * verdict absorbed. Only the validation verdict is absorbed; a schema whose\n * `validate` itself throws is a definition bug and stays loud.\n *\n * Wrapped in an object rather than returned bare, because a valid Case State\n * may legitimately *be* `null` and a sweep must not confuse the two.\n */\nexport const resolveStoredState = async (\n definition: AnyCaseType,\n value: unknown,\n): Promise<{ readonly state: unknown } | null> => {\n try {\n return { state: await validateCaseState(definition, value) }\n } catch (error) {\n if (error instanceof CaseStateValidationError) return null\n throw error\n }\n}\n\nconst resolved = async (\n handle: CaseHandle<unknown>,\n caseTypeFor: CaseTypeLookup,\n): Promise<ResolvedCase> => {\n const definition = caseTypeFor(handle.caseTypeName)\n return {\n definition,\n handle,\n state: await validateCaseState(definition, handle.state),\n }\n}\n\n/** Load a case and resolve it against the registered definitions. Loud — see this module's note. */\nexport const resolveCase = async (\n db: Queryable,\n caseTypeFor: CaseTypeLookup,\n caseId: string,\n): Promise<ResolvedCase> =>\n resolved(await selectCaseUntyped(db, caseId), caseTypeFor)\n\n/**\n * {@link resolveCase} taking the case row's lock — the execution lifecycle's\n * serialization point.\n */\nexport const resolveCaseForUpdate = async (\n tx: Transaction,\n caseTypeFor: CaseTypeLookup,\n caseId: string,\n): Promise<ResolvedCase> =>\n resolved(await selectCaseForUpdate(tx, caseId), caseTypeFor)\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/store/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAEtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAgBlD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,UAAgC,EAChC,KAAc,EACd,OAAO,GAAG,cAAc,EACN,EAAE,CAAC,qBAAqB,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,UAAgC,EAChC,KAAc,EAC+B,EAAE;IAC/C,IAAI,CAAC;QACH,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAA;IAC9D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,wBAAwB;YAAE,OAAO,IAAI,CAAA;QAC1D,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,MAA2B,EAC3B,WAAoC,EACJ,EAAE;IAClC,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACnD,OAAO;QACL,UAAU;QACV,MAAM;QACN,KAAK,EAAE,MAAM,iBAAiB,CAC5B,UAAU,EACV,MAAM,CAAC,KAAK,EACZ,0BAA0B,MAAM,CAAC,EAAE,GAAG,CACvC;KACF,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Where the registry and the store meet: turning a case row into a Case Type\n * definition and a Case State that can be trusted.\n *\n * Every read path needs the same three moves, in the same order — load the\n * row, resolve `case_type` against the registered definitions, validate the\n * stored document against that definition's schema — because the schema to\n * validate against is only knowable *from* the row. What to do with a\n * document that fails validation is a real decision, so it is expressed\n * here as the interface rather than left to each caller:\n *\n * - {@link resolveCase} are **loud**. Their\n * callers were handed a case id by somebody and owe them an answer about\n * *that* case; a document that no longer validates is an app bug and says\n * so ({@link CaseStateValidationError}).\n * - {@link resolveStoredState} is **lenient**. Its callers sweep — a\n * migration scanning a case type — and one\n * unreadable case must not take the sweep down.\n *\n * The same lenient/loud pair the model draws around scope selection\n * (`addressTarget`'s value vs `resolveTarget`'s throw), for the same reason.\n */\n\nimport type { AnyCaseType } from '../model/index.js'\nimport { CaseStateValidationError } from './errors.js'\nimport type { CaseHandle } from './store.js'\nimport { validateAgainstSchema } from './store.js'\n\n/** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */\nexport type CaseTypeLookup<TCommit = unknown> = (\n caseTypeName: string,\n) => AnyCaseType<TCommit>\n\n/** A case row, its Case Type definition, and its validated Case State. */\nexport interface ResolvedCase<TCommit = unknown> {\n readonly definition: AnyCaseType<TCommit>\n /** The row as persisted. Its `state` is the raw document; prefer {@link ResolvedCase.state}. */\n readonly handle: CaseHandle<unknown>\n /** The stored Case State, validated against the definition's schema (defaults applied). */\n readonly state: unknown\n}\n\n/**\n * Validate a Case State document against a Case Type's schema, loudly.\n *\n * `context` names what is being validated, and lands in the error message:\n * `'stored state'` for a document read back, `\"state returned by step 'x'\"`\n * for a handler's return. One function, because \"does this document satisfy\n * the case type\" is one question however the document was obtained.\n */\nexport const validateCaseState = async <TCommit>(\n definition: AnyCaseType<TCommit>,\n value: unknown,\n context = 'stored state',\n): Promise<unknown> => validateAgainstSchema(definition.state, value, context)\n\n/**\n * Validate a Case State document already in hand, leniently: `null` when it\n * no longer satisfies its Case Type's schema.\n *\n * The lenient twin of {@link validateCaseState} — literally: the same single\n * Standard-Schema invocation (`validateAgainstSchema`), with the loud\n * verdict absorbed. Only the validation verdict is absorbed; a schema whose\n * `validate` itself throws is a definition bug and stays loud.\n *\n * Wrapped in an object rather than returned bare, because a valid Case State\n * may legitimately *be* `null` and a sweep must not confuse the two.\n */\nexport const resolveStoredState = async <TCommit>(\n definition: AnyCaseType<TCommit>,\n value: unknown,\n): Promise<{ readonly state: unknown } | null> => {\n try {\n return { state: await validateCaseState(definition, value) }\n } catch (error) {\n if (error instanceof CaseStateValidationError) return null\n throw error\n }\n}\n\nexport const resolveCase = async <TCommit>(\n handle: CaseHandle<unknown>,\n caseTypeFor: CaseTypeLookup<TCommit>,\n): Promise<ResolvedCase<TCommit>> => {\n const definition = caseTypeFor(handle.caseTypeName)\n return {\n definition,\n handle,\n state: await validateCaseState(\n definition,\n handle.state,\n `stored state for case '${handle.id}'`,\n ),\n }\n}\n"]}
@@ -1,11 +1,10 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { Queryable, Transaction } from './queryable.js';
3
2
  /**
4
3
  * A typed handle to one Case as persisted: the materialized Case State plus
5
- * the row-level bookkeeping the engine builds on.
4
+ * the stored bookkeeping the engine builds on.
6
5
  */
7
6
  export interface CaseHandle<State> {
8
- /** Case id — a UUID generated by the store on creation. */
7
+ /** Opaque case id generated by the storage adapter on creation. */
9
8
  id: string;
10
9
  /** Name of the Case Type this case is an instance of. */
11
10
  caseTypeName: string;
@@ -25,42 +24,6 @@ export interface CaseHandle<State> {
25
24
  * that validates a Case State goes through one of the two.
26
25
  */
27
26
  export declare const validateAgainstSchema: <S extends StandardSchemaV1>(schema: S, value: unknown, context: string) => Promise<StandardSchemaV1.InferOutput<S>>;
28
- /**
29
- * `createCase` against an explicit {@link Queryable} — the shared-transaction
30
- * seam. State is stringified explicitly so array-rooted documents are stored
31
- * as jsonb rather than misread as Postgres arrays.
32
- */
33
- export declare const insertCase: <S extends StandardSchemaV1>(db: Queryable, caseTypeName: string, stateSchema: S, initialState: StandardSchemaV1.InferInput<S>) => Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>>;
34
- /** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */
35
- export declare const selectCase: <S extends StandardSchemaV1>(db: Queryable, id: string, stateSchema: S) => Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>>;
36
- /**
37
- * Load a Case by id with the stored state **unvalidated** (`unknown`).
38
- *
39
- * Additive export for the engine: the engine learns which state
40
- * schema applies only *from* the loaded row — `case_type` names the
41
- * registered case type — so it must read the row before it can validate.
42
- * Every other caller should prefer {@link selectCase} / `loadCase`, which
43
- * validate; whoever consumes this handle owns validating `state` against the
44
- * type's schema before trusting it.
45
- */
46
- export declare const selectCaseUntyped: (db: Queryable, id: string) => Promise<CaseHandle<unknown>>;
47
- /**
48
- * Load a Case by id **for update** — `select … for update`, so the row lock is
49
- * held until the calling transaction ends.
50
- *
51
- * This is the execution lifecycle's serialization point: the claim and the
52
- * commit both take the case row's lock first, which is what makes the
53
- * one-in-flight-execution check (and an expired claim's takeover) a decision
54
- * no two transactions can make concurrently.
55
- */
56
- export declare const selectCaseForUpdate: (tx: Transaction, id: string) => Promise<CaseHandle<unknown>>;
57
- /** The dormancy transition a committing Execution applies to the case row (`end()` / `reopen()`). */
27
+ /** An unvalidated persisted record; core resolves its definition before use. */
28
+ export type StoredCase = CaseHandle<unknown>;
58
29
  export type Dormancy = 'ended' | 'reopened';
59
- /**
60
- * Write the next Case State, bump `seq`, and apply the Execution's dormancy
61
- * transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,
62
- * `null` leaves it exactly as it was. The state document is **not** validated
63
- * here — the execution lifecycle validates the handler's return against the
64
- * case type's schema before calling this, and reports a failure there.
65
- */
66
- export declare const updateCaseState: (tx: Transaction, id: string, state: unknown, dormancy?: Dormancy | null) => Promise<CaseHandle<unknown>>;
@@ -1,19 +1,4 @@
1
- import { FRAMEWORK_SCHEMA } from './bootstrap.js';
2
- import { CaseNotFoundError, CaseStateValidationError } from './errors.js';
3
- import { mintId } from './ids.js';
4
- const CASES = `${FRAMEWORK_SCHEMA}.cases`;
5
- const CASE_COLUMNS = 'id, case_type, state, seq, ended_at, created_at, updated_at';
6
- const toHandle = (row, state) => ({
7
- id: row.id,
8
- caseTypeName: row.case_type,
9
- state,
10
- // Number() is safe: seq counts executions of one human-paced case and will
11
- // never approach 2^53.
12
- seq: Number(row.seq),
13
- endedAt: row.ended_at,
14
- createdAt: row.created_at,
15
- updatedAt: row.updated_at,
16
- });
1
+ import { CaseStateValidationError } from './errors.js';
17
2
  /**
18
3
  * Validate a document against a state schema, loudly. The one implementation
19
4
  * of "this document must satisfy this schema or the caller hears about it" —
@@ -26,83 +11,4 @@ export const validateAgainstSchema = async (schema, value, context) => {
26
11
  throw new CaseStateValidationError(context, result.issues);
27
12
  return result.value;
28
13
  };
29
- /**
30
- * `createCase` against an explicit {@link Queryable} — the shared-transaction
31
- * seam. State is stringified explicitly so array-rooted documents are stored
32
- * as jsonb rather than misread as Postgres arrays.
33
- */
34
- export const insertCase = async (db, caseTypeName, stateSchema, initialState) => {
35
- const state = await validateAgainstSchema(stateSchema, initialState, 'initial state');
36
- const id = mintId('case');
37
- const { rows } = await db.query(`insert into ${CASES} (id, case_type, state)
38
- values ($1, $2, $3::jsonb)
39
- returning ${CASE_COLUMNS}`, [id, caseTypeName, JSON.stringify(state)]);
40
- const row = rows[0];
41
- if (!row)
42
- throw new Error(`insert into ${CASES} returned no row`);
43
- return toHandle(row, state);
44
- };
45
- /** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */
46
- export const selectCase = async (db, id, stateSchema) => {
47
- const handle = await selectCaseUntyped(db, id);
48
- const state = await validateAgainstSchema(stateSchema, handle.state, 'stored state');
49
- return { ...handle, state };
50
- };
51
- /**
52
- * Load a Case by id with the stored state **unvalidated** (`unknown`).
53
- *
54
- * Additive export for the engine: the engine learns which state
55
- * schema applies only *from* the loaded row — `case_type` names the
56
- * registered case type — so it must read the row before it can validate.
57
- * Every other caller should prefer {@link selectCase} / `loadCase`, which
58
- * validate; whoever consumes this handle owns validating `state` against the
59
- * type's schema before trusting it.
60
- */
61
- export const selectCaseUntyped = async (db, id) => {
62
- const { rows } = await db.query(`select ${CASE_COLUMNS} from ${CASES} where id = $1`, [id]);
63
- const row = rows[0];
64
- if (!row)
65
- throw new CaseNotFoundError(id);
66
- return toHandle(row, row.state);
67
- };
68
- /**
69
- * Load a Case by id **for update** — `select … for update`, so the row lock is
70
- * held until the calling transaction ends.
71
- *
72
- * This is the execution lifecycle's serialization point: the claim and the
73
- * commit both take the case row's lock first, which is what makes the
74
- * one-in-flight-execution check (and an expired claim's takeover) a decision
75
- * no two transactions can make concurrently.
76
- */
77
- export const selectCaseForUpdate = async (tx, id) => {
78
- const { rows } = await tx.query(`select ${CASE_COLUMNS} from ${CASES} where id = $1 for update`, [id]);
79
- const row = rows[0];
80
- if (!row)
81
- throw new CaseNotFoundError(id);
82
- return toHandle(row, row.state);
83
- };
84
- /**
85
- * Write the next Case State, bump `seq`, and apply the Execution's dormancy
86
- * transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,
87
- * `null` leaves it exactly as it was. The state document is **not** validated
88
- * here — the execution lifecycle validates the handler's return against the
89
- * case type's schema before calling this, and reports a failure there.
90
- */
91
- export const updateCaseState = async (tx, id, state, dormancy = null) => {
92
- const { rows } = await tx.query(`update ${CASES}
93
- set state = $2::jsonb,
94
- seq = seq + 1,
95
- ended_at = case
96
- when $3::text = 'ended' then now()
97
- when $3::text = 'reopened' then null
98
- else ended_at
99
- end,
100
- updated_at = now()
101
- where id = $1
102
- returning ${CASE_COLUMNS}`, [id, JSON.stringify(state), dormancy]);
103
- const row = rows[0];
104
- if (!row)
105
- throw new CaseNotFoundError(id);
106
- return toHandle(row, row.state);
107
- };
108
14
  //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/store/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec'\nimport { FRAMEWORK_SCHEMA } from './bootstrap.js'\nimport { CaseNotFoundError, CaseStateValidationError } from './errors.js'\nimport { mintId } from './ids.js'\nimport type { Queryable, Transaction } from './queryable.js'\n\nconst CASES = `${FRAMEWORK_SCHEMA}.cases`\nconst CASE_COLUMNS =\n 'id, case_type, state, seq, ended_at, created_at, updated_at'\n\n/**\n * A typed handle to one Case as persisted: the materialized Case State plus\n * the row-level bookkeeping the engine builds on.\n */\nexport interface CaseHandle<State> {\n /** Case id — a UUID generated by the store on creation. */\n id: string\n /** Name of the Case Type this case is an instance of. */\n caseTypeName: string\n /** The materialized Case State document, validated against the schema. */\n state: State\n /** Per-case monotonic sequence counter, 0 at creation, bumped by every committed Execution. */\n seq: number\n /** Dormancy marker set by `end()`; null while the case is active. */\n endedAt: Date | null\n createdAt: Date\n updatedAt: Date\n}\n\n/** Row shape of `affordance.cases` (int8 arrives as text from the pg driver). */\ntype CaseRow = {\n id: string\n case_type: string\n state: unknown\n seq: string | number\n ended_at: Date | null\n created_at: Date\n updated_at: Date\n}\n\nconst toHandle = <State>(row: CaseRow, state: State): CaseHandle<State> => ({\n id: row.id,\n caseTypeName: row.case_type,\n state,\n // Number() is safe: seq counts executions of one human-paced case and will\n // never approach 2^53.\n seq: Number(row.seq),\n endedAt: row.ended_at,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n})\n\n/**\n * Validate a document against a state schema, loudly. The one implementation\n * of \"this document must satisfy this schema or the caller hears about it\" —\n * `resolve.ts` layers the case-type registry on top of it, and everything\n * that validates a Case State goes through one of the two.\n */\nexport const validateAgainstSchema = async <S extends StandardSchemaV1>(\n schema: S,\n value: unknown,\n context: string,\n): Promise<StandardSchemaV1.InferOutput<S>> => {\n const result = await schema['~standard'].validate(value)\n if (result.issues) throw new CaseStateValidationError(context, result.issues)\n return result.value as StandardSchemaV1.InferOutput<S>\n}\n\n/**\n * `createCase` against an explicit {@link Queryable} — the shared-transaction\n * seam. State is stringified explicitly so array-rooted documents are stored\n * as jsonb rather than misread as Postgres arrays.\n */\nexport const insertCase = async <S extends StandardSchemaV1>(\n db: Queryable,\n caseTypeName: string,\n stateSchema: S,\n initialState: StandardSchemaV1.InferInput<S>,\n): Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>> => {\n const state = await validateAgainstSchema(\n stateSchema,\n initialState,\n 'initial state',\n )\n const id = mintId('case')\n const { rows } = await db.query<CaseRow>(\n `insert into ${CASES} (id, case_type, state)\n values ($1, $2, $3::jsonb)\n returning ${CASE_COLUMNS}`,\n [id, caseTypeName, JSON.stringify(state)],\n )\n const row = rows[0]\n if (!row) throw new Error(`insert into ${CASES} returned no row`)\n return toHandle(row, state)\n}\n\n/** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */\nexport const selectCase = async <S extends StandardSchemaV1>(\n db: Queryable,\n id: string,\n stateSchema: S,\n): Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>> => {\n const handle = await selectCaseUntyped(db, id)\n const state = await validateAgainstSchema(\n stateSchema,\n handle.state,\n 'stored state',\n )\n return { ...handle, state }\n}\n\n/**\n * Load a Case by id with the stored state **unvalidated** (`unknown`).\n *\n * Additive export for the engine: the engine learns which state\n * schema applies only *from* the loaded row — `case_type` names the\n * registered case type — so it must read the row before it can validate.\n * Every other caller should prefer {@link selectCase} / `loadCase`, which\n * validate; whoever consumes this handle owns validating `state` against the\n * type's schema before trusting it.\n */\nexport const selectCaseUntyped = async (\n db: Queryable,\n id: string,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await db.query<CaseRow>(\n `select ${CASE_COLUMNS} from ${CASES} where id = $1`,\n [id],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n\n/**\n * Load a Case by id **for update** — `select … for update`, so the row lock is\n * held until the calling transaction ends.\n *\n * This is the execution lifecycle's serialization point: the claim and the\n * commit both take the case row's lock first, which is what makes the\n * one-in-flight-execution check (and an expired claim's takeover) a decision\n * no two transactions can make concurrently.\n */\nexport const selectCaseForUpdate = async (\n tx: Transaction,\n id: string,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await tx.query<CaseRow>(\n `select ${CASE_COLUMNS} from ${CASES} where id = $1 for update`,\n [id],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n\n/** The dormancy transition a committing Execution applies to the case row (`end()` / `reopen()`). */\nexport type Dormancy = 'ended' | 'reopened'\n\n/**\n * Write the next Case State, bump `seq`, and apply the Execution's dormancy\n * transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,\n * `null` leaves it exactly as it was. The state document is **not** validated\n * here — the execution lifecycle validates the handler's return against the\n * case type's schema before calling this, and reports a failure there.\n */\nexport const updateCaseState = async (\n tx: Transaction,\n id: string,\n state: unknown,\n dormancy: Dormancy | null = null,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await tx.query<CaseRow>(\n `update ${CASES}\n set state = $2::jsonb,\n seq = seq + 1,\n ended_at = case\n when $3::text = 'ended' then now()\n when $3::text = 'reopened' then null\n else ended_at\n end,\n updated_at = now()\n where id = $1\n returning ${CASE_COLUMNS}`,\n [id, JSON.stringify(state), dormancy],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/store/store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAqBtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EACxC,MAAS,EACT,KAAc,EACd,OAAe,EAC2B,EAAE;IAC5C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACxD,IAAI,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IAC7E,OAAO,MAAM,CAAC,KAAwC,CAAA;AACxD,CAAC,CAAA","sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec'\nimport { CaseStateValidationError } from './errors.js'\n\n/**\n * A typed handle to one Case as persisted: the materialized Case State plus\n * the stored bookkeeping the engine builds on.\n */\nexport interface CaseHandle<State> {\n /** Opaque case id generated by the storage adapter on creation. */\n id: string\n /** Name of the Case Type this case is an instance of. */\n caseTypeName: string\n /** The materialized Case State document, validated against the schema. */\n state: State\n /** Per-case monotonic sequence counter, 0 at creation, bumped by every committed Execution. */\n seq: number\n /** Dormancy marker set by `end()`; null while the case is active. */\n endedAt: Date | null\n createdAt: Date\n updatedAt: Date\n}\n\n/**\n * Validate a document against a state schema, loudly. The one implementation\n * of \"this document must satisfy this schema or the caller hears about it\" —\n * `resolve.ts` layers the case-type registry on top of it, and everything\n * that validates a Case State goes through one of the two.\n */\nexport const validateAgainstSchema = async <S extends StandardSchemaV1>(\n schema: S,\n value: unknown,\n context: string,\n): Promise<StandardSchemaV1.InferOutput<S>> => {\n const result = await schema['~standard'].validate(value)\n if (result.issues) throw new CaseStateValidationError(context, result.issues)\n return result.value as StandardSchemaV1.InferOutput<S>\n}\n\n/** An unvalidated persisted record; core resolves its definition before use. */\nexport type StoredCase = CaseHandle<unknown>\n\nexport type Dormancy = 'ended' | 'reopened'\n"]}