@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.
- package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
- package/dist/browser/chunk-6SJ44Q64.js +473 -0
- package/dist/browser/chunk-BQOYZBWA.js +282 -0
- package/dist/browser/chunk-IG5BJ2MT.js +91 -0
- package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
- package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
- package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
- package/dist/browser/expectations/index.d.ts +180 -0
- package/dist/browser/expectations/index.js +14 -0
- package/dist/browser/factory/index.d.ts +149 -0
- package/dist/browser/factory/index.js +15 -0
- package/dist/browser/index.d.ts +274 -3
- package/dist/browser/index.js +407 -54
- package/dist/browser/integrations/svelte.d.ts +3 -2
- package/dist/browser/integrations/svelte.js +3 -2
- package/dist/browser/project/index.d.ts +176 -0
- package/dist/browser/project/index.js +19 -0
- package/dist/browser/reactive-engine.svelte-DgVTqHLc.d.ts +223 -0
- package/dist/browser/{reactive-engine.svelte-DjynI82A.d.ts → rules-i1LHpnGd.d.ts} +13 -221
- package/dist/node/chunk-2IUFZBH3.js +87 -0
- package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
- package/dist/node/chunk-AZLNISFI.js +1690 -0
- package/dist/node/chunk-IG5BJ2MT.js +91 -0
- package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
- package/dist/node/chunk-PGVSB6NR.js +59 -0
- package/dist/node/{chunk-5JQJZADT.js → chunk-ZO2LU4G4.js} +4 -4
- package/dist/node/cli/index.cjs +1126 -211
- package/dist/node/cli/index.js +21 -2
- package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
- package/dist/node/index.cjs +5623 -2765
- package/dist/node/index.d.cts +1181 -1
- package/dist/node/index.d.ts +1181 -1
- package/dist/node/index.js +1646 -79
- package/dist/node/integrations/svelte.js +4 -3
- package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
- package/dist/node/rules-4DAJ4Z4N.js +7 -0
- package/dist/node/server-FKLVY57V.js +363 -0
- package/dist/node/{validate-EN3M4FUR.js → validate-5PSWJTIC.js} +5 -3
- package/package.json +50 -3
- package/src/__tests__/chronos-project.test.ts +799 -0
- package/src/__tests__/decision-ledger.test.ts +857 -402
- package/src/__tests__/expectations.test.ts +364 -0
- package/src/__tests__/factory.test.ts +426 -0
- package/src/__tests__/mcp-server.test.ts +310 -0
- package/src/__tests__/project.test.ts +396 -0
- package/src/chronos/diff.ts +336 -0
- package/src/chronos/hooks.ts +227 -0
- package/src/chronos/index.ts +83 -0
- package/src/chronos/project-chronicle.ts +198 -0
- package/src/chronos/timeline.ts +152 -0
- package/src/cli/index.ts +28 -0
- package/src/decision-ledger/analyzer-types.ts +280 -0
- package/src/decision-ledger/analyzer.ts +518 -0
- package/src/decision-ledger/contract-verification.ts +456 -0
- package/src/decision-ledger/derivation.ts +158 -0
- package/src/decision-ledger/index.ts +59 -0
- package/src/decision-ledger/report.ts +378 -0
- package/src/decision-ledger/suggestions.ts +287 -0
- package/src/expectations/expectations.ts +471 -0
- package/src/expectations/index.ts +29 -0
- package/src/expectations/types.ts +95 -0
- package/src/factory/factory.ts +634 -0
- package/src/factory/index.ts +27 -0
- package/src/factory/types.ts +64 -0
- package/src/index.browser.ts +83 -0
- package/src/index.ts +134 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +485 -0
- package/src/mcp/types.ts +161 -0
- package/src/project/index.ts +31 -0
- package/src/project/project.ts +423 -0
- package/src/project/types.ts +87 -0
- package/dist/node/chunk-PTH6MD6P.js +0 -487
- /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
|
@@ -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
|
+
});
|