@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 +7 -0
- package/dist/node/cli/index.cjs +3 -0
- package/dist/node/cli/index.js +1 -1
- package/dist/node/{verify-QRYKRIDU.js → verify-KLJRXVJS.js} +3 -0
- package/docs/decision-ledger/DOGFOODING.md +85 -0
- package/docs/decision-ledger/contract-index.json +757 -0
- package/package.json +5 -2
- package/src/__tests__/edge-cases.test.ts +135 -4
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" } }]
|
package/dist/node/cli/index.cjs
CHANGED
|
@@ -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)) {
|
package/dist/node/cli/index.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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({
|
|
281
|
-
|
|
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({
|
|
285
|
-
|
|
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 }>();
|