principles-disciple 1.14.0 → 1.16.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 +4 -2
- package/scripts/bootstrap-rules.mjs +66 -0
- package/scripts/validate-live-path.ts +356 -0
- package/src/core/bootstrap-rules.ts +177 -0
- package/src/core/principle-tree-migration.ts +196 -0
- package/src/service/evolution-worker.ts +81 -61
- package/src/service/monitoring-query-service.ts +277 -0
- package/src/service/nocturnal-service.ts +9 -1
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +10 -2
- package/tests/core/bootstrap-rules.test.ts +582 -0
- package/tests/core/principle-tree-migration.test.ts +77 -0
- package/tests/scripts/validate-live-path.test.ts +286 -0
- package/tests/service/evolution-worker.nocturnal.test.ts +208 -0
- package/tests/service/monitoring-query-service.test.ts +113 -0
- package/tests/service/nocturnal-runtime-hardening.test.ts +85 -0
- package/ui/src/charts.tsx +4 -1
- package/ui/src/pages/ThinkingModelsPage.tsx +9 -1
|
@@ -1174,7 +1174,15 @@ async function executeNocturnalReflectionWithAdapter(
|
|
|
1174
1174
|
quotaCheckPassed: true,
|
|
1175
1175
|
},
|
|
1176
1176
|
};
|
|
1177
|
-
diagnostics.idle = {
|
|
1177
|
+
diagnostics.idle = {
|
|
1178
|
+
isIdle: true,
|
|
1179
|
+
mostRecentActivityAt: 0,
|
|
1180
|
+
idleForMs: 0,
|
|
1181
|
+
userActiveSessions: 0,
|
|
1182
|
+
abandonedSessionIds: [],
|
|
1183
|
+
trajectoryGuardrailConfirmsIdle: true,
|
|
1184
|
+
reason: 'selector skipped (override provided)',
|
|
1185
|
+
};
|
|
1178
1186
|
} else {
|
|
1179
1187
|
// Normal Selector path
|
|
1180
1188
|
const extractor = createNocturnalTrajectoryExtractor(workspaceDir, stateDir);
|
|
@@ -234,6 +234,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
234
234
|
// When principleId is provided, we pass it as principleIdOverride to skip Selector.
|
|
235
235
|
// When principleId is missing, Selector will choose a principle from training store.
|
|
236
236
|
this.logger.info(`[PD:NocturnalWorkflow] Calling executeNocturnalReflectionAsync for full pipeline (principleId=${principleId ?? 'auto-select'})`);
|
|
237
|
+
const pipelineStart = Date.now();
|
|
237
238
|
|
|
238
239
|
// #213: Wrap fire-and-forget Promise with .catch() to prevent
|
|
239
240
|
// unhandled promise rejections if anything throws outside the try-catch
|
|
@@ -263,25 +264,32 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
263
264
|
);
|
|
264
265
|
|
|
265
266
|
if (result.success) {
|
|
267
|
+
this.logger.info(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline step 4/4: Completed successfully, artifactId=${result.diagnostics?.persistedPath}`);
|
|
268
|
+
this.store.updateWorkflowState(workflowId, 'completed');
|
|
266
269
|
this.store.recordEvent(workflowId, 'nocturnal_completed', null, 'completed', 'Full pipeline completed via executeNocturnalReflectionAsync', {
|
|
267
270
|
artifactId: result.diagnostics?.persistedPath,
|
|
268
271
|
});
|
|
269
272
|
this.completedWorkflows.set(workflowId, Date.now());
|
|
270
273
|
} else {
|
|
271
274
|
const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
|
|
275
|
+
this.logger.warn(`[PD:NocturnalWorkflow] [${workflowId}] Pipeline failed: reason=${reason}, noTargetSelected=${result.noTargetSelected}, skipReason=${result.skipReason ?? 'none'}, validationFailures=${result.validationFailures?.length ?? 0}`);
|
|
276
|
+
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
272
277
|
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', reason, {
|
|
273
278
|
failures: result.validationFailures,
|
|
274
279
|
skipReason: result.skipReason,
|
|
275
280
|
});
|
|
276
281
|
}
|
|
277
282
|
} catch (err) {
|
|
278
|
-
|
|
283
|
+
const errDuration = Date.now() - pipelineStart;
|
|
284
|
+
this.logger.error(`[PD:NocturnalWorkflow] [${workflowId}] executeNocturnalReflectionAsync threw after ${errDuration}ms: ${String(err)}`);
|
|
285
|
+
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
279
286
|
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
|
|
280
287
|
}
|
|
281
288
|
}).catch((err) => {
|
|
282
|
-
// #213: Outer catch
|
|
289
|
+
// #213: Outer catch – catches errors from the fire-and-forget promise
|
|
283
290
|
// itself (e.g., if the async callback throws before entering try).
|
|
284
291
|
this.logger.error(`[PD:NocturnalWorkflow] Unhandled error in async pipeline for ${workflowId}: ${String(err)}`);
|
|
292
|
+
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
285
293
|
this.store.recordEvent(workflowId, 'nocturnal_failed', null, 'terminal_error', String(err), { workflowId });
|
|
286
294
|
});
|
|
287
295
|
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap Rules Tests (Phase 17)
|
|
3
|
+
*
|
|
4
|
+
* TDD test suite for minimal rule bootstrap functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import { safeRmDir } from '../test-utils.js';
|
|
11
|
+
import {
|
|
12
|
+
bootstrapRules,
|
|
13
|
+
selectPrinciplesForBootstrap,
|
|
14
|
+
validateBootstrap,
|
|
15
|
+
} from '../../src/core/bootstrap-rules.js';
|
|
16
|
+
import {
|
|
17
|
+
createRule,
|
|
18
|
+
loadLedger,
|
|
19
|
+
saveLedger,
|
|
20
|
+
type HybridLedgerStore,
|
|
21
|
+
type LedgerPrinciple,
|
|
22
|
+
type LedgerRule,
|
|
23
|
+
} from '../../src/core/principle-tree-ledger.js';
|
|
24
|
+
import type { LegacyPrincipleTrainingStore, LegacyPrincipleTrainingState } from '../../src/core/principle-tree-ledger.js';
|
|
25
|
+
|
|
26
|
+
describe('bootstrap-rules', () => {
|
|
27
|
+
let tempDir: string;
|
|
28
|
+
let stateDir: string;
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
tempDir = fs.mkdtempSync(path.join(process.env.TMP || '/tmp', 'pd-bootstrap-'));
|
|
32
|
+
stateDir = path.join(tempDir, '.state');
|
|
33
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
safeRmDir(tempDir);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
// Clean up state file after each test
|
|
42
|
+
const stateFile = path.join(stateDir, 'principle_training_state.json');
|
|
43
|
+
if (fs.existsSync(stateFile)) {
|
|
44
|
+
fs.unlinkSync(stateFile);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Helper: Create a minimal ledger principle
|
|
49
|
+
function createLedgerPrinciple(principleId: string, overrides: Partial<LedgerPrinciple> = {}): LedgerPrinciple {
|
|
50
|
+
return {
|
|
51
|
+
id: principleId,
|
|
52
|
+
version: 1,
|
|
53
|
+
text: `Test principle ${principleId}`,
|
|
54
|
+
triggerPattern: 'test',
|
|
55
|
+
action: 'test action',
|
|
56
|
+
status: 'active',
|
|
57
|
+
priority: 'P1',
|
|
58
|
+
scope: 'general',
|
|
59
|
+
evaluability: 'deterministic',
|
|
60
|
+
valueScore: 0,
|
|
61
|
+
adherenceRate: 0,
|
|
62
|
+
painPreventedCount: 0,
|
|
63
|
+
derivedFromPainIds: [],
|
|
64
|
+
ruleIds: [],
|
|
65
|
+
conflictsWithPrincipleIds: [],
|
|
66
|
+
createdAt: '2026-04-10T00:00:00.000Z',
|
|
67
|
+
updatedAt: '2026-04-10T00:00:00.000Z',
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Helper: Setup ledger with training store and principles
|
|
73
|
+
function setupLedger(trainingStates: LegacyPrincipleTrainingState[], principles: LedgerPrinciple[]): void {
|
|
74
|
+
const trainingStore: LegacyPrincipleTrainingStore = {};
|
|
75
|
+
for (const state of trainingStates) {
|
|
76
|
+
trainingStore[state.principleId] = state;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tree = {
|
|
80
|
+
principles: {} as Record<string, LedgerPrinciple>,
|
|
81
|
+
rules: {} as Record<string, LedgerRule>,
|
|
82
|
+
implementations: {},
|
|
83
|
+
metrics: {},
|
|
84
|
+
lastUpdated: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const principle of principles) {
|
|
88
|
+
tree.principles[principle.id] = principle;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const store: HybridLedgerStore = {
|
|
92
|
+
trainingStore,
|
|
93
|
+
tree,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
saveLedger(stateDir, store);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('selectPrinciplesForBootstrap', () => {
|
|
100
|
+
it('selects deterministic principles sorted by violation count', () => {
|
|
101
|
+
// Setup: 4 principles (3 deterministic with violations 10, 5, 1; 1 manual_only with 100)
|
|
102
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
103
|
+
{
|
|
104
|
+
principleId: 'P_001',
|
|
105
|
+
evaluability: 'deterministic',
|
|
106
|
+
applicableOpportunityCount: 20,
|
|
107
|
+
observedViolationCount: 10,
|
|
108
|
+
complianceRate: 0.5,
|
|
109
|
+
violationTrend: 0,
|
|
110
|
+
generatedSampleCount: 0,
|
|
111
|
+
approvedSampleCount: 0,
|
|
112
|
+
includedTrainRunIds: [],
|
|
113
|
+
deployedCheckpointIds: [],
|
|
114
|
+
internalizationStatus: 'prompt_only',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
principleId: 'P_002',
|
|
118
|
+
evaluability: 'deterministic',
|
|
119
|
+
applicableOpportunityCount: 15,
|
|
120
|
+
observedViolationCount: 5,
|
|
121
|
+
complianceRate: 0.6,
|
|
122
|
+
violationTrend: 0,
|
|
123
|
+
generatedSampleCount: 0,
|
|
124
|
+
approvedSampleCount: 0,
|
|
125
|
+
includedTrainRunIds: [],
|
|
126
|
+
deployedCheckpointIds: [],
|
|
127
|
+
internalizationStatus: 'prompt_only',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
principleId: 'P_003',
|
|
131
|
+
evaluability: 'deterministic',
|
|
132
|
+
applicableOpportunityCount: 10,
|
|
133
|
+
observedViolationCount: 1,
|
|
134
|
+
complianceRate: 0.7,
|
|
135
|
+
violationTrend: 0,
|
|
136
|
+
generatedSampleCount: 0,
|
|
137
|
+
approvedSampleCount: 0,
|
|
138
|
+
includedTrainRunIds: [],
|
|
139
|
+
deployedCheckpointIds: [],
|
|
140
|
+
internalizationStatus: 'prompt_only',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
principleId: 'P_004',
|
|
144
|
+
evaluability: 'manual_only',
|
|
145
|
+
applicableOpportunityCount: 100,
|
|
146
|
+
observedViolationCount: 100,
|
|
147
|
+
complianceRate: 0.5,
|
|
148
|
+
violationTrend: 0,
|
|
149
|
+
generatedSampleCount: 0,
|
|
150
|
+
approvedSampleCount: 0,
|
|
151
|
+
includedTrainRunIds: [],
|
|
152
|
+
deployedCheckpointIds: [],
|
|
153
|
+
internalizationStatus: 'prompt_only',
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId, { evaluability: s.evaluability }));
|
|
158
|
+
setupLedger(trainingStates, principles);
|
|
159
|
+
|
|
160
|
+
// Act: Select top 2
|
|
161
|
+
const selected = selectPrinciplesForBootstrap(stateDir, 2);
|
|
162
|
+
|
|
163
|
+
// Assert: Should get P_001 (10 violations) and P_002 (5 violations)
|
|
164
|
+
expect(selected).toHaveLength(2);
|
|
165
|
+
expect(selected).toContain('P_001');
|
|
166
|
+
expect(selected).toContain('P_002');
|
|
167
|
+
expect(selected).not.toContain('P_003'); // Only 1 violation, not in top 2
|
|
168
|
+
expect(selected).not.toContain('P_004'); // manual_only, excluded
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('falls back to all deterministic when violation data is sparse', () => {
|
|
172
|
+
// Setup: 2 deterministic principles with 0 violations
|
|
173
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
174
|
+
{
|
|
175
|
+
principleId: 'P_001',
|
|
176
|
+
evaluability: 'deterministic',
|
|
177
|
+
applicableOpportunityCount: 0,
|
|
178
|
+
observedViolationCount: 0,
|
|
179
|
+
complianceRate: 1,
|
|
180
|
+
violationTrend: 0,
|
|
181
|
+
generatedSampleCount: 0,
|
|
182
|
+
approvedSampleCount: 0,
|
|
183
|
+
includedTrainRunIds: [],
|
|
184
|
+
deployedCheckpointIds: [],
|
|
185
|
+
internalizationStatus: 'prompt_only',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
principleId: 'P_002',
|
|
189
|
+
evaluability: 'deterministic',
|
|
190
|
+
applicableOpportunityCount: 0,
|
|
191
|
+
observedViolationCount: 0,
|
|
192
|
+
complianceRate: 1,
|
|
193
|
+
violationTrend: 0,
|
|
194
|
+
generatedSampleCount: 0,
|
|
195
|
+
approvedSampleCount: 0,
|
|
196
|
+
includedTrainRunIds: [],
|
|
197
|
+
deployedCheckpointIds: [],
|
|
198
|
+
internalizationStatus: 'prompt_only',
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
203
|
+
setupLedger(trainingStates, principles);
|
|
204
|
+
|
|
205
|
+
// Act: Select up to 3
|
|
206
|
+
const selected = selectPrinciplesForBootstrap(stateDir, 3);
|
|
207
|
+
|
|
208
|
+
// Assert: Both deterministic principles returned
|
|
209
|
+
expect(selected).toHaveLength(2);
|
|
210
|
+
expect(selected).toContain('P_001');
|
|
211
|
+
expect(selected).toContain('P_002');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws when no deterministic principles exist', () => {
|
|
215
|
+
// Setup: Only manual_only principles
|
|
216
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
217
|
+
{
|
|
218
|
+
principleId: 'P_001',
|
|
219
|
+
evaluability: 'manual_only',
|
|
220
|
+
applicableOpportunityCount: 10,
|
|
221
|
+
observedViolationCount: 5,
|
|
222
|
+
complianceRate: 0.5,
|
|
223
|
+
violationTrend: 0,
|
|
224
|
+
generatedSampleCount: 0,
|
|
225
|
+
approvedSampleCount: 0,
|
|
226
|
+
includedTrainRunIds: [],
|
|
227
|
+
deployedCheckpointIds: [],
|
|
228
|
+
internalizationStatus: 'prompt_only',
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId, { evaluability: s.evaluability }));
|
|
233
|
+
setupLedger(trainingStates, principles);
|
|
234
|
+
|
|
235
|
+
// Act & Assert: Should throw
|
|
236
|
+
expect(() => selectPrinciplesForBootstrap(stateDir, 3)).toThrow('No deterministic principles');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('bootstrapRules', () => {
|
|
241
|
+
it('creates stub rules with correct ID format and fields', () => {
|
|
242
|
+
// Setup: 2 deterministic principles
|
|
243
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
244
|
+
{
|
|
245
|
+
principleId: 'P_001',
|
|
246
|
+
evaluability: 'deterministic',
|
|
247
|
+
applicableOpportunityCount: 10,
|
|
248
|
+
observedViolationCount: 5,
|
|
249
|
+
complianceRate: 0.5,
|
|
250
|
+
violationTrend: 0,
|
|
251
|
+
generatedSampleCount: 0,
|
|
252
|
+
approvedSampleCount: 0,
|
|
253
|
+
includedTrainRunIds: [],
|
|
254
|
+
deployedCheckpointIds: [],
|
|
255
|
+
internalizationStatus: 'prompt_only',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
principleId: 'P_002',
|
|
259
|
+
evaluability: 'deterministic',
|
|
260
|
+
applicableOpportunityCount: 8,
|
|
261
|
+
observedViolationCount: 3,
|
|
262
|
+
complianceRate: 0.6,
|
|
263
|
+
violationTrend: 0,
|
|
264
|
+
generatedSampleCount: 0,
|
|
265
|
+
approvedSampleCount: 0,
|
|
266
|
+
includedTrainRunIds: [],
|
|
267
|
+
deployedCheckpointIds: [],
|
|
268
|
+
internalizationStatus: 'prompt_only',
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
273
|
+
setupLedger(trainingStates, principles);
|
|
274
|
+
|
|
275
|
+
// Act: Bootstrap 2 principles
|
|
276
|
+
const results = bootstrapRules(stateDir, 2);
|
|
277
|
+
|
|
278
|
+
// Assert: Check results
|
|
279
|
+
expect(results).toHaveLength(2);
|
|
280
|
+
|
|
281
|
+
const ledger = loadLedger(stateDir);
|
|
282
|
+
|
|
283
|
+
for (const result of results) {
|
|
284
|
+
expect(result.status).toBe('created');
|
|
285
|
+
expect(result.ruleId).toBe(`${result.principleId}_stub_bootstrap`);
|
|
286
|
+
|
|
287
|
+
const rule = ledger.tree.rules[result.ruleId];
|
|
288
|
+
expect(rule).toBeDefined();
|
|
289
|
+
expect(rule?.id).toBe(result.ruleId);
|
|
290
|
+
expect(rule?.type).toBe('hook');
|
|
291
|
+
expect(rule?.triggerCondition).toBe('stub: bootstrap placeholder');
|
|
292
|
+
expect(rule?.enforcement).toBe('warn');
|
|
293
|
+
expect(rule?.action).toBe('allow (stub)');
|
|
294
|
+
expect(rule?.status).toBe('proposed');
|
|
295
|
+
expect(rule?.coverageRate).toBe(0);
|
|
296
|
+
expect(rule?.falsePositiveRate).toBe(0);
|
|
297
|
+
expect(rule?.principleId).toBe(result.principleId);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('links principles to rules via suggestedRules array', () => {
|
|
302
|
+
// Setup: 2 deterministic principles
|
|
303
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
304
|
+
{
|
|
305
|
+
principleId: 'P_001',
|
|
306
|
+
evaluability: 'deterministic',
|
|
307
|
+
applicableOpportunityCount: 10,
|
|
308
|
+
observedViolationCount: 5,
|
|
309
|
+
complianceRate: 0.5,
|
|
310
|
+
violationTrend: 0,
|
|
311
|
+
generatedSampleCount: 0,
|
|
312
|
+
approvedSampleCount: 0,
|
|
313
|
+
includedTrainRunIds: [],
|
|
314
|
+
deployedCheckpointIds: [],
|
|
315
|
+
internalizationStatus: 'prompt_only',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
principleId: 'P_002',
|
|
319
|
+
evaluability: 'deterministic',
|
|
320
|
+
applicableOpportunityCount: 8,
|
|
321
|
+
observedViolationCount: 3,
|
|
322
|
+
complianceRate: 0.6,
|
|
323
|
+
violationTrend: 0,
|
|
324
|
+
generatedSampleCount: 0,
|
|
325
|
+
approvedSampleCount: 0,
|
|
326
|
+
includedTrainRunIds: [],
|
|
327
|
+
deployedCheckpointIds: [],
|
|
328
|
+
internalizationStatus: 'prompt_only',
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
333
|
+
setupLedger(trainingStates, principles);
|
|
334
|
+
|
|
335
|
+
// Act: Bootstrap
|
|
336
|
+
const results = bootstrapRules(stateDir, 2);
|
|
337
|
+
|
|
338
|
+
// Assert: Check suggestedRules linkage
|
|
339
|
+
const ledger = loadLedger(stateDir);
|
|
340
|
+
|
|
341
|
+
for (const result of results) {
|
|
342
|
+
const principle = ledger.tree.principles[result.principleId];
|
|
343
|
+
expect(principle).toBeDefined();
|
|
344
|
+
expect(principle?.suggestedRules).toContain(result.ruleId);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('is idempotent - second run skips existing rules', () => {
|
|
349
|
+
// Setup: 2 deterministic principles
|
|
350
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
351
|
+
{
|
|
352
|
+
principleId: 'P_001',
|
|
353
|
+
evaluability: 'deterministic',
|
|
354
|
+
applicableOpportunityCount: 10,
|
|
355
|
+
observedViolationCount: 5,
|
|
356
|
+
complianceRate: 0.5,
|
|
357
|
+
violationTrend: 0,
|
|
358
|
+
generatedSampleCount: 0,
|
|
359
|
+
approvedSampleCount: 0,
|
|
360
|
+
includedTrainRunIds: [],
|
|
361
|
+
deployedCheckpointIds: [],
|
|
362
|
+
internalizationStatus: 'prompt_only',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
principleId: 'P_002',
|
|
366
|
+
evaluability: 'deterministic',
|
|
367
|
+
applicableOpportunityCount: 8,
|
|
368
|
+
observedViolationCount: 3,
|
|
369
|
+
complianceRate: 0.6,
|
|
370
|
+
violationTrend: 0,
|
|
371
|
+
generatedSampleCount: 0,
|
|
372
|
+
approvedSampleCount: 0,
|
|
373
|
+
includedTrainRunIds: [],
|
|
374
|
+
deployedCheckpointIds: [],
|
|
375
|
+
internalizationStatus: 'prompt_only',
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
380
|
+
setupLedger(trainingStates, principles);
|
|
381
|
+
|
|
382
|
+
// Act: First bootstrap
|
|
383
|
+
const firstResults = bootstrapRules(stateDir, 2);
|
|
384
|
+
expect(firstResults.every((r) => r.status === 'created')).toBe(true);
|
|
385
|
+
|
|
386
|
+
// Act: Second bootstrap
|
|
387
|
+
const secondResults = bootstrapRules(stateDir, 2);
|
|
388
|
+
|
|
389
|
+
// Assert: All skipped
|
|
390
|
+
expect(secondResults).toHaveLength(2);
|
|
391
|
+
expect(secondResults.every((r) => r.status === 'skipped')).toBe(true);
|
|
392
|
+
|
|
393
|
+
// Verify ledger unchanged
|
|
394
|
+
const ledger = loadLedger(stateDir);
|
|
395
|
+
expect(Object.keys(ledger.tree.rules)).toHaveLength(2);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('limits bootstrap to requested count', () => {
|
|
399
|
+
// Setup: 5 deterministic principles
|
|
400
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
401
|
+
{
|
|
402
|
+
principleId: 'P_001',
|
|
403
|
+
evaluability: 'deterministic',
|
|
404
|
+
applicableOpportunityCount: 20,
|
|
405
|
+
observedViolationCount: 10,
|
|
406
|
+
complianceRate: 0.5,
|
|
407
|
+
violationTrend: 0,
|
|
408
|
+
generatedSampleCount: 0,
|
|
409
|
+
approvedSampleCount: 0,
|
|
410
|
+
includedTrainRunIds: [],
|
|
411
|
+
deployedCheckpointIds: [],
|
|
412
|
+
internalizationStatus: 'prompt_only',
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
principleId: 'P_002',
|
|
416
|
+
evaluability: 'deterministic',
|
|
417
|
+
applicableOpportunityCount: 15,
|
|
418
|
+
observedViolationCount: 8,
|
|
419
|
+
complianceRate: 0.6,
|
|
420
|
+
violationTrend: 0,
|
|
421
|
+
generatedSampleCount: 0,
|
|
422
|
+
approvedSampleCount: 0,
|
|
423
|
+
includedTrainRunIds: [],
|
|
424
|
+
deployedCheckpointIds: [],
|
|
425
|
+
internalizationStatus: 'prompt_only',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
principleId: 'P_003',
|
|
429
|
+
evaluability: 'deterministic',
|
|
430
|
+
applicableOpportunityCount: 12,
|
|
431
|
+
observedViolationCount: 6,
|
|
432
|
+
complianceRate: 0.7,
|
|
433
|
+
violationTrend: 0,
|
|
434
|
+
generatedSampleCount: 0,
|
|
435
|
+
approvedSampleCount: 0,
|
|
436
|
+
includedTrainRunIds: [],
|
|
437
|
+
deployedCheckpointIds: [],
|
|
438
|
+
internalizationStatus: 'prompt_only',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
principleId: 'P_004',
|
|
442
|
+
evaluability: 'deterministic',
|
|
443
|
+
applicableOpportunityCount: 10,
|
|
444
|
+
observedViolationCount: 4,
|
|
445
|
+
complianceRate: 0.8,
|
|
446
|
+
violationTrend: 0,
|
|
447
|
+
generatedSampleCount: 0,
|
|
448
|
+
approvedSampleCount: 0,
|
|
449
|
+
includedTrainRunIds: [],
|
|
450
|
+
deployedCheckpointIds: [],
|
|
451
|
+
internalizationStatus: 'prompt_only',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
principleId: 'P_005',
|
|
455
|
+
evaluability: 'deterministic',
|
|
456
|
+
applicableOpportunityCount: 5,
|
|
457
|
+
observedViolationCount: 2,
|
|
458
|
+
complianceRate: 0.9,
|
|
459
|
+
violationTrend: 0,
|
|
460
|
+
generatedSampleCount: 0,
|
|
461
|
+
approvedSampleCount: 0,
|
|
462
|
+
includedTrainRunIds: [],
|
|
463
|
+
deployedCheckpointIds: [],
|
|
464
|
+
internalizationStatus: 'prompt_only',
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
469
|
+
setupLedger(trainingStates, principles);
|
|
470
|
+
|
|
471
|
+
// Act: Bootstrap only 3
|
|
472
|
+
const results = bootstrapRules(stateDir, 3);
|
|
473
|
+
|
|
474
|
+
// Assert: Only 3 rules created
|
|
475
|
+
expect(results).toHaveLength(3);
|
|
476
|
+
expect(results.every((r) => r.status === 'created')).toBe(true);
|
|
477
|
+
|
|
478
|
+
// Verify only top 3 by violation count
|
|
479
|
+
const ledger = loadLedger(stateDir);
|
|
480
|
+
expect(Object.keys(ledger.tree.rules)).toHaveLength(3);
|
|
481
|
+
expect(ledger.tree.rules['P_001_stub_bootstrap']).toBeDefined();
|
|
482
|
+
expect(ledger.tree.rules['P_002_stub_bootstrap']).toBeDefined();
|
|
483
|
+
expect(ledger.tree.rules['P_003_stub_bootstrap']).toBeDefined();
|
|
484
|
+
expect(ledger.tree.rules['P_004_stub_bootstrap']).toBeUndefined();
|
|
485
|
+
expect(ledger.tree.rules['P_005_stub_bootstrap']).toBeUndefined();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('throws when no deterministic principles exist', () => {
|
|
489
|
+
// Setup: Only manual_only principles
|
|
490
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
491
|
+
{
|
|
492
|
+
principleId: 'P_001',
|
|
493
|
+
evaluability: 'manual_only',
|
|
494
|
+
applicableOpportunityCount: 10,
|
|
495
|
+
observedViolationCount: 5,
|
|
496
|
+
complianceRate: 0.5,
|
|
497
|
+
violationTrend: 0,
|
|
498
|
+
generatedSampleCount: 0,
|
|
499
|
+
approvedSampleCount: 0,
|
|
500
|
+
includedTrainRunIds: [],
|
|
501
|
+
deployedCheckpointIds: [],
|
|
502
|
+
internalizationStatus: 'prompt_only',
|
|
503
|
+
},
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId, { evaluability: s.evaluability }));
|
|
507
|
+
setupLedger(trainingStates, principles);
|
|
508
|
+
|
|
509
|
+
// Act & Assert: Should throw
|
|
510
|
+
expect(() => bootstrapRules(stateDir, 3)).toThrow('No deterministic principles');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('validateBootstrap', () => {
|
|
515
|
+
it('returns true for correctly bootstrapped state', () => {
|
|
516
|
+
// Setup: Bootstrap 1 principle
|
|
517
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
518
|
+
{
|
|
519
|
+
principleId: 'P_001',
|
|
520
|
+
evaluability: 'deterministic',
|
|
521
|
+
applicableOpportunityCount: 10,
|
|
522
|
+
observedViolationCount: 5,
|
|
523
|
+
complianceRate: 0.5,
|
|
524
|
+
violationTrend: 0,
|
|
525
|
+
generatedSampleCount: 0,
|
|
526
|
+
approvedSampleCount: 0,
|
|
527
|
+
includedTrainRunIds: [],
|
|
528
|
+
deployedCheckpointIds: [],
|
|
529
|
+
internalizationStatus: 'prompt_only',
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
534
|
+
setupLedger(trainingStates, principles);
|
|
535
|
+
|
|
536
|
+
const results = bootstrapRules(stateDir, 1);
|
|
537
|
+
const bootstrappedIds = results.map((r) => r.principleId);
|
|
538
|
+
|
|
539
|
+
// Act: Validate
|
|
540
|
+
const valid = validateBootstrap(stateDir, bootstrappedIds);
|
|
541
|
+
|
|
542
|
+
// Assert: Should pass
|
|
543
|
+
expect(valid).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('throws when suggestedRules is empty', () => {
|
|
547
|
+
// Setup: Bootstrap 1 principle
|
|
548
|
+
const trainingStates: LegacyPrincipleTrainingState[] = [
|
|
549
|
+
{
|
|
550
|
+
principleId: 'P_001',
|
|
551
|
+
evaluability: 'deterministic',
|
|
552
|
+
applicableOpportunityCount: 10,
|
|
553
|
+
observedViolationCount: 5,
|
|
554
|
+
complianceRate: 0.5,
|
|
555
|
+
violationTrend: 0,
|
|
556
|
+
generatedSampleCount: 0,
|
|
557
|
+
approvedSampleCount: 0,
|
|
558
|
+
includedTrainRunIds: [],
|
|
559
|
+
deployedCheckpointIds: [],
|
|
560
|
+
internalizationStatus: 'prompt_only',
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
const principles = trainingStates.map((s) => createLedgerPrinciple(s.principleId));
|
|
565
|
+
setupLedger(trainingStates, principles);
|
|
566
|
+
|
|
567
|
+
const results = bootstrapRules(stateDir, 1);
|
|
568
|
+
const bootstrappedIds = results.map((r) => r.principleId);
|
|
569
|
+
|
|
570
|
+
// Manually clear suggestedRules
|
|
571
|
+
const ledger = loadLedger(stateDir);
|
|
572
|
+
const principle = ledger.tree.principles[bootstrappedIds[0]];
|
|
573
|
+
if (principle) {
|
|
574
|
+
principle.suggestedRules = [];
|
|
575
|
+
saveLedger(stateDir, ledger);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Act & Assert: Should throw
|
|
579
|
+
expect(() => validateBootstrap(stateDir, bootstrappedIds)).toThrow();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|