@affordance/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/engine/compute.d.ts +147 -0
  4. package/dist/engine/compute.js +152 -0
  5. package/dist/engine/compute.js.map +1 -0
  6. package/dist/engine/engine.d.ts +162 -0
  7. package/dist/engine/engine.js +115 -0
  8. package/dist/engine/engine.js.map +1 -0
  9. package/dist/engine/errors.d.ts +6 -0
  10. package/dist/engine/errors.js +11 -0
  11. package/dist/engine/errors.js.map +1 -0
  12. package/dist/engine/index.d.ts +16 -0
  13. package/dist/engine/index.js +15 -0
  14. package/dist/engine/index.js.map +1 -0
  15. package/dist/errors.d.ts +46 -0
  16. package/dist/errors.js +42 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/execution/delta.d.ts +42 -0
  19. package/dist/execution/delta.js +93 -0
  20. package/dist/execution/delta.js.map +1 -0
  21. package/dist/execution/errors.d.ts +94 -0
  22. package/dist/execution/errors.js +132 -0
  23. package/dist/execution/errors.js.map +1 -0
  24. package/dist/execution/execute.d.ts +179 -0
  25. package/dist/execution/execute.js +356 -0
  26. package/dist/execution/execute.js.map +1 -0
  27. package/dist/execution/index.d.ts +25 -0
  28. package/dist/execution/index.js +20 -0
  29. package/dist/execution/index.js.map +1 -0
  30. package/dist/execution/journal.d.ts +190 -0
  31. package/dist/execution/journal.js +192 -0
  32. package/dist/execution/journal.js.map +1 -0
  33. package/dist/execution/port.d.ts +81 -0
  34. package/dist/execution/port.js +101 -0
  35. package/dist/execution/port.js.map +1 -0
  36. package/dist/execution/replay.d.ts +58 -0
  37. package/dist/execution/replay.js +59 -0
  38. package/dist/execution/replay.js.map +1 -0
  39. package/dist/execution/timers.d.ts +21 -0
  40. package/dist/execution/timers.js +26 -0
  41. package/dist/execution/timers.js.map +1 -0
  42. package/dist/execution/transaction.d.ts +24 -0
  43. package/dist/execution/transaction.js +49 -0
  44. package/dist/execution/transaction.js.map +1 -0
  45. package/dist/guards/condition.d.ts +86 -0
  46. package/dist/guards/condition.js +11 -0
  47. package/dist/guards/condition.js.map +1 -0
  48. package/dist/guards/evaluate.d.ts +143 -0
  49. package/dist/guards/evaluate.js +167 -0
  50. package/dist/guards/evaluate.js.map +1 -0
  51. package/dist/guards/guard.d.ts +136 -0
  52. package/dist/guards/guard.js +118 -0
  53. package/dist/guards/guard.js.map +1 -0
  54. package/dist/guards/index.d.ts +20 -0
  55. package/dist/guards/index.js +17 -0
  56. package/dist/guards/index.js.map +1 -0
  57. package/dist/guards/time.d.ts +32 -0
  58. package/dist/guards/time.js +45 -0
  59. package/dist/guards/time.js.map +1 -0
  60. package/dist/index.d.ts +31 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/ingestion/correlation.d.ts +54 -0
  64. package/dist/ingestion/correlation.js +78 -0
  65. package/dist/ingestion/correlation.js.map +1 -0
  66. package/dist/ingestion/index.d.ts +16 -0
  67. package/dist/ingestion/index.js +15 -0
  68. package/dist/ingestion/index.js.map +1 -0
  69. package/dist/ingestion/ingest.d.ts +196 -0
  70. package/dist/ingestion/ingest.js +275 -0
  71. package/dist/ingestion/ingest.js.map +1 -0
  72. package/dist/migration/index.d.ts +10 -0
  73. package/dist/migration/index.js +10 -0
  74. package/dist/migration/index.js.map +1 -0
  75. package/dist/migration/migrate.d.ts +119 -0
  76. package/dist/migration/migrate.js +195 -0
  77. package/dist/migration/migrate.js.map +1 -0
  78. package/dist/model/casetype.d.ts +53 -0
  79. package/dist/model/casetype.js +51 -0
  80. package/dist/model/casetype.js.map +1 -0
  81. package/dist/model/errors.d.ts +28 -0
  82. package/dist/model/errors.js +39 -0
  83. package/dist/model/errors.js.map +1 -0
  84. package/dist/model/handler.d.ts +120 -0
  85. package/dist/model/handler.js +16 -0
  86. package/dist/model/handler.js.map +1 -0
  87. package/dist/model/index.d.ts +24 -0
  88. package/dist/model/index.js +19 -0
  89. package/dist/model/index.js.map +1 -0
  90. package/dist/model/retry.d.ts +40 -0
  91. package/dist/model/retry.js +51 -0
  92. package/dist/model/retry.js.map +1 -0
  93. package/dist/model/scope.d.ts +67 -0
  94. package/dist/model/scope.js +19 -0
  95. package/dist/model/scope.js.map +1 -0
  96. package/dist/model/step.d.ts +232 -0
  97. package/dist/model/step.js +210 -0
  98. package/dist/model/step.js.map +1 -0
  99. package/dist/model/target.d.ts +175 -0
  100. package/dist/model/target.js +236 -0
  101. package/dist/model/target.js.map +1 -0
  102. package/dist/store/bootstrap.d.ts +57 -0
  103. package/dist/store/bootstrap.js +268 -0
  104. package/dist/store/bootstrap.js.map +1 -0
  105. package/dist/store/errors.d.ts +18 -0
  106. package/dist/store/errors.js +26 -0
  107. package/dist/store/errors.js.map +1 -0
  108. package/dist/store/ids.d.ts +15 -0
  109. package/dist/store/ids.js +12 -0
  110. package/dist/store/ids.js.map +1 -0
  111. package/dist/store/index.d.ts +12 -0
  112. package/dist/store/index.js +9 -0
  113. package/dist/store/index.js.map +1 -0
  114. package/dist/store/queryable.d.ts +60 -0
  115. package/dist/store/queryable.js +7 -0
  116. package/dist/store/queryable.js.map +1 -0
  117. package/dist/store/resolve.d.ts +66 -0
  118. package/dist/store/resolve.js +71 -0
  119. package/dist/store/resolve.js.map +1 -0
  120. package/dist/store/sql.d.ts +26 -0
  121. package/dist/store/sql.js +21 -0
  122. package/dist/store/sql.js.map +1 -0
  123. package/dist/store/store.d.ts +66 -0
  124. package/dist/store/store.js +108 -0
  125. package/dist/store/store.js.map +1 -0
  126. package/package.json +61 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Per-step retry policy.
3
+ *
4
+ * Handlers are short-lived, at-least-once and idempotent, so a
5
+ * failed attempt is retried in place: same Execution, same `ctx.executionId`
6
+ * (the idempotency key), same claim. Retries are bounded — "exhausted retries
7
+ * journal a *failed* execution and release the case" — so a permanently
8
+ * failing handler can never hold a case hostage.
9
+ *
10
+ * Only *indeterminate* failures are retried: a handler that threw. A
11
+ * deterministic failure (the returned Case State does not satisfy the case
12
+ * type's schema) fails the Execution immediately — retrying is guaranteed to
13
+ * produce the same defect.
14
+ */
15
+ /** A normalized retry policy, as held on a {@link StepDefinition}. */
16
+ export interface RetryPolicy {
17
+ /** Total attempts, including the first — 1 disables retry. */
18
+ readonly maxAttempts: number;
19
+ /** Delay before the attempt *after* the given (1-based) failed attempt, in milliseconds. */
20
+ readonly delayMs: (attempt: number) => number;
21
+ }
22
+ /** Retry configuration as authored on a step; every field optional. */
23
+ export interface RetryOptions {
24
+ /** Total attempts, including the first. Integer ≥ 1; defaults to 3. */
25
+ readonly maxAttempts?: number;
26
+ /**
27
+ * Delay between attempts: a fixed number of milliseconds, or a function of
28
+ * the (1-based) attempt that just failed. Defaults to exponential backoff
29
+ * (100ms, 200ms, 400ms …) capped at 5s.
30
+ */
31
+ readonly delayMs?: number | ((attempt: number) => number);
32
+ }
33
+ /** The policy a step gets when it declares none: three attempts, exponential backoff. */
34
+ export declare const DEFAULT_RETRY: RetryPolicy;
35
+ /**
36
+ * Normalize authored retry options into a {@link RetryPolicy}, validating
37
+ * loudly (definition-time validation: a malformed policy should fail the
38
+ * deploy, not an execution).
39
+ */
40
+ export declare const normalizeRetry: (stepName: string, options: RetryOptions | undefined) => RetryPolicy;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-step retry policy.
3
+ *
4
+ * Handlers are short-lived, at-least-once and idempotent, so a
5
+ * failed attempt is retried in place: same Execution, same `ctx.executionId`
6
+ * (the idempotency key), same claim. Retries are bounded — "exhausted retries
7
+ * journal a *failed* execution and release the case" — so a permanently
8
+ * failing handler can never hold a case hostage.
9
+ *
10
+ * Only *indeterminate* failures are retried: a handler that threw. A
11
+ * deterministic failure (the returned Case State does not satisfy the case
12
+ * type's schema) fails the Execution immediately — retrying is guaranteed to
13
+ * produce the same defect.
14
+ */
15
+ /** Exponential backoff from 100ms, capped at 5s. */
16
+ const defaultDelayMs = (attempt) => Math.min(5_000, 100 * 2 ** (attempt - 1));
17
+ /** The policy a step gets when it declares none: three attempts, exponential backoff. */
18
+ export const DEFAULT_RETRY = {
19
+ maxAttempts: 3,
20
+ delayMs: defaultDelayMs,
21
+ };
22
+ /**
23
+ * Normalize authored retry options into a {@link RetryPolicy}, validating
24
+ * loudly (definition-time validation: a malformed policy should fail the
25
+ * deploy, not an execution).
26
+ */
27
+ export const normalizeRetry = (stepName, options) => {
28
+ if (options === undefined)
29
+ return DEFAULT_RETRY;
30
+ if (typeof options !== 'object' ||
31
+ options === null ||
32
+ Array.isArray(options)) {
33
+ throw new TypeError(`step '${stepName}': retry must be { maxAttempts?, delayMs? }`);
34
+ }
35
+ const { maxAttempts = DEFAULT_RETRY.maxAttempts, delayMs = defaultDelayMs } = options;
36
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
37
+ throw new TypeError(`step '${stepName}': retry.maxAttempts must be an integer >= 1`);
38
+ }
39
+ if (typeof delayMs === 'number') {
40
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
41
+ throw new TypeError(`step '${stepName}': retry.delayMs must be a non-negative number or a function`);
42
+ }
43
+ const fixed = delayMs;
44
+ return { maxAttempts, delayMs: () => fixed };
45
+ }
46
+ if (typeof delayMs !== 'function') {
47
+ throw new TypeError(`step '${stepName}': retry.delayMs must be a non-negative number or a function`);
48
+ }
49
+ return { maxAttempts, delayMs };
50
+ };
51
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/model/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAsBH,oDAAoD;AACpD,MAAM,cAAc,GAAG,CAAC,OAAe,EAAU,EAAE,CACjD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAA;AAE3C,yFAAyF;AACzF,MAAM,CAAC,MAAM,aAAa,GAAgB;IACxC,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,cAAc;CACxB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,QAAgB,EAChB,OAAiC,EACpB,EAAE;IACf,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,aAAa,CAAA;IAC/C,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,KAAK,IAAI;QAChB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,SAAS,QAAQ,6CAA6C,CAC/D,CAAA;IACH,CAAC;IACD,MAAM,EAAE,WAAW,GAAG,aAAa,CAAC,WAAW,EAAE,OAAO,GAAG,cAAc,EAAE,GACzE,OAAO,CAAA;IACT,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,SAAS,CACjB,SAAS,QAAQ,8CAA8C,CAChE,CAAA;IACH,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,SAAS,CACjB,SAAS,QAAQ,8DAA8D,CAChF,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAA;QACrB,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAA;IAC9C,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;QAClC,MAAM,IAAI,SAAS,CACjB,SAAS,QAAQ,8DAA8D,CAChF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAA;AACjC,CAAC,CAAA","sourcesContent":["/**\n * Per-step retry policy.\n *\n * Handlers are short-lived, at-least-once and idempotent, so a\n * failed attempt is retried in place: same Execution, same `ctx.executionId`\n * (the idempotency key), same claim. Retries are bounded — \"exhausted retries\n * journal a *failed* execution and release the case\" — so a permanently\n * failing handler can never hold a case hostage.\n *\n * Only *indeterminate* failures are retried: a handler that threw. A\n * deterministic failure (the returned Case State does not satisfy the case\n * type's schema) fails the Execution immediately — retrying is guaranteed to\n * produce the same defect.\n */\n\n/** A normalized retry policy, as held on a {@link StepDefinition}. */\nexport interface RetryPolicy {\n /** Total attempts, including the first — 1 disables retry. */\n readonly maxAttempts: number\n /** Delay before the attempt *after* the given (1-based) failed attempt, in milliseconds. */\n readonly delayMs: (attempt: number) => number\n}\n\n/** Retry configuration as authored on a step; every field optional. */\nexport interface RetryOptions {\n /** Total attempts, including the first. Integer ≥ 1; defaults to 3. */\n readonly maxAttempts?: number\n /**\n * Delay between attempts: a fixed number of milliseconds, or a function of\n * the (1-based) attempt that just failed. Defaults to exponential backoff\n * (100ms, 200ms, 400ms …) capped at 5s.\n */\n readonly delayMs?: number | ((attempt: number) => number)\n}\n\n/** Exponential backoff from 100ms, capped at 5s. */\nconst defaultDelayMs = (attempt: number): number =>\n Math.min(5_000, 100 * 2 ** (attempt - 1))\n\n/** The policy a step gets when it declares none: three attempts, exponential backoff. */\nexport const DEFAULT_RETRY: RetryPolicy = {\n maxAttempts: 3,\n delayMs: defaultDelayMs,\n}\n\n/**\n * Normalize authored retry options into a {@link RetryPolicy}, validating\n * loudly (definition-time validation: a malformed policy should fail the\n * deploy, not an execution).\n */\nexport const normalizeRetry = (\n stepName: string,\n options: RetryOptions | undefined,\n): RetryPolicy => {\n if (options === undefined) return DEFAULT_RETRY\n if (\n typeof options !== 'object' ||\n options === null ||\n Array.isArray(options)\n ) {\n throw new TypeError(\n `step '${stepName}': retry must be { maxAttempts?, delayMs? }`,\n )\n }\n const { maxAttempts = DEFAULT_RETRY.maxAttempts, delayMs = defaultDelayMs } =\n options\n if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n throw new TypeError(\n `step '${stepName}': retry.maxAttempts must be an integer >= 1`,\n )\n }\n if (typeof delayMs === 'number') {\n if (!Number.isFinite(delayMs) || delayMs < 0) {\n throw new TypeError(\n `step '${stepName}': retry.delayMs must be a non-negative number or a function`,\n )\n }\n const fixed = delayMs\n return { maxAttempts, delayMs: () => fixed }\n }\n if (typeof delayMs !== 'function') {\n throw new TypeError(\n `step '${stepName}': retry.delayMs must be a non-negative number or a function`,\n )\n }\n return { maxAttempts, delayMs }\n}\n"]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Scoped steps.
3
+ *
4
+ * A step may declare a **scope** over a state collection: each element the
5
+ * scope selects yields one independent affordance, and the affordance's
6
+ * identity is (step name × scope key) — `escalate-verification(buyer_007)`. Journal
7
+ * entries, correlation keys, and execution requests all address a scoped
8
+ * step by that pair, so the key must be **stable**: derived from the
9
+ * element's own identity (an id the app owns), never from its position in
10
+ * the collection.
11
+ */
12
+ import type { ConditionContext, ConditionMapEntry, ConditionOutcome } from '../guards/index.js';
13
+ /**
14
+ * A step's scope declaration: which elements of case state the step ranges
15
+ * over, and how each element is identified.
16
+ *
17
+ * - `select` — pure selector over case state returning the matching elements
18
+ * (e.g. `s => s.buyers.filter(b => b.verification?.status === 'review')`).
19
+ * Subject to the same discipline as conditions: synchronous,
20
+ * side-effect-free, total over historical state.
21
+ * - `key` — derives the element's **scope key**, a non-empty string that is
22
+ * stable for the life of the element and unique within one selection
23
+ * (typically `i => i.id`). The framework deliberately does not require an
24
+ * `id` field on elements: state schemas are app-owned, so identity
25
+ * derivation is declared, not assumed. Duplicate or invalid keys are an
26
+ * identity-corruption bug and fail affordance computation loudly
27
+ * ({@link ScopeKeyError}).
28
+ */
29
+ export interface ScopeDeclaration<TState, TElement> {
30
+ readonly select: (state: TState) => readonly TElement[];
31
+ readonly key: (element: TElement) => string;
32
+ }
33
+ /**
34
+ * The context a scoped step's conditions receive: the base condition context
35
+ * with the bound scope element present and typed. Authors read the element
36
+ * as `ctx.scope` with no need to narrow away `undefined` — the engine always
37
+ * binds it when evaluating a scoped step.
38
+ */
39
+ export type ScopedConditionContext<TElement, TActor = unknown> = ConditionContext<TActor, TElement> & {
40
+ readonly scope: TElement;
41
+ };
42
+ /**
43
+ * A condition of a scoped step: same contract as {@link Condition} — pure,
44
+ * synchronous, total, clock-free — receiving the whole case state first and
45
+ * the bound scope element via `ctx.scope`, so cross-cutting facts stay
46
+ * expressible (`(s, ctx) => (ctx.scope.committed ?? 0) <
47
+ * (s.fundingCall?.amount ?? 0)`).
48
+ */
49
+ export type ScopedCondition<TState, TElement, TActor = unknown> = (state: TState, ctx: ScopedConditionContext<TElement, TActor>) => ConditionOutcome;
50
+ /**
51
+ * What may sit under a name in a scoped step's `requires`/`permits`: a
52
+ * scoped condition.
53
+ *
54
+ * `anyOf` groups are not part of the scoped guard surface (the unscoped
55
+ * algebra keeps them); adding a scoped-typed `anyOf` later is an additive
56
+ * change.
57
+ */
58
+ export type ScopedConditionMapEntry<TState, TElement, TActor = unknown> = ScopedCondition<TState, TElement, TActor>;
59
+ /** A flat AND-map of named scoped conditions — the scoped counterpart of `ConditionMap`. */
60
+ export type ScopedConditionMap<TState, TElement, TActor = unknown> = Readonly<Record<string, ScopedConditionMapEntry<TState, TElement, TActor>>>;
61
+ /**
62
+ * Erase a scoped condition map to the guards module's entry shape so the
63
+ * engine can hand a scoped step's guard to `evaluateGuard` unchanged. Sound
64
+ * because evaluation of a scoped guard always binds `scope` (which the
65
+ * scoped condition context requires).
66
+ */
67
+ export declare const eraseScopedConditionMap: <TState, TElement, TActor>(map: ScopedConditionMap<TState, TElement, TActor> | undefined) => Readonly<Record<string, ConditionMapEntry<TState, TActor>>> | undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Scoped steps.
3
+ *
4
+ * A step may declare a **scope** over a state collection: each element the
5
+ * scope selects yields one independent affordance, and the affordance's
6
+ * identity is (step name × scope key) — `escalate-verification(buyer_007)`. Journal
7
+ * entries, correlation keys, and execution requests all address a scoped
8
+ * step by that pair, so the key must be **stable**: derived from the
9
+ * element's own identity (an id the app owns), never from its position in
10
+ * the collection.
11
+ */
12
+ /**
13
+ * Erase a scoped condition map to the guards module's entry shape so the
14
+ * engine can hand a scoped step's guard to `evaluateGuard` unchanged. Sound
15
+ * because evaluation of a scoped guard always binds `scope` (which the
16
+ * scoped condition context requires).
17
+ */
18
+ export const eraseScopedConditionMap = (map) => map;
19
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/model/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,GAA6D,EACY,EAAE,CAC3E,GAEa,CAAA","sourcesContent":["/**\n * Scoped steps.\n *\n * A step may declare a **scope** over a state collection: each element the\n * scope selects yields one independent affordance, and the affordance's\n * identity is (step name × scope key) — `escalate-verification(buyer_007)`. Journal\n * entries, correlation keys, and execution requests all address a scoped\n * step by that pair, so the key must be **stable**: derived from the\n * element's own identity (an id the app owns), never from its position in\n * the collection.\n */\n\nimport type {\n ConditionContext,\n ConditionMapEntry,\n ConditionOutcome,\n} from '../guards/index.js'\n\n/**\n * A step's scope declaration: which elements of case state the step ranges\n * over, and how each element is identified.\n *\n * - `select` — pure selector over case state returning the matching elements\n * (e.g. `s => s.buyers.filter(b => b.verification?.status === 'review')`).\n * Subject to the same discipline as conditions: synchronous,\n * side-effect-free, total over historical state.\n * - `key` — derives the element's **scope key**, a non-empty string that is\n * stable for the life of the element and unique within one selection\n * (typically `i => i.id`). The framework deliberately does not require an\n * `id` field on elements: state schemas are app-owned, so identity\n * derivation is declared, not assumed. Duplicate or invalid keys are an\n * identity-corruption bug and fail affordance computation loudly\n * ({@link ScopeKeyError}).\n */\nexport interface ScopeDeclaration<TState, TElement> {\n readonly select: (state: TState) => readonly TElement[]\n readonly key: (element: TElement) => string\n}\n\n/**\n * The context a scoped step's conditions receive: the base condition context\n * with the bound scope element present and typed. Authors read the element\n * as `ctx.scope` with no need to narrow away `undefined` — the engine always\n * binds it when evaluating a scoped step.\n */\nexport type ScopedConditionContext<\n TElement,\n TActor = unknown,\n> = ConditionContext<TActor, TElement> & {\n readonly scope: TElement\n}\n\n/**\n * A condition of a scoped step: same contract as {@link Condition} — pure,\n * synchronous, total, clock-free — receiving the whole case state first and\n * the bound scope element via `ctx.scope`, so cross-cutting facts stay\n * expressible (`(s, ctx) => (ctx.scope.committed ?? 0) <\n * (s.fundingCall?.amount ?? 0)`).\n */\nexport type ScopedCondition<TState, TElement, TActor = unknown> = (\n state: TState,\n ctx: ScopedConditionContext<TElement, TActor>,\n) => ConditionOutcome\n\n/**\n * What may sit under a name in a scoped step's `requires`/`permits`: a\n * scoped condition.\n *\n * `anyOf` groups are not part of the scoped guard surface (the unscoped\n * algebra keeps them); adding a scoped-typed `anyOf` later is an additive\n * change.\n */\nexport type ScopedConditionMapEntry<\n TState,\n TElement,\n TActor = unknown,\n> = ScopedCondition<TState, TElement, TActor>\n\n/** A flat AND-map of named scoped conditions — the scoped counterpart of `ConditionMap`. */\nexport type ScopedConditionMap<TState, TElement, TActor = unknown> = Readonly<\n Record<string, ScopedConditionMapEntry<TState, TElement, TActor>>\n>\n\n/**\n * Erase a scoped condition map to the guards module's entry shape so the\n * engine can hand a scoped step's guard to `evaluateGuard` unchanged. Sound\n * because evaluation of a scoped guard always binds `scope` (which the\n * scoped condition context requires).\n */\nexport const eraseScopedConditionMap = <TState, TElement, TActor>(\n map: ScopedConditionMap<TState, TElement, TActor> | undefined,\n): Readonly<Record<string, ConditionMapEntry<TState, TActor>>> | undefined =>\n map as unknown as\n | Readonly<Record<string, ConditionMapEntry<TState, TActor>>>\n | undefined\n"]}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Step definitions.
3
+ *
4
+ * A **step** is an independently-defined unit of possible work on a case: a
5
+ * guard plus a handler (CONTEXT.md). Steps never declare ordering —
6
+ * sequencing is data dependencies between guards. `step()` validates the
7
+ * definition loudly at construction time: a malformed step should fail the
8
+ * deploy, not an evaluation.
9
+ */
10
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
11
+ import { AffordanceError } from '../errors.js';
12
+ import type { ConditionMap, Guard } from '../guards/index.js';
13
+ import type { ErasedStepHandler, ScopedStepHandler, StepHandler } from './handler.js';
14
+ import type { RetryOptions, RetryPolicy } from './retry.js';
15
+ import type { ScopeDeclaration, ScopedConditionMap } from './scope.js';
16
+ /** Options for an unscoped step. */
17
+ export interface StepOptions<TState, TActor = unknown, TInput = undefined> {
18
+ /** The step's name — unique within its case type; half of a scoped affordance's identity. */
19
+ readonly name: string;
20
+ /**
21
+ * A short human label for the step ("Issue the funding call"). Definition
22
+ * metadata, not identity: journal entries and refusals name the step by
23
+ * `name`; adapters serialize the title so a client renders steps without
24
+ * an out-of-band label table.
25
+ */
26
+ readonly title?: string;
27
+ /**
28
+ * A sentence on what the step does and when to take it — what makes the
29
+ * affordance contract a usable tool list for a caller (an agent included)
30
+ * that has never seen this case type.
31
+ */
32
+ readonly description?: string;
33
+ /** Case conditions: when one fails the step is not possible on this case, for anyone. */
34
+ readonly requires?: ConditionMap<TState, TActor>;
35
+ /** Actor conditions: when one fails the step is possible but not permitted for this actor. */
36
+ readonly permits?: ConditionMap<TState, TActor>;
37
+ /**
38
+ * Optional input schema (any Standard Schema — zod v4 qualifies). Validated
39
+ * by `validateStepInput` before the handler runs.
40
+ */
41
+ readonly input?: StandardSchemaV1<unknown, TInput>;
42
+ /**
43
+ * How many times a failed attempt is retried, and how long between
44
+ * attempts. Defaults to three attempts with exponential backoff; set
45
+ * `{ maxAttempts: 1 }` to disable retry for this step.
46
+ */
47
+ readonly retry?: RetryOptions;
48
+ /** The step's effect function — the only thing that mutates Case State. */
49
+ readonly handler: StepHandler<TState, TActor, TInput>;
50
+ }
51
+ /** Options for a scoped step: an unscoped step plus the scope declaration. */
52
+ export interface ScopedStepOptions<TState, TElement, TActor = unknown, TInput = undefined> {
53
+ readonly name: string;
54
+ readonly title?: string;
55
+ readonly description?: string;
56
+ /** The collection the step ranges over and how each element is identified. */
57
+ readonly scope: ScopeDeclaration<TState, TElement>;
58
+ readonly requires?: ScopedConditionMap<TState, TElement, TActor>;
59
+ readonly permits?: ScopedConditionMap<TState, TElement, TActor>;
60
+ readonly input?: StandardSchemaV1<unknown, TInput>;
61
+ readonly retry?: RetryOptions;
62
+ readonly handler: ScopedStepHandler<TState, TElement, TActor, TInput>;
63
+ }
64
+ /**
65
+ * A step definition as held by a case type and consumed by the engine — the
66
+ * authoring generics (scope element, input) erased to the case-state level.
67
+ * The options types above carry the precise authoring shapes; this is the
68
+ * machine-facing normal form.
69
+ */
70
+ export interface StepDefinition<TState, TActor = unknown> {
71
+ readonly name: string;
72
+ /** The declared human label, or `null` — clients fall back to `name`. */
73
+ readonly title: string | null;
74
+ /** The declared what-and-when sentence, or `null`. */
75
+ readonly description: string | null;
76
+ /**
77
+ * The step's guard in the guards module's shape, ready for `evaluateGuard`.
78
+ * For a scoped step the engine evaluates it once per selected element with
79
+ * the element bound as `scope`.
80
+ */
81
+ readonly guard: Guard<TState, TActor>;
82
+ /** The scope declaration (element type erased), or `null` for an unscoped step. */
83
+ readonly scope: {
84
+ readonly select: (state: TState) => readonly unknown[];
85
+ readonly key: (element: unknown) => string;
86
+ } | null;
87
+ /** The declared input schema, or `null` when the step takes no input. */
88
+ readonly input: StandardSchemaV1 | null;
89
+ /** The normalized retry policy the execution lifecycle applies to this step. */
90
+ readonly retry: RetryPolicy;
91
+ /** The step's handler — invoked only by the execution lifecycle. */
92
+ readonly handler: ErasedStepHandler<TState, TActor>;
93
+ }
94
+ /**
95
+ * A step's declared human metadata, as `Engine.stepMetadataFor` answers it —
96
+ * the serializable slice of a {@link StepDefinition} an adapter puts on the
97
+ * wire.
98
+ */
99
+ export interface StepMetadata {
100
+ readonly title: string | null;
101
+ readonly description: string | null;
102
+ }
103
+ /** Whether a value is a Standard-Schema instance — the one spelling of the check. */
104
+ export declare const isStandardSchema: (value: unknown) => value is StandardSchemaV1;
105
+ /**
106
+ * Whether a value is plausibly a {@link StepDefinition} — the structural
107
+ * check `caseType()` applies to every step it is given.
108
+ */
109
+ export declare const looksLikeStepDefinition: (value: unknown) => value is StepDefinition<unknown, unknown>;
110
+ /**
111
+ * Define a step. Two shapes, discriminated by the presence of `scope`:
112
+ *
113
+ * ```ts
114
+ * // Standalone form (internal — apps author through {@link stepsOf}, which
115
+ * // binds the generics once and takes the same options annotation-free):
116
+ * // type parameters are inferred from annotations in the options — annotate
117
+ * // the state parameter of a condition (and the actor on a permits ctx).
118
+ * step({
119
+ * name: 'issue-funding-call',
120
+ * requires: { escrowReady: (s: Purchase) => s.escrow?.status === 'open' },
121
+ * permits: { isOrganizer: (_s: Purchase, ctx: ConditionContext<Ops>) => ctx.actor.roles.includes('organizer') },
122
+ * handler: async (s, ctx) => s,
123
+ * })
124
+ *
125
+ * // Scoped, standalone form: annotating scope.select anchors both the state
126
+ * // and element types; conditions then read the bound element as ctx.scope,
127
+ * // fully typed (through stepsOf, even the select annotation goes away):
128
+ * step({
129
+ * name: 'escalate-verification',
130
+ * scope: { select: (s: Purchase) => s.buyers.filter(b => b.verification?.status === 'review'), key: b => b.id },
131
+ * requires: { flagged: (s: Purchase, ctx) => (ctx.scope as Buyer).verification?.flaggedAt != null },
132
+ * handler: async (s, ctx) => s,
133
+ * })
134
+ * ```
135
+ *
136
+ * Validates loudly at construction time — malformed guard entries, a
137
+ * malformed scope declaration, a non-function handler, or a non-schema
138
+ * `input` all throw `TypeError` (definition-time validation; a definition
139
+ * bug should fail the deploy, not an evaluation).
140
+ *
141
+ * The scoped overload is declared first: type parameters are meant to be
142
+ * inferred, and inference resolves each call shape against its own overload
143
+ * (explicit type-argument lists interact badly with overloaded
144
+ * context-sensitive options — annotate inside the options instead).
145
+ */
146
+ export declare function step<TState, TElement, TActor = unknown, TInput = undefined>(options: ScopedStepOptions<TState, TElement, TActor, TInput>): StepDefinition<TState, TActor>;
147
+ export declare function step<TState, TActor = unknown, TInput = undefined>(options: StepOptions<TState, TActor, TInput>): StepDefinition<TState, TActor>;
148
+ /**
149
+ * The `step()` authoring surface with the case's state and actor types fixed.
150
+ *
151
+ * Same two call shapes as `step()` — scoped first, discriminated by the
152
+ * presence of `scope` — but `TState`/`TActor` are already substituted, so
153
+ * only the per-step generics (scope element, input) remain to be inferred.
154
+ * That is what makes annotation-free authoring work: conditions and handlers
155
+ * no longer participate in inferring the state type, they just receive it
156
+ * contextually, and a scoped step's element type anchors on `scope.select`
157
+ * alone (so `ctx.scope` is the element, with no `undefined` to narrow away).
158
+ */
159
+ export interface BoundStep<TState, TActor = unknown> {
160
+ <TElement, TInput = undefined>(options: ScopedStepOptions<TState, TElement, TActor, TInput>): StepDefinition<TState, TActor>;
161
+ <TInput = undefined>(options: StepOptions<TState, TActor, TInput>): StepDefinition<TState, TActor>;
162
+ }
163
+ /**
164
+ * A value-level carrier for `stepsOf`'s actor type — nothing but the type.
165
+ * Exists because `TActor` has no value to be inferred from (a case's actor
166
+ * shape is app-defined and never materializes at definition time), and
167
+ * spelling it as a type argument would force spelling the schema's type too
168
+ * (TypeScript has no partial type-argument inference).
169
+ */
170
+ export interface ActorMarker<TActor> {
171
+ readonly __actor?: TActor;
172
+ }
173
+ /** Name the actor type of a `stepsOf` factory: `stepsOf(PurchaseState, actor<PurchaseActor>())`. */
174
+ export declare const actor: <TActor>() => ActorMarker<TActor>;
175
+ /**
176
+ * Bind `step()` to a case's state schema — the schema-anchored authoring
177
+ * factory.
178
+ *
179
+ * ```ts
180
+ * const purchaseStep = stepsOf(PurchaseState, actor<PurchaseActor>())
181
+ *
182
+ * purchaseStep({
183
+ * name: 'issue-funding-call',
184
+ * requires: { escrowReady: s => s.escrow.status === 'open' }, // s: inferred from the schema
185
+ * permits: { isOrganizer: (_s, ctx) => hasRole(ctx.actor, 'organizer') },
186
+ * handler: async s => s,
187
+ * })
188
+ *
189
+ * purchaseStep({
190
+ * name: 'escalate-verification',
191
+ * scope: { select: s => s.buyers.filter(b => b.verification.status === 'review'), key: b => b.id },
192
+ * requires: { flagged: (_s, ctx) => ctx.scope.verification.flaggedAt !== null }, // ctx.scope: Buyer
193
+ * handler: async s => s,
194
+ * })
195
+ * ```
196
+ *
197
+ * The state type is derived from the schema *value* — the same
198
+ * `InferOutput` derivation `caseType` performs — so the factory and the case
199
+ * type are anchored to one declaration and cannot drift apart: the state a
200
+ * condition sees is definitionally the state the engine validates against.
201
+ * The per-condition annotations the bare `step()` needs
202
+ * (`(s: Purchase) => …`) disappear, because `TState` is no longer inferred
203
+ * from the options.
204
+ *
205
+ * The second argument exists only to name the actor type and carries no
206
+ * runtime information; omit it for an untyped actor. Define one factory per
207
+ * case type module, next to the schema, and author every step of that case
208
+ * type through it.
209
+ *
210
+ * Returns `step` itself, re-typed — a step authored through the factory is
211
+ * bit-for-bit an ordinary step definition. `step` is deliberately not part
212
+ * of the package barrel: this factory is the public authoring surface, and
213
+ * a helper that builds steps generically should accept a
214
+ * {@link BoundStep} rather than reach for the unbound `step`.
215
+ * Throws at definition time when `state` is not a Standard Schema, like
216
+ * every other malformed-definition case in this module.
217
+ */
218
+ export declare const stepsOf: <S extends StandardSchemaV1, TActor = unknown>(state: S, _actor?: ActorMarker<TActor>) => BoundStep<StandardSchemaV1.InferOutput<S>, TActor>;
219
+ /** A step's declared input failed validation against its input schema. */
220
+ export declare class StepInputValidationError extends AffordanceError {
221
+ readonly stepName: string;
222
+ readonly issues: readonly StandardSchemaV1.Issue[];
223
+ constructor(stepName: string, issues: readonly StandardSchemaV1.Issue[]);
224
+ }
225
+ /**
226
+ * Validate a step's input against its declared input schema — the validation
227
+ * plumbing the execution lifecycle runs before invoking the handler ("validated before the
228
+ * handler runs"). Returns the schema *output* (defaults applied). A step
229
+ * without an input schema accepts only `undefined` and yields `undefined`;
230
+ * anything else is a caller bug and throws.
231
+ */
232
+ export declare const validateStepInput: <TState, TActor>(definition: StepDefinition<TState, TActor>, input: unknown) => Promise<unknown>;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Step definitions.
3
+ *
4
+ * A **step** is an independently-defined unit of possible work on a case: a
5
+ * guard plus a handler (CONTEXT.md). Steps never declare ordering —
6
+ * sequencing is data dependencies between guards. `step()` validates the
7
+ * definition loudly at construction time: a malformed step should fail the
8
+ * deploy, not an evaluation.
9
+ */
10
+ import { AffordanceError } from '../errors.js';
11
+ import { guardEntries } from '../guards/index.js';
12
+ import { normalizeRetry } from './retry.js';
13
+ import { eraseScopedConditionMap } from './scope.js';
14
+ /** Whether a value is a Standard-Schema instance — the one spelling of the check. */
15
+ export const isStandardSchema = (value) => typeof value === 'object' &&
16
+ value !== null &&
17
+ typeof value['~standard'] === 'object';
18
+ /**
19
+ * Whether a value is plausibly a {@link StepDefinition} — the structural
20
+ * check `caseType()` applies to every step it is given.
21
+ */
22
+ export const looksLikeStepDefinition = (value) => typeof value === 'object' &&
23
+ value !== null &&
24
+ typeof value.name === 'string' &&
25
+ typeof value.handler === 'function' &&
26
+ typeof value.guard === 'object';
27
+ /**
28
+ * Validate one guard section map at definition time: every entry must be a
29
+ * condition function or an `anyOf(...)` group
30
+ * (unscoped only — scoped maps admit conditions alone).
31
+ *
32
+ * The classification and the `requires.escrowReady` addressing both come from
33
+ * `guardEntries`, so what a definition is allowed to contain is decided
34
+ * against the same walk evaluation uses — a definition `step()` accepts is
35
+ * one `evaluateGuard` can read.
36
+ */
37
+ const validateConditionMap = (stepName, section, map, allowAnyOf) => {
38
+ if (map === undefined)
39
+ return;
40
+ if (typeof map !== 'object' || map === null || Array.isArray(map)) {
41
+ throw new TypeError(`step '${stepName}': ${section} must be a plain object of named conditions`);
42
+ }
43
+ const section_ = { [section]: map };
44
+ for (const entry of guardEntries(section_)) {
45
+ if (entry.kind === 'condition')
46
+ continue;
47
+ if (entry.kind === 'anyOf' && allowAnyOf)
48
+ continue;
49
+ const allowed = allowAnyOf
50
+ ? 'a condition function or an anyOf(...) group'
51
+ : 'a condition function (anyOf is not part of the scoped guard surface)';
52
+ throw new TypeError(`step '${stepName}': ${entry.address} must be ${allowed}`);
53
+ }
54
+ };
55
+ const validateCommon = (options) => {
56
+ const { name } = options;
57
+ if (typeof name !== 'string' || name.trim() === '') {
58
+ throw new TypeError('step: name must be a non-empty string');
59
+ }
60
+ if (typeof options.handler !== 'function') {
61
+ throw new TypeError(`step '${name}': handler must be an async function`);
62
+ }
63
+ if (options.input !== undefined && !isStandardSchema(options.input)) {
64
+ throw new TypeError(`step '${name}': input must be a Standard Schema (e.g. a zod schema)`);
65
+ }
66
+ for (const field of ['title', 'description']) {
67
+ const value = options[field];
68
+ if (value !== undefined &&
69
+ (typeof value !== 'string' || value.trim() === '')) {
70
+ throw new TypeError(`step '${name}': ${field} must be a non-empty string when given`);
71
+ }
72
+ }
73
+ return name;
74
+ };
75
+ export function step(options) {
76
+ const name = validateCommon(options);
77
+ const scoped = 'scope' in options && options.scope !== undefined;
78
+ let scope = null;
79
+ let guard;
80
+ if (scoped) {
81
+ const declaration = options.scope;
82
+ if (typeof declaration !== 'object' ||
83
+ declaration === null ||
84
+ typeof declaration.select !== 'function' ||
85
+ typeof declaration.key !== 'function') {
86
+ throw new TypeError(`step '${name}': scope must be { select: state => elements, key: element => string }`);
87
+ }
88
+ validateConditionMap(name, 'requires', options.requires, false);
89
+ validateConditionMap(name, 'permits', options.permits, false);
90
+ scope = { select: declaration.select, key: declaration.key };
91
+ // Erasure, not conversion: scoped maps are runtime-identical to unscoped
92
+ // ones; evaluation binds the scope element the scoped types promise.
93
+ const scopedOptions = options;
94
+ guard = {
95
+ ...(scopedOptions.requires !== undefined && {
96
+ requires: eraseScopedConditionMap(scopedOptions.requires),
97
+ }),
98
+ ...(scopedOptions.permits !== undefined && {
99
+ permits: eraseScopedConditionMap(scopedOptions.permits),
100
+ }),
101
+ };
102
+ }
103
+ else {
104
+ const unscoped = options;
105
+ validateConditionMap(name, 'requires', unscoped.requires, true);
106
+ validateConditionMap(name, 'permits', unscoped.permits, true);
107
+ guard = {
108
+ ...(unscoped.requires !== undefined && { requires: unscoped.requires }),
109
+ ...(unscoped.permits !== undefined && { permits: unscoped.permits }),
110
+ };
111
+ }
112
+ return {
113
+ name,
114
+ title: options.title ?? null,
115
+ description: options.description ?? null,
116
+ guard,
117
+ scope,
118
+ input: options.input ?? null,
119
+ retry: normalizeRetry(name, options.retry),
120
+ // The one erasure cast for handlers: the authored context (typed input,
121
+ // typed scope element) is what the execution lifecycle constructs; see
122
+ // ErasedStepHandler.
123
+ handler: options.handler,
124
+ };
125
+ }
126
+ const ACTOR_MARKER = Object.freeze({});
127
+ /** Name the actor type of a `stepsOf` factory: `stepsOf(PurchaseState, actor<PurchaseActor>())`. */
128
+ export const actor = () => ACTOR_MARKER;
129
+ /**
130
+ * Bind `step()` to a case's state schema — the schema-anchored authoring
131
+ * factory.
132
+ *
133
+ * ```ts
134
+ * const purchaseStep = stepsOf(PurchaseState, actor<PurchaseActor>())
135
+ *
136
+ * purchaseStep({
137
+ * name: 'issue-funding-call',
138
+ * requires: { escrowReady: s => s.escrow.status === 'open' }, // s: inferred from the schema
139
+ * permits: { isOrganizer: (_s, ctx) => hasRole(ctx.actor, 'organizer') },
140
+ * handler: async s => s,
141
+ * })
142
+ *
143
+ * purchaseStep({
144
+ * name: 'escalate-verification',
145
+ * scope: { select: s => s.buyers.filter(b => b.verification.status === 'review'), key: b => b.id },
146
+ * requires: { flagged: (_s, ctx) => ctx.scope.verification.flaggedAt !== null }, // ctx.scope: Buyer
147
+ * handler: async s => s,
148
+ * })
149
+ * ```
150
+ *
151
+ * The state type is derived from the schema *value* — the same
152
+ * `InferOutput` derivation `caseType` performs — so the factory and the case
153
+ * type are anchored to one declaration and cannot drift apart: the state a
154
+ * condition sees is definitionally the state the engine validates against.
155
+ * The per-condition annotations the bare `step()` needs
156
+ * (`(s: Purchase) => …`) disappear, because `TState` is no longer inferred
157
+ * from the options.
158
+ *
159
+ * The second argument exists only to name the actor type and carries no
160
+ * runtime information; omit it for an untyped actor. Define one factory per
161
+ * case type module, next to the schema, and author every step of that case
162
+ * type through it.
163
+ *
164
+ * Returns `step` itself, re-typed — a step authored through the factory is
165
+ * bit-for-bit an ordinary step definition. `step` is deliberately not part
166
+ * of the package barrel: this factory is the public authoring surface, and
167
+ * a helper that builds steps generically should accept a
168
+ * {@link BoundStep} rather than reach for the unbound `step`.
169
+ * Throws at definition time when `state` is not a Standard Schema, like
170
+ * every other malformed-definition case in this module.
171
+ */
172
+ export const stepsOf = (state, _actor) => {
173
+ if (!isStandardSchema(state)) {
174
+ throw new TypeError('stepsOf: state must be a Standard Schema (e.g. a zod schema)');
175
+ }
176
+ return step;
177
+ };
178
+ /** A step's declared input failed validation against its input schema. */
179
+ export class StepInputValidationError extends AffordanceError {
180
+ stepName;
181
+ issues;
182
+ constructor(stepName, issues) {
183
+ super('invalid-input', `invalid input for step '${stepName}': ${issues.map((issue) => issue.message).join('; ')}`);
184
+ this.name = 'StepInputValidationError';
185
+ this.stepName = stepName;
186
+ this.issues = issues;
187
+ }
188
+ }
189
+ /**
190
+ * Validate a step's input against its declared input schema — the validation
191
+ * plumbing the execution lifecycle runs before invoking the handler ("validated before the
192
+ * handler runs"). Returns the schema *output* (defaults applied). A step
193
+ * without an input schema accepts only `undefined` and yields `undefined`;
194
+ * anything else is a caller bug and throws.
195
+ */
196
+ export const validateStepInput = async (definition, input) => {
197
+ if (definition.input === null) {
198
+ if (input !== undefined) {
199
+ throw new StepInputValidationError(definition.name, [
200
+ { message: 'step declares no input schema, but input was provided' },
201
+ ]);
202
+ }
203
+ return undefined;
204
+ }
205
+ const result = await definition.input['~standard'].validate(input);
206
+ if (result.issues)
207
+ throw new StepInputValidationError(definition.name, result.issues);
208
+ return result.value;
209
+ };
210
+ //# sourceMappingURL=step.js.map