@plures/praxis 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/FRAMEWORK.md +420 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1310 -0
  4. package/dist/adapters/cli.d.ts +43 -0
  5. package/dist/adapters/cli.d.ts.map +1 -0
  6. package/dist/adapters/cli.js +126 -0
  7. package/dist/adapters/cli.js.map +1 -0
  8. package/dist/cli/commands/auth.d.ts +26 -0
  9. package/dist/cli/commands/auth.d.ts.map +1 -0
  10. package/dist/cli/commands/auth.js +233 -0
  11. package/dist/cli/commands/auth.js.map +1 -0
  12. package/dist/cli/commands/cloud.d.ts +27 -0
  13. package/dist/cli/commands/cloud.d.ts.map +1 -0
  14. package/dist/cli/commands/cloud.js +232 -0
  15. package/dist/cli/commands/cloud.js.map +1 -0
  16. package/dist/cli/commands/generate.d.ts +25 -0
  17. package/dist/cli/commands/generate.d.ts.map +1 -0
  18. package/dist/cli/commands/generate.js +168 -0
  19. package/dist/cli/commands/generate.js.map +1 -0
  20. package/dist/cli/index.d.ts +8 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +179 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cloud/auth.d.ts +51 -0
  25. package/dist/cloud/auth.d.ts.map +1 -0
  26. package/dist/cloud/auth.js +194 -0
  27. package/dist/cloud/auth.js.map +1 -0
  28. package/dist/cloud/billing.d.ts +184 -0
  29. package/dist/cloud/billing.d.ts.map +1 -0
  30. package/dist/cloud/billing.js +179 -0
  31. package/dist/cloud/billing.js.map +1 -0
  32. package/dist/cloud/client.d.ts +39 -0
  33. package/dist/cloud/client.d.ts.map +1 -0
  34. package/dist/cloud/client.js +176 -0
  35. package/dist/cloud/client.js.map +1 -0
  36. package/dist/cloud/index.d.ts +44 -0
  37. package/dist/cloud/index.d.ts.map +1 -0
  38. package/dist/cloud/index.js +44 -0
  39. package/dist/cloud/index.js.map +1 -0
  40. package/dist/cloud/marketplace.d.ts +166 -0
  41. package/dist/cloud/marketplace.d.ts.map +1 -0
  42. package/dist/cloud/marketplace.js +159 -0
  43. package/dist/cloud/marketplace.js.map +1 -0
  44. package/dist/cloud/provisioning.d.ts +110 -0
  45. package/dist/cloud/provisioning.d.ts.map +1 -0
  46. package/dist/cloud/provisioning.js +148 -0
  47. package/dist/cloud/provisioning.js.map +1 -0
  48. package/dist/cloud/relay/endpoints.d.ts +62 -0
  49. package/dist/cloud/relay/endpoints.d.ts.map +1 -0
  50. package/dist/cloud/relay/endpoints.js +217 -0
  51. package/dist/cloud/relay/endpoints.js.map +1 -0
  52. package/dist/cloud/relay/health/index.d.ts +5 -0
  53. package/dist/cloud/relay/health/index.d.ts.map +1 -0
  54. package/dist/cloud/relay/health/index.js +9 -0
  55. package/dist/cloud/relay/health/index.js.map +1 -0
  56. package/dist/cloud/relay/stats/index.d.ts +5 -0
  57. package/dist/cloud/relay/stats/index.d.ts.map +1 -0
  58. package/dist/cloud/relay/stats/index.js +9 -0
  59. package/dist/cloud/relay/stats/index.js.map +1 -0
  60. package/dist/cloud/relay/sync/index.d.ts +5 -0
  61. package/dist/cloud/relay/sync/index.d.ts.map +1 -0
  62. package/dist/cloud/relay/sync/index.js +9 -0
  63. package/dist/cloud/relay/sync/index.js.map +1 -0
  64. package/dist/cloud/relay/usage/index.d.ts +5 -0
  65. package/dist/cloud/relay/usage/index.d.ts.map +1 -0
  66. package/dist/cloud/relay/usage/index.js +9 -0
  67. package/dist/cloud/relay/usage/index.js.map +1 -0
  68. package/dist/cloud/sponsors.d.ts +81 -0
  69. package/dist/cloud/sponsors.d.ts.map +1 -0
  70. package/dist/cloud/sponsors.js +130 -0
  71. package/dist/cloud/sponsors.js.map +1 -0
  72. package/dist/cloud/types.d.ts +169 -0
  73. package/dist/cloud/types.d.ts.map +1 -0
  74. package/dist/cloud/types.js +7 -0
  75. package/dist/cloud/types.js.map +1 -0
  76. package/dist/components/index.d.ts +43 -0
  77. package/dist/components/index.d.ts.map +1 -0
  78. package/dist/components/index.js +17 -0
  79. package/dist/components/index.js.map +1 -0
  80. package/dist/core/actors.d.ts +95 -0
  81. package/dist/core/actors.d.ts.map +1 -0
  82. package/dist/core/actors.js +158 -0
  83. package/dist/core/actors.js.map +1 -0
  84. package/dist/core/component/generator.d.ts +122 -0
  85. package/dist/core/component/generator.d.ts.map +1 -0
  86. package/dist/core/component/generator.js +307 -0
  87. package/dist/core/component/generator.js.map +1 -0
  88. package/dist/core/engine.d.ts +92 -0
  89. package/dist/core/engine.d.ts.map +1 -0
  90. package/dist/core/engine.js +199 -0
  91. package/dist/core/engine.js.map +1 -0
  92. package/dist/core/introspection.d.ts +141 -0
  93. package/dist/core/introspection.d.ts.map +1 -0
  94. package/dist/core/introspection.js +208 -0
  95. package/dist/core/introspection.js.map +1 -0
  96. package/dist/core/logic/generator.d.ts +76 -0
  97. package/dist/core/logic/generator.d.ts.map +1 -0
  98. package/dist/core/logic/generator.js +339 -0
  99. package/dist/core/logic/generator.js.map +1 -0
  100. package/dist/core/pluresdb/generator.d.ts +58 -0
  101. package/dist/core/pluresdb/generator.d.ts.map +1 -0
  102. package/dist/core/pluresdb/generator.js +162 -0
  103. package/dist/core/pluresdb/generator.js.map +1 -0
  104. package/dist/core/protocol.d.ts +121 -0
  105. package/dist/core/protocol.d.ts.map +1 -0
  106. package/dist/core/protocol.js +46 -0
  107. package/dist/core/protocol.js.map +1 -0
  108. package/dist/core/rules.d.ts +120 -0
  109. package/dist/core/rules.d.ts.map +1 -0
  110. package/dist/core/rules.js +81 -0
  111. package/dist/core/rules.js.map +1 -0
  112. package/dist/core/schema/loader.d.ts +47 -0
  113. package/dist/core/schema/loader.d.ts.map +1 -0
  114. package/dist/core/schema/loader.js +189 -0
  115. package/dist/core/schema/loader.js.map +1 -0
  116. package/dist/core/schema/normalize.d.ts +72 -0
  117. package/dist/core/schema/normalize.d.ts.map +1 -0
  118. package/dist/core/schema/normalize.js +190 -0
  119. package/dist/core/schema/normalize.js.map +1 -0
  120. package/dist/core/schema/types.d.ts +370 -0
  121. package/dist/core/schema/types.d.ts.map +1 -0
  122. package/dist/core/schema/types.js +161 -0
  123. package/dist/core/schema/types.js.map +1 -0
  124. package/dist/dsl/index.d.ts +152 -0
  125. package/dist/dsl/index.d.ts.map +1 -0
  126. package/dist/dsl/index.js +132 -0
  127. package/dist/dsl/index.js.map +1 -0
  128. package/dist/dsl.d.ts +124 -0
  129. package/dist/dsl.d.ts.map +1 -0
  130. package/dist/dsl.js +130 -0
  131. package/dist/dsl.js.map +1 -0
  132. package/dist/examples/advanced-todo/index.d.ts +55 -0
  133. package/dist/examples/advanced-todo/index.d.ts.map +1 -0
  134. package/dist/examples/advanced-todo/index.js +222 -0
  135. package/dist/examples/advanced-todo/index.js.map +1 -0
  136. package/dist/examples/auth-basic/index.d.ts +17 -0
  137. package/dist/examples/auth-basic/index.d.ts.map +1 -0
  138. package/dist/examples/auth-basic/index.js +122 -0
  139. package/dist/examples/auth-basic/index.js.map +1 -0
  140. package/dist/examples/cart/index.d.ts +19 -0
  141. package/dist/examples/cart/index.d.ts.map +1 -0
  142. package/dist/examples/cart/index.js +202 -0
  143. package/dist/examples/cart/index.js.map +1 -0
  144. package/dist/examples/hero-ecommerce/index.d.ts +39 -0
  145. package/dist/examples/hero-ecommerce/index.d.ts.map +1 -0
  146. package/dist/examples/hero-ecommerce/index.js +506 -0
  147. package/dist/examples/hero-ecommerce/index.js.map +1 -0
  148. package/dist/examples/svelte-counter/index.d.ts +31 -0
  149. package/dist/examples/svelte-counter/index.d.ts.map +1 -0
  150. package/dist/examples/svelte-counter/index.js +123 -0
  151. package/dist/examples/svelte-counter/index.js.map +1 -0
  152. package/dist/flows.d.ts +125 -0
  153. package/dist/flows.d.ts.map +1 -0
  154. package/dist/flows.js +160 -0
  155. package/dist/flows.js.map +1 -0
  156. package/dist/index.d.ts +67 -0
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +59 -0
  159. package/dist/index.js.map +1 -0
  160. package/dist/integrations/pluresdb.d.ts +56 -0
  161. package/dist/integrations/pluresdb.d.ts.map +1 -0
  162. package/dist/integrations/pluresdb.js +46 -0
  163. package/dist/integrations/pluresdb.js.map +1 -0
  164. package/dist/integrations/svelte.d.ts +306 -0
  165. package/dist/integrations/svelte.d.ts.map +1 -0
  166. package/dist/integrations/svelte.js +447 -0
  167. package/dist/integrations/svelte.js.map +1 -0
  168. package/dist/registry.d.ts +94 -0
  169. package/dist/registry.d.ts.map +1 -0
  170. package/dist/registry.js +181 -0
  171. package/dist/registry.js.map +1 -0
  172. package/dist/runtime/terminal-adapter.d.ts +105 -0
  173. package/dist/runtime/terminal-adapter.d.ts.map +1 -0
  174. package/dist/runtime/terminal-adapter.js +113 -0
  175. package/dist/runtime/terminal-adapter.js.map +1 -0
  176. package/dist/step.d.ts +34 -0
  177. package/dist/step.d.ts.map +1 -0
  178. package/dist/step.js +111 -0
  179. package/dist/step.js.map +1 -0
  180. package/dist/types.d.ts +63 -0
  181. package/dist/types.d.ts.map +1 -0
  182. package/dist/types.js +6 -0
  183. package/dist/types.js.map +1 -0
  184. package/docs/MONETIZATION.md +394 -0
  185. package/docs/TERMINAL_NODE.md +588 -0
  186. package/docs/guides/canvas.md +389 -0
  187. package/docs/guides/getting-started.md +347 -0
  188. package/docs/guides/history-state-pattern.md +618 -0
  189. package/docs/guides/orchestration.md +617 -0
  190. package/docs/guides/parallel-state-pattern.md +767 -0
  191. package/docs/guides/svelte-integration.md +691 -0
  192. package/package.json +96 -0
  193. package/src/__tests__/actors.test.ts +270 -0
  194. package/src/__tests__/billing.test.ts +175 -0
  195. package/src/__tests__/cloud.test.ts +247 -0
  196. package/src/__tests__/dsl.test.ts +154 -0
  197. package/src/__tests__/edge-cases.test.ts +475 -0
  198. package/src/__tests__/engine.test.ts +137 -0
  199. package/src/__tests__/generators.test.ts +270 -0
  200. package/src/__tests__/introspection.test.ts +321 -0
  201. package/src/__tests__/protocol.test.ts +40 -0
  202. package/src/__tests__/provisioning.test.ts +162 -0
  203. package/src/__tests__/schema.test.ts +241 -0
  204. package/src/__tests__/svelte-integration.test.ts +431 -0
  205. package/src/__tests__/terminal-node.test.ts +352 -0
  206. package/src/adapters/cli.ts +175 -0
  207. package/src/cli/commands/auth.ts +271 -0
  208. package/src/cli/commands/cloud.ts +281 -0
  209. package/src/cli/commands/generate.ts +225 -0
  210. package/src/cli/index.ts +190 -0
  211. package/src/cloud/README.md +383 -0
  212. package/src/cloud/auth.ts +245 -0
  213. package/src/cloud/billing.ts +336 -0
  214. package/src/cloud/client.ts +221 -0
  215. package/src/cloud/index.ts +121 -0
  216. package/src/cloud/marketplace.ts +303 -0
  217. package/src/cloud/provisioning.ts +254 -0
  218. package/src/cloud/relay/endpoints.ts +307 -0
  219. package/src/cloud/relay/health/function.json +17 -0
  220. package/src/cloud/relay/health/index.ts +10 -0
  221. package/src/cloud/relay/host.json +15 -0
  222. package/src/cloud/relay/local.settings.json +8 -0
  223. package/src/cloud/relay/stats/function.json +17 -0
  224. package/src/cloud/relay/stats/index.ts +10 -0
  225. package/src/cloud/relay/sync/function.json +17 -0
  226. package/src/cloud/relay/sync/index.ts +10 -0
  227. package/src/cloud/relay/usage/function.json +17 -0
  228. package/src/cloud/relay/usage/index.ts +10 -0
  229. package/src/cloud/sponsors.ts +213 -0
  230. package/src/cloud/types.ts +198 -0
  231. package/src/components/README.md +125 -0
  232. package/src/components/TerminalNode.svelte +457 -0
  233. package/src/components/index.ts +46 -0
  234. package/src/core/actors.ts +205 -0
  235. package/src/core/component/generator.ts +432 -0
  236. package/src/core/engine.ts +243 -0
  237. package/src/core/introspection.ts +329 -0
  238. package/src/core/logic/generator.ts +420 -0
  239. package/src/core/pluresdb/generator.ts +229 -0
  240. package/src/core/protocol.ts +132 -0
  241. package/src/core/rules.ts +167 -0
  242. package/src/core/schema/loader.ts +247 -0
  243. package/src/core/schema/normalize.ts +322 -0
  244. package/src/core/schema/types.ts +557 -0
  245. package/src/dsl/index.ts +218 -0
  246. package/src/dsl.ts +214 -0
  247. package/src/examples/advanced-todo/App.svelte +506 -0
  248. package/src/examples/advanced-todo/README.md +371 -0
  249. package/src/examples/advanced-todo/index.ts +309 -0
  250. package/src/examples/auth-basic/index.ts +163 -0
  251. package/src/examples/cart/index.ts +259 -0
  252. package/src/examples/hero-ecommerce/index.ts +657 -0
  253. package/src/examples/svelte-counter/index.ts +168 -0
  254. package/src/flows.ts +268 -0
  255. package/src/index.ts +154 -0
  256. package/src/integrations/pluresdb.ts +93 -0
  257. package/src/integrations/svelte.ts +617 -0
  258. package/src/registry.ts +223 -0
  259. package/src/runtime/terminal-adapter.ts +175 -0
  260. package/src/step.ts +151 -0
  261. package/src/types.ts +70 -0
  262. package/templates/basic-app/README.md +147 -0
  263. package/templates/fullstack-app/README.md +279 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Edge cases and failure path tests
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { createPraxisEngine } from "../core/engine.js";
7
+ import { PraxisRegistry } from "../core/rules.js";
8
+ import {
9
+ defineRule,
10
+ defineConstraint,
11
+ defineEvent,
12
+ defineFact,
13
+ defineModule,
14
+ } from "../dsl/index.js";
15
+
16
+ describe("Edge Cases and Failure Paths", () => {
17
+ describe("Rule Errors", () => {
18
+ it("should handle rule that throws an error", () => {
19
+ const ErrorEvent = defineEvent<"ERROR", {}>("ERROR");
20
+
21
+ const errorRule = defineRule<{ value: number }>({
22
+ id: "error.rule",
23
+ description: "Rule that throws",
24
+ impl: () => {
25
+ throw new Error("Intentional error");
26
+ },
27
+ });
28
+
29
+ const registry = new PraxisRegistry<{ value: number }>();
30
+ registry.registerRule(errorRule);
31
+
32
+ const engine = createPraxisEngine({
33
+ initialContext: { value: 0 },
34
+ registry,
35
+ });
36
+
37
+ const result = engine.step([ErrorEvent.create({})]);
38
+
39
+ expect(result.diagnostics).toHaveLength(1);
40
+ expect(result.diagnostics[0]?.kind).toBe("rule-error");
41
+ expect(result.diagnostics[0]?.message).toContain("Intentional error");
42
+ });
43
+
44
+ it("should continue processing other rules after one fails", () => {
45
+ const TestEvent = defineEvent<"TEST", {}>("TEST");
46
+ const Success = defineFact<"Success", { ruleId: string }>("Success");
47
+
48
+ const errorRule = defineRule<{ count: number }>({
49
+ id: "error.rule",
50
+ description: "Rule that throws",
51
+ impl: () => {
52
+ throw new Error("Error");
53
+ },
54
+ });
55
+
56
+ const successRule = defineRule<{ count: number }>({
57
+ id: "success.rule",
58
+ description: "Rule that succeeds",
59
+ impl: (state, events) => {
60
+ if (events.some(TestEvent.is)) {
61
+ state.context.count += 1;
62
+ return [Success.create({ ruleId: "success.rule" })];
63
+ }
64
+ return [];
65
+ },
66
+ });
67
+
68
+ const registry = new PraxisRegistry<{ count: number }>();
69
+ registry.registerRule(errorRule);
70
+ registry.registerRule(successRule);
71
+
72
+ const engine = createPraxisEngine({
73
+ initialContext: { count: 0 },
74
+ registry,
75
+ });
76
+
77
+ const result = engine.step([TestEvent.create({})]);
78
+
79
+ // Should have both error diagnostic and success fact
80
+ expect(result.diagnostics).toHaveLength(1);
81
+ expect(result.diagnostics[0]?.kind).toBe("rule-error");
82
+ expect(result.state.facts.some((f) => f.tag === "Success")).toBe(true);
83
+ expect(engine.getContext().count).toBe(1);
84
+ });
85
+
86
+ it("should handle rule returning invalid data", () => {
87
+ const TestEvent = defineEvent<"TEST", {}>("TEST");
88
+
89
+ const invalidRule = defineRule<{ value: number }>({
90
+ id: "invalid.rule",
91
+ description: "Rule that returns invalid facts",
92
+ impl: () => {
93
+ // Return invalid fact structure
94
+ return [{ tag: "Invalid", notPayload: "wrong" }] as any;
95
+ },
96
+ });
97
+
98
+ const registry = new PraxisRegistry<{ value: number }>();
99
+ registry.registerRule(invalidRule);
100
+
101
+ const engine = createPraxisEngine({
102
+ initialContext: { value: 0 },
103
+ registry,
104
+ });
105
+
106
+ // Should not throw - just add the facts as-is
107
+ const result = engine.step([TestEvent.create({})]);
108
+ expect(result.state.facts).toHaveLength(1);
109
+ });
110
+ });
111
+
112
+ describe("Constraint Violations", () => {
113
+ it("should handle constraint that throws an error", () => {
114
+ const errorConstraint = defineConstraint<{ value: number }>({
115
+ id: "error.constraint",
116
+ description: "Constraint that throws",
117
+ impl: () => {
118
+ throw new Error("Constraint error");
119
+ },
120
+ });
121
+
122
+ const registry = new PraxisRegistry<{ value: number }>();
123
+ registry.registerConstraint(errorConstraint);
124
+
125
+ const engine = createPraxisEngine({
126
+ initialContext: { value: 0 },
127
+ registry,
128
+ });
129
+
130
+ const result = engine.step([]);
131
+
132
+ expect(result.diagnostics).toHaveLength(1);
133
+ expect(result.diagnostics[0]?.kind).toBe("constraint-violation");
134
+ expect(result.diagnostics[0]?.message).toContain("Constraint error");
135
+ });
136
+
137
+ it("should report constraint violation with false return", () => {
138
+ const failConstraint = defineConstraint<{ value: number }>({
139
+ id: "fail.constraint",
140
+ description: "Always fails",
141
+ impl: () => false,
142
+ });
143
+
144
+ const registry = new PraxisRegistry<{ value: number }>();
145
+ registry.registerConstraint(failConstraint);
146
+
147
+ const engine = createPraxisEngine({
148
+ initialContext: { value: 0 },
149
+ registry,
150
+ });
151
+
152
+ const result = engine.step([]);
153
+
154
+ expect(result.diagnostics).toHaveLength(1);
155
+ expect(result.diagnostics[0]?.kind).toBe("constraint-violation");
156
+ expect(result.diagnostics[0]?.message).toContain("fail.constraint");
157
+ });
158
+
159
+ it("should report constraint violation with custom message", () => {
160
+ const customMessageConstraint = defineConstraint<{ value: number }>({
161
+ id: "custom.constraint",
162
+ description: "Custom message constraint",
163
+ impl: (state) => {
164
+ return state.context.value >= 0 || "Value must be non-negative";
165
+ },
166
+ });
167
+
168
+ const registry = new PraxisRegistry<{ value: number }>();
169
+ registry.registerConstraint(customMessageConstraint);
170
+
171
+ const engine = createPraxisEngine({
172
+ initialContext: { value: -5 },
173
+ registry,
174
+ });
175
+
176
+ const result = engine.step([]);
177
+
178
+ expect(result.diagnostics).toHaveLength(1);
179
+ expect(result.diagnostics[0]?.message).toBe("Value must be non-negative");
180
+ });
181
+
182
+ it("should check multiple constraints and report all violations", () => {
183
+ const constraint1 = defineConstraint<{ value: number }>({
184
+ id: "constraint1",
185
+ description: "Constraint 1",
186
+ impl: () => "Violation 1",
187
+ });
188
+
189
+ const constraint2 = defineConstraint<{ value: number }>({
190
+ id: "constraint2",
191
+ description: "Constraint 2",
192
+ impl: () => "Violation 2",
193
+ });
194
+
195
+ const registry = new PraxisRegistry<{ value: number }>();
196
+ registry.registerConstraint(constraint1);
197
+ registry.registerConstraint(constraint2);
198
+
199
+ const engine = createPraxisEngine({
200
+ initialContext: { value: 0 },
201
+ registry,
202
+ });
203
+
204
+ const result = engine.step([]);
205
+
206
+ expect(result.diagnostics).toHaveLength(2);
207
+ expect(result.diagnostics[0]?.message).toBe("Violation 1");
208
+ expect(result.diagnostics[1]?.message).toBe("Violation 2");
209
+ });
210
+ });
211
+
212
+ describe("Registry Edge Cases", () => {
213
+ it("should handle non-existent rule ID in config", () => {
214
+ const registry = new PraxisRegistry<{ value: number }>();
215
+ const engine = createPraxisEngine({
216
+ initialContext: { value: 0 },
217
+ registry,
218
+ });
219
+
220
+ const result = engine.stepWithConfig([], {
221
+ ruleIds: ["nonexistent.rule"],
222
+ constraintIds: [],
223
+ });
224
+
225
+ expect(result.diagnostics).toHaveLength(1);
226
+ expect(result.diagnostics[0]?.kind).toBe("rule-error");
227
+ expect(result.diagnostics[0]?.message).toContain("not found in registry");
228
+ });
229
+
230
+ it("should handle non-existent constraint ID in config", () => {
231
+ const registry = new PraxisRegistry<{ value: number }>();
232
+ const engine = createPraxisEngine({
233
+ initialContext: { value: 0 },
234
+ registry,
235
+ });
236
+
237
+ const result = engine.stepWithConfig([], {
238
+ ruleIds: [],
239
+ constraintIds: ["nonexistent.constraint"],
240
+ });
241
+
242
+ expect(result.diagnostics).toHaveLength(1);
243
+ expect(result.diagnostics[0]?.kind).toBe("constraint-violation");
244
+ expect(result.diagnostics[0]?.message).toContain("not found in registry");
245
+ });
246
+
247
+ it("should throw when registering duplicate rule IDs", () => {
248
+ const rule = defineRule({
249
+ id: "duplicate",
250
+ description: "Test",
251
+ impl: () => [],
252
+ });
253
+
254
+ const registry = new PraxisRegistry();
255
+ registry.registerRule(rule);
256
+
257
+ expect(() => registry.registerRule(rule)).toThrow(
258
+ 'Rule with id "duplicate" already registered'
259
+ );
260
+ });
261
+
262
+ it("should throw when registering duplicate constraint IDs", () => {
263
+ const constraint = defineConstraint({
264
+ id: "duplicate",
265
+ description: "Test",
266
+ impl: () => true,
267
+ });
268
+
269
+ const registry = new PraxisRegistry();
270
+ registry.registerConstraint(constraint);
271
+
272
+ expect(() => registry.registerConstraint(constraint)).toThrow(
273
+ 'Constraint with id "duplicate" already registered'
274
+ );
275
+ });
276
+
277
+ it("should register module with multiple rules and constraints", () => {
278
+ const module = defineModule({
279
+ rules: [
280
+ defineRule({ id: "rule1", description: "Rule 1", impl: () => [] }),
281
+ defineRule({ id: "rule2", description: "Rule 2", impl: () => [] }),
282
+ ],
283
+ constraints: [
284
+ defineConstraint({ id: "c1", description: "C1", impl: () => true }),
285
+ defineConstraint({ id: "c2", description: "C2", impl: () => true }),
286
+ ],
287
+ meta: { version: "1.0.0" },
288
+ });
289
+
290
+ const registry = new PraxisRegistry();
291
+ registry.registerModule(module);
292
+
293
+ expect(registry.getRuleIds()).toHaveLength(2);
294
+ expect(registry.getConstraintIds()).toHaveLength(2);
295
+ });
296
+ });
297
+
298
+ describe("Context and State Edge Cases", () => {
299
+ it("should handle empty events array", () => {
300
+ const registry = new PraxisRegistry<{ value: number }>();
301
+ const engine = createPraxisEngine({
302
+ initialContext: { value: 0 },
303
+ registry,
304
+ });
305
+
306
+ const result = engine.step([]);
307
+ expect(result.state.facts).toHaveLength(0);
308
+ expect(result.diagnostics).toHaveLength(0);
309
+ });
310
+
311
+ it("should handle complex nested context", () => {
312
+ interface ComplexContext {
313
+ nested: {
314
+ deep: {
315
+ value: number;
316
+ array: string[];
317
+ };
318
+ };
319
+ map: Map<string, number>;
320
+ }
321
+
322
+ const TestEvent = defineEvent<"TEST", {}>("TEST");
323
+ const rule = defineRule<ComplexContext>({
324
+ id: "complex.rule",
325
+ description: "Complex context rule",
326
+ impl: (state) => {
327
+ state.context.nested.deep.value += 1;
328
+ state.context.nested.deep.array.push("item");
329
+ return [];
330
+ },
331
+ });
332
+
333
+ const registry = new PraxisRegistry<ComplexContext>();
334
+ registry.registerRule(rule);
335
+
336
+ const engine = createPraxisEngine<ComplexContext>({
337
+ initialContext: {
338
+ nested: {
339
+ deep: {
340
+ value: 0,
341
+ array: [],
342
+ },
343
+ },
344
+ map: new Map([["key", 1]]),
345
+ },
346
+ registry,
347
+ });
348
+
349
+ engine.step([TestEvent.create({})]);
350
+ const context = engine.getContext();
351
+
352
+ expect(context.nested.deep.value).toBe(1);
353
+ expect(context.nested.deep.array).toHaveLength(1);
354
+ });
355
+
356
+ it("should handle null and undefined in payloads", () => {
357
+ const NullFact = defineFact<"NullFact", { value: null }>("NullFact");
358
+ const UndefinedFact = defineFact<"UndefinedFact", { value: undefined }>(
359
+ "UndefinedFact"
360
+ );
361
+
362
+ const fact1 = NullFact.create({ value: null });
363
+ const fact2 = UndefinedFact.create({ value: undefined });
364
+
365
+ expect(fact1.payload.value).toBeNull();
366
+ expect(fact2.payload.value).toBeUndefined();
367
+ });
368
+
369
+ it("should handle large number of facts", () => {
370
+ const TestEvent = defineEvent<"TEST", {}>("TEST");
371
+ const TestFact = defineFact<"TestFact", { index: number }>("TestFact");
372
+
373
+ const rule = defineRule<{ count: number }>({
374
+ id: "many.facts",
375
+ description: "Generate many facts",
376
+ impl: (_state, events) => {
377
+ if (events.some(TestEvent.is)) {
378
+ const facts = [];
379
+ for (let i = 0; i < 1000; i++) {
380
+ facts.push(TestFact.create({ index: i }));
381
+ }
382
+ return facts;
383
+ }
384
+ return [];
385
+ },
386
+ });
387
+
388
+ const registry = new PraxisRegistry<{ count: number }>();
389
+ registry.registerRule(rule);
390
+
391
+ const engine = createPraxisEngine({
392
+ initialContext: { count: 0 },
393
+ registry,
394
+ });
395
+
396
+ const result = engine.step([TestEvent.create({})]);
397
+ expect(result.state.facts).toHaveLength(1000);
398
+ });
399
+
400
+ it("should isolate context mutations between getContext calls", () => {
401
+ const registry = new PraxisRegistry<{ value: number }>();
402
+ const engine = createPraxisEngine({
403
+ initialContext: { value: 10 },
404
+ registry,
405
+ });
406
+
407
+ const context1 = engine.getContext();
408
+ context1.value = 999;
409
+
410
+ const context2 = engine.getContext();
411
+ expect(context2.value).toBe(10); // Should be unchanged
412
+ });
413
+ });
414
+
415
+ describe("Event Processing Edge Cases", () => {
416
+ it("should handle duplicate events", () => {
417
+ const TestEvent = defineEvent<"TEST", { id: string }>("TEST");
418
+ const TestFact = defineFact<"TestFact", { count: number }>("TestFact");
419
+
420
+ const rule = defineRule<{ count: number }>({
421
+ id: "count.events",
422
+ description: "Count test events",
423
+ impl: (_state, events) => {
424
+ const testEvents = events.filter(TestEvent.is);
425
+ if (testEvents.length > 0) {
426
+ return [TestFact.create({ count: testEvents.length })];
427
+ }
428
+ return [];
429
+ },
430
+ });
431
+
432
+ const registry = new PraxisRegistry<{ count: number }>();
433
+ registry.registerRule(rule);
434
+
435
+ const engine = createPraxisEngine({
436
+ initialContext: { count: 0 },
437
+ registry,
438
+ });
439
+
440
+ const event = TestEvent.create({ id: "test" });
441
+ const result = engine.step([event, event, event]);
442
+
443
+ const fact = result.state.facts.find((f) => f.tag === "TestFact");
444
+ expect(fact?.payload).toEqual({ count: 3 });
445
+ });
446
+
447
+ it("should handle events with empty payloads", () => {
448
+ const EmptyEvent = defineEvent<"EMPTY", {}>("EMPTY");
449
+ const EmptyFact = defineFact<"EmptyFact", {}>("EmptyFact");
450
+
451
+ const rule = defineRule<{ triggered: boolean }>({
452
+ id: "empty.rule",
453
+ description: "Handle empty events",
454
+ impl: (state, events) => {
455
+ if (events.some(EmptyEvent.is)) {
456
+ state.context.triggered = true;
457
+ return [EmptyFact.create({})];
458
+ }
459
+ return [];
460
+ },
461
+ });
462
+
463
+ const registry = new PraxisRegistry<{ triggered: boolean }>();
464
+ registry.registerRule(rule);
465
+
466
+ const engine = createPraxisEngine({
467
+ initialContext: { triggered: false },
468
+ registry,
469
+ });
470
+
471
+ engine.step([EmptyEvent.create({})]);
472
+ expect(engine.getContext().triggered).toBe(true);
473
+ });
474
+ });
475
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Engine tests
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { createPraxisEngine } from "../core/engine.js";
7
+ import { PraxisRegistry } from "../core/rules.js";
8
+ import { defineRule, defineConstraint, defineFact, defineEvent } from "../dsl/index.js";
9
+
10
+ describe("LogicEngine", () => {
11
+ it("should create an engine with initial context", () => {
12
+ const registry = new PraxisRegistry<{ count: number }>();
13
+ const engine = createPraxisEngine({
14
+ initialContext: { count: 0 },
15
+ registry,
16
+ });
17
+
18
+ expect(engine.getContext()).toEqual({ count: 0 });
19
+ });
20
+
21
+ it("should apply rules when processing events", () => {
22
+ interface Context {
23
+ count: number;
24
+ }
25
+
26
+ const Incremented = defineFact<"Incremented", { amount: number }>("Incremented");
27
+ const Increment = defineEvent<"INCREMENT", {}>("INCREMENT");
28
+
29
+ const incrementRule = defineRule<Context>({
30
+ id: "increment",
31
+ description: "Increment counter",
32
+ impl: (state, events) => {
33
+ if (events.some(Increment.is)) {
34
+ state.context.count += 1;
35
+ return [Incremented.create({ amount: 1 })];
36
+ }
37
+ return [];
38
+ },
39
+ });
40
+
41
+ const registry = new PraxisRegistry<Context>();
42
+ registry.registerRule(incrementRule);
43
+
44
+ const engine = createPraxisEngine({
45
+ initialContext: { count: 0 },
46
+ registry,
47
+ });
48
+
49
+ const result = engine.step([Increment.create({})]);
50
+
51
+ expect(engine.getContext().count).toBe(1);
52
+ expect(result.state.facts).toHaveLength(1);
53
+ expect(result.state.facts[0]?.tag).toBe("Incremented");
54
+ });
55
+
56
+ it("should check constraints", () => {
57
+ interface Context {
58
+ count: number;
59
+ }
60
+
61
+ const maxConstraint = defineConstraint<Context>({
62
+ id: "max100",
63
+ description: "Count cannot exceed 100",
64
+ impl: (state) => {
65
+ return state.context.count <= 100 || "Count exceeds 100";
66
+ },
67
+ });
68
+
69
+ const registry = new PraxisRegistry<Context>();
70
+ registry.registerConstraint(maxConstraint);
71
+
72
+ const engine = createPraxisEngine({
73
+ initialContext: { count: 50 },
74
+ registry,
75
+ });
76
+
77
+ // Should pass constraint
78
+ let result = engine.step([]);
79
+ expect(result.diagnostics).toHaveLength(0);
80
+
81
+ // Update context to violate constraint
82
+ engine.updateContext(() => ({ count: 150 }));
83
+ result = engine.step([]);
84
+ expect(result.diagnostics).toHaveLength(1);
85
+ expect(result.diagnostics[0]?.kind).toBe("constraint-violation");
86
+ });
87
+
88
+ it("should handle multiple rules", () => {
89
+ interface Context {
90
+ value: number;
91
+ }
92
+
93
+ const Doubled = defineFact<"Doubled", {}>("Doubled");
94
+ const Added = defineFact<"Added", { amount: number }>("Added");
95
+ const DoubleThenAdd = defineEvent<"DOUBLE_ADD", { amount: number }>("DOUBLE_ADD");
96
+
97
+ const doubleRule = defineRule<Context>({
98
+ id: "double",
99
+ description: "Double the value",
100
+ impl: (state, events) => {
101
+ if (events.some(DoubleThenAdd.is)) {
102
+ state.context.value *= 2;
103
+ return [Doubled.create({})];
104
+ }
105
+ return [];
106
+ },
107
+ });
108
+
109
+ const addRule = defineRule<Context>({
110
+ id: "add",
111
+ description: "Add to the value",
112
+ impl: (state, events) => {
113
+ const event = events.find(DoubleThenAdd.is);
114
+ if (event) {
115
+ state.context.value += event.payload.amount;
116
+ return [Added.create({ amount: event.payload.amount })];
117
+ }
118
+ return [];
119
+ },
120
+ });
121
+
122
+ const registry = new PraxisRegistry<Context>();
123
+ registry.registerRule(doubleRule);
124
+ registry.registerRule(addRule);
125
+
126
+ const engine = createPraxisEngine({
127
+ initialContext: { value: 10 },
128
+ registry,
129
+ });
130
+
131
+ const result = engine.step([DoubleThenAdd.create({ amount: 5 })]);
132
+
133
+ // Value should be (10 * 2) + 5 = 25
134
+ expect(engine.getContext().value).toBe(25);
135
+ expect(result.state.facts).toHaveLength(2);
136
+ });
137
+ });