@plures/praxis 1.2.0 → 1.2.11

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 (63) hide show
  1. package/README.md +93 -96
  2. package/dist/browser/{adapter-TM4IS5KT.js → adapter-CIMBGDC7.js} +5 -3
  3. package/dist/browser/{chunk-LE2ZJYFC.js → chunk-K377RW4V.js} +76 -0
  4. package/dist/{node/chunk-JQ64KMLN.js → browser/chunk-MBVHLOU2.js} +12 -1
  5. package/dist/browser/index.d.ts +32 -5
  6. package/dist/browser/index.js +15 -7
  7. package/dist/browser/integrations/svelte.d.ts +2 -2
  8. package/dist/browser/integrations/svelte.js +1 -1
  9. package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
  10. package/dist/node/{adapter-K6DOX6XS.js → adapter-75ISSMWD.js} +5 -3
  11. package/dist/node/chunk-5RH7UAQC.js +486 -0
  12. package/dist/{browser/chunk-JQ64KMLN.js → node/chunk-MBVHLOU2.js} +12 -1
  13. package/dist/node/{chunk-LE2ZJYFC.js → chunk-PRPQO6R5.js} +3 -72
  14. package/dist/node/chunk-R2PSBPKQ.js +150 -0
  15. package/dist/node/chunk-WZ6B3LZ6.js +638 -0
  16. package/dist/node/cli/index.cjs +2316 -832
  17. package/dist/node/cli/index.js +18 -0
  18. package/dist/node/components/index.d.cts +3 -2
  19. package/dist/node/components/index.d.ts +3 -2
  20. package/dist/node/index.cjs +620 -38
  21. package/dist/node/index.d.cts +259 -5
  22. package/dist/node/index.d.ts +259 -5
  23. package/dist/node/index.js +55 -65
  24. package/dist/node/integrations/svelte.cjs +76 -0
  25. package/dist/node/integrations/svelte.d.cts +2 -2
  26. package/dist/node/integrations/svelte.d.ts +2 -2
  27. package/dist/node/integrations/svelte.js +2 -1
  28. package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
  29. package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
  30. package/dist/node/reverse-W7THPV45.js +193 -0
  31. package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
  32. package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
  33. package/dist/node/validate-CNHUULQE.js +180 -0
  34. package/docs/core/pluresdb-integration.md +15 -15
  35. package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
  36. package/docs/decision-ledger/DecisionLedger.tla +180 -0
  37. package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
  38. package/docs/decision-ledger/LATEST.md +166 -0
  39. package/docs/guides/cicd-pipeline.md +142 -0
  40. package/package.json +2 -2
  41. package/src/__tests__/cli-validate.test.ts +197 -0
  42. package/src/__tests__/decision-ledger.test.ts +485 -0
  43. package/src/__tests__/reverse-generator.test.ts +189 -0
  44. package/src/__tests__/scanner.test.ts +215 -0
  45. package/src/cli/commands/reverse.ts +289 -0
  46. package/src/cli/commands/validate.ts +264 -0
  47. package/src/cli/index.ts +47 -0
  48. package/src/core/pluresdb/adapter.ts +45 -2
  49. package/src/core/rules.ts +133 -0
  50. package/src/decision-ledger/README.md +400 -0
  51. package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
  52. package/src/decision-ledger/facts-events.ts +121 -0
  53. package/src/decision-ledger/index.ts +70 -0
  54. package/src/decision-ledger/ledger.ts +246 -0
  55. package/src/decision-ledger/logic-ledger.ts +158 -0
  56. package/src/decision-ledger/reverse-generator.ts +426 -0
  57. package/src/decision-ledger/scanner.ts +506 -0
  58. package/src/decision-ledger/types.ts +247 -0
  59. package/src/decision-ledger/validation.ts +336 -0
  60. package/src/dsl/index.ts +13 -2
  61. package/src/index.browser.ts +2 -0
  62. package/src/index.ts +36 -0
  63. package/src/integrations/pluresdb.ts +14 -2
@@ -0,0 +1,197 @@
1
+ /**
2
+ * CLI Validate Command - Integration Tests
3
+ *
4
+ * Tests for the praxis validate command with decision ledger features.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import { promises as fs } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { exec } from 'node:child_process';
13
+ import { promisify } from 'node:util';
14
+
15
+ const execAsync = promisify(exec);
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ // Go up from src/__tests__ to project root
19
+ const projectRoot = path.resolve(__dirname, '..', '..');
20
+ const cliPath = path.join(projectRoot, 'dist/node/cli/index.js');
21
+ const sampleRegistryPath = path.join(projectRoot, 'examples/sample-registry.js');
22
+
23
+ describe('CLI Validate Command', () => {
24
+ let tempDir: string;
25
+
26
+ beforeEach(async () => {
27
+ tempDir = path.join(tmpdir(), `praxis-test-${Date.now()}`);
28
+ await fs.mkdir(tempDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(async () => {
32
+ try {
33
+ await fs.rm(tempDir, { recursive: true, force: true });
34
+ } catch (error) {
35
+ // Ignore cleanup errors
36
+ }
37
+ });
38
+
39
+ it('should validate registry and output to console', async () => {
40
+ const { stdout, stderr } = await execAsync(
41
+ `node ${cliPath} validate --registry ${sampleRegistryPath}`
42
+ );
43
+
44
+ expect(stdout).toContain('Contract Validation Report');
45
+ expect(stdout).toContain('Total: 5');
46
+ expect(stdout).toContain('auth.login');
47
+ expect(stderr).toContain('[Praxis][WARN]');
48
+ });
49
+
50
+ it('should output validation report as JSON', async () => {
51
+ const { stdout } = await execAsync(
52
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --output json 2>/dev/null`
53
+ );
54
+
55
+ const report = JSON.parse(stdout);
56
+
57
+ expect(report).toHaveProperty('complete');
58
+ expect(report).toHaveProperty('incomplete');
59
+ expect(report).toHaveProperty('missing');
60
+ expect(report).toHaveProperty('total');
61
+ expect(report.total).toBe(5);
62
+ });
63
+
64
+ it('should output validation report as SARIF', async () => {
65
+ const { stdout } = await execAsync(
66
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --output sarif 2>/dev/null`
67
+ );
68
+
69
+ const sarif = JSON.parse(stdout);
70
+
71
+ expect(sarif).toHaveProperty('version', '2.1.0');
72
+ expect(sarif).toHaveProperty('runs');
73
+ expect(sarif.runs).toHaveLength(1);
74
+ expect(sarif.runs[0].tool.driver.name).toBe('Praxis Decision Ledger');
75
+ });
76
+
77
+ it('should create logic ledger snapshots with --ledger option', async () => {
78
+ const ledgerDir = path.join(tempDir, 'ledger');
79
+
80
+ await execAsync(
81
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
82
+ );
83
+
84
+ // Check that ledger directory was created
85
+ const ledgerPath = path.join(ledgerDir, 'logic-ledger');
86
+ const exists = await fs.access(ledgerPath).then(() => true).catch(() => false);
87
+ expect(exists).toBe(true);
88
+
89
+ // Check that index.json was created
90
+ const indexPath = path.join(ledgerPath, 'index.json');
91
+ const indexContent = await fs.readFile(indexPath, 'utf-8');
92
+ const index = JSON.parse(indexContent);
93
+ expect(index).toHaveProperty('byRuleId');
94
+ expect(index.byRuleId).toHaveProperty('auth.login');
95
+
96
+ // Check that a versioned snapshot was created
97
+ // The path in byRuleId is relative to ledgerDir, not ledgerPath
98
+ const ruleDirRelative = index.byRuleId['auth.login'];
99
+ const ruleDir = path.join(ledgerDir, ruleDirRelative);
100
+ const latestPath = path.join(ruleDir, 'LATEST.json');
101
+ const latestContent = await fs.readFile(latestPath, 'utf-8');
102
+ const latest = JSON.parse(latestContent);
103
+ expect(latest).toHaveProperty('ruleId', 'auth.login');
104
+ expect(latest).toHaveProperty('version', 1);
105
+ expect(latest).toHaveProperty('canonicalBehavior');
106
+ expect(latest).toHaveProperty('assumptions');
107
+ expect(latest).toHaveProperty('artifacts');
108
+ expect(latest).toHaveProperty('drift');
109
+ });
110
+
111
+ it('should emit contract gaps as facts with --emit-facts option', async () => {
112
+ const gapFile = path.join(tempDir, 'gaps.json');
113
+
114
+ await execAsync(
115
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --emit-facts --gap-output ${gapFile}`
116
+ );
117
+
118
+ // Check that gap file was created
119
+ const gapContent = await fs.readFile(gapFile, 'utf-8');
120
+ const gaps = JSON.parse(gapContent);
121
+
122
+ expect(gaps).toHaveProperty('facts');
123
+ expect(gaps).toHaveProperty('events');
124
+ expect(Array.isArray(gaps.facts)).toBe(true);
125
+ expect(gaps.facts.length).toBeGreaterThan(0);
126
+
127
+ // Check that facts have correct structure
128
+ const firstFact = gaps.facts[0];
129
+ expect(firstFact).toHaveProperty('tag', 'ContractMissing');
130
+ expect(firstFact).toHaveProperty('payload');
131
+ expect(firstFact.payload).toHaveProperty('ruleId');
132
+ expect(firstFact.payload).toHaveProperty('missing');
133
+ expect(firstFact.payload).toHaveProperty('severity');
134
+ });
135
+
136
+ it('should exit with error code in strict mode if contracts missing', async () => {
137
+ try {
138
+ await execAsync(
139
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --strict 2>&1`
140
+ );
141
+ // Should not reach here - strict mode should exit with error
142
+ throw new Error('Expected command to fail in strict mode');
143
+ } catch (error: any) {
144
+ // Expect non-zero exit code
145
+ expect(error.code).toBe(1);
146
+ // Check that stderr contains error message
147
+ const combinedOutput = error.stdout || error.stderr || '';
148
+ expect(combinedOutput).toContain('Validation failed');
149
+ }
150
+ });
151
+
152
+ it('should handle missing registry gracefully', async () => {
153
+ const { stdout } = await execAsync(
154
+ `node ${cliPath} validate --registry ./non-existent-registry.js`
155
+ );
156
+
157
+ expect(stdout).toContain('Contract Validation Report');
158
+ expect(stdout).toContain('Total: 0');
159
+ expect(stdout).toContain('All contracts validated successfully');
160
+ });
161
+
162
+ it('should track drift when updating contracts', async () => {
163
+ const ledgerDir = path.join(tempDir, 'ledger');
164
+
165
+ // First validation
166
+ await execAsync(
167
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
168
+ );
169
+
170
+ // Second validation (simulates contract update)
171
+ await execAsync(
172
+ `node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
173
+ );
174
+
175
+ // Check that version was incremented
176
+ const indexPath = path.join(ledgerDir, 'logic-ledger', 'index.json');
177
+ const indexContent = await fs.readFile(indexPath, 'utf-8');
178
+ const index = JSON.parse(indexContent);
179
+
180
+ const ruleDirRelative = index.byRuleId['auth.login'];
181
+ const ruleDir = path.join(ledgerDir, ruleDirRelative);
182
+ const latestPath = path.join(ruleDir, 'LATEST.json');
183
+ const latestContent = await fs.readFile(latestPath, 'utf-8');
184
+ const latest = JSON.parse(latestContent);
185
+
186
+ // Second run should have version 2
187
+ expect(latest.version).toBe(2);
188
+
189
+ // Check that v0001.json and v0002.json exist
190
+ const v1Path = path.join(ruleDir, 'v0001.json');
191
+ const v2Path = path.join(ruleDir, 'v0002.json');
192
+ const v1Exists = await fs.access(v1Path).then(() => true).catch(() => false);
193
+ const v2Exists = await fs.access(v2Path).then(() => true).catch(() => false);
194
+ expect(v1Exists).toBe(true);
195
+ expect(v2Exists).toBe(true);
196
+ });
197
+ });
@@ -0,0 +1,485 @@
1
+ /**
2
+ * Decision Ledger - Tests
3
+ *
4
+ * Tests for contract definition, validation, and ledger operations.
5
+ *
6
+ * These tests are derived from the behavior ledger examples and assumptions.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { PraxisRegistry } from '../core/rules.js';
11
+ import { defineRule } from '../dsl/index.js';
12
+ import {
13
+ defineContract,
14
+ getContract,
15
+ isContract,
16
+ validateContracts,
17
+ formatValidationReport,
18
+ ContractMissing,
19
+ AcknowledgeContractGap,
20
+ BehaviorLedger,
21
+ createBehaviorLedger,
22
+ } from '../decision-ledger/index.js';
23
+
24
+ describe('Decision Ledger', () => {
25
+ describe('Contract Definition', () => {
26
+ // Example 1 from behavior ledger: Defining a Contract for a Rule
27
+ it('should define a contract with behavior, examples, and invariants', () => {
28
+ const loginContract = defineContract({
29
+ ruleId: 'auth.login',
30
+ behavior: 'Process login events and create user session facts',
31
+ examples: [
32
+ {
33
+ given: 'User provides valid credentials',
34
+ when: 'LOGIN event is received',
35
+ then: 'UserSessionCreated fact is emitted',
36
+ },
37
+ ],
38
+ invariants: ['Session must have unique ID', 'Session must have timestamp'],
39
+ assumptions: [
40
+ {
41
+ id: 'assume-unique-username',
42
+ statement: 'Usernames are unique across the system',
43
+ confidence: 0.9,
44
+ justification: 'Standard practice in authentication systems',
45
+ impacts: ['spec', 'tests'],
46
+ status: 'active',
47
+ },
48
+ ],
49
+ references: [{ type: 'doc', url: 'https://docs.example.com/auth' }],
50
+ });
51
+
52
+ expect(loginContract.ruleId).toBe('auth.login');
53
+ expect(loginContract.behavior).toBe('Process login events and create user session facts');
54
+ expect(loginContract.examples).toHaveLength(1);
55
+ expect(loginContract.examples[0].given).toBe('User provides valid credentials');
56
+ expect(loginContract.invariants).toHaveLength(2);
57
+ expect(loginContract.assumptions).toHaveLength(1);
58
+ expect(loginContract.assumptions![0].id).toBe('assume-unique-username');
59
+ expect(loginContract.references).toHaveLength(1);
60
+ });
61
+
62
+ it('should throw error if contract has no examples', () => {
63
+ expect(() => {
64
+ defineContract({
65
+ ruleId: 'test.rule',
66
+ behavior: 'Test behavior',
67
+ examples: [],
68
+ invariants: [],
69
+ });
70
+ }).toThrow('Contract must have at least one example');
71
+ });
72
+
73
+ it('should validate contract structure with type guard', () => {
74
+ const validContract = {
75
+ ruleId: 'test.rule',
76
+ behavior: 'Test behavior',
77
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
78
+ invariants: ['test'],
79
+ };
80
+
81
+ expect(isContract(validContract)).toBe(true);
82
+
83
+ const invalidContract = {
84
+ ruleId: 'test.rule',
85
+ behavior: 'Test behavior',
86
+ examples: [],
87
+ invariants: [],
88
+ };
89
+
90
+ expect(isContract(invalidContract)).toBe(false);
91
+ });
92
+
93
+ it('should extract contract from rule metadata', () => {
94
+ const contract = defineContract({
95
+ ruleId: 'test.rule',
96
+ behavior: 'Test behavior',
97
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
98
+ invariants: [],
99
+ });
100
+
101
+ const rule = defineRule({
102
+ id: 'test.rule',
103
+ description: 'Test rule',
104
+ impl: () => [],
105
+ meta: { contract },
106
+ });
107
+
108
+ const extracted = getContract(rule.meta);
109
+ expect(extracted).toBeDefined();
110
+ expect(extracted?.ruleId).toBe('test.rule');
111
+ });
112
+ });
113
+
114
+ describe('Contract Validation', () => {
115
+ // Example 2 from behavior ledger: Build-time Validation
116
+ it('should validate registry and produce complete/incomplete report', () => {
117
+ const registry = new PraxisRegistry();
118
+
119
+ // Rule with complete contract
120
+ const completeContract = defineContract({
121
+ ruleId: 'auth.login',
122
+ behavior: 'Process login events',
123
+ examples: [{ given: 'valid creds', when: 'LOGIN', then: 'session created' }],
124
+ invariants: ['unique session ID'],
125
+ });
126
+
127
+ registry.registerRule(
128
+ defineRule({
129
+ id: 'auth.login',
130
+ description: 'Login rule',
131
+ impl: () => [],
132
+ meta: { contract: completeContract },
133
+ })
134
+ );
135
+
136
+ // Rule without contract
137
+ registry.registerRule(
138
+ defineRule({
139
+ id: 'cart.addItem',
140
+ description: 'Add item to cart',
141
+ impl: () => [],
142
+ })
143
+ );
144
+
145
+ const report = validateContracts(registry);
146
+
147
+ expect(report.total).toBe(2);
148
+ expect(report.complete).toHaveLength(1);
149
+ expect(report.complete[0].ruleId).toBe('auth.login');
150
+ expect(report.missing).toHaveLength(1);
151
+ expect(report.missing).toContain('cart.addItem');
152
+ // Rules without contracts only appear in missing array, not incomplete
153
+ expect(report.incomplete).toHaveLength(0);
154
+ });
155
+
156
+ it('should validate contract completeness', () => {
157
+ const registry = new PraxisRegistry();
158
+
159
+ // Contract missing behavior
160
+ const incompleteContract = defineContract({
161
+ ruleId: 'test.rule',
162
+ behavior: '', // Empty behavior
163
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
164
+ invariants: [],
165
+ });
166
+
167
+ registry.registerRule(
168
+ defineRule({
169
+ id: 'test.rule',
170
+ description: 'Test',
171
+ impl: () => [],
172
+ meta: { contract: incompleteContract },
173
+ })
174
+ );
175
+
176
+ const report = validateContracts(registry, {
177
+ requiredFields: ['behavior', 'examples'],
178
+ });
179
+
180
+ expect(report.incomplete).toHaveLength(1);
181
+ expect(report.incomplete[0].missing).toContain('behavior');
182
+ });
183
+
184
+ it('should format validation report as text', () => {
185
+ const registry = new PraxisRegistry();
186
+
187
+ registry.registerRule(
188
+ defineRule({
189
+ id: 'test.rule',
190
+ description: 'Test',
191
+ impl: () => [],
192
+ meta: {
193
+ contract: defineContract({
194
+ ruleId: 'test.rule',
195
+ behavior: 'Test',
196
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
197
+ invariants: [],
198
+ }),
199
+ },
200
+ })
201
+ );
202
+
203
+ const report = validateContracts(registry);
204
+ const formatted = formatValidationReport(report);
205
+
206
+ expect(formatted).toContain('Contract Validation Report');
207
+ expect(formatted).toContain('✓ Complete Contracts:');
208
+ expect(formatted).toContain('test.rule');
209
+ });
210
+
211
+ it('should support strict validation mode', () => {
212
+ const registry = new PraxisRegistry();
213
+
214
+ registry.registerRule(
215
+ defineRule({
216
+ id: 'missing.contract',
217
+ description: 'No contract',
218
+ impl: () => [],
219
+ })
220
+ );
221
+
222
+ const report = validateContracts(registry, { strict: true });
223
+
224
+ // Rules without contracts only appear in missing array, not incomplete
225
+ expect(report.missing).toHaveLength(1);
226
+ expect(report.missing).toContain('missing.contract');
227
+ expect(report.incomplete).toHaveLength(0);
228
+ });
229
+ });
230
+
231
+ describe('Facts and Events', () => {
232
+ // Example 3 from behavior ledger: Runtime Validation
233
+ it('should create ContractMissing fact', () => {
234
+ const fact = ContractMissing.create({
235
+ ruleId: 'test.rule',
236
+ missing: ['behavior', 'examples'],
237
+ severity: 'warning',
238
+ });
239
+
240
+ expect(fact.tag).toBe('ContractMissing');
241
+ expect(fact.payload.ruleId).toBe('test.rule');
242
+ expect(fact.payload.missing).toContain('behavior');
243
+ expect(fact.payload.severity).toBe('warning');
244
+ });
245
+
246
+ // Example 4 from behavior ledger: Contract Gap Acknowledgment
247
+ it('should create AcknowledgeContractGap event', () => {
248
+ const event = AcknowledgeContractGap.create({
249
+ ruleId: 'legacy.process',
250
+ missing: ['spec', 'tests'],
251
+ justification: 'Legacy rule to be deprecated in v2.0',
252
+ expiresAt: '2025-12-31',
253
+ });
254
+
255
+ expect(event.tag).toBe('ACKNOWLEDGE_CONTRACT_GAP');
256
+ expect(event.payload.ruleId).toBe('legacy.process');
257
+ expect(event.payload.justification).toBe('Legacy rule to be deprecated in v2.0');
258
+ expect(event.payload.expiresAt).toBe('2025-12-31');
259
+ });
260
+
261
+ it('should use type guards for facts and events', () => {
262
+ const fact = ContractMissing.create({
263
+ ruleId: 'test',
264
+ missing: ['contract'],
265
+ severity: 'warning',
266
+ });
267
+
268
+ expect(ContractMissing.is(fact)).toBe(true);
269
+
270
+ const event = AcknowledgeContractGap.create({
271
+ ruleId: 'test',
272
+ missing: ['contract'],
273
+ justification: 'test',
274
+ });
275
+
276
+ expect(AcknowledgeContractGap.is(event)).toBe(true);
277
+ });
278
+ });
279
+
280
+ describe('Behavior Ledger', () => {
281
+ // Invariant: Ledger Append-Only
282
+ it('should maintain append-only ledger', () => {
283
+ const ledger = createBehaviorLedger();
284
+
285
+ const contract1 = defineContract({
286
+ ruleId: 'test.rule',
287
+ behavior: 'Version 1',
288
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
289
+ invariants: [],
290
+ });
291
+
292
+ ledger.append({
293
+ id: 'entry-1',
294
+ timestamp: new Date().toISOString(),
295
+ status: 'active',
296
+ author: 'system',
297
+ contract: contract1,
298
+ });
299
+
300
+ expect(ledger.getAllEntries()).toHaveLength(1);
301
+
302
+ // Cannot append entry with same ID
303
+ expect(() => {
304
+ ledger.append({
305
+ id: 'entry-1',
306
+ timestamp: new Date().toISOString(),
307
+ status: 'active',
308
+ author: 'system',
309
+ contract: contract1,
310
+ });
311
+ }).toThrow('already exists');
312
+ });
313
+
314
+ // Invariant: Ledger Unique IDs
315
+ it('should enforce unique entry IDs', () => {
316
+ const ledger = createBehaviorLedger();
317
+
318
+ const contract = defineContract({
319
+ ruleId: 'test',
320
+ behavior: 'test',
321
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
322
+ invariants: [],
323
+ });
324
+
325
+ ledger.append({
326
+ id: 'unique-1',
327
+ timestamp: new Date().toISOString(),
328
+ status: 'active',
329
+ author: 'system',
330
+ contract,
331
+ });
332
+
333
+ expect(() => {
334
+ ledger.append({
335
+ id: 'unique-1',
336
+ timestamp: new Date().toISOString(),
337
+ status: 'active',
338
+ author: 'system',
339
+ contract,
340
+ });
341
+ }).toThrow();
342
+ });
343
+
344
+ it('should supersede previous entries', () => {
345
+ const ledger = createBehaviorLedger();
346
+
347
+ const contract1 = defineContract({
348
+ ruleId: 'test.rule',
349
+ behavior: 'Version 1',
350
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
351
+ invariants: [],
352
+ version: '1.0.0',
353
+ });
354
+
355
+ ledger.append({
356
+ id: 'entry-1',
357
+ timestamp: '2025-01-01T00:00:00Z',
358
+ status: 'active',
359
+ author: 'system',
360
+ contract: contract1,
361
+ });
362
+
363
+ const contract2 = defineContract({
364
+ ruleId: 'test.rule',
365
+ behavior: 'Version 2',
366
+ examples: [{ given: 'x', when: 'y', then: 'z' }],
367
+ invariants: [],
368
+ version: '2.0.0',
369
+ });
370
+
371
+ ledger.append({
372
+ id: 'entry-2',
373
+ timestamp: '2025-01-02T00:00:00Z',
374
+ status: 'active',
375
+ author: 'system',
376
+ contract: contract2,
377
+ supersedes: 'entry-1',
378
+ });
379
+
380
+ const latest = ledger.getLatestEntry('test.rule');
381
+ expect(latest?.id).toBe('entry-2');
382
+ expect(latest?.contract.version).toBe('2.0.0');
383
+
384
+ const entry1 = ledger.getEntry('entry-1');
385
+ expect(entry1?.status).toBe('superseded');
386
+ });
387
+
388
+ it('should track assumptions', () => {
389
+ const ledger = createBehaviorLedger();
390
+
391
+ const contract = defineContract({
392
+ ruleId: 'test.rule',
393
+ behavior: 'Test',
394
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
395
+ invariants: [],
396
+ assumptions: [
397
+ {
398
+ id: 'test-assumption',
399
+ statement: 'Test assumption',
400
+ confidence: 0.8,
401
+ justification: 'For testing',
402
+ impacts: ['tests'],
403
+ status: 'active',
404
+ },
405
+ ],
406
+ });
407
+
408
+ ledger.append({
409
+ id: 'entry-1',
410
+ timestamp: new Date().toISOString(),
411
+ status: 'active',
412
+ author: 'system',
413
+ contract,
414
+ });
415
+
416
+ const assumptions = ledger.getActiveAssumptions();
417
+ expect(assumptions.size).toBe(1);
418
+ expect(assumptions.get('test-assumption')?.statement).toBe('Test assumption');
419
+ });
420
+
421
+ it('should export and import ledger as JSON', () => {
422
+ const ledger1 = createBehaviorLedger();
423
+
424
+ const contract = defineContract({
425
+ ruleId: 'test.rule',
426
+ behavior: 'Test',
427
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
428
+ invariants: [],
429
+ });
430
+
431
+ ledger1.append({
432
+ id: 'entry-1',
433
+ timestamp: new Date().toISOString(),
434
+ status: 'active',
435
+ author: 'system',
436
+ contract,
437
+ });
438
+
439
+ const json = ledger1.toJSON();
440
+ const ledger2 = BehaviorLedger.fromJSON(json);
441
+
442
+ expect(ledger2.getAllEntries()).toHaveLength(1);
443
+ expect(ledger2.getEntry('entry-1')?.contract.ruleId).toBe('test.rule');
444
+ });
445
+
446
+ it('should provide ledger statistics', () => {
447
+ const ledger = createBehaviorLedger();
448
+
449
+ const contract1 = defineContract({
450
+ ruleId: 'rule1',
451
+ behavior: 'Rule 1',
452
+ examples: [{ given: 'a', when: 'b', then: 'c' }],
453
+ invariants: [],
454
+ });
455
+
456
+ const contract2 = defineContract({
457
+ ruleId: 'rule2',
458
+ behavior: 'Rule 2',
459
+ examples: [{ given: 'x', when: 'y', then: 'z' }],
460
+ invariants: [],
461
+ });
462
+
463
+ ledger.append({
464
+ id: 'entry-1',
465
+ timestamp: new Date().toISOString(),
466
+ status: 'active',
467
+ author: 'system',
468
+ contract: contract1,
469
+ });
470
+
471
+ ledger.append({
472
+ id: 'entry-2',
473
+ timestamp: new Date().toISOString(),
474
+ status: 'active',
475
+ author: 'system',
476
+ contract: contract2,
477
+ });
478
+
479
+ const stats = ledger.getStats();
480
+ expect(stats.totalEntries).toBe(2);
481
+ expect(stats.activeEntries).toBe(2);
482
+ expect(stats.uniqueRules).toBe(2);
483
+ });
484
+ });
485
+ });