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