@plures/praxis 1.1.3 → 1.2.10
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/FRAMEWORK.md +106 -15
- package/README.md +194 -119
- package/dist/browser/adapter-CIMBGDC7.js +14 -0
- package/dist/browser/chunk-K377RW4V.js +230 -0
- package/dist/browser/chunk-MBVHLOU2.js +152 -0
- package/dist/browser/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/browser/engine-YJZV4SLD.js +8 -0
- package/dist/browser/index.d.ts +161 -5
- package/dist/browser/index.js +156 -141
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/adapter-75ISSMWD.js +15 -0
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/node/chunk-MBVHLOU2.js +152 -0
- package/dist/node/chunk-PRPQO6R5.js +85 -0
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-S54337I5.js +446 -0
- package/dist/node/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2936 -897
- package/dist/node/cli/index.js +27 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/docs-JFNYTOJA.js +102 -0
- package/dist/node/engine-2DQBKBJC.js +9 -0
- package/dist/node/index.cjs +1114 -354
- package/dist/node/index.d.cts +388 -5
- package/dist/node/index.d.ts +388 -5
- package/dist/node/index.js +201 -640
- package/dist/node/integrations/svelte.cjs +76 -0
- package/dist/node/integrations/svelte.d.cts +2 -2
- package/dist/node/integrations/svelte.d.ts +2 -2
- package/dist/node/integrations/svelte.js +3 -1
- package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
- package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
- package/dist/node/reverse-W7THPV45.js +193 -0
- package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
- package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
- package/dist/node/validate-CNHUULQE.js +180 -0
- package/docs/core/pluresdb-integration.md +15 -15
- package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
- package/docs/decision-ledger/DecisionLedger.tla +180 -0
- package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
- package/docs/decision-ledger/LATEST.md +166 -0
- package/docs/guides/cicd-pipeline.md +142 -0
- package/package.json +2 -2
- package/src/__tests__/cli-validate.test.ts +197 -0
- package/src/__tests__/decision-ledger.test.ts +485 -0
- package/src/__tests__/reverse-generator.test.ts +189 -0
- package/src/__tests__/scanner.test.ts +215 -0
- package/src/cli/commands/docs.ts +147 -0
- package/src/cli/commands/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +68 -0
- package/src/core/pluresdb/adapter.ts +46 -3
- package/src/core/reactive-engine.svelte.ts +6 -1
- package/src/core/reactive-engine.ts +1 -1
- package/src/core/rules.ts +133 -0
- package/src/decision-ledger/README.md +400 -0
- package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
- package/src/decision-ledger/facts-events.ts +121 -0
- package/src/decision-ledger/index.ts +70 -0
- package/src/decision-ledger/ledger.ts +246 -0
- package/src/decision-ledger/logic-ledger.ts +158 -0
- package/src/decision-ledger/reverse-generator.ts +426 -0
- package/src/decision-ledger/scanner.ts +506 -0
- package/src/decision-ledger/types.ts +247 -0
- package/src/decision-ledger/validation.ts +336 -0
- package/src/dsl/index.ts +13 -2
- package/src/index.browser.ts +6 -0
- package/src/index.ts +40 -0
- package/src/integrations/pluresdb.ts +14 -2
- package/src/integrations/unified.ts +350 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Ledger - Reverse Generator Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for reverse contract generation from existing code.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { generateContractFromRule } from '../decision-ledger/reverse-generator.js';
|
|
9
|
+
import type { RuleDescriptor } from '../core/rules.js';
|
|
10
|
+
|
|
11
|
+
describe('Reverse Contract Generator', () => {
|
|
12
|
+
describe('generateContractFromRule', () => {
|
|
13
|
+
it('should generate contract with default values when no files provided', async () => {
|
|
14
|
+
const rule: RuleDescriptor = {
|
|
15
|
+
id: 'test.rule',
|
|
16
|
+
description: 'Test rule description',
|
|
17
|
+
impl: () => [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const result = await generateContractFromRule(rule, {
|
|
21
|
+
aiProvider: 'none',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.contract.ruleId).toBe('test.rule');
|
|
25
|
+
expect(result.contract.behavior).toContain('Test rule description');
|
|
26
|
+
expect(result.contract.examples.length).toBeGreaterThan(0);
|
|
27
|
+
expect(result.contract.invariants.length).toBeGreaterThan(0);
|
|
28
|
+
expect(result.method).toBe('heuristic');
|
|
29
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should generate assumptions when requested', async () => {
|
|
33
|
+
const rule: RuleDescriptor = {
|
|
34
|
+
id: 'auth.login',
|
|
35
|
+
description: 'Process login events',
|
|
36
|
+
impl: () => [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await generateContractFromRule(rule, {
|
|
40
|
+
aiProvider: 'none',
|
|
41
|
+
includeAssumptions: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.contract.assumptions).toBeDefined();
|
|
45
|
+
expect(result.contract.assumptions!.length).toBeGreaterThan(0);
|
|
46
|
+
|
|
47
|
+
const assumption = result.contract.assumptions![0];
|
|
48
|
+
expect(assumption.id).toBeDefined();
|
|
49
|
+
expect(assumption.statement).toBeDefined();
|
|
50
|
+
expect(assumption.confidence).toBeGreaterThanOrEqual(0);
|
|
51
|
+
expect(assumption.confidence).toBeLessThanOrEqual(1);
|
|
52
|
+
expect(assumption.status).toBe('active');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not generate assumptions when not requested', async () => {
|
|
56
|
+
const rule: RuleDescriptor = {
|
|
57
|
+
id: 'test.rule',
|
|
58
|
+
description: 'Test rule',
|
|
59
|
+
impl: () => [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = await generateContractFromRule(rule, {
|
|
63
|
+
aiProvider: 'none',
|
|
64
|
+
includeAssumptions: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.contract.assumptions).toBeDefined();
|
|
68
|
+
expect(result.contract.assumptions!.length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should increase confidence when artifacts are provided', async () => {
|
|
72
|
+
const rule: RuleDescriptor = {
|
|
73
|
+
id: 'test.rule',
|
|
74
|
+
description: 'Test rule',
|
|
75
|
+
impl: () => [],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Baseline: no artifacts
|
|
79
|
+
const resultBaseline = await generateContractFromRule(rule, {
|
|
80
|
+
aiProvider: 'none',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// With test files (should increase confidence)
|
|
84
|
+
const resultWithTests = await generateContractFromRule(rule, {
|
|
85
|
+
aiProvider: 'none',
|
|
86
|
+
testFiles: ['/path/to/test1.ts', '/path/to/test2.ts'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// With spec files (should increase confidence)
|
|
90
|
+
const resultWithSpecs = await generateContractFromRule(rule, {
|
|
91
|
+
aiProvider: 'none',
|
|
92
|
+
specFiles: ['/path/to/spec.tla'],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Confidence should increase with more artifacts
|
|
96
|
+
expect(resultWithTests.confidence).toBeGreaterThan(resultBaseline.confidence);
|
|
97
|
+
expect(resultWithSpecs.confidence).toBeGreaterThan(resultBaseline.confidence);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should include warnings for missing information', async () => {
|
|
101
|
+
const rule: RuleDescriptor = {
|
|
102
|
+
id: 'test.rule',
|
|
103
|
+
description: 'Test rule',
|
|
104
|
+
impl: () => [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = await generateContractFromRule(rule, {
|
|
108
|
+
aiProvider: 'none',
|
|
109
|
+
generateExamples: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.warnings).toBeDefined();
|
|
113
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
114
|
+
|
|
115
|
+
// Should warn about missing test files
|
|
116
|
+
const hasTestWarning = result.warnings.some((w) =>
|
|
117
|
+
w.includes('test') || w.includes('example')
|
|
118
|
+
);
|
|
119
|
+
expect(hasTestWarning).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use rule description as behavior if no other source', async () => {
|
|
123
|
+
const rule: RuleDescriptor = {
|
|
124
|
+
id: 'custom.rule',
|
|
125
|
+
description: 'Custom rule that does something specific',
|
|
126
|
+
impl: () => [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await generateContractFromRule(rule, {
|
|
130
|
+
aiProvider: 'none',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result.contract.behavior).toContain('Custom rule that does something specific');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should generate default examples when no tests provided', async () => {
|
|
137
|
+
const rule: RuleDescriptor = {
|
|
138
|
+
id: 'test.rule',
|
|
139
|
+
description: 'Test rule',
|
|
140
|
+
impl: () => [],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = await generateContractFromRule(rule, {
|
|
144
|
+
aiProvider: 'none',
|
|
145
|
+
generateExamples: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.contract.examples.length).toBeGreaterThan(0);
|
|
149
|
+
|
|
150
|
+
const example = result.contract.examples[0];
|
|
151
|
+
expect(example.given).toBeDefined();
|
|
152
|
+
expect(example.when).toBeDefined();
|
|
153
|
+
expect(example.then).toBeDefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle constraint descriptors', async () => {
|
|
157
|
+
const constraint = {
|
|
158
|
+
id: 'test.constraint',
|
|
159
|
+
description: 'Test constraint',
|
|
160
|
+
impl: () => true,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = await generateContractFromRule(constraint, {
|
|
164
|
+
aiProvider: 'none',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.contract.ruleId).toBe('test.constraint');
|
|
168
|
+
expect(result.contract.behavior).toContain('Test constraint');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should cap confidence at reasonable level for heuristic method', async () => {
|
|
172
|
+
const rule: RuleDescriptor = {
|
|
173
|
+
id: 'test.rule',
|
|
174
|
+
description: 'Test rule',
|
|
175
|
+
impl: () => [],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = await generateContractFromRule(rule, {
|
|
179
|
+
aiProvider: 'none',
|
|
180
|
+
sourceFile: '/path/to/rule.ts',
|
|
181
|
+
testFiles: ['/path/to/test1.ts', '/path/to/test2.ts'],
|
|
182
|
+
specFiles: ['/path/to/spec.tla'],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Heuristic should not claim perfect confidence
|
|
186
|
+
expect(result.confidence).toBeLessThan(1.0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|