@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,167 @@
1
+ /**
2
+ * Guard evaluation — pure, deterministic, explainable.
3
+ *
4
+ * `evaluateGuard(guard, ctx)` turns a guard and an evaluation context
5
+ * (case state, actor, asOf) into a full per-condition evaluation record:
6
+ * the raw material for affordance computation, `explain`, and
7
+ * the journal's guard-results field.
8
+ */
9
+ import { thrownMessage } from '../errors.js';
10
+ import { conditionAddress, guardEntries } from './guard.js';
11
+ import { toIso } from './time.js';
12
+ /**
13
+ * Marker for evaluating a guard with no actor at hand — `explain`'s
14
+ * requires-only probe. A `permits` condition is the only kind of entry
15
+ * that reads the actor, so it alone is skipped: reported failed with
16
+ * {@link NOT_EVALUATED_REASON} rather than run against nothing and absorbed
17
+ * as a thrown `TypeError`.
18
+ */
19
+ export const NO_ACTOR = Symbol('affordance.no-actor');
20
+ /** The stated reason a `permits` condition reports under {@link NO_ACTOR}. */
21
+ export const NOT_EVALUATED_REASON = 'not evaluated: no actor supplied';
22
+ const withReason = (result, reason) => reason === undefined ? result : { ...result, reason };
23
+ /** Evaluate one plain condition, absorbing throws and malformed outcomes into a failed result. */
24
+ const evaluateCondition = (name, section, condition, state, ctx) => {
25
+ if (section === 'permits' && ctx.actor === NO_ACTOR) {
26
+ return {
27
+ name,
28
+ section,
29
+ kind: 'condition',
30
+ passed: false,
31
+ reason: NOT_EVALUATED_REASON,
32
+ };
33
+ }
34
+ let outcome;
35
+ try {
36
+ outcome = condition(state, ctx);
37
+ }
38
+ catch (err) {
39
+ return {
40
+ name,
41
+ section,
42
+ kind: 'condition',
43
+ passed: false,
44
+ reason: `condition threw: ${thrownMessage(err)}`,
45
+ };
46
+ }
47
+ if (typeof outcome === 'boolean') {
48
+ return { name, section, kind: 'condition', passed: outcome };
49
+ }
50
+ if (typeof outcome === 'object' &&
51
+ outcome !== null &&
52
+ typeof outcome.ok === 'boolean') {
53
+ return withReason({ name, section, kind: 'condition', passed: outcome.ok }, outcome.reason);
54
+ }
55
+ return {
56
+ name,
57
+ section,
58
+ kind: 'condition',
59
+ passed: false,
60
+ reason: 'condition returned neither a boolean nor an { ok, reason? } verdict',
61
+ };
62
+ };
63
+ const MALFORMED = 'entry is not a condition function or an anyOf(...) group';
64
+ const malformed = (name, section) => ({
65
+ name,
66
+ section,
67
+ kind: 'condition',
68
+ passed: false,
69
+ reason: MALFORMED,
70
+ });
71
+ /** Evaluate one arm of an `anyOf` group; the arm's own name is what it reports under. */
72
+ const evaluateArm = (arm, state, ctx) => {
73
+ if (arm.kind === 'condition') {
74
+ return evaluateCondition(arm.arm, arm.section, arm.entry, state, ctx);
75
+ }
76
+ return malformed(arm.arm, arm.section);
77
+ };
78
+ /** Evaluate one walked guard entry (plain condition or anyOf group). */
79
+ const evaluateEntry = (entry, state, ctx) => {
80
+ const { name, section } = entry;
81
+ if (entry.kind === 'condition') {
82
+ return evaluateCondition(name, section, entry.entry, state, ctx);
83
+ }
84
+ if (entry.kind === 'anyOf') {
85
+ const arms = entry.arms.map((arm) => evaluateArm(arm, state, ctx));
86
+ return {
87
+ name,
88
+ section,
89
+ kind: 'anyOf',
90
+ passed: arms.some((a) => a.passed),
91
+ arms,
92
+ };
93
+ }
94
+ return malformed(name, section);
95
+ };
96
+ /**
97
+ * Evaluate a guard against (case state, actor, asOf) and return the full
98
+ * evaluation record.
99
+ *
100
+ * Guarantees the rest of the engine leans on:
101
+ *
102
+ * - **Pure and deterministic.** No I/O, no ambient clock; the same
103
+ * (guard, state, actor, asOf) always yields a deeply-equal record.
104
+ * - **Complete.** Every condition — including every arm of every `anyOf`
105
+ * group — is evaluated and reported; there is no short-circuiting, so the
106
+ * record always answers "why / why not" in full.
107
+ * - **Total.** A condition that throws, or returns a malformed
108
+ * outcome, becomes a failed result with a diagnostic `reason`; one
109
+ * defective condition can never take down affordance computation for a
110
+ * case. (The only throw is a `TypeError` when `ctx.asOf` itself is not a
111
+ * determinable instant — that is a caller bug, not case data.)
112
+ * - **Serializable.** The record is a plain object with only JSON-safe
113
+ * values, ready for the journal's guard-results field verbatim.
114
+ */
115
+ export const evaluateGuard = (guard, ctx) => {
116
+ const asOf = toIso(ctx.asOf);
117
+ const conditionCtx = ctx.scope === undefined
118
+ ? { actor: ctx.actor }
119
+ : { actor: ctx.actor, scope: ctx.scope };
120
+ const conditions = guardEntries(guard).map((entry) => evaluateEntry(entry, ctx.state, conditionCtx));
121
+ const holds = (section) => conditions.every((result) => result.section !== section || result.passed);
122
+ const possible = holds('requires');
123
+ const permitted = holds('permits');
124
+ return {
125
+ asOf,
126
+ possible,
127
+ permitted,
128
+ available: possible && permitted,
129
+ conditions,
130
+ };
131
+ };
132
+ /**
133
+ * The unmet conditions of an evaluation — the derived view every consumer
134
+ * of a refusal renders. A view, not a convention: the error message, the
135
+ * blocked entry and the wire payload all call this instead of each spelling
136
+ * `conditions.filter(!passed)` for themselves.
137
+ */
138
+ export const unmetConditions = (evaluation) => evaluation.conditions.filter((condition) => !condition.passed);
139
+ /**
140
+ * The unmet conditions of an evaluation, flattened to addresses. An `anyOf`
141
+ * group that failed names each failing arm — the same addresses `explain`
142
+ * uses — so a refusal never says less than the
143
+ * evaluation knows.
144
+ */
145
+ export const unmetAddresses = (evaluation) => unmetConditions(evaluation).flatMap((result) => {
146
+ if (result.kind === 'anyOf') {
147
+ return result.arms
148
+ .filter((arm) => !arm.passed)
149
+ .map((arm) => ({
150
+ address: conditionAddress(result.section, result.name, arm.name),
151
+ ...(arm.reason !== undefined && { reason: arm.reason }),
152
+ }));
153
+ }
154
+ return [
155
+ {
156
+ address: conditionAddress(result.section, result.name),
157
+ ...(result.reason !== undefined && { reason: result.reason }),
158
+ },
159
+ ];
160
+ });
161
+ /** Render the unmet conditions as one message clause: `requires.a — why; permits.b`. */
162
+ export const describeUnmet = (evaluation) => unmetAddresses(evaluation)
163
+ .map((unmet) => unmet.reason === undefined
164
+ ? unmet.address
165
+ : `${unmet.address} — ${unmet.reason}`)
166
+ .join('; ');
167
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../../src/guards/evaluate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Guard evaluation — pure, deterministic, explainable.\n *\n * `evaluateGuard(guard, ctx)` turns a guard and an evaluation context\n * (case state, actor, asOf) into a full per-condition evaluation record:\n * the raw material for affordance computation, `explain`, and\n * the journal's guard-results field.\n */\n\nimport { thrownMessage } from '../errors.js'\nimport type {\n Condition,\n ConditionContext,\n ConditionOutcome,\n} from './condition.js'\nimport type { Guard, GuardArm, GuardEntry, GuardSection } from './guard.js'\nimport { conditionAddress, guardEntries } from './guard.js'\nimport type { Instant } from './time.js'\nimport { toIso } from './time.js'\n\n/**\n * Everything guard evaluation is a function of: case state, the acting\n * actor, and the instant to evaluate as of. Nothing else — no ambient\n * clock, no I/O — so the same context always produces the same record.\n *\n * `asOf` is explicit and mandatory: it is the instant the evaluation is\n * made as of, stated on the record so the journal's claim-time evidence is\n * self-contained.\n *\n * Scoped steps: when evaluating a scoped step's guard for\n * one element of its scoped collection, bind the element as `scope`. It is\n * threaded through to conditions via {@link ConditionContext}. When\n * `scope` is absent, evaluation is exactly what it always was.\n */\nexport interface GuardEvaluationContext<TState, TActor = unknown> {\n readonly state: TState\n readonly actor: TActor\n readonly asOf: Instant\n /**\n * The bound scope element, when evaluating a scoped step's guard for one\n * element. Threaded to conditions as `ctx.scope`. Omit for unscoped\n * evaluation. `undefined` means \"not scoped\" — a scope binding is always a\n * present element of a state collection.\n */\n readonly scope?: unknown\n}\n\n/** Fields common to every per-condition result in an evaluation record. */\nexport interface ConditionResultBase {\n /** The condition's name in its section map (or its arm name, within an anyOf group). */\n readonly name: string\n /** Which guard section the condition came from — `requires` or `permits`. */\n readonly section: GuardSection\n readonly passed: boolean\n /**\n * Present when the condition supplied one via the `{ ok, reason? }`\n * escape hatch, or when evaluation had to absorb a defect (a condition\n * that threw, or returned an unrecognized result).\n */\n readonly reason?: string\n}\n\n/** Result of one plain (predicate) condition. */\nexport interface SingleConditionResult extends ConditionResultBase {\n readonly kind: 'condition'\n}\n\n/** Result of one arm of an `anyOf` group; `name` is the arm name. */\nexport type AnyOfArmResult = SingleConditionResult\n\n/**\n * Result of an `anyOf` group: passed when at least one arm passed. Every\n * arm is always evaluated and reported, so a failing group names each\n * failing arm and a passing group shows which arm carried it.\n */\nexport interface AnyOfConditionResult extends ConditionResultBase {\n readonly kind: 'anyOf'\n readonly arms: readonly AnyOfArmResult[]\n}\n\n/** Result of one named entry in a guard section. */\nexport type ConditionResult = SingleConditionResult | AnyOfConditionResult\n\n/**\n * The full record of one guard evaluation — a plain, JSON-serializable\n * object, deterministic in (guard, state, actor, asOf). This is what\n * affordance computation filters on, what `explain` renders, and what the\n * journal stores as an execution's guard results.\n *\n * The `possible` / `permitted` pair is the requires/permits split made mechanical:\n * `possible` is false when a `requires` condition is unmet (the step is not\n * possible on this case, for anyone); `permitted` is false when a `permits`\n * condition is unmet (not permitted for this actor). `available` is their\n * conjunction. An omitted section is vacuously satisfied.\n */\nexport interface GuardEvaluation {\n /** The instant evaluated as of, normalized to ISO-8601 UTC. */\n readonly asOf: string\n /** Every `requires` condition holds — the step is possible on this case. */\n readonly possible: boolean\n /** Every `permits` condition holds — the actor is permitted. */\n readonly permitted: boolean\n /** `possible && permitted` — the step is an affordance for this actor. */\n readonly available: boolean\n /** Per-condition results, in guard declaration order (`requires` first). */\n readonly conditions: readonly ConditionResult[]\n}\n\n/**\n * Marker for evaluating a guard with no actor at hand — `explain`'s\n * requires-only probe. A `permits` condition is the only kind of entry\n * that reads the actor, so it alone is skipped: reported failed with\n * {@link NOT_EVALUATED_REASON} rather than run against nothing and absorbed\n * as a thrown `TypeError`.\n */\nexport const NO_ACTOR: unique symbol = Symbol('affordance.no-actor')\n\n/** The stated reason a `permits` condition reports under {@link NO_ACTOR}. */\nexport const NOT_EVALUATED_REASON = 'not evaluated: no actor supplied'\n\nconst withReason = <T extends object>(\n result: T,\n reason: string | undefined,\n): T & { reason?: string } =>\n reason === undefined ? result : { ...result, reason }\n\n/** Evaluate one plain condition, absorbing throws and malformed outcomes into a failed result. */\nconst evaluateCondition = <TState, TActor>(\n name: string,\n section: GuardSection,\n condition: Condition<TState, TActor>,\n state: TState,\n ctx: ConditionContext<TActor>,\n): SingleConditionResult => {\n if (section === 'permits' && (ctx.actor as unknown) === NO_ACTOR) {\n return {\n name,\n section,\n kind: 'condition',\n passed: false,\n reason: NOT_EVALUATED_REASON,\n }\n }\n let outcome: ConditionOutcome\n try {\n outcome = condition(state, ctx)\n } catch (err) {\n return {\n name,\n section,\n kind: 'condition',\n passed: false,\n reason: `condition threw: ${thrownMessage(err)}`,\n }\n }\n if (typeof outcome === 'boolean') {\n return { name, section, kind: 'condition', passed: outcome }\n }\n if (\n typeof outcome === 'object' &&\n outcome !== null &&\n typeof outcome.ok === 'boolean'\n ) {\n return withReason(\n { name, section, kind: 'condition' as const, passed: outcome.ok },\n outcome.reason,\n )\n }\n return {\n name,\n section,\n kind: 'condition',\n passed: false,\n reason:\n 'condition returned neither a boolean nor an { ok, reason? } verdict',\n }\n}\n\nconst MALFORMED = 'entry is not a condition function or an anyOf(...) group'\n\nconst malformed = (\n name: string,\n section: GuardSection,\n): SingleConditionResult => ({\n name,\n section,\n kind: 'condition',\n passed: false,\n reason: MALFORMED,\n})\n\n/** Evaluate one arm of an `anyOf` group; the arm's own name is what it reports under. */\nconst evaluateArm = <TState, TActor>(\n arm: GuardArm<TState, TActor>,\n state: TState,\n ctx: ConditionContext<TActor>,\n): AnyOfArmResult => {\n if (arm.kind === 'condition') {\n return evaluateCondition(\n arm.arm,\n arm.section,\n arm.entry as Condition<TState, TActor>,\n state,\n ctx,\n )\n }\n return malformed(arm.arm, arm.section)\n}\n\n/** Evaluate one walked guard entry (plain condition or anyOf group). */\nconst evaluateEntry = <TState, TActor>(\n entry: GuardEntry<TState, TActor>,\n state: TState,\n ctx: ConditionContext<TActor>,\n): ConditionResult => {\n const { name, section } = entry\n if (entry.kind === 'condition') {\n return evaluateCondition(\n name,\n section,\n entry.entry as Condition<TState, TActor>,\n state,\n ctx,\n )\n }\n if (entry.kind === 'anyOf') {\n const arms = entry.arms.map((arm) => evaluateArm(arm, state, ctx))\n return {\n name,\n section,\n kind: 'anyOf',\n passed: arms.some((a) => a.passed),\n arms,\n }\n }\n return malformed(name, section)\n}\n\n/**\n * Evaluate a guard against (case state, actor, asOf) and return the full\n * evaluation record.\n *\n * Guarantees the rest of the engine leans on:\n *\n * - **Pure and deterministic.** No I/O, no ambient clock; the same\n * (guard, state, actor, asOf) always yields a deeply-equal record.\n * - **Complete.** Every condition — including every arm of every `anyOf`\n * group — is evaluated and reported; there is no short-circuiting, so the\n * record always answers \"why / why not\" in full.\n * - **Total.** A condition that throws, or returns a malformed\n * outcome, becomes a failed result with a diagnostic `reason`; one\n * defective condition can never take down affordance computation for a\n * case. (The only throw is a `TypeError` when `ctx.asOf` itself is not a\n * determinable instant — that is a caller bug, not case data.)\n * - **Serializable.** The record is a plain object with only JSON-safe\n * values, ready for the journal's guard-results field verbatim.\n */\nexport const evaluateGuard = <TState, TActor = unknown>(\n guard: Guard<TState, TActor>,\n ctx: GuardEvaluationContext<TState, TActor>,\n): GuardEvaluation => {\n const asOf = toIso(ctx.asOf)\n const conditionCtx: ConditionContext<TActor> =\n ctx.scope === undefined\n ? { actor: ctx.actor }\n : { actor: ctx.actor, scope: ctx.scope }\n const conditions = guardEntries(guard).map((entry) =>\n evaluateEntry(entry, ctx.state, conditionCtx),\n )\n const holds = (section: GuardSection): boolean =>\n conditions.every((result) => result.section !== section || result.passed)\n const possible = holds('requires')\n const permitted = holds('permits')\n return {\n asOf,\n possible,\n permitted,\n available: possible && permitted,\n conditions,\n }\n}\n\n/**\n * The unmet conditions of an evaluation — the derived view every consumer\n * of a refusal renders. A view, not a convention: the error message, the\n * blocked entry and the wire payload all call this instead of each spelling\n * `conditions.filter(!passed)` for themselves.\n */\nexport const unmetConditions = (\n evaluation: GuardEvaluation,\n): readonly ConditionResult[] =>\n evaluation.conditions.filter((condition) => !condition.passed)\n\n/** One unmet condition, addressed — `requires.financing.preApproved` — with its reason when it gave one. */\nexport interface AddressedUnmet {\n /** See {@link conditionAddress}: `section.name`, or `section.name.arm` inside an `anyOf`. */\n readonly address: string\n readonly reason?: string\n}\n\n/**\n * The unmet conditions of an evaluation, flattened to addresses. An `anyOf`\n * group that failed names each failing arm — the same addresses `explain`\n * uses — so a refusal never says less than the\n * evaluation knows.\n */\nexport const unmetAddresses = (\n evaluation: GuardEvaluation,\n): readonly AddressedUnmet[] =>\n unmetConditions(evaluation).flatMap((result): readonly AddressedUnmet[] => {\n if (result.kind === 'anyOf') {\n return result.arms\n .filter((arm) => !arm.passed)\n .map((arm) => ({\n address: conditionAddress(result.section, result.name, arm.name),\n ...(arm.reason !== undefined && { reason: arm.reason }),\n }))\n }\n return [\n {\n address: conditionAddress(result.section, result.name),\n ...(result.reason !== undefined && { reason: result.reason }),\n },\n ]\n })\n\n/** Render the unmet conditions as one message clause: `requires.a — why; permits.b`. */\nexport const describeUnmet = (evaluation: GuardEvaluation): string =>\n unmetAddresses(evaluation)\n .map((unmet) =>\n unmet.reason === undefined\n ? unmet.address\n : `${unmet.address} — ${unmet.reason}`,\n )\n .join('; ')\n"]}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The guard model.
3
+ *
4
+ * A guard is two flat AND-maps of named conditions — a map is satisfied
5
+ * only when every entry is: `requires` (case conditions — is this step
6
+ * possible on this case?) and `permits` (actor conditions — is this actor
7
+ * permitted to take it?). The algebra is
8
+ * deliberately small: a map value is a plain condition or exactly one level
9
+ * of named `anyOf` group. No nested and/or trees — the type of an `anyOf`
10
+ * arm makes deeper nesting inexpressible.
11
+ */
12
+ import type { Condition } from './condition.js';
13
+ /**
14
+ * What may sit inside an `anyOf` group: a plain condition. Never another
15
+ * `anyOf` — the algebra allows exactly one level of grouping, and this type
16
+ * is what enforces it.
17
+ */
18
+ export type AnyOfArm<TState, TActor = unknown> = Condition<TState, TActor>;
19
+ /**
20
+ * A named disjunction within a guard's AND-map: the group is satisfied when
21
+ * at least one arm is. The group is named by its key in the map; each arm is
22
+ * named by its key in `arms`, and every arm is always evaluated and reported
23
+ * individually in the evaluation record.
24
+ */
25
+ export interface AnyOfGroup<TState, TActor = unknown> {
26
+ readonly kind: 'anyOf';
27
+ readonly arms: Readonly<Record<string, AnyOfArm<TState, TActor>>>;
28
+ }
29
+ /**
30
+ * Declare a named disjunction: satisfied when at least one arm is.
31
+ *
32
+ * ```ts
33
+ * requires: {
34
+ * financing: anyOf({
35
+ * preApproved: s => s.buyer?.preApproved ?? false,
36
+ * proofOfFunds: s => s.buyer?.proofOfFunds ?? false,
37
+ * }),
38
+ * }
39
+ * ```
40
+ *
41
+ * Arms are plain conditions; nesting another `anyOf` is a type error (one
42
+ * level only). Throws at definition time on an empty or malformed arms map —
43
+ * a guard definition bug should fail the deploy, not an evaluation.
44
+ */
45
+ export declare const anyOf: <TState, TActor = unknown>(arms: Readonly<Record<string, AnyOfArm<TState, TActor>>>) => AnyOfGroup<TState, TActor>;
46
+ /**
47
+ * Anything that may sit under a name in a guard section: a plain condition
48
+ * or one `anyOf` group.
49
+ */
50
+ export type ConditionMapEntry<TState, TActor = unknown> = Condition<TState, TActor> | AnyOfGroup<TState, TActor>;
51
+ /**
52
+ * A flat AND-map of named conditions: the section is satisfied when every
53
+ * entry is. An empty or absent map is vacuously satisfied. Entry names are
54
+ * the unit of explainability — they surface verbatim in evaluation records,
55
+ * `explain`, and the journal.
56
+ */
57
+ export type ConditionMap<TState, TActor = unknown> = Readonly<Record<string, ConditionMapEntry<TState, TActor>>>;
58
+ /** The two sections of a guard: `requires` (case) and `permits` (actor). */
59
+ export type GuardSection = 'requires' | 'permits';
60
+ /**
61
+ * A step's guard: the full set of named conditions under which the step is
62
+ * available (CONTEXT.md).
63
+ *
64
+ * - `requires` — case conditions. When one fails, the step is **not
65
+ * possible** on this case, for anyone.
66
+ * - `permits` — actor conditions. When one fails (and `requires` holds), the
67
+ * step is possible but **not permitted for this actor**.
68
+ *
69
+ * The two sections evaluate identically; the split exists so evaluation can
70
+ * mechanically distinguish those two answers. An omitted section is
71
+ * vacuously satisfied.
72
+ */
73
+ export interface Guard<TState, TActor = unknown> {
74
+ readonly requires?: ConditionMap<TState, TActor>;
75
+ readonly permits?: ConditionMap<TState, TActor>;
76
+ }
77
+ /**
78
+ * The stable address of one condition within a guard: `requires.escrowReady`,
79
+ * or `requires.financing.preApproved` for an arm of an `anyOf` group.
80
+ *
81
+ * Human-legible and unique within a guard, which is why a refusal's list of
82
+ * unmet conditions names each condition by it.
83
+ */
84
+ export declare const conditionAddress: (section: GuardSection, name: string, arm?: string) => string;
85
+ /**
86
+ * What kind of thing sits under a name in a guard section. `'unknown'` is an
87
+ * entry that is neither: a definition bug, which `step()` rejects
88
+ * at construction and evaluation absorbs into a failed condition — never a
89
+ * crash.
90
+ */
91
+ export type GuardEntryKind = 'condition' | 'anyOf' | 'unknown';
92
+ /** One arm of an `anyOf` group, located, classified and addressed. */
93
+ export interface GuardArm<TState, TActor = unknown> {
94
+ readonly section: GuardSection;
95
+ /** The *group's* name in its section map. */
96
+ readonly name: string;
97
+ /** This arm's name within the group. */
98
+ readonly arm: string;
99
+ /** Never `'anyOf'` — the algebra allows exactly one level of grouping. */
100
+ readonly kind: 'condition' | 'unknown';
101
+ readonly entry: AnyOfArm<TState, TActor>;
102
+ /** `requires.financing.preApproved` — see {@link conditionAddress}. */
103
+ readonly address: string;
104
+ }
105
+ /** One named entry of a guard section, located, classified and addressed. */
106
+ export interface GuardEntry<TState, TActor = unknown> {
107
+ readonly section: GuardSection;
108
+ readonly name: string;
109
+ readonly kind: GuardEntryKind;
110
+ readonly entry: ConditionMapEntry<TState, TActor>;
111
+ /** `requires.escrowReady` — see {@link conditionAddress}. */
112
+ readonly address: string;
113
+ /** The group's arms when `kind` is `'anyOf'`; empty otherwise. */
114
+ readonly arms: readonly GuardArm<TState, TActor>[];
115
+ }
116
+ /**
117
+ * Walk a guard: every named entry of both sections, in declaration order
118
+ * (`requires` first), classified and addressed, with `anyOf` arms attached.
119
+ *
120
+ * **This is the only place that knows the guard's shape.** Evaluation
121
+ * (`evaluateGuard`) and definition-time validation (`step()`) are maps and
122
+ * filters over this — so the algebra grows in one
123
+ * edit instead of two, and a condition's address is spelled once instead
124
+ * of twice.
125
+ *
126
+ * Total: a malformed entry is classified `'unknown'` and reported — the
127
+ * walk itself never throws — because every consumer's own totality rests on
128
+ * this walk.
129
+ *
130
+ * Cached per guard object: a guard is constructed once (by `step()`, or by
131
+ * hand in a test) and never mutated afterwards — every map on it is declared
132
+ * readonly — so the walk is a pure function of the guard's identity.
133
+ * Evaluation calls this on every affordance computation, and the cache makes
134
+ * each call after the first a lookup instead of a re-walk.
135
+ */
136
+ export declare const guardEntries: <TState, TActor = unknown>(guard: Guard<TState, TActor>) => readonly GuardEntry<TState, TActor>[];
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The guard model.
3
+ *
4
+ * A guard is two flat AND-maps of named conditions — a map is satisfied
5
+ * only when every entry is: `requires` (case conditions — is this step
6
+ * possible on this case?) and `permits` (actor conditions — is this actor
7
+ * permitted to take it?). The algebra is
8
+ * deliberately small: a map value is a plain condition or exactly one level
9
+ * of named `anyOf` group. No nested and/or trees — the type of an `anyOf`
10
+ * arm makes deeper nesting inexpressible.
11
+ */
12
+ /**
13
+ * Declare a named disjunction: satisfied when at least one arm is.
14
+ *
15
+ * ```ts
16
+ * requires: {
17
+ * financing: anyOf({
18
+ * preApproved: s => s.buyer?.preApproved ?? false,
19
+ * proofOfFunds: s => s.buyer?.proofOfFunds ?? false,
20
+ * }),
21
+ * }
22
+ * ```
23
+ *
24
+ * Arms are plain conditions; nesting another `anyOf` is a type error (one
25
+ * level only). Throws at definition time on an empty or malformed arms map —
26
+ * a guard definition bug should fail the deploy, not an evaluation.
27
+ */
28
+ export const anyOf = (arms) => {
29
+ const names = Object.keys(arms ?? {});
30
+ if (names.length === 0) {
31
+ throw new TypeError('anyOf(arms): at least one named arm is required');
32
+ }
33
+ for (const name of names) {
34
+ const arm = arms[name];
35
+ if (typeof arm !== 'function') {
36
+ throw new TypeError(`anyOf(arms): arm '${name}' must be a condition function — nested anyOf groups are not part of the guard algebra`);
37
+ }
38
+ }
39
+ return { kind: 'anyOf', arms: { ...arms } };
40
+ };
41
+ /**
42
+ * The stable address of one condition within a guard: `requires.escrowReady`,
43
+ * or `requires.financing.preApproved` for an arm of an `anyOf` group.
44
+ *
45
+ * Human-legible and unique within a guard, which is why a refusal's list of
46
+ * unmet conditions names each condition by it.
47
+ */
48
+ export const conditionAddress = (section, name, arm) => arm === undefined ? `${section}.${name}` : `${section}.${name}.${arm}`;
49
+ const entryKind = (entry) => {
50
+ if (typeof entry === 'function')
51
+ return 'condition';
52
+ if (typeof entry !== 'object' || entry === null)
53
+ return 'unknown';
54
+ if (entry.kind === 'anyOf')
55
+ return 'anyOf';
56
+ return 'unknown';
57
+ };
58
+ const walkedEntries = new WeakMap();
59
+ /**
60
+ * Walk a guard: every named entry of both sections, in declaration order
61
+ * (`requires` first), classified and addressed, with `anyOf` arms attached.
62
+ *
63
+ * **This is the only place that knows the guard's shape.** Evaluation
64
+ * (`evaluateGuard`) and definition-time validation (`step()`) are maps and
65
+ * filters over this — so the algebra grows in one
66
+ * edit instead of two, and a condition's address is spelled once instead
67
+ * of twice.
68
+ *
69
+ * Total: a malformed entry is classified `'unknown'` and reported — the
70
+ * walk itself never throws — because every consumer's own totality rests on
71
+ * this walk.
72
+ *
73
+ * Cached per guard object: a guard is constructed once (by `step()`, or by
74
+ * hand in a test) and never mutated afterwards — every map on it is declared
75
+ * readonly — so the walk is a pure function of the guard's identity.
76
+ * Evaluation calls this on every affordance computation, and the cache makes
77
+ * each call after the first a lookup instead of a re-walk.
78
+ */
79
+ export const guardEntries = (guard) => {
80
+ const cached = walkedEntries.get(guard);
81
+ if (cached !== undefined) {
82
+ return cached;
83
+ }
84
+ const entries = [];
85
+ for (const section of ['requires', 'permits']) {
86
+ for (const [name, entry] of Object.entries(guard[section] ?? {})) {
87
+ const kind = entryKind(entry);
88
+ const arms = [];
89
+ if (kind === 'anyOf') {
90
+ const group = entry;
91
+ for (const [arm, armEntry] of Object.entries(group.arms ?? {})) {
92
+ const armKind = entryKind(armEntry);
93
+ arms.push({
94
+ section,
95
+ name,
96
+ arm,
97
+ // A nested group is inexpressible in the type and meaningless
98
+ // here, so it is classified as a malformed arm.
99
+ kind: armKind === 'anyOf' ? 'unknown' : armKind,
100
+ entry: armEntry,
101
+ address: conditionAddress(section, name, arm),
102
+ });
103
+ }
104
+ }
105
+ entries.push({
106
+ section,
107
+ name,
108
+ kind,
109
+ entry,
110
+ address: conditionAddress(section, name),
111
+ arms,
112
+ });
113
+ }
114
+ }
115
+ walkedEntries.set(guard, entries);
116
+ return entries;
117
+ };
118
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/guards/guard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The guard model.\n *\n * A guard is two flat AND-maps of named conditions — a map is satisfied\n * only when every entry is: `requires` (case conditions — is this step\n * possible on this case?) and `permits` (actor conditions — is this actor\n * permitted to take it?). The algebra is\n * deliberately small: a map value is a plain condition or exactly one level\n * of named `anyOf` group. No nested and/or trees — the type of an `anyOf`\n * arm makes deeper nesting inexpressible.\n */\n\nimport type { Condition } from './condition.js'\n\n/**\n * What may sit inside an `anyOf` group: a plain condition. Never another\n * `anyOf` — the algebra allows exactly one level of grouping, and this type\n * is what enforces it.\n */\nexport type AnyOfArm<TState, TActor = unknown> = Condition<TState, TActor>\n\n/**\n * A named disjunction within a guard's AND-map: the group is satisfied when\n * at least one arm is. The group is named by its key in the map; each arm is\n * named by its key in `arms`, and every arm is always evaluated and reported\n * individually in the evaluation record.\n */\nexport interface AnyOfGroup<TState, TActor = unknown> {\n readonly kind: 'anyOf'\n readonly arms: Readonly<Record<string, AnyOfArm<TState, TActor>>>\n}\n\n/**\n * Declare a named disjunction: satisfied when at least one arm is.\n *\n * ```ts\n * requires: {\n * financing: anyOf({\n * preApproved: s => s.buyer?.preApproved ?? false,\n * proofOfFunds: s => s.buyer?.proofOfFunds ?? false,\n * }),\n * }\n * ```\n *\n * Arms are plain conditions; nesting another `anyOf` is a type error (one\n * level only). Throws at definition time on an empty or malformed arms map —\n * a guard definition bug should fail the deploy, not an evaluation.\n */\nexport const anyOf = <TState, TActor = unknown>(\n arms: Readonly<Record<string, AnyOfArm<TState, TActor>>>,\n): AnyOfGroup<TState, TActor> => {\n const names = Object.keys(arms ?? {})\n if (names.length === 0) {\n throw new TypeError('anyOf(arms): at least one named arm is required')\n }\n for (const name of names) {\n const arm: unknown = arms[name]\n if (typeof arm !== 'function') {\n throw new TypeError(\n `anyOf(arms): arm '${name}' must be a condition function — nested anyOf groups are not part of the guard algebra`,\n )\n }\n }\n return { kind: 'anyOf', arms: { ...arms } }\n}\n\n/**\n * Anything that may sit under a name in a guard section: a plain condition\n * or one `anyOf` group.\n */\nexport type ConditionMapEntry<TState, TActor = unknown> =\n | Condition<TState, TActor>\n | AnyOfGroup<TState, TActor>\n\n/**\n * A flat AND-map of named conditions: the section is satisfied when every\n * entry is. An empty or absent map is vacuously satisfied. Entry names are\n * the unit of explainability — they surface verbatim in evaluation records,\n * `explain`, and the journal.\n */\nexport type ConditionMap<TState, TActor = unknown> = Readonly<\n Record<string, ConditionMapEntry<TState, TActor>>\n>\n\n/** The two sections of a guard: `requires` (case) and `permits` (actor). */\nexport type GuardSection = 'requires' | 'permits'\n\n/**\n * A step's guard: the full set of named conditions under which the step is\n * available (CONTEXT.md).\n *\n * - `requires` — case conditions. When one fails, the step is **not\n * possible** on this case, for anyone.\n * - `permits` — actor conditions. When one fails (and `requires` holds), the\n * step is possible but **not permitted for this actor**.\n *\n * The two sections evaluate identically; the split exists so evaluation can\n * mechanically distinguish those two answers. An omitted section is\n * vacuously satisfied.\n */\nexport interface Guard<TState, TActor = unknown> {\n readonly requires?: ConditionMap<TState, TActor>\n readonly permits?: ConditionMap<TState, TActor>\n}\n\n/**\n * The stable address of one condition within a guard: `requires.escrowReady`,\n * or `requires.financing.preApproved` for an arm of an `anyOf` group.\n *\n * Human-legible and unique within a guard, which is why a refusal's list of\n * unmet conditions names each condition by it.\n */\nexport const conditionAddress = (\n section: GuardSection,\n name: string,\n arm?: string,\n): string =>\n arm === undefined ? `${section}.${name}` : `${section}.${name}.${arm}`\n\n/**\n * What kind of thing sits under a name in a guard section. `'unknown'` is an\n * entry that is neither: a definition bug, which `step()` rejects\n * at construction and evaluation absorbs into a failed condition — never a\n * crash.\n */\nexport type GuardEntryKind = 'condition' | 'anyOf' | 'unknown'\n\nconst entryKind = (entry: unknown): GuardEntryKind => {\n if (typeof entry === 'function') return 'condition'\n if (typeof entry !== 'object' || entry === null) return 'unknown'\n if ((entry as { kind?: unknown }).kind === 'anyOf') return 'anyOf'\n return 'unknown'\n}\n\n/** One arm of an `anyOf` group, located, classified and addressed. */\nexport interface GuardArm<TState, TActor = unknown> {\n readonly section: GuardSection\n /** The *group's* name in its section map. */\n readonly name: string\n /** This arm's name within the group. */\n readonly arm: string\n /** Never `'anyOf'` — the algebra allows exactly one level of grouping. */\n readonly kind: 'condition' | 'unknown'\n readonly entry: AnyOfArm<TState, TActor>\n /** `requires.financing.preApproved` — see {@link conditionAddress}. */\n readonly address: string\n}\n\n/** One named entry of a guard section, located, classified and addressed. */\nexport interface GuardEntry<TState, TActor = unknown> {\n readonly section: GuardSection\n readonly name: string\n readonly kind: GuardEntryKind\n readonly entry: ConditionMapEntry<TState, TActor>\n /** `requires.escrowReady` — see {@link conditionAddress}. */\n readonly address: string\n /** The group's arms when `kind` is `'anyOf'`; empty otherwise. */\n readonly arms: readonly GuardArm<TState, TActor>[]\n}\n\nconst walkedEntries = new WeakMap<object, readonly GuardEntry<never, never>[]>()\n\n/**\n * Walk a guard: every named entry of both sections, in declaration order\n * (`requires` first), classified and addressed, with `anyOf` arms attached.\n *\n * **This is the only place that knows the guard's shape.** Evaluation\n * (`evaluateGuard`) and definition-time validation (`step()`) are maps and\n * filters over this — so the algebra grows in one\n * edit instead of two, and a condition's address is spelled once instead\n * of twice.\n *\n * Total: a malformed entry is classified `'unknown'` and reported — the\n * walk itself never throws — because every consumer's own totality rests on\n * this walk.\n *\n * Cached per guard object: a guard is constructed once (by `step()`, or by\n * hand in a test) and never mutated afterwards — every map on it is declared\n * readonly — so the walk is a pure function of the guard's identity.\n * Evaluation calls this on every affordance computation, and the cache makes\n * each call after the first a lookup instead of a re-walk.\n */\nexport const guardEntries = <TState, TActor = unknown>(\n guard: Guard<TState, TActor>,\n): readonly GuardEntry<TState, TActor>[] => {\n const cached = walkedEntries.get(guard)\n if (cached !== undefined) {\n return cached as readonly GuardEntry<TState, TActor>[]\n }\n const entries: GuardEntry<TState, TActor>[] = []\n for (const section of ['requires', 'permits'] as const) {\n for (const [name, entry] of Object.entries(guard[section] ?? {})) {\n const kind = entryKind(entry)\n const arms: GuardArm<TState, TActor>[] = []\n if (kind === 'anyOf') {\n const group = entry as AnyOfGroup<TState, TActor>\n for (const [arm, armEntry] of Object.entries(group.arms ?? {})) {\n const armKind = entryKind(armEntry)\n arms.push({\n section,\n name,\n arm,\n // A nested group is inexpressible in the type and meaningless\n // here, so it is classified as a malformed arm.\n kind: armKind === 'anyOf' ? 'unknown' : armKind,\n entry: armEntry,\n address: conditionAddress(section, name, arm),\n })\n }\n }\n entries.push({\n section,\n name,\n kind,\n entry,\n address: conditionAddress(section, name),\n arms,\n })\n }\n }\n walkedEntries.set(guard, entries)\n return entries\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The guard model and evaluation engine.
3
+ *
4
+ * A **guard** is a step's full set of named **conditions** — pure,
5
+ * synchronous predicates over (case state, actor) — split into
6
+ * `requires` (case conditions) and `permits` (actor conditions).
7
+ * `evaluateGuard` turns a guard and (state, actor, asOf) into a full,
8
+ * serializable per-condition evaluation record: the raw material for
9
+ * affordances, `explain`, and the journal.
10
+ *
11
+ * See `docs/architecture.md` for why guards are shaped this way, and
12
+ * CONTEXT.md for the vocabulary.
13
+ */
14
+ export type { Condition, ConditionContext, ConditionOutcome, ConditionVerdict, } from './condition.js';
15
+ export type { AddressedUnmet, AnyOfArmResult, AnyOfConditionResult, ConditionResult, ConditionResultBase, GuardEvaluation, GuardEvaluationContext, SingleConditionResult, } from './evaluate.js';
16
+ export { describeUnmet, evaluateGuard, NO_ACTOR, NOT_EVALUATED_REASON, unmetAddresses, unmetConditions, } from './evaluate.js';
17
+ export type { AnyOfArm, AnyOfGroup, ConditionMap, ConditionMapEntry, Guard, GuardArm, GuardEntry, GuardEntryKind, GuardSection, } from './guard.js';
18
+ export { anyOf, conditionAddress, guardEntries } from './guard.js';
19
+ export type { Instant } from './time.js';
20
+ export { toEpochMs, toIso } from './time.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The guard model and evaluation engine.
3
+ *
4
+ * A **guard** is a step's full set of named **conditions** — pure,
5
+ * synchronous predicates over (case state, actor) — split into
6
+ * `requires` (case conditions) and `permits` (actor conditions).
7
+ * `evaluateGuard` turns a guard and (state, actor, asOf) into a full,
8
+ * serializable per-condition evaluation record: the raw material for
9
+ * affordances, `explain`, and the journal.
10
+ *
11
+ * See `docs/architecture.md` for why guards are shaped this way, and
12
+ * CONTEXT.md for the vocabulary.
13
+ */
14
+ export { describeUnmet, evaluateGuard, NO_ACTOR, NOT_EVALUATED_REASON, unmetAddresses, unmetConditions, } from './evaluate.js';
15
+ export { anyOf, conditionAddress, guardEntries } from './guard.js';
16
+ export { toEpochMs, toIso } from './time.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/guards/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,OAAO,EACL,aAAa,EACb,aAAa,EACb,QAAQ,EACR,oBAAoB,EACpB,cAAc,EACd,eAAe,GAChB,MAAM,eAAe,CAAA;AAYtB,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAElE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA","sourcesContent":["/**\n * The guard model and evaluation engine.\n *\n * A **guard** is a step's full set of named **conditions** — pure,\n * synchronous predicates over (case state, actor) — split into\n * `requires` (case conditions) and `permits` (actor conditions).\n * `evaluateGuard` turns a guard and (state, actor, asOf) into a full,\n * serializable per-condition evaluation record: the raw material for\n * affordances, `explain`, and the journal.\n *\n * See `docs/architecture.md` for why guards are shaped this way, and\n * CONTEXT.md for the vocabulary.\n */\n\nexport type {\n Condition,\n ConditionContext,\n ConditionOutcome,\n ConditionVerdict,\n} from './condition.js'\nexport type {\n AddressedUnmet,\n AnyOfArmResult,\n AnyOfConditionResult,\n ConditionResult,\n ConditionResultBase,\n GuardEvaluation,\n GuardEvaluationContext,\n SingleConditionResult,\n} from './evaluate.js'\nexport {\n describeUnmet,\n evaluateGuard,\n NO_ACTOR,\n NOT_EVALUATED_REASON,\n unmetAddresses,\n unmetConditions,\n} from './evaluate.js'\nexport type {\n AnyOfArm,\n AnyOfGroup,\n ConditionMap,\n ConditionMapEntry,\n Guard,\n GuardArm,\n GuardEntry,\n GuardEntryKind,\n GuardSection,\n} from './guard.js'\nexport { anyOf, conditionAddress, guardEntries } from './guard.js'\nexport type { Instant } from './time.js'\nexport { toEpochMs, toIso } from './time.js'\n"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Instant normalization.
3
+ *
4
+ * Conditions never read the clock. Evaluation takes an explicit `asOf`
5
+ * instant, so "evaluate this guard as of T" is always well-defined and the
6
+ * evaluation record can state the instant it was made as of. The helpers
7
+ * here normalize the forms callers and case state plausibly hold an instant
8
+ * in.
9
+ */
10
+ /**
11
+ * An instant in time, in any of the forms case state and callers plausibly
12
+ * hold one: a `Date`, an ISO-8601 (or otherwise `Date.parse`-able) string,
13
+ * or epoch milliseconds. Normalized internally; evaluation records always
14
+ * report instants as ISO-8601 UTC strings.
15
+ */
16
+ export type Instant = Date | string | number;
17
+ /**
18
+ * Normalize any {@link Instant} form to epoch milliseconds; `null` when the
19
+ * value is absent or not a determinable instant. **Total** by design — never
20
+ * throws, because absence and rubbish in historical Case State are ordinary
21
+ * and mean "not determinable", not "error".
22
+ */
23
+ export declare const toEpochMs: (value: unknown) => number | null;
24
+ /**
25
+ * Normalize any {@link Instant} to ISO-8601 UTC, **loudly** — every record
26
+ * the framework emits reports instants as strings.
27
+ *
28
+ * The loud counterpart of {@link toEpochMs}: this one reads *caller input*
29
+ * (an `asOf`, a dormancy marker), where a value that is not an instant is a
30
+ * caller bug rather than a pending fact.
31
+ */
32
+ export declare const toIso: (value: Instant) => string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Instant normalization.
3
+ *
4
+ * Conditions never read the clock. Evaluation takes an explicit `asOf`
5
+ * instant, so "evaluate this guard as of T" is always well-defined and the
6
+ * evaluation record can state the instant it was made as of. The helpers
7
+ * here normalize the forms callers and case state plausibly hold an instant
8
+ * in.
9
+ */
10
+ /**
11
+ * Normalize any {@link Instant} form to epoch milliseconds; `null` when the
12
+ * value is absent or not a determinable instant. **Total** by design — never
13
+ * throws, because absence and rubbish in historical Case State are ordinary
14
+ * and mean "not determinable", not "error".
15
+ */
16
+ export const toEpochMs = (value) => {
17
+ if (value instanceof Date) {
18
+ const ms = value.getTime();
19
+ return Number.isNaN(ms) ? null : ms;
20
+ }
21
+ if (typeof value === 'number') {
22
+ return Number.isFinite(value) ? value : null;
23
+ }
24
+ if (typeof value === 'string') {
25
+ const ms = Date.parse(value);
26
+ return Number.isNaN(ms) ? null : ms;
27
+ }
28
+ return null;
29
+ };
30
+ /**
31
+ * Normalize any {@link Instant} to ISO-8601 UTC, **loudly** — every record
32
+ * the framework emits reports instants as strings.
33
+ *
34
+ * The loud counterpart of {@link toEpochMs}: this one reads *caller input*
35
+ * (an `asOf`, a dormancy marker), where a value that is not an instant is a
36
+ * caller bug rather than a pending fact.
37
+ */
38
+ export const toIso = (value) => {
39
+ const ms = toEpochMs(value);
40
+ if (ms === null) {
41
+ throw new TypeError('asOf/endedAt must be a Date, ISO-8601 string, or epoch milliseconds');
42
+ }
43
+ return new Date(ms).toISOString();
44
+ };
45
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../../src/guards/time.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAc,EAAiB,EAAE;IACzD,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAA;QAC1B,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACrC,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9C,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC5B,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACrC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAU,EAAE;IAC9C,MAAM,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC3B,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,SAAS,CACjB,qEAAqE,CACtE,CAAA;IACH,CAAC;IACD,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AACnC,CAAC,CAAA","sourcesContent":["/**\n * Instant normalization.\n *\n * Conditions never read the clock. Evaluation takes an explicit `asOf`\n * instant, so \"evaluate this guard as of T\" is always well-defined and the\n * evaluation record can state the instant it was made as of. The helpers\n * here normalize the forms callers and case state plausibly hold an instant\n * in.\n */\n\n/**\n * An instant in time, in any of the forms case state and callers plausibly\n * hold one: a `Date`, an ISO-8601 (or otherwise `Date.parse`-able) string,\n * or epoch milliseconds. Normalized internally; evaluation records always\n * report instants as ISO-8601 UTC strings.\n */\nexport type Instant = Date | string | number\n\n/**\n * Normalize any {@link Instant} form to epoch milliseconds; `null` when the\n * value is absent or not a determinable instant. **Total** by design — never\n * throws, because absence and rubbish in historical Case State are ordinary\n * and mean \"not determinable\", not \"error\".\n */\nexport const toEpochMs = (value: unknown): number | null => {\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isNaN(ms) ? null : ms\n }\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : null\n }\n if (typeof value === 'string') {\n const ms = Date.parse(value)\n return Number.isNaN(ms) ? null : ms\n }\n return null\n}\n\n/**\n * Normalize any {@link Instant} to ISO-8601 UTC, **loudly** — every record\n * the framework emits reports instants as strings.\n *\n * The loud counterpart of {@link toEpochMs}: this one reads *caller input*\n * (an `asOf`, a dormancy marker), where a value that is not an instant is a\n * caller bug rather than a pending fact.\n */\nexport const toIso = (value: Instant): string => {\n const ms = toEpochMs(value)\n if (ms === null) {\n throw new TypeError(\n 'asOf/endedAt must be a Date, ISO-8601 string, or epoch milliseconds',\n )\n }\n return new Date(ms).toISOString()\n}\n"]}