@plures/praxis 1.2.0 → 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/README.md +10 -96
- package/dist/browser/{adapter-TM4IS5KT.js → adapter-CIMBGDC7.js} +5 -3
- package/dist/browser/{chunk-LE2ZJYFC.js → chunk-K377RW4V.js} +76 -0
- package/dist/{node/chunk-JQ64KMLN.js → browser/chunk-MBVHLOU2.js} +12 -1
- package/dist/browser/index.d.ts +32 -5
- package/dist/browser/index.js +15 -7
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +1 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/{adapter-K6DOX6XS.js → adapter-75ISSMWD.js} +5 -3
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/{browser/chunk-JQ64KMLN.js → node/chunk-MBVHLOU2.js} +12 -1
- package/dist/node/{chunk-LE2ZJYFC.js → chunk-PRPQO6R5.js} +3 -72
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2316 -832
- package/dist/node/cli/index.js +18 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/index.cjs +620 -38
- package/dist/node/index.d.cts +259 -5
- package/dist/node/index.d.ts +259 -5
- package/dist/node/index.js +55 -65
- 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 +2 -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/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +47 -0
- package/src/core/pluresdb/adapter.ts +45 -2
- 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 +2 -0
- package/src/index.ts +36 -0
- 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
|
+
});
|