@plures/praxis 1.2.11 → 1.2.12

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.
package/README.md CHANGED
@@ -315,6 +315,13 @@ See [src/decision-ledger/README.md](./src/decision-ledger/README.md) for complet
315
315
  - [Decision Ledger Guide](./src/decision-ledger/README.md)
316
316
  - [Examples](./examples/)
317
317
 
318
+ ## Decision Ledger
319
+
320
+ Praxis dogfoods its Decision Ledger to keep rule/constraint behavior explicit and enforceable.
321
+
322
+ - [Behavior Ledger](./docs/decision-ledger/BEHAVIOR_LEDGER.md)
323
+ - [Dogfooding Guide](./docs/decision-ledger/DOGFOODING.md)
324
+
318
325
  ## Contributing
319
326
  PRs and discussions welcome. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) and [SECURITY.md](./SECURITY.md).
320
327
  console.log(result.state.facts); // [{ tag: "UserLoggedIn", payload: { userId: "alice" } }]
@@ -214263,6 +214263,9 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
214263
214263
  function analyzeRuleFile(filePath) {
214264
214264
  const fileContent = fs8.readFileSync(filePath, "utf-8");
214265
214265
  const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
214266
+ return analyzeRuleSource(sourceFile, fileContent);
214267
+ }
214268
+ function analyzeRuleSource(sourceFile, fileContent) {
214266
214269
  const results = [];
214267
214270
  function visit(node) {
214268
214271
  if (ts.isVariableDeclaration(node)) {
@@ -689,7 +689,7 @@ cloudCmd.command("usage").description("View cloud usage metrics").action(async (
689
689
  });
690
690
  program.command("verify <type>").description("Verify project implementation (e.g., implementation)").option("-d, --detailed", "Show detailed analysis").action(async (type, options) => {
691
691
  try {
692
- const { verify } = await import("../verify-QRYKRIDU.js");
692
+ const { verify } = await import("../verify-KLJRXVJS.js");
693
693
  await verify(type, options);
694
694
  } catch (error) {
695
695
  console.error("Error verifying:", error);
@@ -210756,6 +210756,9 @@ import * as fs from "fs";
210756
210756
  function analyzeRuleFile(filePath) {
210757
210757
  const fileContent = fs.readFileSync(filePath, "utf-8");
210758
210758
  const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
210759
+ return analyzeRuleSource(sourceFile, fileContent);
210760
+ }
210761
+ function analyzeRuleSource(sourceFile, fileContent) {
210759
210762
  const results = [];
210760
210763
  function visit(node) {
210761
210764
  if (ts.isVariableDeclaration(node)) {
@@ -0,0 +1,85 @@
1
+ # Decision Ledger Dogfooding (Praxis-on-Praxis)
2
+
3
+ This document defines how Praxis dogfoods the Decision Ledger across the entire repo. The goal is to ensure every rule/constraint has a contract, tests, specs, and a ledger entry that tracks behavior changes over time.
4
+
5
+ ## Goals
6
+
7
+ - Contracts for **all** rules and constraints.
8
+ - Tests for **every** contract example and invariant.
9
+ - Specs (TLA+ or Praxis invariants) for each module where feasible.
10
+ - Immutable ledger snapshots for every rule ID.
11
+ - CI-visible contract coverage and drift detection.
12
+
13
+ ## Dogfood Workflow
14
+
15
+ ### 1) Generate a rule/constraint index (reverse-engineer scan)
16
+
17
+ This uses the AST analyzer to enumerate `defineRule`/`defineConstraint` usages and capture rule IDs.
18
+
19
+ ```bash
20
+ npm run scan:rules
21
+ ```
22
+
23
+ Output:
24
+ - `docs/decision-ledger/contract-index.json`
25
+
26
+ The index includes:
27
+ - Rule/constraint IDs
28
+ - Source file locations
29
+ - Simple inference signals (guards, mutations, events) for rules
30
+ - Whether a contract is already attached (when statically detectable)
31
+
32
+ ### 2) Add/Update Contracts
33
+
34
+ For every rule/constraint in the index:
35
+
36
+ - Define a contract with:
37
+ - `behavior` (canonical description)
38
+ - `examples[]` (Given/When/Then)
39
+ - `invariants[]`
40
+ - `assumptions[]` (explicit, with confidence)
41
+ - `references[]` (docs, tickets)
42
+ - Attach via `meta.contract`.
43
+
44
+ ### 3) Add Tests and Specs
45
+
46
+ - Create tests that directly mirror the Given/When/Then examples.
47
+ - Add specs (TLA+ or Praxis invariants) where appropriate.
48
+ - Ensure tests are named to include the rule ID (so validation can locate them).
49
+
50
+ ### 4) Validate Contract Coverage
51
+
52
+ ```bash
53
+ npm run build
54
+ npm run validate:contracts
55
+ ```
56
+
57
+ This produces a deterministic report of missing or incomplete contracts and artifacts.
58
+
59
+ ### 5) Emit Contract Gap Payloads (optional assistant handoff)
60
+
61
+ ```bash
62
+ node ./dist/node/cli/index.js validate --emit-facts --gap-output docs/decision-ledger/contract-gap.json
63
+ ```
64
+
65
+ The gap payload can be used by Copilot or humans to generate tests/specs or refine contracts.
66
+
67
+ ### 6) Write Ledger Snapshots (per rule ID)
68
+
69
+ After contracts/tests/specs are updated, write ledger snapshots:
70
+
71
+ ```bash
72
+ node ./dist/node/cli/index.js validate --ledger docs/decision-ledger/logic-ledger --author "dogfood"
73
+ ```
74
+
75
+ ## Governance & CI
76
+
77
+ - Missing contracts/tests/specs are **warnings** during development.
78
+ - CI will tighten to **errors** once coverage reaches agreed thresholds.
79
+ - All contract-driven changes must update the Behavior Ledger (`docs/decision-ledger/BEHAVIOR_LEDGER.md`) and `LATEST.md`.
80
+
81
+ ## References
82
+
83
+ - `docs/decision-ledger/BEHAVIOR_LEDGER.md`
84
+ - `docs/decision-ledger/LATEST.md`
85
+ - `src/decision-ledger/README.md`
@@ -0,0 +1,757 @@
1
+ {
2
+ "generatedAt": "2026-01-30T07:07:22.057Z",
3
+ "root": "/home/runner/work/praxis/praxis",
4
+ "summary": {
5
+ "rules": 45,
6
+ "constraints": 8,
7
+ "files": 117
8
+ },
9
+ "rules": [
10
+ {
11
+ "id": "auth.checkSession",
12
+ "kind": "rule",
13
+ "file": "src/examples/hero-ecommerce/index.ts",
14
+ "description": "Check for session expiration (30 minute timeout)",
15
+ "hasContract": false,
16
+ "analysis": {
17
+ "ruleId": "auth.checkSession",
18
+ "guards": [
19
+ "!checkEvent || !state.context.currentUser || !state.context.sessionStartTime"
20
+ ],
21
+ "mutations": [
22
+ "state.context.currentUser = null",
23
+ "state.context.sessionStartTime = null",
24
+ "state.context.cart = { items: [], total: 0, discountApplied: 0 }"
25
+ ],
26
+ "events": [
27
+ "CheckSession"
28
+ ]
29
+ }
30
+ },
31
+ {
32
+ "id": "auth.login",
33
+ "kind": "rule",
34
+ "file": "examples/decision-ledger/index.js",
35
+ "description": "Process login events",
36
+ "hasContract": true,
37
+ "analysis": {
38
+ "ruleId": "auth.login",
39
+ "guards": [],
40
+ "mutations": [],
41
+ "events": []
42
+ }
43
+ },
44
+ {
45
+ "id": "auth.login",
46
+ "kind": "rule",
47
+ "file": "examples/sample-registry.js",
48
+ "description": "Process login events",
49
+ "hasContract": true,
50
+ "analysis": {
51
+ "ruleId": "auth.login",
52
+ "guards": [],
53
+ "mutations": [],
54
+ "events": []
55
+ }
56
+ },
57
+ {
58
+ "id": "auth.login",
59
+ "kind": "rule",
60
+ "file": "examples/sample-registry.ts",
61
+ "description": "Process login events",
62
+ "hasContract": true,
63
+ "analysis": {
64
+ "ruleId": "auth.login",
65
+ "guards": [],
66
+ "mutations": [],
67
+ "events": []
68
+ }
69
+ },
70
+ {
71
+ "id": "auth.login",
72
+ "kind": "rule",
73
+ "file": "src/examples/auth-basic/index.ts",
74
+ "description": "Process login event and create UserLoggedIn fact",
75
+ "hasContract": false,
76
+ "analysis": {
77
+ "ruleId": "auth.login",
78
+ "guards": [
79
+ "!loginEvent"
80
+ ],
81
+ "mutations": [],
82
+ "events": [
83
+ "Login"
84
+ ]
85
+ }
86
+ },
87
+ {
88
+ "id": "auth.login",
89
+ "kind": "rule",
90
+ "file": "src/examples/hero-ecommerce/index.ts",
91
+ "description": "Process login event",
92
+ "hasContract": false,
93
+ "analysis": {
94
+ "ruleId": "auth.login",
95
+ "guards": [
96
+ "!loginEvent"
97
+ ],
98
+ "mutations": [
99
+ "state.context.currentUser = loginEvent.payload.username",
100
+ "state.context.sessionStartTime = Date.now()"
101
+ ],
102
+ "events": [
103
+ "Login"
104
+ ]
105
+ }
106
+ },
107
+ {
108
+ "id": "auth.logout",
109
+ "kind": "rule",
110
+ "file": "examples/sample-registry.js",
111
+ "description": "Process logout events",
112
+ "hasContract": true,
113
+ "analysis": {
114
+ "ruleId": "auth.logout",
115
+ "guards": [],
116
+ "mutations": [],
117
+ "events": []
118
+ }
119
+ },
120
+ {
121
+ "id": "auth.logout",
122
+ "kind": "rule",
123
+ "file": "examples/sample-registry.ts",
124
+ "description": "Process logout events",
125
+ "hasContract": true,
126
+ "analysis": {
127
+ "ruleId": "auth.logout",
128
+ "guards": [],
129
+ "mutations": [],
130
+ "events": []
131
+ }
132
+ },
133
+ {
134
+ "id": "auth.logout",
135
+ "kind": "rule",
136
+ "file": "src/examples/auth-basic/index.ts",
137
+ "description": "Process logout event and create UserLoggedOut fact",
138
+ "hasContract": false,
139
+ "analysis": {
140
+ "ruleId": "auth.logout",
141
+ "guards": [
142
+ "!logoutEvent || !state.context.currentUser"
143
+ ],
144
+ "mutations": [],
145
+ "events": [
146
+ "Logout"
147
+ ]
148
+ }
149
+ },
150
+ {
151
+ "id": "auth.logout",
152
+ "kind": "rule",
153
+ "file": "src/examples/hero-ecommerce/index.ts",
154
+ "description": "Process logout event",
155
+ "hasContract": false,
156
+ "analysis": {
157
+ "ruleId": "auth.logout",
158
+ "guards": [
159
+ "!logoutEvent || !state.context.currentUser"
160
+ ],
161
+ "mutations": [
162
+ "state.context.currentUser = null",
163
+ "state.context.sessionStartTime = null",
164
+ "state.context.cart = {\n items: [],\n total: 0,\n discountApplied: 0,\n }"
165
+ ],
166
+ "events": [
167
+ "Logout"
168
+ ]
169
+ }
170
+ },
171
+ {
172
+ "id": "auth.updateContext",
173
+ "kind": "rule",
174
+ "file": "src/examples/auth-basic/index.ts",
175
+ "description": "Update context based on login/logout facts",
176
+ "hasContract": false,
177
+ "analysis": {
178
+ "ruleId": "auth.updateContext",
179
+ "guards": [],
180
+ "mutations": [
181
+ "state.context.currentUser = latestLogin.payload.userId",
182
+ "state.context.currentUser = null"
183
+ ],
184
+ "events": []
185
+ }
186
+ },
187
+ {
188
+ "id": "cart.addItem",
189
+ "kind": "rule",
190
+ "file": "examples/decision-ledger/index.js",
191
+ "description": "Add item to cart",
192
+ "hasContract": true,
193
+ "analysis": {
194
+ "ruleId": "cart.addItem",
195
+ "guards": [],
196
+ "mutations": [],
197
+ "events": []
198
+ }
199
+ },
200
+ {
201
+ "id": "cart.addItem",
202
+ "kind": "rule",
203
+ "file": "examples/sample-registry.js",
204
+ "description": "Add item to shopping cart (no contract)",
205
+ "hasContract": false,
206
+ "analysis": {
207
+ "ruleId": "cart.addItem",
208
+ "guards": [],
209
+ "mutations": [],
210
+ "events": []
211
+ }
212
+ },
213
+ {
214
+ "id": "cart.addItem",
215
+ "kind": "rule",
216
+ "file": "examples/sample-registry.ts",
217
+ "description": "Add item to shopping cart (no contract)",
218
+ "hasContract": false,
219
+ "analysis": {
220
+ "ruleId": "cart.addItem",
221
+ "guards": [],
222
+ "mutations": [],
223
+ "events": []
224
+ }
225
+ },
226
+ {
227
+ "id": "cart.addItem",
228
+ "kind": "rule",
229
+ "file": "src/examples/cart/index.ts",
230
+ "description": "Add item to cart",
231
+ "hasContract": false,
232
+ "analysis": {
233
+ "ruleId": "cart.addItem",
234
+ "guards": [],
235
+ "mutations": [],
236
+ "events": []
237
+ }
238
+ },
239
+ {
240
+ "id": "cart.addItem",
241
+ "kind": "rule",
242
+ "file": "src/examples/hero-ecommerce/index.ts",
243
+ "description": "Add item to cart",
244
+ "hasContract": false,
245
+ "analysis": {
246
+ "ruleId": "cart.addItem",
247
+ "guards": [
248
+ "!state.context.currentUser || addEvents.length === 0"
249
+ ],
250
+ "mutations": [],
251
+ "events": []
252
+ }
253
+ },
254
+ {
255
+ "id": "cart.applyDiscount",
256
+ "kind": "rule",
257
+ "file": "src/examples/cart/index.ts",
258
+ "description": "Apply discount code",
259
+ "hasContract": false,
260
+ "analysis": {
261
+ "ruleId": "cart.applyDiscount",
262
+ "guards": [
263
+ "!discountEvent || state.context.discountApplied"
264
+ ],
265
+ "mutations": [],
266
+ "events": [
267
+ "ApplyDiscount"
268
+ ]
269
+ }
270
+ },
271
+ {
272
+ "id": "cart.applyDiscount",
273
+ "kind": "rule",
274
+ "file": "src/examples/hero-ecommerce/index.ts",
275
+ "description": "Apply discount codes",
276
+ "hasContract": false,
277
+ "analysis": {
278
+ "ruleId": "cart.applyDiscount",
279
+ "guards": [
280
+ "!discountEvent || !state.context.currentUser"
281
+ ],
282
+ "mutations": [],
283
+ "events": [
284
+ "ApplyDiscount"
285
+ ]
286
+ }
287
+ },
288
+ {
289
+ "id": "cart.checkout",
290
+ "kind": "rule",
291
+ "file": "src/examples/hero-ecommerce/index.ts",
292
+ "description": "Process checkout",
293
+ "hasContract": false,
294
+ "analysis": {
295
+ "ruleId": "cart.checkout",
296
+ "guards": [
297
+ "!checkoutEvent || !state.context.currentUser || state.context.cart.items.length === 0"
298
+ ],
299
+ "mutations": [
300
+ "state.context.cart = { items: [], total: 0, discountApplied: 0 }"
301
+ ],
302
+ "events": [
303
+ "Checkout"
304
+ ]
305
+ }
306
+ },
307
+ {
308
+ "id": "cart.clear",
309
+ "kind": "rule",
310
+ "file": "src/examples/cart/index.ts",
311
+ "description": "Clear cart",
312
+ "hasContract": false,
313
+ "analysis": {
314
+ "ruleId": "cart.clear",
315
+ "guards": [
316
+ "!clearEvent"
317
+ ],
318
+ "mutations": [],
319
+ "events": [
320
+ "ClearCart"
321
+ ]
322
+ }
323
+ },
324
+ {
325
+ "id": "cart.removeItem",
326
+ "kind": "rule",
327
+ "file": "src/examples/cart/index.ts",
328
+ "description": "Remove item from cart",
329
+ "hasContract": false,
330
+ "analysis": {
331
+ "ruleId": "cart.removeItem",
332
+ "guards": [
333
+ "!removeEvent"
334
+ ],
335
+ "mutations": [],
336
+ "events": [
337
+ "RemoveFromCart"
338
+ ]
339
+ }
340
+ },
341
+ {
342
+ "id": "cart.removeItem",
343
+ "kind": "rule",
344
+ "file": "src/examples/hero-ecommerce/index.ts",
345
+ "description": "Remove item from cart",
346
+ "hasContract": false,
347
+ "analysis": {
348
+ "ruleId": "cart.removeItem",
349
+ "guards": [
350
+ "!removeEvent || !state.context.currentUser"
351
+ ],
352
+ "mutations": [],
353
+ "events": [
354
+ "RemoveFromCart"
355
+ ]
356
+ }
357
+ },
358
+ {
359
+ "id": "cart.updateContext",
360
+ "kind": "rule",
361
+ "file": "src/examples/cart/index.ts",
362
+ "description": "Update cart context based on facts",
363
+ "hasContract": false,
364
+ "analysis": {
365
+ "ruleId": "cart.updateContext",
366
+ "guards": [],
367
+ "mutations": [
368
+ "state.context.items = []",
369
+ "state.context.total = 0",
370
+ "state.context.discountApplied = false",
371
+ "state.context.items = Array.from(itemMap.entries()).map(([productId, data]) => ({\n productId,\n quantity: data.quantity,\n price: data.price,\n }))",
372
+ "state.context.discountApplied = true",
373
+ "state.context.total = Math.round(total * 100) / 100"
374
+ ],
375
+ "events": []
376
+ }
377
+ },
378
+ {
379
+ "id": "cart.updateContext",
380
+ "kind": "rule",
381
+ "file": "src/examples/hero-ecommerce/index.ts",
382
+ "description": "Update cart context from facts",
383
+ "hasContract": false,
384
+ "analysis": {
385
+ "ruleId": "cart.updateContext",
386
+ "guards": [],
387
+ "mutations": [
388
+ "state.context.cart = { items: [], total: 0, discountApplied: 0 }",
389
+ "state.context.cart.items = Array.from(itemMap.entries()).map(([productId, data]) => ({\n productId,\n quantity: data.quantity,\n price: data.price,\n }))",
390
+ "state.context.cart.discountApplied = totalDiscount",
391
+ "state.context.cart.total = total * (1 - totalDiscount)"
392
+ ],
393
+ "events": []
394
+ }
395
+ },
396
+ {
397
+ "id": "counter.decrement",
398
+ "kind": "rule",
399
+ "file": "examples/reactive-counter/index.ts",
400
+ "description": "Decrement the counter",
401
+ "hasContract": false
402
+ },
403
+ {
404
+ "id": "counter.decrement",
405
+ "kind": "rule",
406
+ "file": "src/examples/svelte-counter/index.ts",
407
+ "description": "Decrement the counter",
408
+ "hasContract": false,
409
+ "analysis": {
410
+ "ruleId": "counter.decrement",
411
+ "guards": [
412
+ "!decrementEvent"
413
+ ],
414
+ "mutations": [],
415
+ "events": [
416
+ "Decrement"
417
+ ]
418
+ }
419
+ },
420
+ {
421
+ "id": "counter.increment",
422
+ "kind": "rule",
423
+ "file": "examples/reactive-counter/index.ts",
424
+ "description": "Increment the counter",
425
+ "hasContract": false
426
+ },
427
+ {
428
+ "id": "counter.increment",
429
+ "kind": "rule",
430
+ "file": "examples/unified-app/index.js",
431
+ "description": "Increment counter",
432
+ "hasContract": false
433
+ },
434
+ {
435
+ "id": "counter.increment",
436
+ "kind": "rule",
437
+ "file": "src/examples/svelte-counter/index.ts",
438
+ "description": "Increment the counter",
439
+ "hasContract": false,
440
+ "analysis": {
441
+ "ruleId": "counter.increment",
442
+ "guards": [
443
+ "!incrementEvent"
444
+ ],
445
+ "mutations": [],
446
+ "events": [
447
+ "Increment"
448
+ ]
449
+ }
450
+ },
451
+ {
452
+ "id": "counter.reset",
453
+ "kind": "rule",
454
+ "file": "examples/reactive-counter/index.ts",
455
+ "description": "Reset the counter",
456
+ "hasContract": false
457
+ },
458
+ {
459
+ "id": "counter.reset",
460
+ "kind": "rule",
461
+ "file": "src/examples/svelte-counter/index.ts",
462
+ "description": "Reset the counter",
463
+ "hasContract": false,
464
+ "analysis": {
465
+ "ruleId": "counter.reset",
466
+ "guards": [
467
+ "!resetEvent"
468
+ ],
469
+ "mutations": [
470
+ "state.context.count = 0",
471
+ "state.context.history = [0]"
472
+ ],
473
+ "events": [
474
+ "Reset"
475
+ ]
476
+ }
477
+ },
478
+ {
479
+ "id": "features.disable",
480
+ "kind": "rule",
481
+ "file": "src/examples/hero-ecommerce/index.ts",
482
+ "description": "Disable a feature flag",
483
+ "hasContract": false,
484
+ "analysis": {
485
+ "ruleId": "features.disable",
486
+ "guards": [
487
+ "!disableEvent"
488
+ ],
489
+ "mutations": [
490
+ "state.context.features.freeShippingEnabled = false",
491
+ "state.context.features.loyaltyProgramEnabled = false",
492
+ "state.context.features.newCheckoutFlowEnabled = false"
493
+ ],
494
+ "events": [
495
+ "DisableFeature"
496
+ ]
497
+ }
498
+ },
499
+ {
500
+ "id": "features.enable",
501
+ "kind": "rule",
502
+ "file": "src/examples/hero-ecommerce/index.ts",
503
+ "description": "Enable a feature flag",
504
+ "hasContract": false,
505
+ "analysis": {
506
+ "ruleId": "features.enable",
507
+ "guards": [
508
+ "!enableEvent"
509
+ ],
510
+ "mutations": [
511
+ "state.context.features.freeShippingEnabled = true",
512
+ "state.context.features.loyaltyProgramEnabled = true",
513
+ "state.context.features.newCheckoutFlowEnabled = true"
514
+ ],
515
+ "events": [
516
+ "EnableFeature"
517
+ ]
518
+ }
519
+ },
520
+ {
521
+ "id": "legacy.process",
522
+ "kind": "rule",
523
+ "file": "examples/decision-ledger/index.js",
524
+ "description": "Legacy processing rule",
525
+ "hasContract": false,
526
+ "analysis": {
527
+ "ruleId": "legacy.process",
528
+ "guards": [],
529
+ "mutations": [],
530
+ "events": []
531
+ }
532
+ },
533
+ {
534
+ "id": "messages.receive",
535
+ "kind": "rule",
536
+ "file": "examples/unified-app/index.js",
537
+ "description": "Receive message",
538
+ "hasContract": false
539
+ },
540
+ {
541
+ "id": "order.process",
542
+ "kind": "rule",
543
+ "file": "examples/sample-registry.js",
544
+ "description": "Process order checkout (incomplete contract)",
545
+ "hasContract": true,
546
+ "analysis": {
547
+ "ruleId": "order.process",
548
+ "guards": [],
549
+ "mutations": [],
550
+ "events": []
551
+ }
552
+ },
553
+ {
554
+ "id": "order.process",
555
+ "kind": "rule",
556
+ "file": "examples/sample-registry.ts",
557
+ "description": "Process order checkout (incomplete contract)",
558
+ "hasContract": true,
559
+ "analysis": {
560
+ "ruleId": "order.process",
561
+ "guards": [],
562
+ "mutations": [],
563
+ "events": []
564
+ }
565
+ },
566
+ {
567
+ "id": "task.complete",
568
+ "kind": "rule",
569
+ "file": "examples/cloud-sync/index.ts",
570
+ "description": "Complete a task",
571
+ "hasContract": false,
572
+ "analysis": {
573
+ "ruleId": "task.complete",
574
+ "guards": [],
575
+ "mutations": [],
576
+ "events": []
577
+ }
578
+ },
579
+ {
580
+ "id": "task.create",
581
+ "kind": "rule",
582
+ "file": "examples/cloud-sync/index.ts",
583
+ "description": "Create a new task",
584
+ "hasContract": false,
585
+ "analysis": {
586
+ "ruleId": "task.create",
587
+ "guards": [],
588
+ "mutations": [],
589
+ "events": []
590
+ }
591
+ },
592
+ {
593
+ "id": "todo.add",
594
+ "kind": "rule",
595
+ "file": "src/examples/advanced-todo/index.ts",
596
+ "description": "Add a new todo item",
597
+ "hasContract": false,
598
+ "analysis": {
599
+ "ruleId": "todo.add",
600
+ "guards": [
601
+ "!event || !event.payload.text.trim()"
602
+ ],
603
+ "mutations": [],
604
+ "events": [
605
+ "AddTodo"
606
+ ]
607
+ }
608
+ },
609
+ {
610
+ "id": "todo.clearCompleted",
611
+ "kind": "rule",
612
+ "file": "src/examples/advanced-todo/index.ts",
613
+ "description": "Remove all completed todos",
614
+ "hasContract": false,
615
+ "analysis": {
616
+ "ruleId": "todo.clearCompleted",
617
+ "guards": [
618
+ "!event"
619
+ ],
620
+ "mutations": [
621
+ "state.context.todos = state.context.todos.filter((t) => !t.completed)"
622
+ ],
623
+ "events": [
624
+ "ClearCompleted"
625
+ ]
626
+ }
627
+ },
628
+ {
629
+ "id": "todo.completeAll",
630
+ "kind": "rule",
631
+ "file": "src/examples/advanced-todo/index.ts",
632
+ "description": "Mark all todos as completed",
633
+ "hasContract": false,
634
+ "analysis": {
635
+ "ruleId": "todo.completeAll",
636
+ "guards": [
637
+ "!event"
638
+ ],
639
+ "mutations": [],
640
+ "events": [
641
+ "CompleteAll"
642
+ ]
643
+ }
644
+ },
645
+ {
646
+ "id": "todo.remove",
647
+ "kind": "rule",
648
+ "file": "src/examples/advanced-todo/index.ts",
649
+ "description": "Remove a todo item",
650
+ "hasContract": false,
651
+ "analysis": {
652
+ "ruleId": "todo.remove",
653
+ "guards": [
654
+ "!event"
655
+ ],
656
+ "mutations": [],
657
+ "events": [
658
+ "RemoveTodo"
659
+ ]
660
+ }
661
+ },
662
+ {
663
+ "id": "todo.setFilter",
664
+ "kind": "rule",
665
+ "file": "src/examples/advanced-todo/index.ts",
666
+ "description": "Change the filter",
667
+ "hasContract": false,
668
+ "analysis": {
669
+ "ruleId": "todo.setFilter",
670
+ "guards": [
671
+ "!event"
672
+ ],
673
+ "mutations": [
674
+ "state.context.filter = event.payload.filter"
675
+ ],
676
+ "events": [
677
+ "SetFilter"
678
+ ]
679
+ }
680
+ },
681
+ {
682
+ "id": "todo.toggle",
683
+ "kind": "rule",
684
+ "file": "src/examples/advanced-todo/index.ts",
685
+ "description": "Toggle todo completion status",
686
+ "hasContract": false,
687
+ "analysis": {
688
+ "ruleId": "todo.toggle",
689
+ "guards": [
690
+ "!event"
691
+ ],
692
+ "mutations": [],
693
+ "events": [
694
+ "ToggleTodo"
695
+ ]
696
+ }
697
+ }
698
+ ],
699
+ "constraints": [
700
+ {
701
+ "id": "auth.maxSessions",
702
+ "kind": "constraint",
703
+ "file": "examples/sample-registry.js",
704
+ "description": "User cannot have more than 5 concurrent sessions",
705
+ "hasContract": true
706
+ },
707
+ {
708
+ "id": "auth.maxSessions",
709
+ "kind": "constraint",
710
+ "file": "examples/sample-registry.ts",
711
+ "description": "User cannot have more than 5 concurrent sessions",
712
+ "hasContract": true
713
+ },
714
+ {
715
+ "id": "auth.singleSession",
716
+ "kind": "constraint",
717
+ "file": "src/examples/auth-basic/index.ts",
718
+ "description": "Only one user can be logged in at a time",
719
+ "hasContract": false
720
+ },
721
+ {
722
+ "id": "auth.singleSession",
723
+ "kind": "constraint",
724
+ "file": "src/examples/hero-ecommerce/index.ts",
725
+ "description": "Only one user can be logged in at a time",
726
+ "hasContract": false
727
+ },
728
+ {
729
+ "id": "cart.maxItems",
730
+ "kind": "constraint",
731
+ "file": "src/examples/cart/index.ts",
732
+ "description": "Cart cannot exceed 100 items",
733
+ "hasContract": false
734
+ },
735
+ {
736
+ "id": "cart.maxItems",
737
+ "kind": "constraint",
738
+ "file": "src/examples/hero-ecommerce/index.ts",
739
+ "description": "Cart cannot exceed 100 items",
740
+ "hasContract": false
741
+ },
742
+ {
743
+ "id": "cart.maxTotal",
744
+ "kind": "constraint",
745
+ "file": "src/examples/cart/index.ts",
746
+ "description": "Cart total cannot exceed $10,000",
747
+ "hasContract": false
748
+ },
749
+ {
750
+ "id": "cart.requiresAuth",
751
+ "kind": "constraint",
752
+ "file": "src/examples/hero-ecommerce/index.ts",
753
+ "description": "Cart operations require authentication",
754
+ "hasContract": false
755
+ }
756
+ ]
757
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plures/praxis",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "The Full Plures Application Framework - declarative schemas, logic engine, component generation, and local-first data",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -77,7 +77,10 @@
77
77
  "test:watch": "vitest",
78
78
  "test:ui": "vitest --ui",
79
79
  "typecheck": "tsc --noEmit",
80
- "cli": "node ./dist/src/cli/index.js"
80
+ "cli": "node ./dist/src/cli/index.js",
81
+ "scan:rules": "tsx tools/decision-ledger/scan-rules.ts",
82
+ "validate:contracts": "node ./dist/node/cli/index.js validate --output console",
83
+ "validate:contracts:sarif": "node ./dist/node/cli/index.js validate --output sarif"
81
84
  },
82
85
  "keywords": [
83
86
  "praxis",
@@ -12,6 +12,7 @@ import {
12
12
  defineFact,
13
13
  defineModule,
14
14
  } from '../dsl/index.js';
15
+ import { defineContract } from '../decision-ledger/types.js';
15
16
 
16
17
  describe('Edge Cases and Failure Paths', () => {
17
18
  describe('Rule Errors', () => {
@@ -24,6 +25,12 @@ describe('Edge Cases and Failure Paths', () => {
24
25
  impl: () => {
25
26
  throw new Error('Intentional error');
26
27
  },
28
+ contract: defineContract({
29
+ ruleId: 'error.rule',
30
+ behavior: 'Test rule that intentionally throws an error',
31
+ examples: [{ given: 'Any state', when: 'Rule is executed', then: 'Error is thrown' }],
32
+ invariants: ['Always throws an error'],
33
+ }),
27
34
  });
28
35
 
29
36
  const registry = new PraxisRegistry<{ value: number }>();
@@ -51,6 +58,12 @@ describe('Edge Cases and Failure Paths', () => {
51
58
  impl: () => {
52
59
  throw new Error('Error');
53
60
  },
61
+ contract: defineContract({
62
+ ruleId: 'error.rule',
63
+ behavior: 'Test rule that intentionally throws an error',
64
+ examples: [{ given: 'Any state', when: 'Rule is executed', then: 'Error is thrown' }],
65
+ invariants: ['Always throws an error'],
66
+ }),
54
67
  });
55
68
 
56
69
  const successRule = defineRule<{ count: number }>({
@@ -63,6 +76,12 @@ describe('Edge Cases and Failure Paths', () => {
63
76
  }
64
77
  return [];
65
78
  },
79
+ contract: defineContract({
80
+ ruleId: 'success.rule',
81
+ behavior: 'Test rule that succeeds and increments count',
82
+ examples: [{ given: 'Test event', when: 'Rule is executed', then: 'Count is incremented and Success fact is emitted' }],
83
+ invariants: ['Count is incremented on success'],
84
+ }),
66
85
  });
67
86
 
68
87
  const registry = new PraxisRegistry<{ count: number }>();
@@ -93,6 +112,12 @@ describe('Edge Cases and Failure Paths', () => {
93
112
  // Return invalid fact structure
94
113
  return [{ tag: 'Invalid', notPayload: 'wrong' }] as any;
95
114
  },
115
+ contract: defineContract({
116
+ ruleId: 'invalid.rule',
117
+ behavior: 'Test rule that returns invalid fact structure',
118
+ examples: [{ given: 'Any state', when: 'Rule is executed', then: 'Invalid fact is returned' }],
119
+ invariants: ['Returns malformed facts'],
120
+ }),
96
121
  });
97
122
 
98
123
  const registry = new PraxisRegistry<{ value: number }>();
@@ -117,6 +142,12 @@ describe('Edge Cases and Failure Paths', () => {
117
142
  impl: () => {
118
143
  throw new Error('Constraint error');
119
144
  },
145
+ contract: defineContract({
146
+ ruleId: 'error.constraint',
147
+ behavior: 'Test constraint that intentionally throws an error',
148
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Error is thrown' }],
149
+ invariants: ['Always throws an error'],
150
+ }),
120
151
  });
121
152
 
122
153
  const registry = new PraxisRegistry<{ value: number }>();
@@ -139,6 +170,12 @@ describe('Edge Cases and Failure Paths', () => {
139
170
  id: 'fail.constraint',
140
171
  description: 'Always fails',
141
172
  impl: () => false,
173
+ contract: defineContract({
174
+ ruleId: 'fail.constraint',
175
+ behavior: 'Test constraint that always fails',
176
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Returns false' }],
177
+ invariants: ['Always returns false'],
178
+ }),
142
179
  });
143
180
 
144
181
  const registry = new PraxisRegistry<{ value: number }>();
@@ -163,6 +200,12 @@ describe('Edge Cases and Failure Paths', () => {
163
200
  impl: (state) => {
164
201
  return state.context.value >= 0 || 'Value must be non-negative';
165
202
  },
203
+ contract: defineContract({
204
+ ruleId: 'custom.constraint',
205
+ behavior: 'Test constraint that returns custom error message',
206
+ examples: [{ given: 'Negative value', when: 'Constraint is checked', then: 'Returns custom error message' }],
207
+ invariants: ['Returns custom message for negative values'],
208
+ }),
166
209
  });
167
210
 
168
211
  const registry = new PraxisRegistry<{ value: number }>();
@@ -184,12 +227,24 @@ describe('Edge Cases and Failure Paths', () => {
184
227
  id: 'constraint1',
185
228
  description: 'Constraint 1',
186
229
  impl: () => 'Violation 1',
230
+ contract: defineContract({
231
+ ruleId: 'constraint1',
232
+ behavior: 'Test constraint that returns violation message',
233
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Returns "Violation 1"' }],
234
+ invariants: ['Always returns "Violation 1"'],
235
+ }),
187
236
  });
188
237
 
189
238
  const constraint2 = defineConstraint<{ value: number }>({
190
239
  id: 'constraint2',
191
240
  description: 'Constraint 2',
192
241
  impl: () => 'Violation 2',
242
+ contract: defineContract({
243
+ ruleId: 'constraint2',
244
+ behavior: 'Test constraint that returns violation message',
245
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Returns "Violation 2"' }],
246
+ invariants: ['Always returns "Violation 2"'],
247
+ }),
193
248
  });
194
249
 
195
250
  const registry = new PraxisRegistry<{ value: number }>();
@@ -249,6 +304,12 @@ describe('Edge Cases and Failure Paths', () => {
249
304
  id: 'duplicate',
250
305
  description: 'Test',
251
306
  impl: () => [],
307
+ contract: defineContract({
308
+ ruleId: 'duplicate',
309
+ behavior: 'Test rule for duplicate ID testing',
310
+ examples: [{ given: 'Rule is registered twice', when: 'Second registration', then: 'Throws error' }],
311
+ invariants: ['Rule IDs must be unique'],
312
+ }),
252
313
  });
253
314
 
254
315
  const registry = new PraxisRegistry();
@@ -264,6 +325,12 @@ describe('Edge Cases and Failure Paths', () => {
264
325
  id: 'duplicate',
265
326
  description: 'Test',
266
327
  impl: () => true,
328
+ contract: defineContract({
329
+ ruleId: 'duplicate',
330
+ behavior: 'Test constraint for duplicate ID testing',
331
+ examples: [{ given: 'Constraint is registered twice', when: 'Second registration', then: 'Throws error' }],
332
+ invariants: ['Constraint IDs must be unique'],
333
+ }),
267
334
  });
268
335
 
269
336
  const registry = new PraxisRegistry();
@@ -277,12 +344,52 @@ describe('Edge Cases and Failure Paths', () => {
277
344
  it('should register module with multiple rules and constraints', () => {
278
345
  const module = defineModule({
279
346
  rules: [
280
- defineRule({ id: 'rule1', description: 'Rule 1', impl: () => [] }),
281
- defineRule({ id: 'rule2', description: 'Rule 2', impl: () => [] }),
347
+ defineRule({
348
+ id: 'rule1',
349
+ description: 'Rule 1',
350
+ impl: () => [],
351
+ contract: defineContract({
352
+ ruleId: 'rule1',
353
+ behavior: 'Test rule 1 for module registration',
354
+ examples: [{ given: 'Any state', when: 'Rule is executed', then: 'Empty array returned' }],
355
+ invariants: ['Always returns empty array'],
356
+ }),
357
+ }),
358
+ defineRule({
359
+ id: 'rule2',
360
+ description: 'Rule 2',
361
+ impl: () => [],
362
+ contract: defineContract({
363
+ ruleId: 'rule2',
364
+ behavior: 'Test rule 2 for module registration',
365
+ examples: [{ given: 'Any state', when: 'Rule is executed', then: 'Empty array returned' }],
366
+ invariants: ['Always returns empty array'],
367
+ }),
368
+ }),
282
369
  ],
283
370
  constraints: [
284
- defineConstraint({ id: 'c1', description: 'C1', impl: () => true }),
285
- defineConstraint({ id: 'c2', description: 'C2', impl: () => true }),
371
+ defineConstraint({
372
+ id: 'c1',
373
+ description: 'C1',
374
+ impl: () => true,
375
+ contract: defineContract({
376
+ ruleId: 'c1',
377
+ behavior: 'Test constraint 1 for module registration',
378
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Returns true' }],
379
+ invariants: ['Always returns true'],
380
+ }),
381
+ }),
382
+ defineConstraint({
383
+ id: 'c2',
384
+ description: 'C2',
385
+ impl: () => true,
386
+ contract: defineContract({
387
+ ruleId: 'c2',
388
+ behavior: 'Test constraint 2 for module registration',
389
+ examples: [{ given: 'Any state', when: 'Constraint is checked', then: 'Returns true' }],
390
+ invariants: ['Always returns true'],
391
+ }),
392
+ }),
286
393
  ],
287
394
  meta: { version: '1.0.0' },
288
395
  });
@@ -328,6 +435,12 @@ describe('Edge Cases and Failure Paths', () => {
328
435
  state.context.nested.deep.array.push('item');
329
436
  return [];
330
437
  },
438
+ contract: defineContract({
439
+ ruleId: 'complex.rule',
440
+ behavior: 'Test rule that manipulates complex nested context',
441
+ examples: [{ given: 'Complex nested context', when: 'Rule is executed', then: 'Nested values are updated' }],
442
+ invariants: ['Context structure is preserved'],
443
+ }),
331
444
  });
332
445
 
333
446
  const registry = new PraxisRegistry<ComplexContext>();
@@ -381,6 +494,12 @@ describe('Edge Cases and Failure Paths', () => {
381
494
  }
382
495
  return [];
383
496
  },
497
+ contract: defineContract({
498
+ ruleId: 'many.facts',
499
+ behavior: 'Test rule that generates many facts',
500
+ examples: [{ given: 'Test event', when: 'Rule is executed', then: '1000 facts are generated' }],
501
+ invariants: ['Generates exactly 1000 facts when triggered'],
502
+ }),
384
503
  });
385
504
 
386
505
  const registry = new PraxisRegistry<{ count: number }>();
@@ -425,6 +544,12 @@ describe('Edge Cases and Failure Paths', () => {
425
544
  }
426
545
  return [];
427
546
  },
547
+ contract: defineContract({
548
+ ruleId: 'count.events',
549
+ behavior: 'Test rule that counts events',
550
+ examples: [{ given: 'Multiple test events', when: 'Rule is executed', then: 'Count fact is emitted' }],
551
+ invariants: ['Count matches number of test events'],
552
+ }),
428
553
  });
429
554
 
430
555
  const registry = new PraxisRegistry<{ count: number }>();
@@ -456,6 +581,12 @@ describe('Edge Cases and Failure Paths', () => {
456
581
  }
457
582
  return [];
458
583
  },
584
+ contract: defineContract({
585
+ ruleId: 'empty.rule',
586
+ behavior: 'Test rule that handles empty events',
587
+ examples: [{ given: 'Empty event', when: 'Rule is executed', then: 'Context is updated and fact is emitted' }],
588
+ invariants: ['Triggered flag is set to true'],
589
+ }),
459
590
  });
460
591
 
461
592
  const registry = new PraxisRegistry<{ triggered: boolean }>();