@manifesto-ai/core 2.5.0 → 2.6.1

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 (182) hide show
  1. package/README.md +8 -7
  2. package/dist/index.d.ts +1749 -19
  3. package/dist/index.js +18456 -36
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/dist/__tests__/apply.test.d.ts +0 -2
  7. package/dist/__tests__/apply.test.d.ts.map +0 -1
  8. package/dist/__tests__/apply.test.js +0 -279
  9. package/dist/__tests__/apply.test.js.map +0 -1
  10. package/dist/__tests__/defaults.test.d.ts +0 -2
  11. package/dist/__tests__/defaults.test.d.ts.map +0 -1
  12. package/dist/__tests__/defaults.test.js +0 -74
  13. package/dist/__tests__/defaults.test.js.map +0 -1
  14. package/dist/__tests__/jcs.test.d.ts +0 -2
  15. package/dist/__tests__/jcs.test.d.ts.map +0 -1
  16. package/dist/__tests__/jcs.test.js +0 -45
  17. package/dist/__tests__/jcs.test.js.map +0 -1
  18. package/dist/core/apply.d.ts +0 -17
  19. package/dist/core/apply.d.ts.map +0 -1
  20. package/dist/core/apply.js +0 -198
  21. package/dist/core/apply.js.map +0 -1
  22. package/dist/core/compute.d.ts +0 -17
  23. package/dist/core/compute.d.ts.map +0 -1
  24. package/dist/core/compute.js +0 -305
  25. package/dist/core/compute.js.map +0 -1
  26. package/dist/core/compute.test.d.ts +0 -2
  27. package/dist/core/compute.test.d.ts.map +0 -1
  28. package/dist/core/compute.test.js +0 -950
  29. package/dist/core/compute.test.js.map +0 -1
  30. package/dist/core/explain.d.ts +0 -14
  31. package/dist/core/explain.d.ts.map +0 -1
  32. package/dist/core/explain.js +0 -78
  33. package/dist/core/explain.js.map +0 -1
  34. package/dist/core/index.d.ts +0 -5
  35. package/dist/core/index.d.ts.map +0 -1
  36. package/dist/core/index.js +0 -5
  37. package/dist/core/index.js.map +0 -1
  38. package/dist/core/validate.d.ts +0 -16
  39. package/dist/core/validate.d.ts.map +0 -1
  40. package/dist/core/validate.js +0 -361
  41. package/dist/core/validate.js.map +0 -1
  42. package/dist/core/validate.test.d.ts +0 -2
  43. package/dist/core/validate.test.d.ts.map +0 -1
  44. package/dist/core/validate.test.js +0 -638
  45. package/dist/core/validate.test.js.map +0 -1
  46. package/dist/core/validation-utils.d.ts +0 -20
  47. package/dist/core/validation-utils.d.ts.map +0 -1
  48. package/dist/core/validation-utils.js +0 -292
  49. package/dist/core/validation-utils.js.map +0 -1
  50. package/dist/errors.d.ts +0 -30
  51. package/dist/errors.d.ts.map +0 -1
  52. package/dist/errors.js +0 -51
  53. package/dist/errors.js.map +0 -1
  54. package/dist/evaluator/computed.d.ts +0 -14
  55. package/dist/evaluator/computed.d.ts.map +0 -1
  56. package/dist/evaluator/computed.js +0 -60
  57. package/dist/evaluator/computed.js.map +0 -1
  58. package/dist/evaluator/context.d.ts +0 -62
  59. package/dist/evaluator/context.d.ts.map +0 -1
  60. package/dist/evaluator/context.js +0 -44
  61. package/dist/evaluator/context.js.map +0 -1
  62. package/dist/evaluator/dag.d.ts +0 -30
  63. package/dist/evaluator/dag.d.ts.map +0 -1
  64. package/dist/evaluator/dag.js +0 -121
  65. package/dist/evaluator/dag.js.map +0 -1
  66. package/dist/evaluator/expr.d.ts +0 -11
  67. package/dist/evaluator/expr.d.ts.map +0 -1
  68. package/dist/evaluator/expr.js +0 -667
  69. package/dist/evaluator/expr.js.map +0 -1
  70. package/dist/evaluator/expr.test.d.ts +0 -2
  71. package/dist/evaluator/expr.test.d.ts.map +0 -1
  72. package/dist/evaluator/expr.test.js +0 -508
  73. package/dist/evaluator/expr.test.js.map +0 -1
  74. package/dist/evaluator/flow.d.ts +0 -36
  75. package/dist/evaluator/flow.d.ts.map +0 -1
  76. package/dist/evaluator/flow.js +0 -390
  77. package/dist/evaluator/flow.js.map +0 -1
  78. package/dist/evaluator/flow.test.d.ts +0 -2
  79. package/dist/evaluator/flow.test.d.ts.map +0 -1
  80. package/dist/evaluator/flow.test.js +0 -499
  81. package/dist/evaluator/flow.test.js.map +0 -1
  82. package/dist/evaluator/index.d.ts +0 -6
  83. package/dist/evaluator/index.d.ts.map +0 -1
  84. package/dist/evaluator/index.js +0 -6
  85. package/dist/evaluator/index.js.map +0 -1
  86. package/dist/factories.d.ts +0 -22
  87. package/dist/factories.d.ts.map +0 -1
  88. package/dist/factories.js +0 -44
  89. package/dist/factories.js.map +0 -1
  90. package/dist/index.d.ts.map +0 -1
  91. package/dist/index.test.d.ts +0 -2
  92. package/dist/index.test.d.ts.map +0 -1
  93. package/dist/index.test.js +0 -14
  94. package/dist/index.test.js.map +0 -1
  95. package/dist/schema/action.d.ts +0 -14
  96. package/dist/schema/action.d.ts.map +0 -1
  97. package/dist/schema/action.js +0 -30
  98. package/dist/schema/action.js.map +0 -1
  99. package/dist/schema/common.d.ts +0 -37
  100. package/dist/schema/common.d.ts.map +0 -1
  101. package/dist/schema/common.js +0 -20
  102. package/dist/schema/common.js.map +0 -1
  103. package/dist/schema/computed.d.ts +0 -23
  104. package/dist/schema/computed.d.ts.map +0 -1
  105. package/dist/schema/computed.js +0 -34
  106. package/dist/schema/computed.js.map +0 -1
  107. package/dist/schema/defaults.d.ts +0 -12
  108. package/dist/schema/defaults.d.ts.map +0 -1
  109. package/dist/schema/defaults.js +0 -19
  110. package/dist/schema/defaults.js.map +0 -1
  111. package/dist/schema/domain.d.ts +0 -50
  112. package/dist/schema/domain.d.ts.map +0 -1
  113. package/dist/schema/domain.js +0 -60
  114. package/dist/schema/domain.js.map +0 -1
  115. package/dist/schema/expr.d.ts +0 -310
  116. package/dist/schema/expr.d.ts.map +0 -1
  117. package/dist/schema/expr.js +0 -289
  118. package/dist/schema/expr.js.map +0 -1
  119. package/dist/schema/field.d.ts +0 -48
  120. package/dist/schema/field.d.ts.map +0 -1
  121. package/dist/schema/field.js +0 -31
  122. package/dist/schema/field.js.map +0 -1
  123. package/dist/schema/flow.d.ts +0 -103
  124. package/dist/schema/flow.d.ts.map +0 -1
  125. package/dist/schema/flow.js +0 -82
  126. package/dist/schema/flow.js.map +0 -1
  127. package/dist/schema/host-context.d.ts +0 -12
  128. package/dist/schema/host-context.d.ts.map +0 -1
  129. package/dist/schema/host-context.js +0 -23
  130. package/dist/schema/host-context.js.map +0 -1
  131. package/dist/schema/index.d.ts +0 -15
  132. package/dist/schema/index.d.ts.map +0 -1
  133. package/dist/schema/index.js +0 -28
  134. package/dist/schema/index.js.map +0 -1
  135. package/dist/schema/patch.d.ts +0 -59
  136. package/dist/schema/patch.d.ts.map +0 -1
  137. package/dist/schema/patch.js +0 -60
  138. package/dist/schema/patch.js.map +0 -1
  139. package/dist/schema/result.d.ts +0 -142
  140. package/dist/schema/result.d.ts.map +0 -1
  141. package/dist/schema/result.js +0 -94
  142. package/dist/schema/result.js.map +0 -1
  143. package/dist/schema/snapshot.d.ts +0 -153
  144. package/dist/schema/snapshot.d.ts.map +0 -1
  145. package/dist/schema/snapshot.js +0 -160
  146. package/dist/schema/snapshot.js.map +0 -1
  147. package/dist/schema/trace.d.ts +0 -98
  148. package/dist/schema/trace.d.ts.map +0 -1
  149. package/dist/schema/trace.js +0 -90
  150. package/dist/schema/trace.js.map +0 -1
  151. package/dist/schema/type-spec.d.ts +0 -34
  152. package/dist/schema/type-spec.d.ts.map +0 -1
  153. package/dist/schema/type-spec.js +0 -40
  154. package/dist/schema/type-spec.js.map +0 -1
  155. package/dist/utils/canonical.d.ts +0 -37
  156. package/dist/utils/canonical.d.ts.map +0 -1
  157. package/dist/utils/canonical.js +0 -122
  158. package/dist/utils/canonical.js.map +0 -1
  159. package/dist/utils/canonical.test.d.ts +0 -2
  160. package/dist/utils/canonical.test.d.ts.map +0 -1
  161. package/dist/utils/canonical.test.js +0 -183
  162. package/dist/utils/canonical.test.js.map +0 -1
  163. package/dist/utils/hash.d.ts +0 -55
  164. package/dist/utils/hash.d.ts.map +0 -1
  165. package/dist/utils/hash.js +0 -183
  166. package/dist/utils/hash.js.map +0 -1
  167. package/dist/utils/hash.test.d.ts +0 -2
  168. package/dist/utils/hash.test.d.ts.map +0 -1
  169. package/dist/utils/hash.test.js +0 -253
  170. package/dist/utils/hash.test.js.map +0 -1
  171. package/dist/utils/index.d.ts +0 -4
  172. package/dist/utils/index.d.ts.map +0 -1
  173. package/dist/utils/index.js +0 -4
  174. package/dist/utils/index.js.map +0 -1
  175. package/dist/utils/path.d.ts +0 -40
  176. package/dist/utils/path.d.ts.map +0 -1
  177. package/dist/utils/path.js +0 -132
  178. package/dist/utils/path.js.map +0 -1
  179. package/dist/utils/path.test.d.ts +0 -2
  180. package/dist/utils/path.test.d.ts.map +0 -1
  181. package/dist/utils/path.test.js +0 -191
  182. package/dist/utils/path.test.js.map +0 -1
@@ -1,950 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { compute, computeSync } from "./compute.js";
3
- import { createSnapshot, createIntent } from "../factories.js";
4
- import { hashSchemaSync } from "../utils/hash.js";
5
- const BASE_STATE_FIELDS = {
6
- dummy: { type: "string", required: true },
7
- count: { type: "number", required: true },
8
- name: { type: "string", required: true },
9
- balance: { type: "number", required: true },
10
- a: { type: "number", required: true },
11
- b: { type: "number", required: true },
12
- loading: { type: "boolean", required: true },
13
- started: { type: "boolean", required: true },
14
- completed: { type: "boolean", required: true },
15
- value: { type: "string", required: true },
16
- todos: {
17
- type: "array",
18
- required: true,
19
- items: {
20
- type: "object",
21
- required: true,
22
- fields: {
23
- completed: { type: "boolean", required: true },
24
- },
25
- },
26
- },
27
- fromBalance: { type: "number", required: true },
28
- toBalance: { type: "number", required: true },
29
- done: { type: "boolean", required: true },
30
- };
31
- const BASE_COMPUTED_FIELDS = {
32
- "computed.dummy": {
33
- expr: { kind: "get", path: "dummy" },
34
- deps: ["dummy"],
35
- },
36
- };
37
- const BASE_ACTIONS = {
38
- noop: {
39
- flow: { kind: "halt", reason: "noop" },
40
- },
41
- };
42
- // Helper to create a minimal domain schema
43
- function createTestSchema(overrides = {}) {
44
- const { state, computed, actions: overrideActions, hash, types, ...restOverrides } = overrides;
45
- const stateFields = {
46
- ...BASE_STATE_FIELDS,
47
- ...(state?.fields ?? {}),
48
- };
49
- const computedFields = {
50
- ...BASE_COMPUTED_FIELDS,
51
- ...(computed?.fields ?? {}),
52
- };
53
- const actions = {
54
- ...BASE_ACTIONS,
55
- ...(overrideActions ?? {}),
56
- };
57
- const schemaWithoutHash = {
58
- id: "manifesto:test",
59
- version: "1.0.0",
60
- ...restOverrides,
61
- types: types ?? {},
62
- state: { fields: stateFields },
63
- computed: { fields: computedFields },
64
- actions,
65
- };
66
- return {
67
- ...schemaWithoutHash,
68
- hash: hash ?? hashSchemaSync(schemaWithoutHash),
69
- };
70
- }
71
- const HOST_CONTEXT = { now: 0, randomSeed: "seed" };
72
- let intentCounter = 0;
73
- const nextIntentId = () => `intent-${intentCounter++}`;
74
- const createTestIntent = (type, input) => input === undefined
75
- ? createIntent(type, nextIntentId())
76
- : createIntent(type, input, nextIntentId());
77
- const createTestSnapshot = (data, schemaHash) => createSnapshot(data, schemaHash, HOST_CONTEXT);
78
- const computeWithContext = (schema, snapshot, intent) => compute(schema, snapshot, intent, HOST_CONTEXT);
79
- describe("compute", () => {
80
- describe("Basic Intent Processing", () => {
81
- it("should process a simple action", async () => {
82
- const schema = createTestSchema({
83
- actions: {
84
- increment: {
85
- flow: {
86
- kind: "patch",
87
- op: "set",
88
- path: "count",
89
- value: {
90
- kind: "add",
91
- left: { kind: "coalesce", args: [{ kind: "get", path: "count" }, { kind: "lit", value: 0 }] },
92
- right: { kind: "lit", value: 1 },
93
- },
94
- },
95
- },
96
- },
97
- });
98
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
99
- const intent = createTestIntent("increment");
100
- const result = await computeWithContext(schema, snapshot, intent);
101
- expect(result.status).toBe("complete");
102
- expect(result.snapshot.data).toEqual({ count: 1 });
103
- expect(result.snapshot.meta.version).toBe(1);
104
- });
105
- it("should handle unknown action", async () => {
106
- const schema = createTestSchema();
107
- const snapshot = createTestSnapshot({}, schema.hash);
108
- const intent = createTestIntent("nonexistent");
109
- const result = await computeWithContext(schema, snapshot, intent);
110
- expect(result.status).toBe("error");
111
- expect(result.snapshot.system.lastError?.code).toBe("UNKNOWN_ACTION");
112
- });
113
- it("should handle action with input", async () => {
114
- const schema = createTestSchema({
115
- actions: {
116
- setName: {
117
- flow: {
118
- kind: "patch",
119
- op: "set",
120
- path: "name",
121
- value: { kind: "get", path: "input.name" },
122
- },
123
- },
124
- },
125
- });
126
- const snapshot = createTestSnapshot({}, schema.hash);
127
- const intent = createTestIntent("setName", { name: "Alice" });
128
- const result = await computeWithContext(schema, snapshot, intent);
129
- expect(result.status).toBe("complete");
130
- expect(result.snapshot.data).toEqual({ name: "Alice" });
131
- });
132
- it("should reject intent without intentId", async () => {
133
- const schema = createTestSchema({
134
- actions: {
135
- increment: {
136
- flow: {
137
- kind: "patch",
138
- op: "set",
139
- path: "count",
140
- value: { kind: "lit", value: 1 },
141
- },
142
- },
143
- },
144
- });
145
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
146
- const intent = { type: "increment", input: undefined, intentId: "" };
147
- const result = await computeWithContext(schema, snapshot, intent);
148
- expect(result.status).toBe("error");
149
- expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
150
- });
151
- });
152
- describe("Meta Access", () => {
153
- it("should expose meta intentId without mutating input", async () => {
154
- const schema = createTestSchema({
155
- actions: {
156
- markIntent: {
157
- flow: {
158
- kind: "patch",
159
- op: "set",
160
- path: "value",
161
- value: { kind: "get", path: "meta.intentId" },
162
- },
163
- },
164
- },
165
- });
166
- const snapshot = createTestSnapshot({}, schema.hash);
167
- const intent = createTestIntent("markIntent", { name: "Alice" });
168
- const result = await computeWithContext(schema, snapshot, intent);
169
- expect(result.status).toBe("complete");
170
- expect(result.snapshot.data).toEqual({ value: intent.intentId });
171
- expect(result.snapshot.input).toEqual({ name: "Alice" });
172
- });
173
- });
174
- describe("Availability Check", () => {
175
- it("should check availability condition", async () => {
176
- const schema = createTestSchema({
177
- actions: {
178
- withdraw: {
179
- available: {
180
- kind: "gt",
181
- left: { kind: "get", path: "balance" },
182
- right: { kind: "lit", value: 0 },
183
- },
184
- flow: {
185
- kind: "patch",
186
- op: "set",
187
- path: "balance",
188
- value: {
189
- kind: "sub",
190
- left: { kind: "get", path: "balance" },
191
- right: { kind: "get", path: "input.amount" },
192
- },
193
- },
194
- },
195
- },
196
- });
197
- // Should succeed when balance > 0
198
- const snapshot1 = createTestSnapshot({ balance: 100 }, schema.hash);
199
- const intent1 = createTestIntent("withdraw", { amount: 50 });
200
- const result1 = await computeWithContext(schema, snapshot1, intent1);
201
- expect(result1.status).toBe("complete");
202
- expect(result1.snapshot.data).toEqual({ balance: 50 });
203
- // Should fail when balance = 0
204
- const snapshot2 = createTestSnapshot({ balance: 0 }, schema.hash);
205
- const intent2 = createTestIntent("withdraw", { amount: 50 });
206
- const result2 = await computeWithContext(schema, snapshot2, intent2);
207
- expect(result2.status).toBe("error");
208
- expect(result2.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
209
- });
210
- it("should fail when availability does not return boolean", async () => {
211
- const schema = createTestSchema({
212
- actions: {
213
- invalidAvailable: {
214
- available: {
215
- kind: "add",
216
- left: { kind: "lit", value: 1 },
217
- right: { kind: "lit", value: 2 },
218
- },
219
- flow: {
220
- kind: "patch",
221
- op: "set",
222
- path: "count",
223
- value: { kind: "lit", value: 1 },
224
- },
225
- },
226
- },
227
- });
228
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
229
- const intent = createTestIntent("invalidAvailable");
230
- const result = await computeWithContext(schema, snapshot, intent);
231
- expect(result.status).toBe("error");
232
- expect(result.snapshot.system.lastError?.code).toBe("TYPE_MISMATCH");
233
- });
234
- });
235
- describe("Input Validation", () => {
236
- it("should reject invalid input types", async () => {
237
- const schema = createTestSchema({
238
- actions: {
239
- setCount: {
240
- input: {
241
- type: "object",
242
- required: true,
243
- fields: {
244
- value: { type: "number", required: true },
245
- },
246
- },
247
- flow: {
248
- kind: "patch",
249
- op: "set",
250
- path: "count",
251
- value: { kind: "get", path: "input.value" },
252
- },
253
- },
254
- },
255
- });
256
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
257
- const intent = createTestIntent("setCount", { value: "not-a-number" });
258
- const result = await computeWithContext(schema, snapshot, intent);
259
- expect(result.status).toBe("error");
260
- expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
261
- });
262
- it("should reject missing required input fields", async () => {
263
- const schema = createTestSchema({
264
- actions: {
265
- setCount: {
266
- input: {
267
- type: "object",
268
- required: true,
269
- fields: {
270
- value: { type: "number", required: true },
271
- },
272
- },
273
- flow: {
274
- kind: "patch",
275
- op: "set",
276
- path: "count",
277
- value: { kind: "get", path: "input.value" },
278
- },
279
- },
280
- },
281
- });
282
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
283
- const intent = createTestIntent("setCount", {});
284
- const result = await computeWithContext(schema, snapshot, intent);
285
- expect(result.status).toBe("error");
286
- expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
287
- });
288
- it("should reject unknown input fields", async () => {
289
- const schema = createTestSchema({
290
- actions: {
291
- setCount: {
292
- input: {
293
- type: "object",
294
- required: true,
295
- fields: {
296
- value: { type: "number", required: true },
297
- },
298
- },
299
- flow: {
300
- kind: "patch",
301
- op: "set",
302
- path: "count",
303
- value: { kind: "get", path: "input.value" },
304
- },
305
- },
306
- },
307
- });
308
- const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
309
- const intent = createTestIntent("setCount", { value: 1, extra: 2 });
310
- const result = await computeWithContext(schema, snapshot, intent);
311
- expect(result.status).toBe("error");
312
- expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
313
- });
314
- });
315
- describe("Computed Values", () => {
316
- it("should recompute computed values after action", async () => {
317
- const schema = createTestSchema({
318
- computed: {
319
- fields: {
320
- "computed.total": {
321
- expr: {
322
- kind: "add",
323
- left: { kind: "coalesce", args: [{ kind: "get", path: "a" }, { kind: "lit", value: 0 }] },
324
- right: { kind: "coalesce", args: [{ kind: "get", path: "b" }, { kind: "lit", value: 0 }] },
325
- },
326
- deps: ["a", "b"],
327
- },
328
- },
329
- },
330
- actions: {
331
- setA: {
332
- flow: {
333
- kind: "patch",
334
- op: "set",
335
- path: "a",
336
- value: { kind: "get", path: "input.value" },
337
- },
338
- },
339
- },
340
- });
341
- const snapshot = createTestSnapshot({ a: 10, b: 20 }, schema.hash);
342
- const intent = createTestIntent("setA", { value: 100 });
343
- const result = await computeWithContext(schema, snapshot, intent);
344
- expect(result.status).toBe("complete");
345
- expect(result.snapshot.data).toEqual({ a: 100, b: 20 });
346
- expect(result.snapshot.computed["computed.total"]).toBe(120);
347
- });
348
- });
349
- describe("Effects (Pending Status)", () => {
350
- it("should return pending status when effect is encountered", async () => {
351
- const schema = createTestSchema({
352
- actions: {
353
- fetchData: {
354
- flow: {
355
- kind: "seq",
356
- steps: [
357
- { kind: "patch", op: "set", path: "loading", value: { kind: "lit", value: true } },
358
- {
359
- kind: "effect",
360
- type: "http",
361
- params: {
362
- url: { kind: "lit", value: "https://api.example.com/data" },
363
- },
364
- },
365
- ],
366
- },
367
- },
368
- },
369
- });
370
- const snapshot = createTestSnapshot({}, schema.hash);
371
- const intent = createTestIntent("fetchData");
372
- const result = await computeWithContext(schema, snapshot, intent);
373
- expect(result.status).toBe("pending");
374
- expect(result.snapshot.data).toEqual({ loading: true });
375
- expect(result.snapshot.system.pendingRequirements).toHaveLength(1);
376
- expect(result.snapshot.system.pendingRequirements[0].type).toBe("http");
377
- });
378
- });
379
- describe("Halt", () => {
380
- it("should return halted status when halt is encountered", async () => {
381
- const schema = createTestSchema({
382
- actions: {
383
- conditionalHalt: {
384
- flow: {
385
- kind: "seq",
386
- steps: [
387
- { kind: "patch", op: "set", path: "started", value: { kind: "lit", value: true } },
388
- {
389
- kind: "if",
390
- cond: { kind: "get", path: "input.shouldHalt" },
391
- then: { kind: "halt", reason: "User requested halt" },
392
- },
393
- { kind: "patch", op: "set", path: "completed", value: { kind: "lit", value: true } },
394
- ],
395
- },
396
- },
397
- },
398
- });
399
- // With halt
400
- const snapshot1 = createTestSnapshot({}, schema.hash);
401
- const intent1 = createTestIntent("conditionalHalt", { shouldHalt: true });
402
- const result1 = await computeWithContext(schema, snapshot1, intent1);
403
- expect(result1.status).toBe("halted");
404
- expect(result1.snapshot.data).toEqual({ started: true });
405
- // Without halt
406
- const snapshot2 = createTestSnapshot({}, schema.hash);
407
- const intent2 = createTestIntent("conditionalHalt", { shouldHalt: false });
408
- const result2 = await computeWithContext(schema, snapshot2, intent2);
409
- expect(result2.status).toBe("complete");
410
- expect(result2.snapshot.data).toEqual({ started: true, completed: true });
411
- });
412
- });
413
- describe("Error Handling", () => {
414
- it("should handle fail flow", async () => {
415
- const schema = createTestSchema({
416
- actions: {
417
- validateInput: {
418
- flow: {
419
- kind: "if",
420
- cond: { kind: "isNull", arg: { kind: "get", path: "input.value" } },
421
- then: { kind: "fail", code: "MISSING_VALUE", message: { kind: "lit", value: "Value is required" } },
422
- else: { kind: "patch", op: "set", path: "value", value: { kind: "get", path: "input.value" } },
423
- },
424
- },
425
- },
426
- });
427
- // With null input
428
- const snapshot1 = createTestSnapshot({}, schema.hash);
429
- const intent1 = createTestIntent("validateInput", { value: null });
430
- const result1 = await computeWithContext(schema, snapshot1, intent1);
431
- expect(result1.status).toBe("error");
432
- expect(result1.snapshot.system.lastError?.message).toBe("Value is required");
433
- // With valid input
434
- const snapshot2 = createTestSnapshot({}, schema.hash);
435
- const intent2 = createTestIntent("validateInput", { value: "test" });
436
- const result2 = await computeWithContext(schema, snapshot2, intent2);
437
- expect(result2.status).toBe("complete");
438
- expect(result2.snapshot.data).toEqual({ value: "test" });
439
- });
440
- });
441
- describe("Trace Generation", () => {
442
- it("should generate trace graph", async () => {
443
- const schema = createTestSchema({
444
- actions: {
445
- simpleAction: {
446
- flow: {
447
- kind: "seq",
448
- steps: [
449
- { kind: "patch", op: "set", path: "a", value: { kind: "lit", value: 1 } },
450
- { kind: "patch", op: "set", path: "b", value: { kind: "lit", value: 2 } },
451
- ],
452
- },
453
- },
454
- },
455
- });
456
- const snapshot = createTestSnapshot({}, schema.hash);
457
- const intent = createTestIntent("simpleAction");
458
- const result = await computeWithContext(schema, snapshot, intent);
459
- expect(result.trace).toBeDefined();
460
- expect(result.trace.intent).toEqual({ type: "simpleAction", input: undefined });
461
- expect(result.trace.baseVersion).toBe(0);
462
- expect(result.trace.resultVersion).toBe(1);
463
- expect(result.trace.duration).toBeGreaterThanOrEqual(0);
464
- expect(result.trace.terminatedBy).toBe("complete");
465
- });
466
- });
467
- describe("Complex Scenarios", () => {
468
- it("should handle a todo app workflow", async () => {
469
- const schema = createTestSchema({
470
- computed: {
471
- fields: {
472
- "computed.activeCount": {
473
- expr: {
474
- kind: "len",
475
- arg: {
476
- kind: "filter",
477
- array: { kind: "coalesce", args: [{ kind: "get", path: "todos" }, { kind: "lit", value: [] }] },
478
- predicate: { kind: "not", arg: { kind: "get", path: "$item.completed" } },
479
- },
480
- },
481
- deps: ["todos"],
482
- },
483
- },
484
- },
485
- actions: {
486
- addTodo: {
487
- flow: {
488
- kind: "patch",
489
- op: "set",
490
- path: "todos",
491
- value: {
492
- kind: "coalesce",
493
- args: [
494
- {
495
- kind: "if",
496
- cond: { kind: "isNull", arg: { kind: "get", path: "todos" } },
497
- then: { kind: "lit", value: [] },
498
- else: { kind: "get", path: "todos" },
499
- },
500
- { kind: "lit", value: [] },
501
- ],
502
- },
503
- },
504
- },
505
- },
506
- });
507
- // First add - initialize todos array
508
- const snapshot = createTestSnapshot({}, schema.hash);
509
- const intent = createTestIntent("addTodo", { text: "Test todo" });
510
- const result = await computeWithContext(schema, snapshot, intent);
511
- expect(result.status).toBe("complete");
512
- expect(result.snapshot.computed["computed.activeCount"]).toBe(0);
513
- });
514
- it("should handle sequential operations with state dependencies", async () => {
515
- const schema = createTestSchema({
516
- actions: {
517
- transfer: {
518
- flow: {
519
- kind: "seq",
520
- steps: [
521
- {
522
- kind: "patch",
523
- op: "set",
524
- path: "fromBalance",
525
- value: {
526
- kind: "sub",
527
- left: { kind: "get", path: "fromBalance" },
528
- right: { kind: "get", path: "input.amount" },
529
- },
530
- },
531
- {
532
- kind: "patch",
533
- op: "set",
534
- path: "toBalance",
535
- value: {
536
- kind: "add",
537
- left: { kind: "get", path: "toBalance" },
538
- right: { kind: "get", path: "input.amount" },
539
- },
540
- },
541
- ],
542
- },
543
- },
544
- },
545
- });
546
- const snapshot = createTestSnapshot({
547
- fromBalance: 100,
548
- toBalance: 50,
549
- }, schema.hash);
550
- const intent = createTestIntent("transfer", { amount: 30 });
551
- const result = await computeWithContext(schema, snapshot, intent);
552
- expect(result.status).toBe("complete");
553
- expect(result.snapshot.data).toEqual({
554
- fromBalance: 70,
555
- toBalance: 80,
556
- });
557
- });
558
- });
559
- describe("Version Management", () => {
560
- it("should increment version on each compute", async () => {
561
- const schema = createTestSchema({
562
- actions: {
563
- noop: {
564
- flow: { kind: "seq", steps: [] },
565
- },
566
- },
567
- });
568
- const snapshot = createTestSnapshot({}, schema.hash);
569
- expect(snapshot.meta.version).toBe(0);
570
- const result1 = await computeWithContext(schema, snapshot, createTestIntent("noop"));
571
- expect(result1.snapshot.meta.version).toBe(1);
572
- const result2 = await computeWithContext(schema, result1.snapshot, createTestIntent("noop"));
573
- expect(result2.snapshot.meta.version).toBe(2);
574
- });
575
- });
576
- describe("System State", () => {
577
- it("should track errors in system.errors", async () => {
578
- const schema = createTestSchema({
579
- actions: {
580
- fail: {
581
- flow: { kind: "fail", code: "TEST_ERROR" },
582
- },
583
- },
584
- });
585
- const snapshot = createTestSnapshot({}, schema.hash);
586
- const result1 = await computeWithContext(schema, snapshot, createTestIntent("fail"));
587
- expect(result1.snapshot.system.errors).toHaveLength(1);
588
- expect(result1.snapshot.system.lastError?.code).toBe("VALIDATION_ERROR");
589
- // Run again to accumulate errors
590
- const result2 = await computeWithContext(schema, result1.snapshot, createTestIntent("fail"));
591
- expect(result2.snapshot.system.errors).toHaveLength(2);
592
- });
593
- it("should reset currentAction after completion", async () => {
594
- const schema = createTestSchema({
595
- actions: {
596
- test: {
597
- flow: { kind: "patch", op: "set", path: "done", value: { kind: "lit", value: true } },
598
- },
599
- },
600
- });
601
- const snapshot = createTestSnapshot({}, schema.hash);
602
- const result = await computeWithContext(schema, snapshot, createTestIntent("test"));
603
- expect(result.snapshot.system.currentAction).toBeNull();
604
- expect(result.snapshot.system.status).toBe("idle");
605
- });
606
- });
607
- describe("Determinism", () => {
608
- it("should produce identical results for same inputs", async () => {
609
- const schema = createTestSchema({
610
- actions: {
611
- increment: {
612
- flow: {
613
- kind: "patch",
614
- op: "set",
615
- path: "count",
616
- value: {
617
- kind: "add",
618
- left: { kind: "get", path: "count" },
619
- right: { kind: "lit", value: 1 },
620
- },
621
- },
622
- },
623
- },
624
- });
625
- const snapshot = createTestSnapshot({ count: 1 }, schema.hash);
626
- const intent = createIntent("increment", "intent-fixed");
627
- const result1 = await compute(schema, snapshot, intent, HOST_CONTEXT);
628
- const result2 = await compute(schema, snapshot, intent, HOST_CONTEXT);
629
- expect(result1).toEqual(result2);
630
- });
631
- });
632
- describe("Availability on Re-Entry (#134)", () => {
633
- it("should keep action valid when re-entering after effect mutates available fields", async () => {
634
- const schema = createTestSchema({
635
- state: {
636
- fields: {
637
- pending: { type: "string", required: false },
638
- result: { type: "string", required: false },
639
- },
640
- },
641
- actions: {
642
- run: {
643
- available: {
644
- kind: "and",
645
- args: [
646
- { kind: "isNull", arg: { kind: "get", path: "pending" } },
647
- { kind: "isNull", arg: { kind: "get", path: "result" } },
648
- ],
649
- },
650
- flow: {
651
- kind: "if",
652
- cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
653
- then: {
654
- kind: "seq",
655
- steps: [
656
- {
657
- kind: "patch",
658
- op: "set",
659
- path: "pending",
660
- value: { kind: "get", path: "meta.intentId" },
661
- },
662
- {
663
- kind: "effect",
664
- type: "demo.exec",
665
- params: {
666
- into: { kind: "lit", value: "result" },
667
- },
668
- },
669
- ],
670
- },
671
- },
672
- },
673
- },
674
- });
675
- const pending = createTestSnapshot({ pending: null, result: null }, schema.hash);
676
- const intent = createIntent("run", "intent-run-1");
677
- const first = await compute(schema, pending, intent, HOST_CONTEXT);
678
- expect(first.status).toBe("pending");
679
- expect(first.snapshot.system.currentAction).toBe("run");
680
- const afterEffect = {
681
- ...first.snapshot,
682
- data: { ...first.snapshot.data, result: "done" },
683
- system: {
684
- ...first.snapshot.system,
685
- pendingRequirements: [],
686
- },
687
- };
688
- const second = await compute(schema, afterEffect, intent, HOST_CONTEXT);
689
- expect(second.status).toBe("complete");
690
- expect(second.snapshot.system.lastError?.code).toBeUndefined();
691
- });
692
- it("should skip availability check on re-entry when currentAction matches", async () => {
693
- // Simulates the issue #134 scenario:
694
- // Action `run` has `available when and(isNull(result), isNull(pending))`
695
- // First compute: available passes, patches pending, declares effect → status "pending"
696
- // After effect fulfillment, Host calls compute again with updated snapshot
697
- // where pending and result are non-null. Without the fix, this would fail
698
- // with ACTION_UNAVAILABLE because `available` re-evaluates to false.
699
- const schema = createTestSchema({
700
- state: {
701
- fields: {
702
- pending: { type: "string", required: false },
703
- result: { type: "string", required: false },
704
- },
705
- },
706
- actions: {
707
- run: {
708
- available: {
709
- kind: "and",
710
- args: [
711
- { kind: "isNull", arg: { kind: "get", path: "pending" } },
712
- { kind: "isNull", arg: { kind: "get", path: "result" } },
713
- ],
714
- },
715
- flow: {
716
- kind: "if",
717
- cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
718
- then: {
719
- kind: "seq",
720
- steps: [
721
- {
722
- kind: "patch",
723
- op: "set",
724
- path: "pending",
725
- value: { kind: "get", path: "meta.intentId" },
726
- },
727
- {
728
- kind: "effect",
729
- type: "demo.exec",
730
- params: {
731
- into: { kind: "lit", value: "result" },
732
- },
733
- },
734
- ],
735
- },
736
- },
737
- },
738
- },
739
- });
740
- // 1st compute: initial invocation — available passes, effect declared
741
- const snapshot1 = createTestSnapshot({ pending: null, result: null }, schema.hash);
742
- const intent = createIntent("run", "intent-run-1");
743
- const result1 = await compute(schema, snapshot1, intent, HOST_CONTEXT);
744
- expect(result1.status).toBe("pending");
745
- expect(result1.snapshot.data).toEqual(expect.objectContaining({ pending: "intent-run-1" }));
746
- expect(result1.snapshot.system.currentAction).toBe("run");
747
- // 2nd compute: simulate re-entry after effect fulfillment
748
- // Host applied effect patches (result is now set) and calls compute again.
749
- // The snapshot has currentAction === "run" (set during 1st compute pending).
750
- const reEntrySnapshot = {
751
- ...result1.snapshot,
752
- data: { ...result1.snapshot.data, result: "done" },
753
- system: {
754
- ...result1.snapshot.system,
755
- // currentAction remains "run" — this signals re-entry
756
- pendingRequirements: [],
757
- },
758
- };
759
- const result2 = await compute(schema, reEntrySnapshot, intent, HOST_CONTEXT);
760
- // Should NOT fail with ACTION_UNAVAILABLE — availability is skipped on re-entry
761
- expect(result2.status).not.toBe("error");
762
- expect(result2.snapshot.system.lastError?.code).not.toBe("ACTION_UNAVAILABLE");
763
- });
764
- it("SCENARIO: different intentId same action type on pending snapshot bypasses availability", async () => {
765
- // Models the reviewer's concern:
766
- // Intent A (type "run") → pending → currentAction = "run"
767
- // Then Intent B (type "run", DIFFERENT intentId) arrives on that snapshot.
768
- // With current fix (type-only guard), B skips availability.
769
- // This test documents whether this is reachable and what happens.
770
- const schema = createTestSchema({
771
- state: {
772
- fields: {
773
- pending: { type: "string", required: false },
774
- result: { type: "string", required: false },
775
- },
776
- },
777
- actions: {
778
- run: {
779
- available: {
780
- kind: "and",
781
- args: [
782
- { kind: "isNull", arg: { kind: "get", path: "pending" } },
783
- { kind: "isNull", arg: { kind: "get", path: "result" } },
784
- ],
785
- },
786
- flow: {
787
- kind: "if",
788
- cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
789
- then: {
790
- kind: "seq",
791
- steps: [
792
- {
793
- kind: "patch",
794
- op: "set",
795
- path: "pending",
796
- value: { kind: "get", path: "meta.intentId" },
797
- },
798
- {
799
- kind: "effect",
800
- type: "demo.exec",
801
- params: {
802
- into: { kind: "lit", value: "result" },
803
- },
804
- },
805
- ],
806
- },
807
- },
808
- },
809
- },
810
- });
811
- // Intent A: pending → currentAction = "run"
812
- const snapshot1 = createTestSnapshot({ pending: null, result: null }, schema.hash);
813
- const intentA = createIntent("run", "intent-A");
814
- const resultA = await compute(schema, snapshot1, intentA, HOST_CONTEXT);
815
- expect(resultA.status).toBe("pending");
816
- expect(resultA.snapshot.system.currentAction).toBe("run");
817
- // Intent B: different intentId, same action type, on A's pending snapshot
818
- const intentB = createIntent("run", "intent-B");
819
- const resultB = await compute(schema, resultA.snapshot, intentB, HOST_CONTEXT);
820
- // With type-only guard, availability IS skipped (isReEntry = true).
821
- // But the flow's own state guard (if isNull(pending)) prevents double-patching.
822
- // pending is already "intent-A", so the if-branch is skipped → no patches, no effects.
823
- // Result: completes as no-op, does NOT corrupt state.
824
- expect(resultB.snapshot.system.lastError?.code).not.toBe("ACTION_UNAVAILABLE");
825
- expect(resultB.snapshot.data.pending).toBe("intent-A"); // unchanged
826
- });
827
- it("SCENARIO: different action type on pending snapshot still checks availability", async () => {
828
- // Ensure that a DIFFERENT action type is NOT treated as re-entry
829
- // even when currentAction is set from a previous pending action.
830
- const schema = createTestSchema({
831
- state: {
832
- fields: {
833
- pending: { type: "string", required: false },
834
- result: { type: "string", required: false },
835
- count: { type: "number", required: false },
836
- },
837
- },
838
- actions: {
839
- run: {
840
- available: {
841
- kind: "isNull",
842
- arg: { kind: "get", path: "pending" },
843
- },
844
- flow: {
845
- kind: "seq",
846
- steps: [
847
- {
848
- kind: "patch",
849
- op: "set",
850
- path: "pending",
851
- value: { kind: "get", path: "meta.intentId" },
852
- },
853
- {
854
- kind: "effect",
855
- type: "demo.exec",
856
- params: {},
857
- },
858
- ],
859
- },
860
- },
861
- increment: {
862
- available: {
863
- kind: "isNull",
864
- arg: { kind: "get", path: "pending" },
865
- },
866
- flow: {
867
- kind: "patch",
868
- op: "set",
869
- path: "count",
870
- value: { kind: "lit", value: 1 },
871
- },
872
- },
873
- },
874
- });
875
- // "run" → pending → currentAction = "run"
876
- const snapshot1 = createTestSnapshot({ pending: null, result: null, count: 0 }, schema.hash);
877
- const intentRun = createIntent("run", "intent-run");
878
- const resultRun = await compute(schema, snapshot1, intentRun, HOST_CONTEXT);
879
- expect(resultRun.status).toBe("pending");
880
- expect(resultRun.snapshot.system.currentAction).toBe("run");
881
- // "increment" on the pending snapshot — different type, should NOT skip availability
882
- const intentInc = createIntent("increment", "intent-inc");
883
- const resultInc = await compute(schema, resultRun.snapshot, intentInc, HOST_CONTEXT);
884
- // currentAction is "run" but intent type is "increment" → isReEntry = false
885
- // available when isNull(pending) → pending is "intent-run" → false → ACTION_UNAVAILABLE
886
- expect(resultInc.status).toBe("error");
887
- expect(resultInc.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
888
- });
889
- it("should still check availability on fresh invocation", async () => {
890
- // Ensure the fix doesn't accidentally skip availability on initial calls
891
- const schema = createTestSchema({
892
- state: {
893
- fields: {
894
- pending: { type: "string", required: false },
895
- result: { type: "string", required: false },
896
- },
897
- },
898
- actions: {
899
- run: {
900
- available: {
901
- kind: "and",
902
- args: [
903
- { kind: "isNull", arg: { kind: "get", path: "pending" } },
904
- { kind: "isNull", arg: { kind: "get", path: "result" } },
905
- ],
906
- },
907
- flow: {
908
- kind: "patch",
909
- op: "set",
910
- path: "pending",
911
- value: { kind: "get", path: "meta.intentId" },
912
- },
913
- },
914
- },
915
- });
916
- // Fresh invocation where available condition is false — should still fail
917
- const snapshot = createTestSnapshot({ pending: "already-set", result: null }, schema.hash);
918
- const intent = createTestIntent("run");
919
- const result = await computeWithContext(schema, snapshot, intent);
920
- expect(result.status).toBe("error");
921
- expect(result.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
922
- });
923
- });
924
- describe("computeSync", () => {
925
- it("should match async compute for the same inputs", async () => {
926
- const schema = createTestSchema({
927
- actions: {
928
- increment: {
929
- flow: {
930
- kind: "patch",
931
- op: "set",
932
- path: "count",
933
- value: {
934
- kind: "add",
935
- left: { kind: "get", path: "count" },
936
- right: { kind: "lit", value: 1 },
937
- },
938
- },
939
- },
940
- },
941
- });
942
- const snapshot = createTestSnapshot({ count: 1 }, schema.hash);
943
- const intent = createIntent("increment", "intent-sync-1");
944
- const asyncResult = await compute(schema, snapshot, intent, HOST_CONTEXT);
945
- const syncResult = computeSync(schema, snapshot, intent, HOST_CONTEXT);
946
- expect(syncResult).toEqual(asyncResult);
947
- });
948
- });
949
- });
950
- //# sourceMappingURL=compute.test.js.map