@affordance/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/engine/compute.d.ts +147 -0
  4. package/dist/engine/compute.js +152 -0
  5. package/dist/engine/compute.js.map +1 -0
  6. package/dist/engine/engine.d.ts +162 -0
  7. package/dist/engine/engine.js +115 -0
  8. package/dist/engine/engine.js.map +1 -0
  9. package/dist/engine/errors.d.ts +6 -0
  10. package/dist/engine/errors.js +11 -0
  11. package/dist/engine/errors.js.map +1 -0
  12. package/dist/engine/index.d.ts +16 -0
  13. package/dist/engine/index.js +15 -0
  14. package/dist/engine/index.js.map +1 -0
  15. package/dist/errors.d.ts +46 -0
  16. package/dist/errors.js +42 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/execution/delta.d.ts +42 -0
  19. package/dist/execution/delta.js +93 -0
  20. package/dist/execution/delta.js.map +1 -0
  21. package/dist/execution/errors.d.ts +94 -0
  22. package/dist/execution/errors.js +132 -0
  23. package/dist/execution/errors.js.map +1 -0
  24. package/dist/execution/execute.d.ts +179 -0
  25. package/dist/execution/execute.js +356 -0
  26. package/dist/execution/execute.js.map +1 -0
  27. package/dist/execution/index.d.ts +25 -0
  28. package/dist/execution/index.js +20 -0
  29. package/dist/execution/index.js.map +1 -0
  30. package/dist/execution/journal.d.ts +190 -0
  31. package/dist/execution/journal.js +192 -0
  32. package/dist/execution/journal.js.map +1 -0
  33. package/dist/execution/port.d.ts +81 -0
  34. package/dist/execution/port.js +101 -0
  35. package/dist/execution/port.js.map +1 -0
  36. package/dist/execution/replay.d.ts +58 -0
  37. package/dist/execution/replay.js +59 -0
  38. package/dist/execution/replay.js.map +1 -0
  39. package/dist/execution/timers.d.ts +21 -0
  40. package/dist/execution/timers.js +26 -0
  41. package/dist/execution/timers.js.map +1 -0
  42. package/dist/execution/transaction.d.ts +24 -0
  43. package/dist/execution/transaction.js +49 -0
  44. package/dist/execution/transaction.js.map +1 -0
  45. package/dist/guards/condition.d.ts +86 -0
  46. package/dist/guards/condition.js +11 -0
  47. package/dist/guards/condition.js.map +1 -0
  48. package/dist/guards/evaluate.d.ts +143 -0
  49. package/dist/guards/evaluate.js +167 -0
  50. package/dist/guards/evaluate.js.map +1 -0
  51. package/dist/guards/guard.d.ts +136 -0
  52. package/dist/guards/guard.js +118 -0
  53. package/dist/guards/guard.js.map +1 -0
  54. package/dist/guards/index.d.ts +20 -0
  55. package/dist/guards/index.js +17 -0
  56. package/dist/guards/index.js.map +1 -0
  57. package/dist/guards/time.d.ts +32 -0
  58. package/dist/guards/time.js +45 -0
  59. package/dist/guards/time.js.map +1 -0
  60. package/dist/index.d.ts +31 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/ingestion/correlation.d.ts +54 -0
  64. package/dist/ingestion/correlation.js +78 -0
  65. package/dist/ingestion/correlation.js.map +1 -0
  66. package/dist/ingestion/index.d.ts +16 -0
  67. package/dist/ingestion/index.js +15 -0
  68. package/dist/ingestion/index.js.map +1 -0
  69. package/dist/ingestion/ingest.d.ts +196 -0
  70. package/dist/ingestion/ingest.js +275 -0
  71. package/dist/ingestion/ingest.js.map +1 -0
  72. package/dist/migration/index.d.ts +10 -0
  73. package/dist/migration/index.js +10 -0
  74. package/dist/migration/index.js.map +1 -0
  75. package/dist/migration/migrate.d.ts +119 -0
  76. package/dist/migration/migrate.js +195 -0
  77. package/dist/migration/migrate.js.map +1 -0
  78. package/dist/model/casetype.d.ts +53 -0
  79. package/dist/model/casetype.js +51 -0
  80. package/dist/model/casetype.js.map +1 -0
  81. package/dist/model/errors.d.ts +28 -0
  82. package/dist/model/errors.js +39 -0
  83. package/dist/model/errors.js.map +1 -0
  84. package/dist/model/handler.d.ts +120 -0
  85. package/dist/model/handler.js +16 -0
  86. package/dist/model/handler.js.map +1 -0
  87. package/dist/model/index.d.ts +24 -0
  88. package/dist/model/index.js +19 -0
  89. package/dist/model/index.js.map +1 -0
  90. package/dist/model/retry.d.ts +40 -0
  91. package/dist/model/retry.js +51 -0
  92. package/dist/model/retry.js.map +1 -0
  93. package/dist/model/scope.d.ts +67 -0
  94. package/dist/model/scope.js +19 -0
  95. package/dist/model/scope.js.map +1 -0
  96. package/dist/model/step.d.ts +232 -0
  97. package/dist/model/step.js +210 -0
  98. package/dist/model/step.js.map +1 -0
  99. package/dist/model/target.d.ts +175 -0
  100. package/dist/model/target.js +236 -0
  101. package/dist/model/target.js.map +1 -0
  102. package/dist/store/bootstrap.d.ts +57 -0
  103. package/dist/store/bootstrap.js +268 -0
  104. package/dist/store/bootstrap.js.map +1 -0
  105. package/dist/store/errors.d.ts +18 -0
  106. package/dist/store/errors.js +26 -0
  107. package/dist/store/errors.js.map +1 -0
  108. package/dist/store/ids.d.ts +15 -0
  109. package/dist/store/ids.js +12 -0
  110. package/dist/store/ids.js.map +1 -0
  111. package/dist/store/index.d.ts +12 -0
  112. package/dist/store/index.js +9 -0
  113. package/dist/store/index.js.map +1 -0
  114. package/dist/store/queryable.d.ts +60 -0
  115. package/dist/store/queryable.js +7 -0
  116. package/dist/store/queryable.js.map +1 -0
  117. package/dist/store/resolve.d.ts +66 -0
  118. package/dist/store/resolve.js +71 -0
  119. package/dist/store/resolve.js.map +1 -0
  120. package/dist/store/sql.d.ts +26 -0
  121. package/dist/store/sql.js +21 -0
  122. package/dist/store/sql.js.map +1 -0
  123. package/dist/store/store.d.ts +66 -0
  124. package/dist/store/store.js +108 -0
  125. package/dist/store/store.js.map +1 -0
  126. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mochicode LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @affordance/core
2
+
3
+ Compute what a case can do now, for a particular actor. A case is a persisted
4
+ object with state and independently guarded steps. Steps become available
5
+ through state changes, without a predefined ordering.
6
+
7
+ Requires Node 22.12+ and Postgres. ESM JavaScript and TypeScript declarations
8
+ are included. Zod is one option for the Standard Schema validation interface.
9
+
10
+ ```bash
11
+ npm install @affordance/core pg zod
12
+ ```
13
+
14
+ ```ts
15
+ import { actor, bootstrap, caseType, createEngine, stepsOf } from '@affordance/core'
16
+ import { Pool } from 'pg'
17
+ import { z } from 'zod'
18
+
19
+ const State = z.object({ ownerId: z.string(), approved: z.boolean() })
20
+ const step = stepsOf(State, actor<{ id: string }>())
21
+ const approval = caseType({
22
+ name: 'approval',
23
+ state: State,
24
+ steps: [step({
25
+ name: 'approve',
26
+ requires: { pending: (state) => !state.approved },
27
+ permits: { owner: (state, ctx) => state.ownerId === ctx.actor.id },
28
+ handler: async (state) => ({ ...state, approved: true }),
29
+ })],
30
+ })
31
+
32
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL })
33
+ try {
34
+ await bootstrap(pool)
35
+ const engine = createEngine({ db: { pool }, caseTypes: [approval] })
36
+ const current = await engine.createCase('approval', { ownerId: 'alice', approved: false })
37
+ console.log(await engine.affordances(current.id, { id: 'alice' }))
38
+ await engine.execute(current.id, 'approve', { actor: { id: 'alice' } })
39
+ console.log(await engine.journal(current.id))
40
+ } finally {
41
+ await pool.end()
42
+ }
43
+ ```
44
+
45
+ The engine claims a case, runs its async handler outside a database transaction,
46
+ then commits state and journal together. Executions serialize per case;
47
+ external effects must tolerate retries.
48
+
49
+ Read the [introduction](https://github.com/mochicodecom/affordance/blob/main/docs/tutorial/README.md)
50
+ 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).
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Affordance computation — the pure core of the engine.
3
+ *
4
+ * `computeAffordances` turns (case type, case snapshot, actor, asOf) into
5
+ * the framework's HATEOAS answer: every step's guard evaluated (with scope
6
+ * fan-out), split into available **affordances** and blocked steps with
7
+ * their unmet named conditions, "not possible" mechanically distinct from
8
+ * "not permitted for this actor". `computeExplanation` is the same
9
+ * machinery pointed at one step, returning the full per-condition record.
10
+ *
11
+ * Both are pure: no I/O, no clock — `asOf` is explicit (the
12
+ * *engine* defaults it to now at its boundary, never a condition). The
13
+ * records are plain JSON-serializable objects, deterministic in their
14
+ * inputs — the affordance JSON contract will serialize them
15
+ * verbatim.
16
+ *
17
+ * Addressing a step and binding its scope element is `../model/target.js`'s
18
+ * job rather than this module's: it is a fact about a step definition and a
19
+ * state, and every other consumer of it — the claim,
20
+ * audit replay — would otherwise have to import the engine to reach it.
21
+ */
22
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
23
+ import type { ConditionResult, GuardEvaluation, Instant } from '../guards/index.js';
24
+ import type { CaseTypeDefinition, ComputationContext } from '../model/index.js';
25
+ /**
26
+ * The slice of a case the pure computation needs: identity, state, dormancy.
27
+ * The engine builds it from a store handle; tests may construct one directly.
28
+ */
29
+ export interface CaseSnapshot<TState> {
30
+ readonly id: string;
31
+ readonly state: TState;
32
+ /** Dormancy marker (`end()`): a dormant case still computes — dormancy is never a freeze. */
33
+ readonly endedAt: Date | string | null;
34
+ }
35
+ /**
36
+ * One available affordance: a step (with its scope binding, if scoped) this
37
+ * actor can take now. Deliberately silent about the step's input — how an
38
+ * input is described to a caller is the adapter's translation
39
+ * (`Engine.inputSchemaFor` is the registry read it serializes from), and a
40
+ * second channel here could only drift from it.
41
+ */
42
+ export interface Affordance {
43
+ readonly step: string;
44
+ /** The bound element's scope key — present iff the step is scoped. */
45
+ readonly scopeKey?: string;
46
+ }
47
+ /**
48
+ * A step (× scope element, if scoped) that is currently not available, with
49
+ * the unmet named conditions saying why. `possible === false` means unmet
50
+ * `requires` — not possible on this case, for anyone; `permitted === false`
51
+ * means unmet `permits` — not permitted for this actor.
52
+ */
53
+ export interface BlockedStep {
54
+ readonly step: string;
55
+ readonly scopeKey?: string;
56
+ readonly possible: boolean;
57
+ readonly permitted: boolean;
58
+ /**
59
+ * The failed condition results, verbatim from guard evaluation — named,
60
+ * sectioned, with reasons and (for `after` conditions) basis/flip instants.
61
+ */
62
+ readonly unmet: readonly ConditionResult[];
63
+ }
64
+ /** The serializable affordances record for one case, one actor, one instant. */
65
+ export interface CaseAffordances {
66
+ readonly caseId: string;
67
+ readonly caseTypeName: string;
68
+ /** The instant evaluated as of (ISO-8601 UTC). */
69
+ readonly asOf: string;
70
+ /** Dormancy marker (ISO-8601 UTC), `null` while the case is active. A dormant case still computes. */
71
+ readonly endedAt: string | null;
72
+ /** Available affordances, in step declaration order (scoped: selection order within a step). */
73
+ readonly affordances: readonly Affordance[];
74
+ /** Blocked steps (× scope element) with their unmet named conditions. */
75
+ readonly blocked: readonly BlockedStep[];
76
+ }
77
+ /** The full per-condition breakdown for one step (× scope element, if scoped). */
78
+ export interface AffordanceExplanation {
79
+ readonly caseId: string;
80
+ readonly caseTypeName: string;
81
+ readonly step: string;
82
+ /** The scope binding the explanation is about — present iff the step is scoped. */
83
+ readonly scopeKey?: string;
84
+ readonly asOf: string;
85
+ readonly endedAt: string | null;
86
+ /** The guards module's full evaluation record: every condition, passed and failed. */
87
+ readonly evaluation: GuardEvaluation;
88
+ }
89
+ /**
90
+ * Compute the affordances record for one case snapshot: every step's guard
91
+ * evaluated against (state, actor, asOf); scoped steps fan out to one
92
+ * independent evaluation per selected element. Handlers are never
93
+ * touched — this is a read (guards advise; enforcement is the claim's job).
94
+ *
95
+ * A scoped step whose selector is defective is absorbed into a blocked entry
96
+ * under the synthetic `$scope` condition (one defective selector must not
97
+ * take down the case's listing); scope-*key* integrity violations throw
98
+ * `ScopeKeyError` (identity corruption — `selectTargets` never absorbs it).
99
+ * A scoped step selecting zero elements contributes nothing to either list.
100
+ */
101
+ export declare const computeAffordances: <S extends StandardSchemaV1, TActor>(definition: CaseTypeDefinition<S, TActor>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, ctx: ComputationContext<TActor>) => CaseAffordances;
102
+ /** An `explain` request as a caller states it: everything optional. */
103
+ export interface ExplainRequest {
104
+ /** Required when the step is scoped; identifies the element. */
105
+ readonly scopeKey?: string;
106
+ /** The actor to evaluate `permits` against; omit to probe `requires` alone. */
107
+ readonly actor?: unknown;
108
+ /** The instant to evaluate as of; defaults through the supplied clock. */
109
+ readonly asOf?: Instant;
110
+ }
111
+ /**
112
+ * Normalize an {@link ExplainRequest} into the {@link ComputationContext}
113
+ * the pure computation runs on — the engine boundary's one normalization,
114
+ * stated here so the rule and {@link computeExplanation} share a test
115
+ * surface:
116
+ *
117
+ * - An **absent** `actor` key means the requires-only probe ({@link NO_ACTOR}
118
+ * — `permits` conditions are reported un-evaluated). A key that is
119
+ * *present but `undefined`* is an actor like any other: the caller said
120
+ * who is asking, and the answer is about them.
121
+ * - `asOf` defaults through `now` — the clock stops here; everything below
122
+ * is pure.
123
+ * - `scopeKey` is carried only when given, so "unscoped" stays an absent
124
+ * key rather than an `undefined` value.
125
+ */
126
+ export declare const explainContext: <TActor = unknown>(request: ExplainRequest, now: () => Instant) => ComputationContext<TActor> & {
127
+ readonly scopeKey?: string;
128
+ };
129
+ /**
130
+ * The full per-condition breakdown for one step. Loud where the listing is
131
+ * lenient — `explain` is a targeted probe, so a step name the case type
132
+ * doesn't declare, a missing/unknown scope key on a scoped step, or a scope
133
+ * key on an unscoped step all throw with precise messages (including the
134
+ * currently-valid scope keys, where knowable).
135
+ *
136
+ * One deliberate exception: a scoped step whose selector is **defective**
137
+ * answers rather than throws. The listing publishes exactly that condition
138
+ * as a blocked `$scope` entry with an `explain` link, so the link must be
139
+ * followable — the explanation *is* the `$scope` failure, in the same shape
140
+ * the listing reported it.
141
+ *
142
+ * `actor` is whatever the caller supplies; to ask "why can't *this* actor",
143
+ * pass that actor — `permits` conditions are evaluated against it verbatim.
144
+ */
145
+ export declare const computeExplanation: <S extends StandardSchemaV1, TActor>(definition: CaseTypeDefinition<S, TActor>, snapshot: CaseSnapshot<StandardSchemaV1.InferOutput<S>>, stepName: string, ctx: ComputationContext<TActor> & {
146
+ readonly scopeKey?: string;
147
+ }) => AffordanceExplanation;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Affordance computation — the pure core of the engine.
3
+ *
4
+ * `computeAffordances` turns (case type, case snapshot, actor, asOf) into
5
+ * the framework's HATEOAS answer: every step's guard evaluated (with scope
6
+ * fan-out), split into available **affordances** and blocked steps with
7
+ * their unmet named conditions, "not possible" mechanically distinct from
8
+ * "not permitted for this actor". `computeExplanation` is the same
9
+ * machinery pointed at one step, returning the full per-condition record.
10
+ *
11
+ * Both are pure: no I/O, no clock — `asOf` is explicit (the
12
+ * *engine* defaults it to now at its boundary, never a condition). The
13
+ * records are plain JSON-serializable objects, deterministic in their
14
+ * inputs — the affordance JSON contract will serialize them
15
+ * verbatim.
16
+ *
17
+ * Addressing a step and binding its scope element is `../model/target.js`'s
18
+ * job rather than this module's: it is a fact about a step definition and a
19
+ * state, and every other consumer of it — the claim,
20
+ * audit replay — would otherwise have to import the engine to reach it.
21
+ */
22
+ import { NO_ACTOR, toIso, unmetConditions } from '../guards/index.js';
23
+ import { addressTarget, evaluateTarget, scopeFailureEvaluation, selectTargets, } from '../model/index.js';
24
+ const toIsoOrNull = (value) => value === null ? null : toIso(value);
25
+ // The verdict lives with the model (`scopeFailureEvaluation`); this only
26
+ // reshapes it into a blocked entry.
27
+ const scopeFailureEntry = (stepName, asOf, reason) => {
28
+ const evaluation = scopeFailureEvaluation(asOf, { reason });
29
+ return {
30
+ step: stepName,
31
+ possible: evaluation.possible,
32
+ permitted: evaluation.permitted,
33
+ unmet: unmetConditions(evaluation),
34
+ };
35
+ };
36
+ const toAffordance = (stepName, scopeKey) => ({
37
+ step: stepName,
38
+ ...(scopeKey !== undefined && { scopeKey }),
39
+ });
40
+ const toBlocked = (stepName, evaluation, scopeKey) => ({
41
+ step: stepName,
42
+ ...(scopeKey !== undefined && { scopeKey }),
43
+ possible: evaluation.possible,
44
+ permitted: evaluation.permitted,
45
+ unmet: unmetConditions(evaluation),
46
+ });
47
+ /**
48
+ * Compute the affordances record for one case snapshot: every step's guard
49
+ * evaluated against (state, actor, asOf); scoped steps fan out to one
50
+ * independent evaluation per selected element. Handlers are never
51
+ * touched — this is a read (guards advise; enforcement is the claim's job).
52
+ *
53
+ * A scoped step whose selector is defective is absorbed into a blocked entry
54
+ * under the synthetic `$scope` condition (one defective selector must not
55
+ * take down the case's listing); scope-*key* integrity violations throw
56
+ * `ScopeKeyError` (identity corruption — `selectTargets` never absorbs it).
57
+ * A scoped step selecting zero elements contributes nothing to either list.
58
+ */
59
+ export const computeAffordances = (definition, snapshot, ctx) => {
60
+ const asOf = toIso(ctx.asOf);
61
+ const affordances = [];
62
+ const blocked = [];
63
+ for (const stepDefinition of definition.steps) {
64
+ const { targets, failure } = selectTargets(stepDefinition, snapshot.state);
65
+ if (failure !== null) {
66
+ blocked.push(scopeFailureEntry(stepDefinition.name, asOf, failure.reason));
67
+ continue;
68
+ }
69
+ for (const target of targets) {
70
+ const evaluation = evaluateTarget(target, { actor: ctx.actor, asOf });
71
+ const scopeKey = target.binding?.key;
72
+ if (evaluation.available)
73
+ affordances.push(toAffordance(stepDefinition.name, scopeKey));
74
+ else
75
+ blocked.push(toBlocked(stepDefinition.name, evaluation, scopeKey));
76
+ }
77
+ }
78
+ return {
79
+ caseId: snapshot.id,
80
+ caseTypeName: definition.name,
81
+ asOf,
82
+ endedAt: toIsoOrNull(snapshot.endedAt),
83
+ affordances,
84
+ blocked,
85
+ };
86
+ };
87
+ /**
88
+ * Normalize an {@link ExplainRequest} into the {@link ComputationContext}
89
+ * the pure computation runs on — the engine boundary's one normalization,
90
+ * stated here so the rule and {@link computeExplanation} share a test
91
+ * surface:
92
+ *
93
+ * - An **absent** `actor` key means the requires-only probe ({@link NO_ACTOR}
94
+ * — `permits` conditions are reported un-evaluated). A key that is
95
+ * *present but `undefined`* is an actor like any other: the caller said
96
+ * who is asking, and the answer is about them.
97
+ * - `asOf` defaults through `now` — the clock stops here; everything below
98
+ * is pure.
99
+ * - `scopeKey` is carried only when given, so "unscoped" stays an absent
100
+ * key rather than an `undefined` value.
101
+ */
102
+ export const explainContext = (request, now) => ({
103
+ // The request's actor is caller-supplied and untyped, and NO_ACTOR is the
104
+ // evaluator's own marker; the assertion papers over neither — `permits`
105
+ // conditions must be total over whatever an actor turns out to be.
106
+ actor: ('actor' in request ? request.actor : NO_ACTOR),
107
+ asOf: request.asOf ?? now(),
108
+ ...(request.scopeKey !== undefined && { scopeKey: request.scopeKey }),
109
+ });
110
+ /**
111
+ * The full per-condition breakdown for one step. Loud where the listing is
112
+ * lenient — `explain` is a targeted probe, so a step name the case type
113
+ * doesn't declare, a missing/unknown scope key on a scoped step, or a scope
114
+ * key on an unscoped step all throw with precise messages (including the
115
+ * currently-valid scope keys, where knowable).
116
+ *
117
+ * One deliberate exception: a scoped step whose selector is **defective**
118
+ * answers rather than throws. The listing publishes exactly that condition
119
+ * as a blocked `$scope` entry with an `explain` link, so the link must be
120
+ * followable — the explanation *is* the `$scope` failure, in the same shape
121
+ * the listing reported it.
122
+ *
123
+ * `actor` is whatever the caller supplies; to ask "why can't *this* actor",
124
+ * pass that actor — `permits` conditions are evaluated against it verbatim.
125
+ */
126
+ export const computeExplanation = (definition, snapshot, stepName, ctx) => {
127
+ const asOf = toIso(ctx.asOf);
128
+ const identity = {
129
+ caseId: snapshot.id,
130
+ caseTypeName: definition.name,
131
+ step: stepName,
132
+ asOf,
133
+ endedAt: toIsoOrNull(snapshot.endedAt),
134
+ };
135
+ const address = addressTarget(definition, snapshot.state, stepName, ctx.scopeKey);
136
+ if (address.failure !== null) {
137
+ if (address.failure.kind === 'defective-selector') {
138
+ return {
139
+ ...identity,
140
+ evaluation: scopeFailureEvaluation(asOf, address.failure),
141
+ };
142
+ }
143
+ throw address.failure.error;
144
+ }
145
+ const target = address.target;
146
+ return {
147
+ ...identity,
148
+ ...(target.binding !== null && { scopeKey: target.binding.key }),
149
+ evaluation: evaluateTarget(target, { actor: ctx.actor, asOf }),
150
+ };
151
+ };
152
+ //# sourceMappingURL=compute.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The engine: the case-type registry bound to the case store.
3
+ *
4
+ * `createEngine({ db, caseTypes })` wires the halves together — persistence
5
+ * (`../store`), guard evaluation (`../guards`), the definition API
6
+ * (`../model`) and the execution lifecycle (`../execution`) — into the
7
+ * framework's public face: `affordances`, `explain`, `execute`, `journal`.
8
+ *
9
+ * The engine is where `asOf` defaults to now: conditions never read the
10
+ * clock, so `EngineOptions.now` — wall clock by default — is the one clock,
11
+ * threaded through the environment to everything below, and everything below
12
+ * it is pure and reconstructable.
13
+ */
14
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
15
+ import type { ExecuteOptions, ExecutionResult, JournalEntry, JournalFilter } from '../execution/index.js';
16
+ import type { Instant } from '../guards/index.js';
17
+ import type { Correlation, CorrelationRegistration, DeadLetter, DeadLetterFilter, ExternalEvent, IngestionOptions, IngestionResult } from '../ingestion/index.js';
18
+ import type { MigrationOptions, MigrationReport, MigrationTransform } from '../migration/index.js';
19
+ import type { AnyCaseType, StepMetadata } from '../model/index.js';
20
+ import type { CaseHandle, DatabaseAccess } from '../store/index.js';
21
+ import type { AffordanceExplanation, CaseAffordances, ExplainRequest } from './compute.js';
22
+ /** 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;
33
+ /** Every case type this engine serves; a loaded case's `case_type` must name one of them. */
34
+ readonly caseTypes: readonly AnyCaseType[];
35
+ /**
36
+ * How long an Execution's claim survives without a heartbeat (default 30s).
37
+ * The floor on how long a crashed handler can hold a case.
38
+ */
39
+ readonly claimTtlMs?: number;
40
+ /** How often a running handler refreshes its claim (default 5s). */
41
+ readonly heartbeatMs?: number;
42
+ /**
43
+ * Event ingestion: how an external event's Actor is derived.
44
+ * Correlation needs no configuration — it is a registry, not a policy.
45
+ */
46
+ readonly ingestion?: IngestionOptions;
47
+ /**
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).
55
+ */
56
+ readonly now?: () => Date;
57
+ }
58
+ /**
59
+ * Options for {@link Engine.explain}. Omit `actor` to probe `requires`
60
+ * alone: `permits` conditions are then reported un-evaluated (failed, with
61
+ * the stated reason `'not evaluated: no actor supplied'`) rather than run
62
+ * against nothing. The normalization rule is `explainContext`, stated and
63
+ * tested beside the pure computation it feeds.
64
+ */
65
+ export type ExplainOptions = ExplainRequest;
66
+ export interface Engine {
67
+ /**
68
+ * Create a case of a registered case type. The initial state is validated
69
+ * against the type's schema.
70
+ */
71
+ createCase(caseTypeName: string, initialState: unknown): Promise<CaseHandle<unknown>>;
72
+ /**
73
+ * Compute the affordances record for a case: load it, evaluate every
74
+ * step's guard for `actor` (with scope fan-out), and return the
75
+ * serializable available + blocked answer. Dormant (ended) cases compute
76
+ * like any other — dormancy is annotation, never a freeze.
77
+ */
78
+ affordances(caseId: string, actor: unknown, asOf?: Instant): Promise<CaseAffordances>;
79
+ /**
80
+ * {@link Engine.affordances} for a case already in hand — no second load,
81
+ * no re-validation. Synchronous: a registry read plus the pure
82
+ * computation. The handle must be one this engine issued
83
+ * ({@link Engine.createCase}, {@link Engine.case}) — their `state` is the
84
+ * validated document; a hand-built handle carries no such guarantee.
85
+ * This is how a create route answers with the fresh case's affordances
86
+ * without re-reading what it just wrote.
87
+ */
88
+ affordancesOf(handle: CaseHandle<unknown>, actor: unknown, asOf?: Instant): CaseAffordances;
89
+ /** The full per-condition breakdown for one step (× scope element) of a case. */
90
+ explain(caseId: string, stepName: string, options?: ExplainOptions): Promise<AffordanceExplanation>;
91
+ /**
92
+ * Execute a step on a case: claim → run → commit. The claim
93
+ * re-evaluates the guard transactionally — the enforcement moment — so an
94
+ * affordance that has since gone away rejects with `StepNotAvailableError`
95
+ * carrying the current unmet conditions, and a case with an Execution
96
+ * already in flight rejects with `CaseBusyError`.
97
+ */
98
+ execute(caseId: string, stepName: string, options: ExecuteOptions): Promise<ExecutionResult>;
99
+ /**
100
+ * Read a case's journal, oldest first. Filter by `scopeKey` for
101
+ * a per-track audit — "everything that happened on buyer #7".
102
+ */
103
+ journal(caseId: string, filter?: JournalFilter): Promise<readonly JournalEntry[]>;
104
+ /**
105
+ * Read one case as persisted: the row plus its Case State validated
106
+ * against the registered schema. Loud — an addressed read owes an answer,
107
+ * so an unknown case or a state its schema no longer accepts throws.
108
+ * The read ops surfaces, tests and consoles would otherwise each
109
+ * hand-write in SQL.
110
+ */
111
+ case(caseId: string): Promise<CaseHandle<unknown>>;
112
+ /** Where an external identifier routes — the reverse of {@link Engine.correlations}. */
113
+ correlationOf(system: string, externalId: string): Promise<Correlation | null>;
114
+ /**
115
+ * The declared input schema of one step of a registered case type, or
116
+ * `null` when the step takes no input. Synchronous — a registry read. The
117
+ * engine owns the registry and a case names its type, so an adapter never
118
+ * needs to be handed the case types a second time to describe inputs.
119
+ */
120
+ inputSchemaFor(caseTypeName: string, stepName: string): StandardSchemaV1 | null;
121
+ /**
122
+ * The declared human metadata of one step of a registered case type —
123
+ * `title` and `description`, each `null` when undeclared — or `null` for
124
+ * an unknown step. Synchronous, a registry read like
125
+ * {@link Engine.inputSchemaFor}, and for the same reason: adapters
126
+ * serialize step metadata from here rather than being handed the case
127
+ * types a second time.
128
+ */
129
+ stepMetadataFor(caseTypeName: string, stepName: string): StepMetadata | null;
130
+ /**
131
+ * Ingest one external event: dedup, correlate, then an ordinary Execution
132
+ * with the external system as the actor. Never throws for an
133
+ * event's own sake — an event that could not be applied is dead-lettered
134
+ * with a reason, so a webhook endpoint can acknowledge and move on.
135
+ */
136
+ ingest(event: ExternalEvent): Promise<IngestionResult>;
137
+ /**
138
+ * Register an external identifier against a case out of band. Handlers
139
+ * should prefer `ctx.correlate(...)`, which rides the same commit as the
140
+ * state recording that the interaction was started.
141
+ */
142
+ correlate(registration: CorrelationRegistration): Promise<Correlation>;
143
+ /** Every external identifier registered against a case (× scope element). */
144
+ correlations(caseId: string, scopeKey?: string): Promise<readonly Correlation[]>;
145
+ /** The dead-letter surface: events that arrived and changed nothing, with why. */
146
+ deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>;
147
+ /**
148
+ * Restructure the state of every case of a case type, as journaled system
149
+ * Executions (float's escape hatch). Idempotent: a case that
150
+ * already carries the migration's marker is skipped, so re-running is a
151
+ * no-op and an interrupted run resumes. Reach for it only when no total
152
+ * condition can read the old shape — see `docs/migration.md`.
153
+ */
154
+ migrate(caseTypeName: string, name: string, transform: MigrationTransform, options?: MigrationOptions): Promise<MigrationReport>;
155
+ }
156
+ /**
157
+ * Build an engine from the app's database and its case type definitions.
158
+ * Throws at construction on duplicate case type names — the registry is
159
+ * keyed by name, which is all a case row records (definitions
160
+ * float; only the name is persisted).
161
+ */
162
+ export declare const createEngine: (options: EngineOptions) => Engine;