@plures/praxis 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
  2. package/dist/browser/chunk-6MVRT7CK.js +363 -0
  3. package/dist/browser/chunk-6SJ44Q64.js +473 -0
  4. package/dist/browser/chunk-BQOYZBWA.js +282 -0
  5. package/dist/browser/chunk-IG5BJ2MT.js +91 -0
  6. package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
  7. package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
  8. package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
  9. package/dist/browser/expectations/index.d.ts +180 -0
  10. package/dist/browser/expectations/index.js +14 -0
  11. package/dist/browser/factory/index.d.ts +150 -0
  12. package/dist/browser/factory/index.js +15 -0
  13. package/dist/browser/index.d.ts +277 -3
  14. package/dist/browser/index.js +425 -60
  15. package/dist/browser/integrations/svelte.d.ts +4 -2
  16. package/dist/browser/integrations/svelte.js +3 -2
  17. package/dist/browser/project/index.d.ts +177 -0
  18. package/dist/browser/project/index.js +19 -0
  19. package/dist/browser/reactive-engine.svelte-BwWadvAW.d.ts +224 -0
  20. package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
  21. package/dist/browser/rules-BaWMqxuG.d.ts +277 -0
  22. package/dist/browser/unified/index.d.ts +239 -0
  23. package/dist/browser/unified/index.js +20 -0
  24. package/dist/node/chunk-6MVRT7CK.js +363 -0
  25. package/dist/node/chunk-AZLNISFI.js +1690 -0
  26. package/dist/node/chunk-IG5BJ2MT.js +91 -0
  27. package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
  28. package/dist/node/{chunk-7M3HV4XR.js → chunk-WFRHXZBP.js} +3 -3
  29. package/dist/node/cli/index.cjs +48 -0
  30. package/dist/node/cli/index.js +2 -2
  31. package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
  32. package/dist/node/index.cjs +2114 -0
  33. package/dist/node/index.d.cts +964 -280
  34. package/dist/node/index.d.ts +964 -280
  35. package/dist/node/index.js +575 -10
  36. package/dist/node/integrations/svelte.d.cts +3 -2
  37. package/dist/node/integrations/svelte.d.ts +3 -2
  38. package/dist/node/integrations/svelte.js +3 -2
  39. package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
  40. package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
  41. package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
  42. package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
  43. package/dist/node/{server-SYZPDULV.js → server-FKLVY57V.js} +4 -2
  44. package/dist/node/unified/index.cjs +484 -0
  45. package/dist/node/unified/index.d.cts +240 -0
  46. package/dist/node/unified/index.d.ts +240 -0
  47. package/dist/node/unified/index.js +21 -0
  48. package/dist/node/{validate-TQGVIG7G.js → validate-BY7JNY7H.js} +2 -1
  49. package/package.json +38 -11
  50. package/src/__tests__/chronos-project.test.ts +799 -0
  51. package/src/__tests__/decision-ledger.test.ts +857 -402
  52. package/src/chronos/diff.ts +336 -0
  53. package/src/chronos/hooks.ts +227 -0
  54. package/src/chronos/index.ts +83 -0
  55. package/src/chronos/project-chronicle.ts +198 -0
  56. package/src/chronos/timeline.ts +152 -0
  57. package/src/decision-ledger/analyzer-types.ts +280 -0
  58. package/src/decision-ledger/analyzer.ts +518 -0
  59. package/src/decision-ledger/contract-verification.ts +456 -0
  60. package/src/decision-ledger/derivation.ts +158 -0
  61. package/src/decision-ledger/index.ts +59 -0
  62. package/src/decision-ledger/report.ts +378 -0
  63. package/src/decision-ledger/suggestions.ts +287 -0
  64. package/src/index.browser.ts +103 -0
  65. package/src/index.ts +98 -0
  66. package/src/unified/__tests__/unified.test.ts +396 -0
  67. package/src/unified/core.ts +517 -0
  68. package/src/unified/index.ts +32 -0
  69. package/src/unified/rules.ts +66 -0
  70. package/src/unified/types.ts +148 -0
  71. package/dist/browser/reactive-engine.svelte-DjynI82A.d.ts +0 -688
  72. package/dist/node/chunk-FWOXU4MM.js +0 -487
@@ -0,0 +1,799 @@
1
+ /**
2
+ * Chronos Project-Level Chronicle — Tests
3
+ *
4
+ * Comprehensive tests for:
5
+ * - ProjectChronicle event recording
6
+ * - Timeline queries (filter, range, history)
7
+ * - Behavioral diff (add rule, remove rule, modify contract)
8
+ * - Commit message generation from diffs
9
+ * - Hooks auto-recording
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach } from 'vitest';
13
+ import {
14
+ ProjectChronicle,
15
+ createProjectChronicle,
16
+ } from '../chronos/project-chronicle.js';
17
+ import {
18
+ Timeline,
19
+ createTimeline,
20
+ } from '../chronos/timeline.js';
21
+ import {
22
+ enableProjectChronicle,
23
+ recordAudit,
24
+ } from '../chronos/hooks.js';
25
+ import {
26
+ diffRegistries,
27
+ diffContracts,
28
+ diffExpectations,
29
+ formatDelta,
30
+ formatCommitMessage,
31
+ formatReleaseNotes,
32
+ } from '../chronos/diff.js';
33
+ import type { RegistrySnapshot, RegistryDiff } from '../chronos/diff.js';
34
+ import { PraxisRegistry } from '../core/rules.js';
35
+ import { LogicEngine, createPraxisEngine } from '../core/engine.js';
36
+ import { RuleResult, fact } from '../core/rule-result.js';
37
+ import type { CompletenessReport } from '../core/completeness.js';
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ // § 1 — ProjectChronicle: event recording
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+
43
+ describe('ProjectChronicle', () => {
44
+ let chronicle: ProjectChronicle;
45
+ let ts: number;
46
+
47
+ beforeEach(() => {
48
+ ts = 1000;
49
+ chronicle = createProjectChronicle({ now: () => ts++ });
50
+ });
51
+
52
+ it('records rule registered events', () => {
53
+ chronicle.recordRuleRegistered('auth/login', { description: 'Login rule' });
54
+ expect(chronicle.size).toBe(1);
55
+ const events = chronicle.getEvents();
56
+ expect(events[0]).toMatchObject({
57
+ kind: 'rule',
58
+ action: 'registered',
59
+ subject: 'auth/login',
60
+ timestamp: 1000,
61
+ metadata: { description: 'Login rule' },
62
+ });
63
+ });
64
+
65
+ it('records rule modified with diff', () => {
66
+ chronicle.recordRuleModified(
67
+ 'auth/login',
68
+ { before: { description: 'old' }, after: { description: 'new' } },
69
+ );
70
+ const events = chronicle.getEvents();
71
+ expect(events[0].diff).toEqual({
72
+ before: { description: 'old' },
73
+ after: { description: 'new' },
74
+ });
75
+ });
76
+
77
+ it('records rule removed events', () => {
78
+ chronicle.recordRuleRemoved('auth/login');
79
+ expect(chronicle.getEvents()[0]).toMatchObject({
80
+ kind: 'rule',
81
+ action: 'removed',
82
+ subject: 'auth/login',
83
+ });
84
+ });
85
+
86
+ it('records contract events', () => {
87
+ chronicle.recordContractAdded('auth/login', { behavior: 'Process login' });
88
+ chronicle.recordContractModified(
89
+ 'auth/login',
90
+ { before: 'old behavior', after: 'new behavior' },
91
+ );
92
+ expect(chronicle.size).toBe(2);
93
+ expect(chronicle.getEvents()[0].kind).toBe('contract');
94
+ expect(chronicle.getEvents()[0].action).toBe('added');
95
+ expect(chronicle.getEvents()[1].action).toBe('modified');
96
+ });
97
+
98
+ it('records expectation events', () => {
99
+ chronicle.recordExpectationSatisfied('all-tests-pass');
100
+ chronicle.recordExpectationViolated('no-type-errors');
101
+ expect(chronicle.size).toBe(2);
102
+ expect(chronicle.getEvents()[0]).toMatchObject({
103
+ kind: 'expectation',
104
+ action: 'satisfied',
105
+ subject: 'all-tests-pass',
106
+ });
107
+ expect(chronicle.getEvents()[1]).toMatchObject({
108
+ kind: 'expectation',
109
+ action: 'violated',
110
+ subject: 'no-type-errors',
111
+ });
112
+ });
113
+
114
+ it('records gate transitions with diff', () => {
115
+ chronicle.recordGateTransition('deploy', 'blocked', 'open');
116
+ const event = chronicle.getEvents()[0];
117
+ expect(event).toMatchObject({
118
+ kind: 'gate',
119
+ action: 'open',
120
+ subject: 'deploy',
121
+ });
122
+ expect(event.diff).toEqual({ before: 'blocked', after: 'open' });
123
+ expect(event.metadata).toMatchObject({ from: 'blocked', to: 'open' });
124
+ });
125
+
126
+ it('records build audit events', () => {
127
+ chronicle.recordBuildAudit(85, 5, { rating: 'good' });
128
+ const event = chronicle.getEvents()[0];
129
+ expect(event).toMatchObject({
130
+ kind: 'build',
131
+ action: 'audit-complete',
132
+ subject: 'completeness',
133
+ });
134
+ expect(event.metadata).toMatchObject({ score: 85, delta: 5, rating: 'good' });
135
+ });
136
+
137
+ it('records fact lifecycle events', () => {
138
+ chronicle.recordFactIntroduced('UserLoggedIn');
139
+ chronicle.recordFactDeprecated('OldFact');
140
+ expect(chronicle.size).toBe(2);
141
+ expect(chronicle.getEvents()[0]).toMatchObject({
142
+ kind: 'fact',
143
+ action: 'introduced',
144
+ subject: 'UserLoggedIn',
145
+ });
146
+ expect(chronicle.getEvents()[1]).toMatchObject({
147
+ kind: 'fact',
148
+ action: 'deprecated',
149
+ subject: 'OldFact',
150
+ });
151
+ });
152
+
153
+ it('enforces maxEvents cap', () => {
154
+ const small = createProjectChronicle({ maxEvents: 3, now: () => ts++ });
155
+ for (let i = 0; i < 5; i++) {
156
+ small.recordRuleRegistered(`rule-${i}`);
157
+ }
158
+ expect(small.size).toBe(3);
159
+ // Should have the last 3
160
+ const subjects = small.getEvents().map(e => e.subject);
161
+ expect(subjects).toEqual(['rule-2', 'rule-3', 'rule-4']);
162
+ });
163
+
164
+ it('clear() empties all events', () => {
165
+ chronicle.recordRuleRegistered('a');
166
+ chronicle.recordRuleRegistered('b');
167
+ expect(chronicle.size).toBe(2);
168
+ chronicle.clear();
169
+ expect(chronicle.size).toBe(0);
170
+ });
171
+
172
+ it('getEvents() returns a shallow copy', () => {
173
+ chronicle.recordRuleRegistered('a');
174
+ const events1 = chronicle.getEvents();
175
+ const events2 = chronicle.getEvents();
176
+ expect(events1).not.toBe(events2);
177
+ expect(events1).toEqual(events2);
178
+ });
179
+ });
180
+
181
+ // ═══════════════════════════════════════════════════════════════════════════
182
+ // § 2 — Timeline: queries, filtering, range, history
183
+ // ═══════════════════════════════════════════════════════════════════════════
184
+
185
+ describe('Timeline', () => {
186
+ let chronicle: ProjectChronicle;
187
+ let timeline: Timeline;
188
+ let ts: number;
189
+
190
+ beforeEach(() => {
191
+ ts = 1000;
192
+ chronicle = createProjectChronicle({ now: () => ts++ });
193
+ timeline = createTimeline(chronicle);
194
+
195
+ // Seed events
196
+ chronicle.recordRuleRegistered('auth/login'); // ts=1000
197
+ chronicle.recordContractAdded('auth/login'); // ts=1001
198
+ chronicle.recordRuleRegistered('auth/logout'); // ts=1002
199
+ chronicle.recordGateTransition('deploy', 'closed', 'open'); // ts=1003
200
+ chronicle.recordExpectationSatisfied('tests-pass'); // ts=1004
201
+ chronicle.recordBuildAudit(90, 10); // ts=1005
202
+ chronicle.recordRuleRemoved('auth/logout'); // ts=1006
203
+ });
204
+
205
+ it('getTimeline() returns all events without filter', () => {
206
+ expect(timeline.getTimeline()).toHaveLength(7);
207
+ });
208
+
209
+ it('filters by kind', () => {
210
+ const rules = timeline.getTimeline({ kind: 'rule' });
211
+ expect(rules).toHaveLength(3); // 2 registered + 1 removed
212
+ expect(rules.every(e => e.kind === 'rule')).toBe(true);
213
+ });
214
+
215
+ it('filters by multiple kinds', () => {
216
+ const events = timeline.getTimeline({ kind: ['rule', 'contract'] });
217
+ expect(events).toHaveLength(4); // 3 rule + 1 contract
218
+ });
219
+
220
+ it('filters by action', () => {
221
+ const registered = timeline.getTimeline({ action: 'registered' });
222
+ expect(registered).toHaveLength(2);
223
+ });
224
+
225
+ it('filters by subject', () => {
226
+ const history = timeline.getTimeline({ subject: 'auth/login' });
227
+ expect(history).toHaveLength(2); // registered + contract added
228
+ });
229
+
230
+ it('filters by time range', () => {
231
+ const events = timeline.getTimeline({ since: 1002, until: 1004 });
232
+ expect(events).toHaveLength(3); // ts 1002, 1003, 1004
233
+ });
234
+
235
+ it('combines multiple filters (AND)', () => {
236
+ const events = timeline.getTimeline({ kind: 'rule', action: 'registered' });
237
+ expect(events).toHaveLength(2);
238
+ expect(events.every(e => e.kind === 'rule' && e.action === 'registered')).toBe(true);
239
+ });
240
+
241
+ it('getEventsSince() returns events from timestamp', () => {
242
+ const events = timeline.getEventsSince(1005);
243
+ expect(events).toHaveLength(2); // ts 1005, 1006
244
+ });
245
+
246
+ it('getHistory() returns all events for a subject', () => {
247
+ const history = timeline.getHistory('auth/login');
248
+ expect(history).toHaveLength(2);
249
+ expect(history[0].action).toBe('registered');
250
+ expect(history[1].action).toBe('added');
251
+ });
252
+
253
+ describe('getDelta()', () => {
254
+ it('computes behavioral delta for a time range', () => {
255
+ const delta = timeline.getDelta(1000, 1006);
256
+ expect(delta.from).toBe(1000);
257
+ expect(delta.to).toBe(1006);
258
+ expect(delta.events).toHaveLength(7);
259
+ });
260
+
261
+ it('identifies added subjects', () => {
262
+ // auth/login registered, auth/logout registered then removed
263
+ const delta = timeline.getDelta(1000, 1006);
264
+ // auth/login is added (registered at 1000)
265
+ expect(delta.added).toContain('auth/login');
266
+ });
267
+
268
+ it('identifies removed subjects', () => {
269
+ const delta = timeline.getDelta(1000, 1006);
270
+ // auth/logout was registered then removed — net effect is removed
271
+ expect(delta.removed).toContain('auth/logout');
272
+ });
273
+
274
+ it('provides summary counts by kind', () => {
275
+ const delta = timeline.getDelta(1000, 1006);
276
+ expect(delta.summary.rule).toBe(3);
277
+ expect(delta.summary.contract).toBe(1);
278
+ expect(delta.summary.gate).toBe(1);
279
+ });
280
+ });
281
+ });
282
+
283
+ // ═══════════════════════════════════════════════════════════════════════════
284
+ // § 3 — Behavioral Diff
285
+ // ═══════════════════════════════════════════════════════════════════════════
286
+
287
+ describe('Behavioral Diff', () => {
288
+ describe('diffRegistries', () => {
289
+ it('detects added rules', () => {
290
+ const before: RegistrySnapshot = {
291
+ rules: [{ id: 'a', description: 'Rule A', impl: () => RuleResult.noop() }],
292
+ constraints: [],
293
+ };
294
+ const after: RegistrySnapshot = {
295
+ rules: [
296
+ { id: 'a', description: 'Rule A', impl: () => RuleResult.noop() },
297
+ { id: 'b', description: 'Rule B', impl: () => RuleResult.noop() },
298
+ ],
299
+ constraints: [],
300
+ };
301
+ const diff = diffRegistries(before, after);
302
+ expect(diff.rulesAdded).toEqual(['b']);
303
+ expect(diff.rulesRemoved).toEqual([]);
304
+ });
305
+
306
+ it('detects removed rules', () => {
307
+ const before: RegistrySnapshot = {
308
+ rules: [
309
+ { id: 'a', description: 'Rule A', impl: () => RuleResult.noop() },
310
+ { id: 'b', description: 'Rule B', impl: () => RuleResult.noop() },
311
+ ],
312
+ constraints: [],
313
+ };
314
+ const after: RegistrySnapshot = {
315
+ rules: [{ id: 'a', description: 'Rule A', impl: () => RuleResult.noop() }],
316
+ constraints: [],
317
+ };
318
+ const diff = diffRegistries(before, after);
319
+ expect(diff.rulesRemoved).toEqual(['b']);
320
+ expect(diff.rulesAdded).toEqual([]);
321
+ });
322
+
323
+ it('detects modified rules (description changed)', () => {
324
+ const before: RegistrySnapshot = {
325
+ rules: [{ id: 'a', description: 'Rule A v1', impl: () => RuleResult.noop() }],
326
+ constraints: [],
327
+ };
328
+ const after: RegistrySnapshot = {
329
+ rules: [{ id: 'a', description: 'Rule A v2', impl: () => RuleResult.noop() }],
330
+ constraints: [],
331
+ };
332
+ const diff = diffRegistries(before, after);
333
+ expect(diff.rulesModified).toEqual(['a']);
334
+ });
335
+
336
+ it('handles Map inputs', () => {
337
+ const before: RegistrySnapshot = {
338
+ rules: new Map([['a', { id: 'a', description: 'A', impl: () => RuleResult.noop() }]]),
339
+ constraints: new Map(),
340
+ };
341
+ const after: RegistrySnapshot = {
342
+ rules: new Map([
343
+ ['a', { id: 'a', description: 'A', impl: () => RuleResult.noop() }],
344
+ ['b', { id: 'b', description: 'B', impl: () => RuleResult.noop() }],
345
+ ]),
346
+ constraints: new Map(),
347
+ };
348
+ const diff = diffRegistries(before, after);
349
+ expect(diff.rulesAdded).toEqual(['b']);
350
+ });
351
+
352
+ it('detects constraint changes', () => {
353
+ const before: RegistrySnapshot = {
354
+ rules: [],
355
+ constraints: [{ id: 'c1', description: 'Constraint 1', impl: () => true }],
356
+ };
357
+ const after: RegistrySnapshot = {
358
+ rules: [],
359
+ constraints: [{ id: 'c2', description: 'Constraint 2', impl: () => true }],
360
+ };
361
+ const diff = diffRegistries(before, after);
362
+ expect(diff.constraintsAdded).toEqual(['c2']);
363
+ expect(diff.constraintsRemoved).toEqual(['c1']);
364
+ });
365
+ });
366
+
367
+ describe('diffContracts', () => {
368
+ it('detects newly added contracts', () => {
369
+ const before = { coverage: { 'auth/login': false, 'auth/logout': true } };
370
+ const after = { coverage: { 'auth/login': true, 'auth/logout': true } };
371
+ const diff = diffContracts(before, after);
372
+ expect(diff.contractsAdded).toEqual(['auth/login']);
373
+ expect(diff.contractsRemoved).toEqual([]);
374
+ });
375
+
376
+ it('detects removed contracts', () => {
377
+ const before = { coverage: new Map([['a', true]]) };
378
+ const after = { coverage: new Map([['a', false]]) };
379
+ const diff = diffContracts(before, after);
380
+ expect(diff.contractsRemoved).toEqual(['a']);
381
+ });
382
+
383
+ it('computes coverage ratios', () => {
384
+ const before = { coverage: { a: true, b: false } };
385
+ const after = { coverage: { a: true, b: true } };
386
+ const diff = diffContracts(before, after);
387
+ expect(diff.coverageBefore).toBe(0.5);
388
+ expect(diff.coverageAfter).toBe(1);
389
+ });
390
+ });
391
+
392
+ describe('diffExpectations', () => {
393
+ it('detects newly satisfied expectations', () => {
394
+ const before = { expectations: { 'tests-pass': false, 'lint-clean': true } };
395
+ const after = { expectations: { 'tests-pass': true, 'lint-clean': true } };
396
+ const diff = diffExpectations(before, after);
397
+ expect(diff.newlySatisfied).toEqual(['tests-pass']);
398
+ expect(diff.newlyViolated).toEqual([]);
399
+ expect(diff.unchanged).toContain('lint-clean');
400
+ });
401
+
402
+ it('detects newly violated expectations', () => {
403
+ const before = { expectations: new Map([['a', true]]) };
404
+ const after = { expectations: new Map([['a', false]]) };
405
+ const diff = diffExpectations(before, after);
406
+ expect(diff.newlyViolated).toEqual(['a']);
407
+ });
408
+ });
409
+ });
410
+
411
+ // ═══════════════════════════════════════════════════════════════════════════
412
+ // § 4 — Commit message generation
413
+ // ═══════════════════════════════════════════════════════════════════════════
414
+
415
+ describe('Commit Message Generation', () => {
416
+ it('generates feat commit for added rules', () => {
417
+ const diff: RegistryDiff = {
418
+ rulesAdded: ['auth/login'],
419
+ rulesRemoved: [],
420
+ rulesModified: [],
421
+ constraintsAdded: [],
422
+ constraintsRemoved: [],
423
+ constraintsModified: [],
424
+ };
425
+ const msg = formatCommitMessage(diff);
426
+ expect(msg).toContain('feat(auth)');
427
+ expect(msg).toContain('auth/login');
428
+ });
429
+
430
+ it('generates refactor commit for removed rules', () => {
431
+ const diff: RegistryDiff = {
432
+ rulesAdded: [],
433
+ rulesRemoved: ['auth/legacy'],
434
+ rulesModified: [],
435
+ constraintsAdded: [],
436
+ constraintsRemoved: [],
437
+ constraintsModified: [],
438
+ };
439
+ const msg = formatCommitMessage(diff);
440
+ expect(msg).toContain('refactor(auth)');
441
+ expect(msg).toContain('remove');
442
+ });
443
+
444
+ it('generates refactor commit for modified rules', () => {
445
+ const diff: RegistryDiff = {
446
+ rulesAdded: [],
447
+ rulesRemoved: [],
448
+ rulesModified: ['auth/login'],
449
+ constraintsAdded: [],
450
+ constraintsRemoved: [],
451
+ constraintsModified: [],
452
+ };
453
+ const msg = formatCommitMessage(diff);
454
+ expect(msg).toContain('refactor(auth)');
455
+ expect(msg).toContain('update');
456
+ });
457
+
458
+ it('generates chore commit for no changes', () => {
459
+ const diff: RegistryDiff = {
460
+ rulesAdded: [],
461
+ rulesRemoved: [],
462
+ rulesModified: [],
463
+ constraintsAdded: [],
464
+ constraintsRemoved: [],
465
+ constraintsModified: [],
466
+ };
467
+ const msg = formatCommitMessage(diff);
468
+ expect(msg).toContain('chore');
469
+ });
470
+
471
+ it('includes body with details', () => {
472
+ const diff: RegistryDiff = {
473
+ rulesAdded: ['auth/login', 'auth/logout'],
474
+ rulesRemoved: [],
475
+ rulesModified: [],
476
+ constraintsAdded: [],
477
+ constraintsRemoved: [],
478
+ constraintsModified: [],
479
+ };
480
+ const msg = formatCommitMessage(diff);
481
+ expect(msg).toContain('Rules added: auth/login, auth/logout');
482
+ });
483
+ });
484
+
485
+ describe('formatDelta', () => {
486
+ it('formats a human-readable delta', () => {
487
+ const diff: RegistryDiff = {
488
+ rulesAdded: ['auth/login'],
489
+ rulesRemoved: ['auth/legacy'],
490
+ rulesModified: [],
491
+ constraintsAdded: ['max-retries'],
492
+ constraintsRemoved: [],
493
+ constraintsModified: [],
494
+ };
495
+ const text = formatDelta(diff);
496
+ expect(text).toContain('+ Rules added: auth/login');
497
+ expect(text).toContain('- Rules removed: auth/legacy');
498
+ expect(text).toContain('+ Constraints added: max-retries');
499
+ });
500
+
501
+ it('returns "No behavioral changes." for empty diff', () => {
502
+ const diff: RegistryDiff = {
503
+ rulesAdded: [],
504
+ rulesRemoved: [],
505
+ rulesModified: [],
506
+ constraintsAdded: [],
507
+ constraintsRemoved: [],
508
+ constraintsModified: [],
509
+ };
510
+ expect(formatDelta(diff)).toBe('No behavioral changes.');
511
+ });
512
+ });
513
+
514
+ describe('formatReleaseNotes', () => {
515
+ it('aggregates multiple diffs into release notes', () => {
516
+ const diffs: RegistryDiff[] = [
517
+ {
518
+ rulesAdded: ['auth/login'],
519
+ rulesRemoved: [],
520
+ rulesModified: [],
521
+ constraintsAdded: [],
522
+ constraintsRemoved: [],
523
+ constraintsModified: [],
524
+ },
525
+ {
526
+ rulesAdded: ['data/sync'],
527
+ rulesRemoved: ['auth/legacy'],
528
+ rulesModified: ['auth/login'],
529
+ constraintsAdded: [],
530
+ constraintsRemoved: [],
531
+ constraintsModified: [],
532
+ },
533
+ ];
534
+ const notes = formatReleaseNotes(diffs);
535
+ expect(notes).toContain('## Release Notes');
536
+ expect(notes).toContain('### Added');
537
+ expect(notes).toContain('- auth/login');
538
+ expect(notes).toContain('- data/sync');
539
+ expect(notes).toContain('### Changed');
540
+ expect(notes).toContain('### Removed');
541
+ expect(notes).toContain('- auth/legacy');
542
+ });
543
+
544
+ it('returns message for empty diffs', () => {
545
+ const notes = formatReleaseNotes([]);
546
+ expect(notes).toBe('No behavioral changes in this release.');
547
+ });
548
+ });
549
+
550
+ // ═══════════════════════════════════════════════════════════════════════════
551
+ // § 5 — Hooks: auto-recording
552
+ // ═══════════════════════════════════════════════════════════════════════════
553
+
554
+ describe('Hooks (enableProjectChronicle)', () => {
555
+ let registry: PraxisRegistry;
556
+ let engine: LogicEngine;
557
+
558
+ beforeEach(() => {
559
+ registry = new PraxisRegistry({ compliance: { enabled: false } });
560
+ engine = createPraxisEngine({ initialContext: {}, registry });
561
+ });
562
+
563
+ it('auto-records rule registration', () => {
564
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
565
+
566
+ registry.registerRule({
567
+ id: 'test/rule',
568
+ description: 'Test rule',
569
+ impl: () => RuleResult.noop(),
570
+ });
571
+
572
+ expect(chronicle.size).toBeGreaterThan(0);
573
+ const events = chronicle.getEvents();
574
+ const ruleEvent = events.find(e => e.kind === 'rule' && e.action === 'registered');
575
+ expect(ruleEvent).toBeDefined();
576
+ expect(ruleEvent!.subject).toBe('test/rule');
577
+
578
+ disconnect();
579
+ });
580
+
581
+ it('auto-records rule with contract', () => {
582
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
583
+
584
+ registry.registerRule({
585
+ id: 'test/contracted',
586
+ description: 'Contracted rule',
587
+ contract: {
588
+ ruleId: 'test/contracted',
589
+ behavior: 'Does stuff',
590
+ examples: [{ given: 'x', when: 'y', then: 'z' }],
591
+ invariants: ['always works'],
592
+ },
593
+ impl: () => RuleResult.noop(),
594
+ });
595
+
596
+ const events = chronicle.getEvents();
597
+ const contractEvent = events.find(e => e.kind === 'contract' && e.action === 'added');
598
+ expect(contractEvent).toBeDefined();
599
+ expect(contractEvent!.subject).toBe('test/contracted');
600
+ expect(contractEvent!.metadata).toMatchObject({
601
+ behavior: 'Does stuff',
602
+ examplesCount: 1,
603
+ invariantsCount: 1,
604
+ });
605
+
606
+ disconnect();
607
+ });
608
+
609
+ it('auto-records engine step results', () => {
610
+ // Register a rule first (before hooking, to avoid double-recording confusion)
611
+ registry.registerRule({
612
+ id: 'test/emit',
613
+ description: 'Emit a fact',
614
+ impl: () => RuleResult.emit([fact('test.fact', { value: 42 })]),
615
+ });
616
+
617
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
618
+
619
+ engine.step([{ tag: 'test.event', payload: {} }]);
620
+
621
+ const events = chronicle.getEvents();
622
+ const stepEvent = events.find(e => e.kind === 'build' && e.action === 'step-complete');
623
+ expect(stepEvent).toBeDefined();
624
+ expect(stepEvent!.metadata).toMatchObject({
625
+ eventsProcessed: 1,
626
+ eventTags: ['test.event'],
627
+ });
628
+
629
+ disconnect();
630
+ });
631
+
632
+ it('auto-records constraint violations from step', () => {
633
+ registry.registerRule({
634
+ id: 'test/rule',
635
+ description: 'noop',
636
+ impl: () => RuleResult.noop(),
637
+ });
638
+ registry.registerConstraint({
639
+ id: 'test/constraint',
640
+ description: 'Always fails',
641
+ impl: () => 'This always fails',
642
+ });
643
+
644
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
645
+
646
+ engine.step([{ tag: 'x', payload: {} }]);
647
+
648
+ const events = chronicle.getEvents();
649
+ const violationEvent = events.find(
650
+ e => e.kind === 'expectation' && e.action === 'violated',
651
+ );
652
+ expect(violationEvent).toBeDefined();
653
+ expect(violationEvent!.subject).toBe('test/constraint');
654
+
655
+ disconnect();
656
+ });
657
+
658
+ it('auto-records checkConstraints results', () => {
659
+ registry.registerConstraint({
660
+ id: 'test/always-ok',
661
+ description: 'Always passes',
662
+ impl: () => true,
663
+ });
664
+
665
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
666
+
667
+ engine.checkConstraints();
668
+
669
+ const events = chronicle.getEvents();
670
+ const checkEvent = events.find(e => e.action === 'constraints-checked');
671
+ expect(checkEvent).toBeDefined();
672
+ expect(checkEvent!.metadata).toMatchObject({ violations: 0 });
673
+
674
+ disconnect();
675
+ });
676
+
677
+ it('disconnect() restores original methods', () => {
678
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
679
+
680
+ registry.registerRule({
681
+ id: 'pre-disconnect',
682
+ description: 'before disconnect',
683
+ impl: () => RuleResult.noop(),
684
+ });
685
+
686
+ const countBefore = chronicle.size;
687
+ expect(countBefore).toBeGreaterThan(0);
688
+
689
+ disconnect();
690
+
691
+ // After disconnect, new registrations should NOT be recorded
692
+ registry.registerRule({
693
+ id: 'post-disconnect',
694
+ description: 'after disconnect',
695
+ impl: () => RuleResult.noop(),
696
+ });
697
+
698
+ expect(chronicle.size).toBe(countBefore);
699
+ });
700
+
701
+ it('respects recordSteps: false', () => {
702
+ const { chronicle, disconnect } = enableProjectChronicle(registry, engine, {
703
+ recordSteps: false,
704
+ });
705
+
706
+ registry.registerRule({
707
+ id: 'r1',
708
+ description: 'r1',
709
+ impl: () => RuleResult.noop(),
710
+ });
711
+
712
+ engine.step([{ tag: 'x', payload: {} }]);
713
+
714
+ const stepEvents = chronicle.getEvents().filter(e => e.action === 'step-complete');
715
+ expect(stepEvents).toHaveLength(0);
716
+
717
+ // But rule registration should still be recorded
718
+ const ruleEvents = chronicle.getEvents().filter(e => e.kind === 'rule');
719
+ expect(ruleEvents.length).toBeGreaterThan(0);
720
+
721
+ disconnect();
722
+ });
723
+ });
724
+
725
+ describe('recordAudit', () => {
726
+ it('records a completeness audit', () => {
727
+ const chronicle = createProjectChronicle();
728
+ const report: CompletenessReport = {
729
+ score: 85,
730
+ rating: 'good',
731
+ rules: { total: 10, covered: 8, uncovered: [] },
732
+ constraints: { total: 5, covered: 5, uncovered: [] },
733
+ contracts: { total: 8, withContracts: 6, missing: ['a', 'b'] },
734
+ context: { total: 4, covered: 4, missing: [] },
735
+ events: { total: 6, covered: 5, missing: [] },
736
+ };
737
+
738
+ recordAudit(chronicle, report, 80);
739
+
740
+ expect(chronicle.size).toBe(1);
741
+ const event = chronicle.getEvents()[0];
742
+ expect(event).toMatchObject({
743
+ kind: 'build',
744
+ action: 'audit-complete',
745
+ subject: 'completeness',
746
+ });
747
+ expect(event.metadata).toMatchObject({
748
+ score: 85,
749
+ delta: 5,
750
+ rating: 'good',
751
+ });
752
+ });
753
+ });
754
+
755
+ // ═══════════════════════════════════════════════════════════════════════════
756
+ // § 6 — Integration: end-to-end scenario
757
+ // ═══════════════════════════════════════════════════════════════════════════
758
+
759
+ describe('End-to-end: chronicle + timeline + diff', () => {
760
+ it('chronicles rule lifecycle and queries it', () => {
761
+ let ts = 1000;
762
+ const chronicle = createProjectChronicle({ now: () => ts++ });
763
+ const timeline = createTimeline(chronicle);
764
+
765
+ // Simulate development lifecycle
766
+ chronicle.recordRuleRegistered('auth/login');
767
+ chronicle.recordContractAdded('auth/login');
768
+ chronicle.recordRuleRegistered('auth/logout');
769
+ chronicle.recordRuleModified('auth/login', {
770
+ before: { description: 'v1' },
771
+ after: { description: 'v2' },
772
+ });
773
+ chronicle.recordBuildAudit(75, 0);
774
+ chronicle.recordGateTransition('release', 'closed', 'blocked');
775
+ chronicle.recordExpectationSatisfied('tests-pass');
776
+ chronicle.recordGateTransition('release', 'blocked', 'open');
777
+ chronicle.recordBuildAudit(90, 15);
778
+
779
+ // Query timeline
780
+ const ruleHistory = timeline.getHistory('auth/login');
781
+ expect(ruleHistory).toHaveLength(3); // registered, contract added, modified
782
+
783
+ // Get delta
784
+ const delta = timeline.getDelta(1000, 1010);
785
+ expect(delta.added).toContain('auth/login');
786
+ expect(delta.added).toContain('auth/logout');
787
+ expect(delta.modified).toContain('auth/login');
788
+
789
+ // Check build events
790
+ const builds = timeline.getTimeline({ kind: 'build' });
791
+ expect(builds).toHaveLength(2);
792
+
793
+ // Check gate transitions
794
+ const gates = timeline.getTimeline({ kind: 'gate' });
795
+ expect(gates).toHaveLength(2);
796
+ expect(gates[0].action).toBe('blocked');
797
+ expect(gates[1].action).toBe('open');
798
+ });
799
+ });