principles-disciple 1.107.0 → 1.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/core/init.ts +3 -1
- package/src/core/workspace-dir-validation.ts +3 -3
- package/src/service/evolution-worker.ts +1 -1
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +1 -1
- package/tests/core-anti-growth.test.ts +0 -13
- package/tests/hooks/prompt-characterization.test.ts +1 -11
- package/tests/hooks/prompt-diet.test.ts +3 -11
- package/tests/hooks/prompt-size-guard.test.ts +0 -10
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
- package/tests/index.test.ts +1 -1
- package/tests/runtime-v2-discovery-guard.test.ts +1 -2
- package/vitest.config.ts +2 -3
- package/vitest.unit.config.ts +12 -0
- package/src/core/evolution-hook.ts +0 -74
- package/src/core/file-storage-adapter.ts +0 -203
- package/src/core/merge-gate-audit.ts +0 -314
- package/src/core/pain-context-extractor.ts +0 -306
- package/src/core/pain-lifecycle.ts +0 -38
- package/src/core/pain-signal-adapter.ts +0 -42
- package/src/core/pain-signal.ts +0 -22
- package/src/core/principle-injector.ts +0 -84
- package/src/core/principle-tree-migration.ts +0 -196
- package/src/core/storage-adapter.ts +0 -65
- package/src/core/telemetry-event.ts +0 -109
- package/src/core/training-program.ts +0 -632
- package/src/core/workspace-dir-service.ts +0 -119
- package/src/hooks/lifecycle-routing.ts +0 -125
- package/src/service/event-log-auditor.ts +0 -284
- package/src/service/evolution-queue-lock.ts +0 -47
- package/src/service/failure-classifier.ts +0 -79
- package/src/service/internalization-trigger-adapter.ts +0 -302
- package/src/service/monitoring-query-service.ts +0 -67
- package/src/service/subagent-workflow/index.ts +0 -17
- package/src/tools/critique-prompt.ts +0 -1
- package/src/tools/model-index.ts +0 -1
- package/src/types/event-payload.ts +0 -16
- package/src/utils/glob-match.ts +0 -50
- package/src/utils/nlp.ts +0 -25
- package/src/utils/plugin-logger.ts +0 -97
- package/src/utils/subagent-probe.ts +0 -81
- package/tests/core/evolution-hook.test.ts +0 -123
- package/tests/core/file-storage-adapter.test.ts +0 -285
- package/tests/core/merge-gate-audit.test.ts +0 -117
- package/tests/core/pain-context-extractor.test.ts +0 -279
- package/tests/core/pain-lifecycle.test.ts +0 -38
- package/tests/core/pain-signal-adapter.test.ts +0 -116
- package/tests/core/pain-signal.test.ts +0 -190
- package/tests/core/principle-injector.test.ts +0 -90
- package/tests/core/principle-tree-migration.test.ts +0 -77
- package/tests/core/storage-conformance.test.ts +0 -429
- package/tests/core/telemetry-event.test.ts +0 -119
- package/tests/core/training-program.test.ts +0 -472
- package/tests/core/workspace-dir-service.test.ts +0 -68
- package/tests/core/workspace-dir-validation.test.ts +0 -143
- package/tests/integration/internalization-trigger-guard.test.ts +0 -69
- package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
- package/tests/service/failure-classifier.test.ts +0 -171
- package/tests/service/internalization-trigger-adapter.test.ts +0 -251
- package/tests/service/monitoring-query-service.test.ts +0 -67
- package/tests/utils/nlp.test.ts +0 -35
- package/tests/utils/plugin-logger.test.ts +0 -156
- package/tests/utils/subagent-probe.test.ts +0 -79
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import type { EvolutionHook, PrincipleCreatedEvent, PrinciplePromotedEvent } from '../../src/core/evolution-hook.js';
|
|
3
|
-
import { noOpEvolutionHook } from '../../src/core/evolution-hook.js';
|
|
4
|
-
import type { PainSignal } from '../../src/core/pain-signal.js';
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Helpers
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
function validPainSignal(overrides: Partial<PainSignal> = {}): PainSignal {
|
|
11
|
-
return {
|
|
12
|
-
source: 'tool_failure',
|
|
13
|
-
score: 75,
|
|
14
|
-
timestamp: '2026-04-17T00:00:00.000Z',
|
|
15
|
-
reason: 'File not found',
|
|
16
|
-
sessionId: 'session-001',
|
|
17
|
-
agentId: 'main',
|
|
18
|
-
traceId: 'trace-001',
|
|
19
|
-
triggerTextPreview: 'File not found: test.ts',
|
|
20
|
-
domain: 'coding',
|
|
21
|
-
severity: 'high',
|
|
22
|
-
context: {},
|
|
23
|
-
...overrides,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Tests
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
describe('EvolutionHook', () => {
|
|
32
|
-
it('implements all 3 methods', () => {
|
|
33
|
-
const calls: string[] = [];
|
|
34
|
-
const hook: EvolutionHook = {
|
|
35
|
-
onPainDetected(signal: PainSignal): void { calls.push(`pain:${signal.source}`); },
|
|
36
|
-
onPrincipleCreated(event: PrincipleCreatedEvent): void { calls.push(`created:${event.id}`); },
|
|
37
|
-
onPrinciplePromoted(event: PrinciplePromotedEvent): void { calls.push(`promoted:${event.id}`); },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
hook.onPainDetected(validPainSignal());
|
|
41
|
-
hook.onPrincipleCreated({ id: 'p-1', text: 'Test principle', trigger: 'tool failure' });
|
|
42
|
-
hook.onPrinciplePromoted({ id: 'p-1', from: 'candidate', to: 'active' });
|
|
43
|
-
|
|
44
|
-
expect(calls).toEqual(['pain:tool_failure', 'created:p-1', 'promoted:p-1']);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('onPainDetected receives a PainSignal', () => {
|
|
48
|
-
let received: PainSignal | undefined;
|
|
49
|
-
const hook: EvolutionHook = {
|
|
50
|
-
...noOpEvolutionHook,
|
|
51
|
-
onPainDetected(signal: PainSignal): void { received = signal; },
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const signal = validPainSignal();
|
|
55
|
-
hook.onPainDetected(signal);
|
|
56
|
-
expect(received).toEqual(signal);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('onPrincipleCreated receives a PrincipleCreatedEvent', () => {
|
|
60
|
-
let received: PrincipleCreatedEvent | undefined;
|
|
61
|
-
const hook: EvolutionHook = {
|
|
62
|
-
...noOpEvolutionHook,
|
|
63
|
-
onPrincipleCreated(event: PrincipleCreatedEvent): void { received = event; },
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const event = { id: 'p-1', text: 'Test principle', trigger: 'tool failure' };
|
|
67
|
-
hook.onPrincipleCreated(event);
|
|
68
|
-
expect(received).toEqual(event);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('onPrinciplePromoted receives a PrinciplePromotedEvent', () => {
|
|
72
|
-
let received: PrinciplePromotedEvent | undefined;
|
|
73
|
-
const hook: EvolutionHook = {
|
|
74
|
-
...noOpEvolutionHook,
|
|
75
|
-
onPrinciplePromoted(event: PrinciplePromotedEvent): void { received = event; },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const event = { id: 'p-1', from: 'candidate', to: 'active' };
|
|
79
|
-
hook.onPrinciplePromoted(event);
|
|
80
|
-
expect(received).toEqual(event);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('noOpEvolutionHook', () => {
|
|
85
|
-
it('implements all 3 methods as no-ops', () => {
|
|
86
|
-
expect(() => {
|
|
87
|
-
noOpEvolutionHook.onPainDetected(validPainSignal());
|
|
88
|
-
noOpEvolutionHook.onPrincipleCreated({ id: 'p-1', text: 'Test', trigger: 'test' });
|
|
89
|
-
noOpEvolutionHook.onPrinciplePromoted({ id: 'p-1', from: 'candidate', to: 'active' });
|
|
90
|
-
}).not.toThrow();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('can be spread to override individual methods', () => {
|
|
94
|
-
const calls: string[] = [];
|
|
95
|
-
const hook: EvolutionHook = {
|
|
96
|
-
...noOpEvolutionHook,
|
|
97
|
-
onPainDetected(_signal: PainSignal): void { calls.push('pain'); },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
hook.onPainDetected(validPainSignal());
|
|
101
|
-
hook.onPrincipleCreated({ id: 'p-1', text: 'Test', trigger: 'test' });
|
|
102
|
-
|
|
103
|
-
expect(calls).toEqual(['pain']);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('PrincipleCreatedEvent', () => {
|
|
108
|
-
it('has required fields: id, text, trigger', () => {
|
|
109
|
-
const event: PrincipleCreatedEvent = { id: 'p-1', text: 'Always verify', trigger: 'tool failure' };
|
|
110
|
-
expect(event.id).toBe('p-1');
|
|
111
|
-
expect(event.text).toBe('Always verify');
|
|
112
|
-
expect(event.trigger).toBe('tool failure');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('PrinciplePromotedEvent', () => {
|
|
117
|
-
it('has required fields: id, from, to', () => {
|
|
118
|
-
const event: PrinciplePromotedEvent = { id: 'p-1', from: 'candidate', to: 'active' };
|
|
119
|
-
expect(event.id).toBe('p-1');
|
|
120
|
-
expect(event.from).toBe('candidate');
|
|
121
|
-
expect(event.to).toBe('active');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { FileStorageAdapter } from '../../src/core/file-storage-adapter.js';
|
|
6
|
-
import type { HybridLedgerStore } from '../../src/core/principle-tree-ledger.js';
|
|
7
|
-
import { TREE_NAMESPACE, loadLedger } from '../../src/core/principle-tree-ledger.js';
|
|
8
|
-
import { safeRmDir } from '../test-utils.js';
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
function createTmpDir(): string {
|
|
15
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-file-storage-adapter-test-'));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function createEmptyStore(): HybridLedgerStore {
|
|
19
|
-
return {
|
|
20
|
-
trainingStore: {},
|
|
21
|
-
tree: {
|
|
22
|
-
principles: {},
|
|
23
|
-
rules: {},
|
|
24
|
-
implementations: {},
|
|
25
|
-
metrics: {},
|
|
26
|
-
lastUpdated: new Date(0).toISOString(),
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Tests
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
describe('FileStorageAdapter', () => {
|
|
36
|
-
let tmpDir: string;
|
|
37
|
-
let adapter: FileStorageAdapter;
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
tmpDir = createTmpDir();
|
|
41
|
-
adapter = new FileStorageAdapter(tmpDir, tmpDir);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
safeRmDir(tmpDir);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// -------------------------------------------------------------------------
|
|
49
|
-
// loadLedger
|
|
50
|
-
// -------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
describe('loadLedger', () => {
|
|
53
|
-
it('returns empty store when no file exists', async () => {
|
|
54
|
-
const store = await adapter.loadLedger();
|
|
55
|
-
expect(store.trainingStore).toEqual({});
|
|
56
|
-
expect(store.tree.principles).toEqual({});
|
|
57
|
-
expect(store.tree.rules).toEqual({});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('loads existing persisted store', async () => {
|
|
61
|
-
const original = createEmptyStore();
|
|
62
|
-
original.tree.principles['P-001'] = {
|
|
63
|
-
id: 'P-001',
|
|
64
|
-
version: 1,
|
|
65
|
-
text: 'Write before delete',
|
|
66
|
-
triggerPattern: 'delete',
|
|
67
|
-
action: 'write first',
|
|
68
|
-
status: 'active',
|
|
69
|
-
priority: 'P1',
|
|
70
|
-
scope: 'general',
|
|
71
|
-
evaluability: 'deterministic',
|
|
72
|
-
valueScore: 0,
|
|
73
|
-
adherenceRate: 0,
|
|
74
|
-
painPreventedCount: 0,
|
|
75
|
-
derivedFromPainIds: [],
|
|
76
|
-
ruleIds: [],
|
|
77
|
-
conflictsWithPrincipleIds: [],
|
|
78
|
-
createdAt: '2026-04-17T00:00:00.000Z',
|
|
79
|
-
updatedAt: '2026-04-17T00:00:00.000Z',
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Persist using the low-level ledger to seed the file
|
|
83
|
-
await adapter.saveLedger(original);
|
|
84
|
-
const loaded = await adapter.loadLedger();
|
|
85
|
-
expect(loaded.tree.principles['P-001']).toBeDefined();
|
|
86
|
-
expect(loaded.tree.principles['P-001'].text).toBe('Write before delete');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// -------------------------------------------------------------------------
|
|
91
|
-
// saveLedger
|
|
92
|
-
// -------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
describe('saveLedger', () => {
|
|
95
|
-
it('persists store to disk', async () => {
|
|
96
|
-
const store = createEmptyStore();
|
|
97
|
-
store.trainingStore['test-principle'] = {
|
|
98
|
-
principleId: 'test-principle',
|
|
99
|
-
evaluability: 'manual_only',
|
|
100
|
-
applicableOpportunityCount: 0,
|
|
101
|
-
observedViolationCount: 0,
|
|
102
|
-
complianceRate: 0,
|
|
103
|
-
violationTrend: 0,
|
|
104
|
-
generatedSampleCount: 0,
|
|
105
|
-
approvedSampleCount: 0,
|
|
106
|
-
includedTrainRunIds: [],
|
|
107
|
-
deployedCheckpointIds: [],
|
|
108
|
-
internalizationStatus: 'prompt_only',
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
await adapter.saveLedger(store);
|
|
112
|
-
|
|
113
|
-
// Verify file exists and contains the data
|
|
114
|
-
const filePath = path.join(tmpDir, 'principle_training_state.json');
|
|
115
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
116
|
-
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
117
|
-
expect(raw['test-principle']).toBeDefined();
|
|
118
|
-
expect(raw[TREE_NAMESPACE]).toBeDefined();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('round-trips data through save and load', async () => {
|
|
122
|
-
const store = createEmptyStore();
|
|
123
|
-
store.trainingStore['p-1'] = {
|
|
124
|
-
principleId: 'p-1',
|
|
125
|
-
evaluability: 'weak_heuristic',
|
|
126
|
-
applicableOpportunityCount: 5,
|
|
127
|
-
observedViolationCount: 2,
|
|
128
|
-
complianceRate: 0.6,
|
|
129
|
-
violationTrend: -0.1,
|
|
130
|
-
generatedSampleCount: 3,
|
|
131
|
-
approvedSampleCount: 2,
|
|
132
|
-
includedTrainRunIds: ['run-1'],
|
|
133
|
-
deployedCheckpointIds: [],
|
|
134
|
-
internalizationStatus: 'in_training',
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
await adapter.saveLedger(store);
|
|
138
|
-
const loaded = await adapter.loadLedger();
|
|
139
|
-
expect(loaded.trainingStore['p-1'].evaluability).toBe('weak_heuristic');
|
|
140
|
-
expect(loaded.trainingStore['p-1'].applicableOpportunityCount).toBe(5);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// -------------------------------------------------------------------------
|
|
145
|
-
// mutateLedger
|
|
146
|
-
// -------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
describe('mutateLedger', () => {
|
|
149
|
-
it('reads, mutates, and writes atomically', async () => {
|
|
150
|
-
// Start with empty store
|
|
151
|
-
await adapter.saveLedger(createEmptyStore());
|
|
152
|
-
|
|
153
|
-
const result = await adapter.mutateLedger((store) => {
|
|
154
|
-
store.tree.principles['P-002'] = {
|
|
155
|
-
id: 'P-002',
|
|
156
|
-
version: 1,
|
|
157
|
-
text: 'Test principle',
|
|
158
|
-
triggerPattern: 'test',
|
|
159
|
-
action: 'do something',
|
|
160
|
-
status: 'candidate',
|
|
161
|
-
priority: 'P2',
|
|
162
|
-
scope: 'general',
|
|
163
|
-
evaluability: 'manual_only',
|
|
164
|
-
valueScore: 0,
|
|
165
|
-
adherenceRate: 0,
|
|
166
|
-
painPreventedCount: 0,
|
|
167
|
-
derivedFromPainIds: [],
|
|
168
|
-
ruleIds: [],
|
|
169
|
-
conflictsWithPrincipleIds: [],
|
|
170
|
-
createdAt: '2026-04-17T00:00:00.000Z',
|
|
171
|
-
updatedAt: '2026-04-17T00:00:00.000Z',
|
|
172
|
-
};
|
|
173
|
-
return 42;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(result).toBe(42);
|
|
177
|
-
const loaded = await adapter.loadLedger();
|
|
178
|
-
expect(loaded.tree.principles['P-002']).toBeDefined();
|
|
179
|
-
expect(loaded.tree.principles['P-002'].text).toBe('Test principle');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('returns the value from the mutate function', async () => {
|
|
183
|
-
await adapter.saveLedger(createEmptyStore());
|
|
184
|
-
|
|
185
|
-
const count = await adapter.mutateLedger((store) => {
|
|
186
|
-
return Object.keys(store.tree.principles).length;
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
expect(count).toBe(0);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('supports async mutate functions', async () => {
|
|
193
|
-
await adapter.saveLedger(createEmptyStore());
|
|
194
|
-
|
|
195
|
-
const result = await adapter.mutateLedger(async (store) => {
|
|
196
|
-
// Simulate async work
|
|
197
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
198
|
-
store.trainingStore['async-test'] = {
|
|
199
|
-
principleId: 'async-test',
|
|
200
|
-
evaluability: 'deterministic',
|
|
201
|
-
applicableOpportunityCount: 1,
|
|
202
|
-
observedViolationCount: 0,
|
|
203
|
-
complianceRate: 1.0,
|
|
204
|
-
violationTrend: 0,
|
|
205
|
-
generatedSampleCount: 0,
|
|
206
|
-
approvedSampleCount: 0,
|
|
207
|
-
includedTrainRunIds: [],
|
|
208
|
-
deployedCheckpointIds: [],
|
|
209
|
-
internalizationStatus: 'prompt_only',
|
|
210
|
-
};
|
|
211
|
-
return 'async-done';
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(result).toBe('async-done');
|
|
215
|
-
const loaded = await adapter.loadLedger();
|
|
216
|
-
expect(loaded.trainingStore['async-test']).toBeDefined();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('persists via atomicWriteFileSync (file is not corrupted)', async () => {
|
|
220
|
-
await adapter.saveLedger(createEmptyStore());
|
|
221
|
-
|
|
222
|
-
await adapter.mutateLedger((store) => {
|
|
223
|
-
store.tree.principles['P-003'] = {
|
|
224
|
-
id: 'P-003',
|
|
225
|
-
version: 1,
|
|
226
|
-
text: 'Atomic write test',
|
|
227
|
-
triggerPattern: 'test',
|
|
228
|
-
action: 'verify atomicity',
|
|
229
|
-
status: 'candidate',
|
|
230
|
-
priority: 'P1',
|
|
231
|
-
scope: 'general',
|
|
232
|
-
evaluability: 'manual_only',
|
|
233
|
-
valueScore: 0,
|
|
234
|
-
adherenceRate: 0,
|
|
235
|
-
painPreventedCount: 0,
|
|
236
|
-
derivedFromPainIds: [],
|
|
237
|
-
ruleIds: [],
|
|
238
|
-
conflictsWithPrincipleIds: [],
|
|
239
|
-
createdAt: '2026-04-17T00:00:00.000Z',
|
|
240
|
-
updatedAt: '2026-04-17T00:00:00.000Z',
|
|
241
|
-
};
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Verify no leftover temp file
|
|
245
|
-
const filePath = path.join(tmpDir, 'principle_training_state.json');
|
|
246
|
-
expect(fs.existsSync(filePath + '.tmp')).toBe(false);
|
|
247
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
248
|
-
|
|
249
|
-
// File is valid JSON
|
|
250
|
-
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
251
|
-
expect(raw[TREE_NAMESPACE].principles['P-003']).toBeDefined();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('is compatible with low-level loadLedger', async () => {
|
|
255
|
-
await adapter.saveLedger(createEmptyStore());
|
|
256
|
-
|
|
257
|
-
await adapter.mutateLedger((store) => {
|
|
258
|
-
store.tree.principles['P-COMPAT'] = {
|
|
259
|
-
id: 'P-COMPAT',
|
|
260
|
-
version: 1,
|
|
261
|
-
text: 'Compatibility test',
|
|
262
|
-
triggerPattern: 'compat',
|
|
263
|
-
action: 'verify',
|
|
264
|
-
status: 'active',
|
|
265
|
-
priority: 'P1',
|
|
266
|
-
scope: 'general',
|
|
267
|
-
evaluability: 'deterministic',
|
|
268
|
-
valueScore: 10,
|
|
269
|
-
adherenceRate: 0.8,
|
|
270
|
-
painPreventedCount: 5,
|
|
271
|
-
derivedFromPainIds: ['pain-1'],
|
|
272
|
-
ruleIds: [],
|
|
273
|
-
conflictsWithPrincipleIds: [],
|
|
274
|
-
createdAt: '2026-04-17T00:00:00.000Z',
|
|
275
|
-
updatedAt: '2026-04-17T00:00:00.000Z',
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Load with the low-level function — should see the same data
|
|
280
|
-
const ledger = loadLedger(tmpDir);
|
|
281
|
-
expect(ledger.tree.principles['P-COMPAT']).toBeDefined();
|
|
282
|
-
expect(ledger.tree.principles['P-COMPAT'].text).toBe('Compatibility test');
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
formatMergeGateAuditReport,
|
|
7
|
-
runMergeGateAudit,
|
|
8
|
-
} from '../../src/core/merge-gate-audit.js';
|
|
9
|
-
import { createImplementationAssetDir, getImplementationAssetRoot } from '../../src/core/code-implementation-storage.js';
|
|
10
|
-
import { safeRmDir } from '../test-utils.js';
|
|
11
|
-
|
|
12
|
-
describe('merge-gate-audit', () => {
|
|
13
|
-
let tempDir: string;
|
|
14
|
-
let workspaceDir: string;
|
|
15
|
-
let stateDir: string;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-merge-gate-audit-'));
|
|
19
|
-
workspaceDir = path.join(tempDir, 'workspace');
|
|
20
|
-
stateDir = path.join(tempDir, '.state');
|
|
21
|
-
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
22
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
safeRmDir(tempDir);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('returns defer when audit surfaces are not populated yet', () => {
|
|
30
|
-
const report = runMergeGateAudit(workspaceDir, stateDir);
|
|
31
|
-
|
|
32
|
-
expect(report.overallStatus).toBe('defer');
|
|
33
|
-
expect(report.checks.find((check) => check.id === 'pain_flag_path_contract')?.status).toBe('pass');
|
|
34
|
-
expect(report.checks.find((check) => check.id === 'queue_path_contract')?.status).toBe('pass');
|
|
35
|
-
expect(report.checks.find((check) => check.id === 'replay_evidence_integrity')?.status).toBe('defer');
|
|
36
|
-
expect(report.counts.defer).toBeGreaterThan(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('blocks malformed replay reports that claim pass without evidence', () => {
|
|
40
|
-
createImplementationAssetDir(stateDir, 'IMPL-1', '1.0.0');
|
|
41
|
-
const replayDir = path.join(getImplementationAssetRoot(stateDir, 'IMPL-1'), 'replays');
|
|
42
|
-
fs.mkdirSync(replayDir, { recursive: true });
|
|
43
|
-
fs.writeFileSync(
|
|
44
|
-
path.join(replayDir, 'bad-report.json'),
|
|
45
|
-
JSON.stringify(
|
|
46
|
-
{
|
|
47
|
-
overallDecision: 'pass',
|
|
48
|
-
blockers: [],
|
|
49
|
-
generatedAt: '2026-04-12T09:00:00.000Z',
|
|
50
|
-
implementationId: 'IMPL-1',
|
|
51
|
-
evidenceSummary: {
|
|
52
|
-
evidenceStatus: 'empty',
|
|
53
|
-
totalSamples: 0,
|
|
54
|
-
classifiedCounts: {
|
|
55
|
-
painNegative: 0,
|
|
56
|
-
successPositive: 0,
|
|
57
|
-
principleAnchor: 0,
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
null,
|
|
62
|
-
2,
|
|
63
|
-
),
|
|
64
|
-
'utf-8',
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const report = runMergeGateAudit(workspaceDir, stateDir);
|
|
68
|
-
const replayCheck = report.checks.find((check) => check.id === 'replay_evidence_integrity');
|
|
69
|
-
|
|
70
|
-
expect(report.overallStatus).toBe('block');
|
|
71
|
-
expect(replayCheck?.status).toBe('block');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('blocks when replay reports are malformed', () => {
|
|
75
|
-
createImplementationAssetDir(stateDir, 'IMPL-BAD', '1.0.0');
|
|
76
|
-
const replayDir = path.join(getImplementationAssetRoot(stateDir, 'IMPL-BAD'), 'replays');
|
|
77
|
-
fs.mkdirSync(replayDir, { recursive: true });
|
|
78
|
-
fs.writeFileSync(
|
|
79
|
-
path.join(replayDir, 'malformed.json'),
|
|
80
|
-
'{bad json',
|
|
81
|
-
'utf-8',
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const report = runMergeGateAudit(workspaceDir, stateDir);
|
|
85
|
-
const replayCheck = report.checks.find((c) => c.id === 'replay_evidence_integrity');
|
|
86
|
-
const details = replayCheck?.details as Record<string, string[]> | undefined;
|
|
87
|
-
|
|
88
|
-
expect(report.overallStatus).toBe('block');
|
|
89
|
-
expect(replayCheck?.status).toBe('block');
|
|
90
|
-
expect(details?.malformedReports).toHaveLength(1);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('blocks when replay reports have invalid evidenceSummary shape', () => {
|
|
94
|
-
createImplementationAssetDir(stateDir, 'IMPL-NOEVID', '1.0.0');
|
|
95
|
-
const replayDir = path.join(getImplementationAssetRoot(stateDir, 'IMPL-NOEVID'), 'replays');
|
|
96
|
-
fs.mkdirSync(replayDir, { recursive: true });
|
|
97
|
-
fs.writeFileSync(
|
|
98
|
-
path.join(replayDir, 'bad-evidence.json'),
|
|
99
|
-
JSON.stringify({
|
|
100
|
-
overallDecision: 'pass',
|
|
101
|
-
blockers: [],
|
|
102
|
-
generatedAt: '2026-04-12T09:00:00.000Z',
|
|
103
|
-
implementationId: 'IMPL-NOEVID',
|
|
104
|
-
evidenceSummary: { evidenceStatus: 'observed' },
|
|
105
|
-
}),
|
|
106
|
-
'utf-8',
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const report = runMergeGateAudit(workspaceDir, stateDir);
|
|
110
|
-
const replayCheck = report.checks.find((c) => c.id === 'replay_evidence_integrity');
|
|
111
|
-
const details = replayCheck?.details as Record<string, string[]> | undefined;
|
|
112
|
-
|
|
113
|
-
expect(report.overallStatus).toBe('block');
|
|
114
|
-
expect(replayCheck?.status).toBe('block');
|
|
115
|
-
expect(details?.missingEvidenceSummary).toHaveLength(1);
|
|
116
|
-
});
|
|
117
|
-
});
|