@plures/praxis 1.3.0 → 1.4.4

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 (74) hide show
  1. package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
  2. package/dist/browser/chunk-6SJ44Q64.js +473 -0
  3. package/dist/browser/chunk-BQOYZBWA.js +282 -0
  4. package/dist/browser/chunk-IG5BJ2MT.js +91 -0
  5. package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
  6. package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
  7. package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
  8. package/dist/browser/expectations/index.d.ts +180 -0
  9. package/dist/browser/expectations/index.js +14 -0
  10. package/dist/browser/factory/index.d.ts +149 -0
  11. package/dist/browser/factory/index.js +15 -0
  12. package/dist/browser/index.d.ts +274 -3
  13. package/dist/browser/index.js +407 -54
  14. package/dist/browser/integrations/svelte.d.ts +3 -2
  15. package/dist/browser/integrations/svelte.js +3 -2
  16. package/dist/browser/project/index.d.ts +176 -0
  17. package/dist/browser/project/index.js +19 -0
  18. package/dist/browser/reactive-engine.svelte-DgVTqHLc.d.ts +223 -0
  19. package/dist/browser/{reactive-engine.svelte-DjynI82A.d.ts → rules-i1LHpnGd.d.ts} +13 -221
  20. package/dist/node/chunk-2IUFZBH3.js +87 -0
  21. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  22. package/dist/node/chunk-AZLNISFI.js +1690 -0
  23. package/dist/node/chunk-IG5BJ2MT.js +91 -0
  24. package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
  25. package/dist/node/chunk-PGVSB6NR.js +59 -0
  26. package/dist/node/{chunk-5JQJZADT.js → chunk-ZO2LU4G4.js} +4 -4
  27. package/dist/node/cli/index.cjs +1126 -211
  28. package/dist/node/cli/index.js +21 -2
  29. package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
  30. package/dist/node/index.cjs +5623 -2765
  31. package/dist/node/index.d.cts +1181 -1
  32. package/dist/node/index.d.ts +1181 -1
  33. package/dist/node/index.js +1646 -79
  34. package/dist/node/integrations/svelte.js +4 -3
  35. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  36. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  37. package/dist/node/server-FKLVY57V.js +363 -0
  38. package/dist/node/{validate-EN3M4FUR.js → validate-5PSWJTIC.js} +5 -3
  39. package/package.json +50 -3
  40. package/src/__tests__/chronos-project.test.ts +799 -0
  41. package/src/__tests__/decision-ledger.test.ts +857 -402
  42. package/src/__tests__/expectations.test.ts +364 -0
  43. package/src/__tests__/factory.test.ts +426 -0
  44. package/src/__tests__/mcp-server.test.ts +310 -0
  45. package/src/__tests__/project.test.ts +396 -0
  46. package/src/chronos/diff.ts +336 -0
  47. package/src/chronos/hooks.ts +227 -0
  48. package/src/chronos/index.ts +83 -0
  49. package/src/chronos/project-chronicle.ts +198 -0
  50. package/src/chronos/timeline.ts +152 -0
  51. package/src/cli/index.ts +28 -0
  52. package/src/decision-ledger/analyzer-types.ts +280 -0
  53. package/src/decision-ledger/analyzer.ts +518 -0
  54. package/src/decision-ledger/contract-verification.ts +456 -0
  55. package/src/decision-ledger/derivation.ts +158 -0
  56. package/src/decision-ledger/index.ts +59 -0
  57. package/src/decision-ledger/report.ts +378 -0
  58. package/src/decision-ledger/suggestions.ts +287 -0
  59. package/src/expectations/expectations.ts +471 -0
  60. package/src/expectations/index.ts +29 -0
  61. package/src/expectations/types.ts +95 -0
  62. package/src/factory/factory.ts +634 -0
  63. package/src/factory/index.ts +27 -0
  64. package/src/factory/types.ts +64 -0
  65. package/src/index.browser.ts +83 -0
  66. package/src/index.ts +134 -0
  67. package/src/mcp/index.ts +33 -0
  68. package/src/mcp/server.ts +485 -0
  69. package/src/mcp/types.ts +161 -0
  70. package/src/project/index.ts +31 -0
  71. package/src/project/project.ts +423 -0
  72. package/src/project/types.ts +87 -0
  73. package/dist/node/chunk-PTH6MD6P.js +0 -487
  74. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Tests for Expectations DSL
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ Expectation,
8
+ ExpectationSet,
9
+ expectBehavior,
10
+ verify,
11
+ formatVerificationReport,
12
+ } from '../expectations/expectations.js';
13
+ import type { VerifiableRegistry, VerifiableDescriptor } from '../expectations/types.js';
14
+
15
+ // ─── Mock Registry ──────────────────────────────────────────────────────────
16
+
17
+ function createMockRegistry(
18
+ rules: VerifiableDescriptor[] = [],
19
+ constraints: VerifiableDescriptor[] = [],
20
+ ): VerifiableRegistry {
21
+ return {
22
+ getAllRules: () => rules,
23
+ getAllConstraints: () => constraints,
24
+ getRuleIds: () => rules.map(r => r.id),
25
+ getConstraintIds: () => constraints.map(c => c.id),
26
+ };
27
+ }
28
+
29
+ const toastRule: VerifiableDescriptor = {
30
+ id: 'ui/settings-saved-toast',
31
+ description: 'Shows a toast when settings are saved successfully',
32
+ eventTypes: ['settings.saved'],
33
+ contract: {
34
+ ruleId: 'ui/settings-saved-toast',
35
+ behavior: 'Emits toast fact when settings diff is non-empty and save succeeds',
36
+ examples: [
37
+ {
38
+ given: 'settings.diff is non-empty',
39
+ when: 'save succeeds',
40
+ then: 'toast emitted with changed settings list',
41
+ },
42
+ {
43
+ given: 'settings.diff is empty',
44
+ when: 'save attempted',
45
+ then: 'no toast (skip)',
46
+ },
47
+ ],
48
+ invariants: [
49
+ 'Toast must include which settings changed',
50
+ 'Toast must never appear when diff is empty',
51
+ ],
52
+ },
53
+ };
54
+
55
+ const errorConstraint: VerifiableDescriptor = {
56
+ id: 'ui/no-toast-on-error',
57
+ description: 'Prevents toast display during error state',
58
+ contract: {
59
+ ruleId: 'ui/no-toast-on-error',
60
+ behavior: 'Blocks toast when save fails',
61
+ examples: [
62
+ {
63
+ given: 'save fails',
64
+ when: 'toast attempted',
65
+ then: 'violation — save failure blocks toast',
66
+ },
67
+ ],
68
+ invariants: ['Toast must never appear on save failure'],
69
+ },
70
+ };
71
+
72
+ // ─── Tests ──────────────────────────────────────────────────────────────────
73
+
74
+ describe('Expectations DSL', () => {
75
+ describe('Expectation class', () => {
76
+ it('should create an expectation with a name', () => {
77
+ const exp = new Expectation('my-behavior');
78
+ expect(exp.name).toBe('my-behavior');
79
+ expect(exp.conditions).toHaveLength(0);
80
+ });
81
+
82
+ it('should chain onlyWhen conditions', () => {
83
+ const exp = new Expectation('toast')
84
+ .onlyWhen('diff is non-empty')
85
+ .onlyWhen('save succeeds');
86
+
87
+ expect(exp.conditions).toHaveLength(2);
88
+ expect(exp.conditions[0]).toEqual({ description: 'diff is non-empty', type: 'onlyWhen' });
89
+ expect(exp.conditions[1]).toEqual({ description: 'save succeeds', type: 'onlyWhen' });
90
+ });
91
+
92
+ it('should chain never conditions', () => {
93
+ const exp = new Expectation('toast')
94
+ .never('when save fails')
95
+ .never('when diff is empty');
96
+
97
+ expect(exp.conditions).toHaveLength(2);
98
+ expect(exp.conditions[0].type).toBe('never');
99
+ expect(exp.conditions[1].type).toBe('never');
100
+ });
101
+
102
+ it('should chain always conditions', () => {
103
+ const exp = new Expectation('toast')
104
+ .always('includes changed settings');
105
+
106
+ expect(exp.conditions).toHaveLength(1);
107
+ expect(exp.conditions[0]).toEqual({ description: 'includes changed settings', type: 'always' });
108
+ });
109
+
110
+ it('should support mixed condition types', () => {
111
+ const exp = new Expectation('settings-saved-toast')
112
+ .onlyWhen('settings.diff is non-empty')
113
+ .never('when save fails')
114
+ .always('includes which settings changed');
115
+
116
+ expect(exp.conditions).toHaveLength(3);
117
+ expect(exp.conditions.map(c => c.type)).toEqual(['onlyWhen', 'never', 'always']);
118
+ });
119
+ });
120
+
121
+ describe('expectBehavior builder', () => {
122
+ it('should create an Expectation via builder', () => {
123
+ const exp = expectBehavior('my-toast')
124
+ .onlyWhen('data changed')
125
+ .never('on error');
126
+
127
+ expect(exp).toBeInstanceOf(Expectation);
128
+ expect(exp.name).toBe('my-toast');
129
+ expect(exp.conditions).toHaveLength(2);
130
+ });
131
+ });
132
+
133
+ describe('ExpectationSet', () => {
134
+ it('should create a named set', () => {
135
+ const set = new ExpectationSet({ name: 'settings', description: 'Settings page expectations' });
136
+ expect(set.name).toBe('settings');
137
+ expect(set.description).toBe('Settings page expectations');
138
+ expect(set.size).toBe(0);
139
+ });
140
+
141
+ it('should add expectations', () => {
142
+ const set = new ExpectationSet({ name: 'ui' });
143
+ set.add(expectBehavior('toast').onlyWhen('change detected'));
144
+ set.add(expectBehavior('loading').always('shows spinner'));
145
+
146
+ expect(set.size).toBe(2);
147
+ expect(set.expectations[0].name).toBe('toast');
148
+ expect(set.expectations[1].name).toBe('loading');
149
+ });
150
+
151
+ it('should support chained add', () => {
152
+ const set = new ExpectationSet({ name: 'ui' })
153
+ .add(expectBehavior('a'))
154
+ .add(expectBehavior('b'));
155
+
156
+ expect(set.size).toBe(2);
157
+ });
158
+ });
159
+
160
+ describe('verify', () => {
161
+ it('should return satisfied for expectations matching rule contracts', () => {
162
+ const registry = createMockRegistry([toastRule], [errorConstraint]);
163
+
164
+ const expectations = new ExpectationSet({ name: 'settings' });
165
+ expectations.add(
166
+ expectBehavior('settings-saved-toast')
167
+ .onlyWhen('settings.diff is non-empty'),
168
+ );
169
+
170
+ const report = verify(registry, expectations);
171
+ expect(report.status).toBe('satisfied');
172
+ expect(report.summary.satisfied).toBe(1);
173
+ expect(report.summary.violated).toBe(0);
174
+ });
175
+
176
+ it('should detect unverifiable conditions when no related rules exist', () => {
177
+ const registry = createMockRegistry([], []);
178
+
179
+ const expectations = new ExpectationSet({ name: 'ghost' });
180
+ expectations.add(
181
+ expectBehavior('nonexistent-feature')
182
+ .onlyWhen('something happens'),
183
+ );
184
+
185
+ const report = verify(registry, expectations);
186
+ expect(report.status).not.toBe('satisfied');
187
+ expect(report.allEdgeCases.length).toBeGreaterThan(0);
188
+ });
189
+
190
+ it('should verify never conditions against constraints', () => {
191
+ const registry = createMockRegistry([toastRule], [errorConstraint]);
192
+
193
+ const expectations = new ExpectationSet({ name: 'toast-safety' });
194
+ expectations.add(
195
+ expectBehavior('settings-saved-toast')
196
+ .never('when save fails'),
197
+ );
198
+
199
+ const report = verify(registry, expectations);
200
+ // "save fails" should be caught by the constraint's invariants
201
+ const condResult = report.expectations[0].conditions[0];
202
+ expect(condResult.status).toBe('satisfied');
203
+ });
204
+
205
+ it('should verify always conditions against invariants', () => {
206
+ const registry = createMockRegistry([toastRule], []);
207
+
208
+ const expectations = new ExpectationSet({ name: 'toast-content' });
209
+ expectations.add(
210
+ expectBehavior('settings-saved-toast')
211
+ .always('includes which settings changed'),
212
+ );
213
+
214
+ const report = verify(registry, expectations);
215
+ const condResult = report.expectations[0].conditions[0];
216
+ expect(condResult.status).toBe('satisfied');
217
+ });
218
+
219
+ it('should report partial when some conditions pass and others are unverifiable', () => {
220
+ const registry = createMockRegistry([toastRule], []);
221
+
222
+ const expectations = new ExpectationSet({ name: 'mixed' });
223
+ expectations.add(
224
+ expectBehavior('settings-saved-toast')
225
+ .onlyWhen('settings.diff is non-empty')
226
+ .always('includes confetti animation'), // not in any contract
227
+ );
228
+
229
+ const report = verify(registry, expectations);
230
+ // First condition should be satisfied, second unverifiable => partial
231
+ expect(report.expectations[0].status).toBe('partial');
232
+ });
233
+
234
+ it('should provide mitigations for unverifiable expectations', () => {
235
+ const registry = createMockRegistry([], []);
236
+
237
+ const expectations = new ExpectationSet({ name: 'gaps' });
238
+ expectations.add(
239
+ expectBehavior('missing-feature')
240
+ .onlyWhen('user clicks button'),
241
+ );
242
+
243
+ const report = verify(registry, expectations);
244
+ expect(report.allMitigations.length).toBeGreaterThan(0);
245
+ });
246
+
247
+ it('should handle empty expectation set', () => {
248
+ const registry = createMockRegistry([toastRule]);
249
+ const expectations = new ExpectationSet({ name: 'empty' });
250
+
251
+ const report = verify(registry, expectations);
252
+ expect(report.status).toBe('satisfied');
253
+ expect(report.summary.total).toBe(0);
254
+ });
255
+
256
+ it('should handle expectation with no conditions', () => {
257
+ const registry = createMockRegistry([toastRule]);
258
+ const expectations = new ExpectationSet({ name: 'bare' });
259
+ expectations.add(new Expectation('just-a-name'));
260
+
261
+ const report = verify(registry, expectations);
262
+ expect(report.expectations[0].status).toBe('satisfied'); // vacuously true
263
+ });
264
+
265
+ it('should include report timestamp', () => {
266
+ const registry = createMockRegistry([]);
267
+ const expectations = new ExpectationSet({ name: 'timed' });
268
+
269
+ const report = verify(registry, expectations);
270
+ expect(report.timestamp).toBeDefined();
271
+ expect(new Date(report.timestamp).getTime()).toBeGreaterThan(0);
272
+ });
273
+ });
274
+
275
+ describe('formatVerificationReport', () => {
276
+ it('should format a satisfied report', () => {
277
+ const registry = createMockRegistry([toastRule], [errorConstraint]);
278
+ const expectations = new ExpectationSet({ name: 'settings' });
279
+ expectations.add(
280
+ expectBehavior('settings-saved-toast')
281
+ .onlyWhen('settings.diff is non-empty'),
282
+ );
283
+
284
+ const report = verify(registry, expectations);
285
+ const formatted = formatVerificationReport(report);
286
+
287
+ expect(formatted).toContain('settings');
288
+ expect(formatted).toContain('SATISFIED');
289
+ });
290
+
291
+ it('should format a report with mitigations', () => {
292
+ const registry = createMockRegistry([]);
293
+ const expectations = new ExpectationSet({ name: 'gaps' });
294
+ expectations.add(
295
+ expectBehavior('missing')
296
+ .onlyWhen('something'),
297
+ );
298
+
299
+ const report = verify(registry, expectations);
300
+ const formatted = formatVerificationReport(report);
301
+
302
+ expect(formatted).toContain('mitigation');
303
+ });
304
+
305
+ it('should include condition type labels', () => {
306
+ const registry = createMockRegistry([toastRule]);
307
+ const expectations = new ExpectationSet({ name: 'mixed' });
308
+ expectations.add(
309
+ expectBehavior('settings-saved-toast')
310
+ .onlyWhen('settings.diff is non-empty')
311
+ .never('on error')
312
+ .always('shows details'),
313
+ );
314
+
315
+ const report = verify(registry, expectations);
316
+ const formatted = formatVerificationReport(report);
317
+
318
+ expect(formatted).toContain('onlyWhen');
319
+ expect(formatted).toContain('never');
320
+ expect(formatted).toContain('always');
321
+ });
322
+ });
323
+
324
+ describe('integration with real PraxisRegistry', () => {
325
+ it('should work with actual PraxisRegistry as VerifiableRegistry', async () => {
326
+ const { PraxisRegistry } = await import('../core/rules.js');
327
+ const { RuleResult, fact } = await import('../core/rule-result.js');
328
+
329
+ const registry = new PraxisRegistry({
330
+ compliance: { enabled: false },
331
+ });
332
+
333
+ registry.registerRule({
334
+ id: 'auth/login',
335
+ description: 'Process login and create session',
336
+ eventTypes: 'auth.login',
337
+ contract: {
338
+ ruleId: 'auth/login',
339
+ behavior: 'Creates session when valid credentials provided',
340
+ examples: [
341
+ { given: 'valid credentials', when: 'login event', then: 'session created' },
342
+ { given: 'invalid credentials', when: 'login event', then: 'skip — no session' },
343
+ ],
344
+ invariants: ['Session must have unique ID', 'Invalid credentials must never create a session'],
345
+ },
346
+ impl: (_state, events) => {
347
+ const loginEvt = events.find(e => e.tag === 'auth.login');
348
+ if (!loginEvt) return RuleResult.skip('No login event');
349
+ return RuleResult.emit([fact('auth.session', { userId: 'test' })]);
350
+ },
351
+ });
352
+
353
+ const expectations = new ExpectationSet({ name: 'auth' });
354
+ expectations.add(
355
+ expectBehavior('auth/login')
356
+ .onlyWhen('valid credentials')
357
+ .never('invalid credentials'),
358
+ );
359
+
360
+ const report = verify(registry, expectations);
361
+ expect(report.status).toBe('satisfied');
362
+ });
363
+ });
364
+ });