principles-disciple 1.107.0 → 1.108.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.
Files changed (63) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +2 -2
  3. package/src/core/init.ts +3 -1
  4. package/src/core/workspace-dir-validation.ts +3 -3
  5. package/tests/core-anti-growth.test.ts +0 -13
  6. package/tests/hooks/prompt-characterization.test.ts +1 -11
  7. package/tests/hooks/prompt-diet.test.ts +3 -11
  8. package/tests/hooks/prompt-size-guard.test.ts +0 -10
  9. package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
  10. package/tests/index.test.ts +1 -1
  11. package/tests/runtime-v2-discovery-guard.test.ts +1 -2
  12. package/vitest.config.ts +2 -3
  13. package/vitest.unit.config.ts +12 -0
  14. package/src/core/evolution-hook.ts +0 -74
  15. package/src/core/file-storage-adapter.ts +0 -203
  16. package/src/core/merge-gate-audit.ts +0 -314
  17. package/src/core/pain-context-extractor.ts +0 -306
  18. package/src/core/pain-lifecycle.ts +0 -38
  19. package/src/core/pain-signal-adapter.ts +0 -42
  20. package/src/core/pain-signal.ts +0 -22
  21. package/src/core/principle-injector.ts +0 -84
  22. package/src/core/principle-tree-migration.ts +0 -196
  23. package/src/core/storage-adapter.ts +0 -65
  24. package/src/core/telemetry-event.ts +0 -109
  25. package/src/core/training-program.ts +0 -632
  26. package/src/core/workspace-dir-service.ts +0 -119
  27. package/src/hooks/lifecycle-routing.ts +0 -125
  28. package/src/service/event-log-auditor.ts +0 -284
  29. package/src/service/evolution-queue-lock.ts +0 -47
  30. package/src/service/failure-classifier.ts +0 -79
  31. package/src/service/internalization-trigger-adapter.ts +0 -302
  32. package/src/service/monitoring-query-service.ts +0 -67
  33. package/src/service/subagent-workflow/index.ts +0 -17
  34. package/src/tools/critique-prompt.ts +0 -1
  35. package/src/tools/model-index.ts +0 -1
  36. package/src/types/event-payload.ts +0 -16
  37. package/src/utils/glob-match.ts +0 -50
  38. package/src/utils/nlp.ts +0 -25
  39. package/src/utils/plugin-logger.ts +0 -97
  40. package/src/utils/subagent-probe.ts +0 -81
  41. package/tests/core/evolution-hook.test.ts +0 -123
  42. package/tests/core/file-storage-adapter.test.ts +0 -285
  43. package/tests/core/merge-gate-audit.test.ts +0 -117
  44. package/tests/core/pain-context-extractor.test.ts +0 -279
  45. package/tests/core/pain-lifecycle.test.ts +0 -38
  46. package/tests/core/pain-signal-adapter.test.ts +0 -116
  47. package/tests/core/pain-signal.test.ts +0 -190
  48. package/tests/core/principle-injector.test.ts +0 -90
  49. package/tests/core/principle-tree-migration.test.ts +0 -77
  50. package/tests/core/storage-conformance.test.ts +0 -429
  51. package/tests/core/telemetry-event.test.ts +0 -119
  52. package/tests/core/training-program.test.ts +0 -472
  53. package/tests/core/workspace-dir-service.test.ts +0 -68
  54. package/tests/core/workspace-dir-validation.test.ts +0 -143
  55. package/tests/integration/internalization-trigger-guard.test.ts +0 -69
  56. package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
  57. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
  58. package/tests/service/failure-classifier.test.ts +0 -171
  59. package/tests/service/internalization-trigger-adapter.test.ts +0 -251
  60. package/tests/service/monitoring-query-service.test.ts +0 -67
  61. package/tests/utils/nlp.test.ts +0 -35
  62. package/tests/utils/plugin-logger.test.ts +0 -156
  63. 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
- });