@plures/praxis 1.4.4 → 2.0.3

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 (59) hide show
  1. package/README.md +164 -1067
  2. package/dist/browser/chunk-IUEKGHQN.js +373 -0
  3. package/dist/browser/factory/index.d.ts +2 -1
  4. package/dist/browser/index.d.ts +7 -4
  5. package/dist/browser/index.js +18 -6
  6. package/dist/browser/integrations/svelte.d.ts +4 -3
  7. package/dist/browser/project/index.d.ts +2 -1
  8. package/dist/browser/{reactive-engine.svelte-DgVTqHLc.d.ts → reactive-engine.svelte-BwWadvAW.d.ts} +2 -1
  9. package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
  10. package/dist/browser/{rules-i1LHpnGd.d.ts → rules-BaWMqxuG.d.ts} +2 -205
  11. package/dist/browser/unified/index.d.ts +239 -0
  12. package/dist/browser/unified/index.js +20 -0
  13. package/dist/node/chunk-IUEKGHQN.js +373 -0
  14. package/dist/node/cli/index.js +1 -1
  15. package/dist/node/index.cjs +377 -0
  16. package/dist/node/index.d.cts +4 -2
  17. package/dist/node/index.d.ts +4 -2
  18. package/dist/node/index.js +19 -7
  19. package/dist/node/integrations/svelte.d.cts +3 -2
  20. package/dist/node/integrations/svelte.d.ts +3 -2
  21. package/dist/node/integrations/svelte.js +2 -2
  22. package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
  23. package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
  24. package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
  25. package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
  26. package/dist/node/unified/index.cjs +494 -0
  27. package/dist/node/unified/index.d.cts +240 -0
  28. package/dist/node/unified/index.d.ts +240 -0
  29. package/dist/node/unified/index.js +21 -0
  30. package/docs/README.md +58 -102
  31. package/docs/archive/1.x/CONVERSATIONS_IMPLEMENTATION.md +207 -0
  32. package/docs/archive/1.x/DECISION_LEDGER_IMPLEMENTATION.md +109 -0
  33. package/docs/archive/1.x/DECISION_LEDGER_SUMMARY.md +424 -0
  34. package/docs/archive/1.x/ELEVATION_SUMMARY.md +249 -0
  35. package/docs/archive/1.x/FEATURE_SUMMARY.md +238 -0
  36. package/docs/archive/1.x/GOLDEN_PATH_IMPLEMENTATION.md +280 -0
  37. package/docs/archive/1.x/IMPLEMENTATION.md +166 -0
  38. package/docs/archive/1.x/IMPLEMENTATION_COMPLETE.md +389 -0
  39. package/docs/archive/1.x/IMPLEMENTATION_SUMMARY.md +59 -0
  40. package/docs/archive/1.x/INTEGRATION_ENHANCEMENT_SUMMARY.md +238 -0
  41. package/docs/archive/1.x/KNO_ENG_REFACTORING_SUMMARY.md +198 -0
  42. package/docs/archive/1.x/MONOREPO_SUMMARY.md +158 -0
  43. package/docs/archive/1.x/README.md +28 -0
  44. package/docs/archive/1.x/SVELTE_INTEGRATION_SUMMARY.md +415 -0
  45. package/docs/archive/1.x/TASK_1_COMPLETE.md +235 -0
  46. package/docs/archive/1.x/TASK_1_SUMMARY.md +281 -0
  47. package/docs/archive/1.x/VERSION_0.2.0_RELEASE_NOTES.md +288 -0
  48. package/docs/archive/1.x/ValidationChecklist.md +7 -0
  49. package/package.json +13 -1
  50. package/src/index.browser.ts +20 -0
  51. package/src/index.ts +21 -0
  52. package/src/unified/__tests__/unified-qa.test.ts +761 -0
  53. package/src/unified/__tests__/unified.test.ts +396 -0
  54. package/src/unified/core.ts +534 -0
  55. package/src/unified/index.ts +32 -0
  56. package/src/unified/rules.ts +66 -0
  57. package/src/unified/types.ts +148 -0
  58. package/dist/node/{chunk-ZO2LU4G4.js → chunk-WFRHXZBP.js} +3 -3
  59. package/dist/node/{validate-5PSWJTIC.js → validate-BY7JNY7H.js} +1 -1
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Praxis Unified Reactive Layer — Tests
3
+ *
4
+ * Tests that the unified API works end-to-end:
5
+ * - Schema → query() → subscribe → reactive updates
6
+ * - mutate() → constraint check → rule evaluation → fact emission
7
+ * - Liveness detection
8
+ * - Batch mutations
9
+ * - Timeline logging
10
+ */
11
+
12
+ import { describe, it, expect, vi } from 'vitest';
13
+ import {
14
+ createApp,
15
+ definePath,
16
+ defineRule,
17
+ defineConstraint,
18
+ RuleResult,
19
+ fact,
20
+ } from '../index.js';
21
+
22
+ // ── Test Schema ─────────────────────────────────────────────────────────────
23
+
24
+ interface SprintInfo {
25
+ name: string;
26
+ currentDay: number;
27
+ totalDays: number;
28
+ completedHours: number;
29
+ totalHours: number;
30
+ }
31
+
32
+ const Sprint = definePath<SprintInfo | null>('sprint/current', null);
33
+ const Loading = definePath<boolean>('sprint/loading', false);
34
+ const Items = definePath<Array<{ id: number; state: string; completedWork: number }>>('sprint/items', []);
35
+
36
+ // ── Test Rules ──────────────────────────────────────────────────────────────
37
+
38
+ const sprintBehindRule = defineRule({
39
+ id: 'sprint.behind',
40
+ watch: ['sprint/current'],
41
+ evaluate: (values) => {
42
+ const sprint = values['sprint/current'] as SprintInfo | null;
43
+ if (!sprint) return RuleResult.skip('No sprint');
44
+ const pace = sprint.currentDay / sprint.totalDays;
45
+ const work = sprint.completedHours / sprint.totalHours;
46
+ if (work >= pace) return RuleResult.retract(['sprint.behind']);
47
+ return RuleResult.emit([fact('sprint.behind', { pace, work })]);
48
+ },
49
+ });
50
+
51
+ const loadingRule = defineRule({
52
+ id: 'sprint.loading-check',
53
+ watch: ['sprint/loading', 'sprint/current'],
54
+ evaluate: (values) => {
55
+ const loading = values['sprint/loading'] as boolean;
56
+ const sprint = values['sprint/current'];
57
+ if (loading && !sprint) {
58
+ return RuleResult.emit([fact('sprint.still-loading', { since: Date.now() })]);
59
+ }
60
+ return RuleResult.retract(['sprint.still-loading']);
61
+ },
62
+ });
63
+
64
+ // ── Test Constraints ────────────────────────────────────────────────────────
65
+
66
+ const noCloseWithoutHours = defineConstraint({
67
+ id: 'no-close-without-hours',
68
+ description: 'Cannot have closed items with 0 completed hours',
69
+ watch: ['sprint/items'],
70
+ validate: (values) => {
71
+ const items = (values['sprint/items'] ?? []) as Array<{ id: number; state: string; completedWork: number }>;
72
+ const bad = items.find(i => i.state === 'Closed' && !i.completedWork);
73
+ if (bad) return `Item #${bad.id} cannot be closed with 0 completed hours`;
74
+ return true;
75
+ },
76
+ });
77
+
78
+ // ── Tests ───────────────────────────────────────────────────────────────────
79
+
80
+ describe('Unified Reactive Layer', () => {
81
+ describe('createApp', () => {
82
+ it('creates an app with schema', () => {
83
+ const app = createApp({
84
+ name: 'test',
85
+ schema: [Sprint, Loading, Items],
86
+ });
87
+ expect(app).toBeDefined();
88
+ expect(app.query).toBeTypeOf('function');
89
+ expect(app.mutate).toBeTypeOf('function');
90
+ app.destroy();
91
+ });
92
+ });
93
+
94
+ describe('query()', () => {
95
+ it('returns initial value from schema', () => {
96
+ const app = createApp({ name: 'test', schema: [Sprint, Loading] });
97
+ const sprint = app.query<SprintInfo | null>('sprint/current');
98
+ expect(sprint.current).toBeNull();
99
+ app.destroy();
100
+ });
101
+
102
+ it('is Svelte store compatible (subscribe with immediate callback)', () => {
103
+ const app = createApp({ name: 'test', schema: [Loading] });
104
+ const loading = app.query<boolean>('sprint/loading');
105
+ const values: boolean[] = [];
106
+ const unsub = loading.subscribe(v => values.push(v));
107
+ // Immediate callback on subscribe
108
+ expect(values).toEqual([false]);
109
+ unsub();
110
+ app.destroy();
111
+ });
112
+
113
+ it('updates reactively on mutate', () => {
114
+ const app = createApp({ name: 'test', schema: [Sprint, Loading] });
115
+ const sprint = app.query<SprintInfo | null>('sprint/current');
116
+ const values: Array<SprintInfo | null> = [];
117
+ const unsub = sprint.subscribe(v => values.push(v));
118
+
119
+ // Initial
120
+ expect(values.length).toBe(1);
121
+ expect(values[0]).toBeNull();
122
+
123
+ // Mutate
124
+ const data: SprintInfo = { name: 'Sprint 1', currentDay: 3, totalDays: 10, completedHours: 5, totalHours: 20 };
125
+ app.mutate('sprint/current', data);
126
+
127
+ expect(values.length).toBe(2);
128
+ expect(values[1]).toEqual(data);
129
+ expect(sprint.current).toEqual(data);
130
+
131
+ unsub();
132
+ app.destroy();
133
+ });
134
+
135
+ it('supports query options (where, sort, limit)', () => {
136
+ const app = createApp({ name: 'test', schema: [Items] });
137
+ const activeItems = app.query<Array<{ id: number; state: string; completedWork: number }>>('sprint/items', {
138
+ where: (item) => item.state === 'Active',
139
+ sort: (a, b) => a.id - b.id,
140
+ limit: 2,
141
+ });
142
+
143
+ app.mutate('sprint/items', [
144
+ { id: 3, state: 'Active', completedWork: 1 },
145
+ { id: 1, state: 'Active', completedWork: 0 },
146
+ { id: 2, state: 'Closed', completedWork: 5 },
147
+ { id: 4, state: 'Active', completedWork: 2 },
148
+ ]);
149
+
150
+ expect(activeItems.current).toEqual([
151
+ { id: 1, state: 'Active', completedWork: 0 },
152
+ { id: 3, state: 'Active', completedWork: 1 },
153
+ ]);
154
+ app.destroy();
155
+ });
156
+ });
157
+
158
+ describe('mutate()', () => {
159
+ it('returns accepted: true when no constraints violated', () => {
160
+ const app = createApp({ name: 'test', schema: [Sprint] });
161
+ const result = app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
162
+ expect(result.accepted).toBe(true);
163
+ expect(result.violations).toEqual([]);
164
+ app.destroy();
165
+ });
166
+
167
+ it('rejects mutation when constraint violated', () => {
168
+ const app = createApp({
169
+ name: 'test',
170
+ schema: [Items],
171
+ constraints: [noCloseWithoutHours],
172
+ });
173
+
174
+ const result = app.mutate('sprint/items', [
175
+ { id: 1, state: 'Closed', completedWork: 0 },
176
+ ]);
177
+
178
+ expect(result.accepted).toBe(false);
179
+ expect(result.violations.length).toBe(1);
180
+ expect(result.violations[0].message).toContain('Item #1');
181
+
182
+ // Value should NOT have been written
183
+ const items = app.query('sprint/items');
184
+ expect(items.current).toEqual([]);
185
+
186
+ app.destroy();
187
+ });
188
+
189
+ it('triggers rule evaluation', () => {
190
+ const app = createApp({
191
+ name: 'test',
192
+ schema: [Sprint],
193
+ rules: [sprintBehindRule],
194
+ });
195
+
196
+ // Behind: day 5/10 but only 2/20 hours done
197
+ app.mutate('sprint/current', {
198
+ name: 'Sprint 1',
199
+ currentDay: 5,
200
+ totalDays: 10,
201
+ completedHours: 2,
202
+ totalHours: 20,
203
+ });
204
+
205
+ const facts = app.facts();
206
+ expect(facts.some(f => f.tag === 'sprint.behind')).toBe(true);
207
+
208
+ // On pace: day 5/10, 12/20 hours done
209
+ app.mutate('sprint/current', {
210
+ name: 'Sprint 1',
211
+ currentDay: 5,
212
+ totalDays: 10,
213
+ completedHours: 12,
214
+ totalHours: 20,
215
+ });
216
+
217
+ const factsAfter = app.facts();
218
+ expect(factsAfter.some(f => f.tag === 'sprint.behind')).toBe(false);
219
+
220
+ app.destroy();
221
+ });
222
+ });
223
+
224
+ describe('batch()', () => {
225
+ it('applies multiple mutations atomically', () => {
226
+ const app = createApp({
227
+ name: 'test',
228
+ schema: [Sprint, Loading],
229
+ rules: [loadingRule],
230
+ });
231
+
232
+ const values: Array<SprintInfo | null> = [];
233
+ const sprintRef = app.query<SprintInfo | null>('sprint/current');
234
+ sprintRef.subscribe(v => values.push(v));
235
+
236
+ const result = app.batch((m) => {
237
+ m('sprint/loading', true);
238
+ m('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
239
+ });
240
+
241
+ expect(result.accepted).toBe(true);
242
+ // Sprint was set
243
+ expect(sprintRef.current).toEqual({ name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
244
+
245
+ app.destroy();
246
+ });
247
+
248
+ it('rejects entire batch on constraint violation', () => {
249
+ const app = createApp({
250
+ name: 'test',
251
+ schema: [Sprint, Items],
252
+ constraints: [noCloseWithoutHours],
253
+ });
254
+
255
+ const result = app.batch((m) => {
256
+ m('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
257
+ m('sprint/items', [{ id: 1, state: 'Closed', completedWork: 0 }]);
258
+ });
259
+
260
+ expect(result.accepted).toBe(false);
261
+ // Neither mutation should have been applied
262
+ expect(app.query('sprint/current').current).toBeNull();
263
+
264
+ app.destroy();
265
+ });
266
+ });
267
+
268
+ describe('timeline', () => {
269
+ it('records mutations', () => {
270
+ const app = createApp({ name: 'test', schema: [Loading] });
271
+ app.mutate('sprint/loading', true);
272
+ app.mutate('sprint/loading', false);
273
+
274
+ const tl = app.timeline();
275
+ const mutations = tl.filter(e => e.kind === 'mutation');
276
+ expect(mutations.length).toBe(2);
277
+ expect(mutations[0].path).toBe('sprint/loading');
278
+ expect(mutations[0].data.before).toBe(false);
279
+ expect(mutations[0].data.after).toBe(true);
280
+
281
+ app.destroy();
282
+ });
283
+
284
+ it('records rule evaluations', () => {
285
+ const app = createApp({
286
+ name: 'test',
287
+ schema: [Sprint],
288
+ rules: [sprintBehindRule],
289
+ });
290
+
291
+ app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 5, totalDays: 10, completedHours: 2, totalHours: 20 });
292
+
293
+ const tl = app.timeline();
294
+ const ruleEvals = tl.filter(e => e.kind === 'rule-eval');
295
+ expect(ruleEvals.some(e => e.data.ruleId === 'sprint.behind')).toBe(true);
296
+
297
+ app.destroy();
298
+ });
299
+ });
300
+
301
+ describe('liveness', () => {
302
+ it('detects stale paths', async () => {
303
+ const staleCb = vi.fn();
304
+
305
+ const app = createApp({
306
+ name: 'test',
307
+ schema: [Sprint, Loading],
308
+ liveness: {
309
+ expect: ['sprint/current'],
310
+ timeoutMs: 50,
311
+ onStale: staleCb,
312
+ },
313
+ });
314
+
315
+ // Don't mutate sprint/current — it should go stale
316
+ await new Promise(r => setTimeout(r, 100));
317
+
318
+ expect(staleCb).toHaveBeenCalledWith('sprint/current', expect.any(Number));
319
+
320
+ const status = app.liveness();
321
+ expect(status['sprint/current'].stale).toBe(true);
322
+
323
+ app.destroy();
324
+ });
325
+
326
+ it('does NOT flag paths that were updated', async () => {
327
+ const staleCb = vi.fn();
328
+
329
+ const app = createApp({
330
+ name: 'test',
331
+ schema: [Sprint],
332
+ liveness: {
333
+ expect: ['sprint/current'],
334
+ timeoutMs: 100,
335
+ onStale: staleCb,
336
+ },
337
+ });
338
+
339
+ // Update immediately
340
+ app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
341
+
342
+ await new Promise(r => setTimeout(r, 150));
343
+
344
+ // onStale should NOT have been called
345
+ expect(staleCb).not.toHaveBeenCalled();
346
+
347
+ app.destroy();
348
+ });
349
+ });
350
+
351
+ describe('multiple subscribers', () => {
352
+ it('notifies all subscribers independently', () => {
353
+ const app = createApp({ name: 'test', schema: [Loading] });
354
+
355
+ const values1: boolean[] = [];
356
+ const values2: boolean[] = [];
357
+
358
+ const ref1 = app.query<boolean>('sprint/loading');
359
+ const ref2 = app.query<boolean>('sprint/loading');
360
+
361
+ const unsub1 = ref1.subscribe(v => values1.push(v));
362
+ const unsub2 = ref2.subscribe(v => values2.push(v));
363
+
364
+ app.mutate('sprint/loading', true);
365
+
366
+ expect(values1).toEqual([false, true]);
367
+ expect(values2).toEqual([false, true]);
368
+
369
+ unsub1();
370
+ app.mutate('sprint/loading', false);
371
+
372
+ // Only ref2 should get the update
373
+ expect(values1).toEqual([false, true]);
374
+ expect(values2).toEqual([false, true, false]);
375
+
376
+ unsub2();
377
+ app.destroy();
378
+ });
379
+ });
380
+
381
+ describe('destroy', () => {
382
+ it('cleans up all state', () => {
383
+ const app = createApp({
384
+ name: 'test',
385
+ schema: [Sprint, Loading, Items],
386
+ rules: [sprintBehindRule],
387
+ });
388
+
389
+ app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
390
+ app.destroy();
391
+
392
+ expect(app.facts()).toEqual([]);
393
+ expect(app.timeline()).toEqual([]);
394
+ });
395
+ });
396
+ });