principles-disciple 1.145.0 → 1.145.1
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/dist/core/pain.d.ts +0 -1
- package/dist/core/pain.js +0 -24
- package/dist/hooks/after-tool-call-helpers.d.ts +3 -1
- package/dist/hooks/after-tool-call-helpers.js +6 -2
- package/dist/hooks/llm.js +14 -2
- package/dist/hooks/pain.d.ts +3 -1
- package/dist/hooks/pain.js +11 -4
- package/dist/hooks/prompt.js +34 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/core/observability.d.ts +0 -40
- package/dist/core/observability.js +0 -164
package/dist/core/pain.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export declare function computePainScore(rc: number, isSpiral: boolean, missingTestCommand: boolean, softScore: number, projectDir?: string): number;
|
|
2
|
-
export declare function painSeverityLabel(painScore: number, isSpiral?: boolean, projectDir?: string): string;
|
|
3
2
|
/**
|
|
4
3
|
* Track principle value metrics when a pain signal is written.
|
|
5
4
|
* This is observation-only — it does NOT affect the pain flag write flow.
|
package/dist/core/pain.js
CHANGED
|
@@ -20,30 +20,6 @@ export function computePainScore(rc, isSpiral, missingTestCommand, softScore, pr
|
|
|
20
20
|
}
|
|
21
21
|
return Math.min(100, score);
|
|
22
22
|
}
|
|
23
|
-
export function painSeverityLabel(painScore, isSpiral = false, projectDir) {
|
|
24
|
-
if (isSpiral) {
|
|
25
|
-
return "critical";
|
|
26
|
-
}
|
|
27
|
-
const stateDir = projectDir ? resolvePdPath(projectDir, 'STATE_DIR') : undefined;
|
|
28
|
-
const config = stateDir ? ConfigService.get(stateDir) : null;
|
|
29
|
-
const thresholds = config ? config.get('severity_thresholds') : {
|
|
30
|
-
high: 70,
|
|
31
|
-
medium: 40,
|
|
32
|
-
low: 20
|
|
33
|
-
};
|
|
34
|
-
if (painScore >= thresholds.high) {
|
|
35
|
-
return "high";
|
|
36
|
-
}
|
|
37
|
-
else if (painScore >= thresholds.medium) {
|
|
38
|
-
return "medium";
|
|
39
|
-
}
|
|
40
|
-
else if (painScore >= thresholds.low) {
|
|
41
|
-
return "low";
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
return "info";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
23
|
/**
|
|
48
24
|
* Track principle value metrics when a pain signal is written.
|
|
49
25
|
* This is observation-only — it does NOT affect the pain flag write flow.
|
|
@@ -86,7 +86,9 @@ export declare function evaluatePainAdmissionForToolCall(event: PluginHookAfterT
|
|
|
86
86
|
*
|
|
87
87
|
* Only called when the admission decision is 'admitted'.
|
|
88
88
|
*/
|
|
89
|
-
export declare function emitPainIfAdmitted(wctx: WorkspaceContext, event: PluginHookAfterToolCallEvent, observation: ToolCallObservation, outcome: ToolCallOutcome, admission: PainAdmissionDecision, sessionId: string, agentId: string | undefined, workspaceDir: string, emitPainDetectedEvent: (wctx: WorkspaceContext, event: import('../core/evolution-types.js').EvolutionLoopEvent
|
|
89
|
+
export declare function emitPainIfAdmitted(wctx: WorkspaceContext, event: PluginHookAfterToolCallEvent, observation: ToolCallObservation, outcome: ToolCallOutcome, admission: PainAdmissionDecision, sessionId: string, agentId: string | undefined, workspaceDir: string, emitPainDetectedEvent: (wctx: WorkspaceContext, event: import('../core/evolution-types.js').EvolutionLoopEvent, options?: {
|
|
90
|
+
recordObservability?: boolean;
|
|
91
|
+
}) => Promise<void>): void;
|
|
90
92
|
export { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
91
93
|
/**
|
|
92
94
|
* Reset trigger cooldown state (for tests).
|
|
@@ -417,6 +417,9 @@ export function emitPainIfAdmitted(wctx, event, observation, outcome, admission,
|
|
|
417
417
|
if (!admission.admitted)
|
|
418
418
|
return;
|
|
419
419
|
const failureSource = outcome.failureSource ?? 'tool_failure';
|
|
420
|
+
// PRI-453: Generate painId early so it can be passed as canonicalPainId to
|
|
421
|
+
// recordPainEvent, enabling dedup between legacy trajectory write and SDK path.
|
|
422
|
+
const painId = `pain_${Date.now()}_${observation.errorHash.slice(0, 8)}`;
|
|
420
423
|
// Record to trajectory before Runtime V2 diagnosis
|
|
421
424
|
wctx.trajectory?.recordPainEvent({
|
|
422
425
|
sessionId,
|
|
@@ -426,6 +429,7 @@ export function emitPainIfAdmitted(wctx, event, observation, outcome, admission,
|
|
|
426
429
|
severity: observation.painScore >= 70 ? 'severe' : observation.painScore >= 40 ? 'moderate' : 'mild',
|
|
427
430
|
origin: 'system_infer',
|
|
428
431
|
text: sanitizeForEvidence(observation.params.text ?? observation.params.content, workspaceDir) || undefined,
|
|
432
|
+
canonicalPainId: painId,
|
|
429
433
|
});
|
|
430
434
|
// Observe: track which principles would have prevented this pain (observation-only)
|
|
431
435
|
try {
|
|
@@ -481,7 +485,7 @@ export function emitPainIfAdmitted(wctx, event, observation, outcome, admission,
|
|
|
481
485
|
sessionId,
|
|
482
486
|
});
|
|
483
487
|
// Create painId inline (matches original createPainId)
|
|
484
|
-
|
|
488
|
+
// PRI-453: painId is now generated early (above) to pass as canonicalPainId.
|
|
485
489
|
emitPainDetectedEvent(wctx, {
|
|
486
490
|
ts: new Date().toISOString(),
|
|
487
491
|
type: 'pain_detected',
|
|
@@ -497,7 +501,7 @@ export function emitPainIfAdmitted(wctx, event, observation, outcome, admission,
|
|
|
497
501
|
provenance: 'automatic_hook',
|
|
498
502
|
evidence: buildTrajectoryEvidence(wctx, sessionId),
|
|
499
503
|
},
|
|
500
|
-
});
|
|
504
|
+
}, { recordObservability: false });
|
|
501
505
|
}
|
|
502
506
|
// ── Shared Helpers ──────────────────────────────────────────────────────────
|
|
503
507
|
export { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
package/dist/hooks/llm.js
CHANGED
|
@@ -266,6 +266,18 @@ export function handleLlmOutput(event, ctx) {
|
|
|
266
266
|
reason: matchedReason,
|
|
267
267
|
isRisky: false
|
|
268
268
|
});
|
|
269
|
+
// PRI-453: Generate painId early and write to trajectory.db via legacy
|
|
270
|
+
// recordPainEvent so that disabling SDK observability path does not lose
|
|
271
|
+
// trajectory coverage. canonicalPainId enables dedup.
|
|
272
|
+
const painId = `llm_${Date.now()}`;
|
|
273
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
274
|
+
sessionId: ctx.sessionId || 'unknown',
|
|
275
|
+
source,
|
|
276
|
+
score: painScore,
|
|
277
|
+
reason: matchedReason,
|
|
278
|
+
origin: 'system_infer',
|
|
279
|
+
canonicalPainId: painId,
|
|
280
|
+
});
|
|
269
281
|
if (triageAdmitted) {
|
|
270
282
|
const gate = evaluatePainDiagnosticGate({
|
|
271
283
|
source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
|
|
@@ -286,7 +298,7 @@ export function handleLlmOutput(event, ctx) {
|
|
|
286
298
|
ts: new Date().toISOString(),
|
|
287
299
|
type: 'pain_detected',
|
|
288
300
|
data: {
|
|
289
|
-
painId
|
|
301
|
+
painId,
|
|
290
302
|
painType: 'user_frustration',
|
|
291
303
|
source,
|
|
292
304
|
reason: `${matchedReason}; diagnosticGate=${gate.reason}`,
|
|
@@ -296,7 +308,7 @@ export function handleLlmOutput(event, ctx) {
|
|
|
296
308
|
provenance: 'openclaw_context_bound',
|
|
297
309
|
evidence,
|
|
298
310
|
},
|
|
299
|
-
});
|
|
311
|
+
}, { recordObservability: false });
|
|
300
312
|
}
|
|
301
313
|
else {
|
|
302
314
|
ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
|
package/dist/hooks/pain.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { EvolutionLoopEvent } from '../core/evolution-types.js';
|
|
|
19
19
|
import type { PluginHookAfterToolCallEvent, PluginHookToolContext, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
20
20
|
import { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
21
21
|
export { buildTrajectoryEvidence };
|
|
22
|
-
export declare function emitPainDetectedEvent(wctx: WorkspaceContext, event: EvolutionLoopEvent
|
|
22
|
+
export declare function emitPainDetectedEvent(wctx: WorkspaceContext, event: EvolutionLoopEvent, options?: {
|
|
23
|
+
recordObservability?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
23
25
|
/**
|
|
24
26
|
* Handle after_tool_call hook — decomposed into pipeline stages.
|
|
25
27
|
*
|
package/dist/hooks/pain.js
CHANGED
|
@@ -44,7 +44,7 @@ function createPainToPrincipleService(wctx) {
|
|
|
44
44
|
}
|
|
45
45
|
// buildTrajectoryEvidence is in ./trajectory-evidence.ts (re-exported above)
|
|
46
46
|
// ── Pain Event Emission ─────────────────────────────────────────────────────
|
|
47
|
-
export async function emitPainDetectedEvent(wctx, event) {
|
|
47
|
+
export async function emitPainDetectedEvent(wctx, event, options) {
|
|
48
48
|
try {
|
|
49
49
|
wctx.evolutionReducer.emitSync(event);
|
|
50
50
|
}
|
|
@@ -68,6 +68,10 @@ export async function emitPainDetectedEvent(wctx, event) {
|
|
|
68
68
|
score: painData.score,
|
|
69
69
|
}));
|
|
70
70
|
}
|
|
71
|
+
// PRI-453: Hook paths that already write events_*.jsonl + trajectory.db
|
|
72
|
+
// via legacy writers pass recordObservability: false to avoid triple-write.
|
|
73
|
+
// CLI pd pain record and paths without legacy writers keep the default true.
|
|
74
|
+
const recordObs = options?.recordObservability ?? true;
|
|
71
75
|
const result = await service.recordPain({
|
|
72
76
|
painId: painData.painId,
|
|
73
77
|
painType: painData.painType,
|
|
@@ -80,7 +84,7 @@ export async function emitPainDetectedEvent(wctx, event) {
|
|
|
80
84
|
traceId: painData.traceId,
|
|
81
85
|
provenance: painData.provenance,
|
|
82
86
|
evidence: painData.evidence,
|
|
83
|
-
recordObservability:
|
|
87
|
+
recordObservability: recordObs,
|
|
84
88
|
});
|
|
85
89
|
if (result.status === 'failed' && result.failureCategory) {
|
|
86
90
|
SystemLogger.log(wctx.workspaceDir, 'PAIN_SERVICE_FAILED', JSON.stringify({
|
|
@@ -216,6 +220,8 @@ function handleManualPain(event, ctx, wctx, workspaceDir, sessionId) {
|
|
|
216
220
|
reason: `User intervention: ${reason}`,
|
|
217
221
|
isRisky: true
|
|
218
222
|
});
|
|
223
|
+
// PRI-453: Generate painId early to pass as canonicalPainId for dedup.
|
|
224
|
+
const painId = createPainId(sessionId);
|
|
219
225
|
wctx.trajectory?.recordPainEvent?.({
|
|
220
226
|
sessionId,
|
|
221
227
|
source: 'manual',
|
|
@@ -223,6 +229,7 @@ function handleManualPain(event, ctx, wctx, workspaceDir, sessionId) {
|
|
|
223
229
|
reason: `User intervention: ${reason}`,
|
|
224
230
|
origin: 'user_manual',
|
|
225
231
|
text: reason,
|
|
232
|
+
canonicalPainId: painId,
|
|
226
233
|
});
|
|
227
234
|
// Log to EvolutionLogger
|
|
228
235
|
const evoLogger = getEvolutionLogger(workspaceDir, wctx.trajectory);
|
|
@@ -279,7 +286,7 @@ function handleManualPain(event, ctx, wctx, workspaceDir, sessionId) {
|
|
|
279
286
|
ts: new Date().toISOString(),
|
|
280
287
|
type: 'pain_detected',
|
|
281
288
|
data: {
|
|
282
|
-
painId
|
|
289
|
+
painId,
|
|
283
290
|
painType: 'user_frustration',
|
|
284
291
|
source: event.toolName,
|
|
285
292
|
reason: `User intervention: ${reason}`,
|
|
@@ -290,5 +297,5 @@ function handleManualPain(event, ctx, wctx, workspaceDir, sessionId) {
|
|
|
290
297
|
provenance: (sessionId && sessionId !== 'unknown') ? 'openclaw_context_bound' : 'owner_reported_no_host_trace',
|
|
291
298
|
evidence: buildTrajectoryEvidence(wctx, sessionId),
|
|
292
299
|
},
|
|
293
|
-
});
|
|
300
|
+
}, { recordObservability: false });
|
|
294
301
|
}
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -328,6 +328,27 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
328
328
|
source: 'user_empathy',
|
|
329
329
|
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
330
330
|
isRisky: false,
|
|
331
|
+
origin: 'system_infer',
|
|
332
|
+
severity: gfiPainScore >= 40 ? 'moderate' : 'mild',
|
|
333
|
+
confidence: 0.5,
|
|
334
|
+
detection_mode: 'structured',
|
|
335
|
+
deduped: false,
|
|
336
|
+
trigger_text_excerpt: sanitizeForEvidence(latestUserMessage, workspaceDir).substring(0, 120),
|
|
337
|
+
raw_score: gfiPainScore,
|
|
338
|
+
calibrated_score: gfiPainScore,
|
|
339
|
+
eventId: `empathy_gfi_${Date.now()}`,
|
|
340
|
+
});
|
|
341
|
+
// PRI-453: Generate painId early and write to trajectory.db via legacy
|
|
342
|
+
// recordPainEvent so that disabling SDK observability path does not lose
|
|
343
|
+
// trajectory coverage. canonicalPainId enables dedup.
|
|
344
|
+
const gfiPainId = `empathy_gfi_${Date.now()}`;
|
|
345
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
346
|
+
sessionId,
|
|
347
|
+
source: 'user_empathy',
|
|
348
|
+
score: gfiPainScore,
|
|
349
|
+
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
350
|
+
origin: 'system_infer',
|
|
351
|
+
canonicalPainId: gfiPainId,
|
|
331
352
|
});
|
|
332
353
|
if (gate.shouldDiagnose) {
|
|
333
354
|
logger?.info?.(`[PD:Empathy] Gate approved, calling emitPainDetectedEvent...`);
|
|
@@ -337,7 +358,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
337
358
|
ts: new Date().toISOString(),
|
|
338
359
|
type: 'pain_detected',
|
|
339
360
|
data: {
|
|
340
|
-
painId:
|
|
361
|
+
painId: gfiPainId,
|
|
341
362
|
painType: 'user_frustration',
|
|
342
363
|
source: 'user_empathy',
|
|
343
364
|
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
@@ -347,7 +368,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
347
368
|
provenance: 'openclaw_context_bound',
|
|
348
369
|
evidence,
|
|
349
370
|
},
|
|
350
|
-
});
|
|
371
|
+
}, { recordObservability: false });
|
|
351
372
|
logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
|
|
352
373
|
}
|
|
353
374
|
catch (emitErr) {
|
|
@@ -392,6 +413,10 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
392
413
|
calibrated_score: painScore,
|
|
393
414
|
eventId,
|
|
394
415
|
});
|
|
416
|
+
// PRI-453: Generate painId early to pass as canonicalPainId for dedup.
|
|
417
|
+
// Declared outside try block so it's accessible to emitPainDetectedEvent
|
|
418
|
+
// later (lineage consistency: same id for trajectory + emitted event).
|
|
419
|
+
const observerPainId = `empathy_gfi_${Date.now()}`;
|
|
395
420
|
try {
|
|
396
421
|
wctx.trajectory?.recordPainEvent?.({
|
|
397
422
|
sessionId,
|
|
@@ -402,6 +427,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
402
427
|
origin: 'system_infer',
|
|
403
428
|
confidence: result.confidence,
|
|
404
429
|
text: sanitizeForEvidence(latestUserMessage, workspaceDir),
|
|
430
|
+
canonicalPainId: observerPainId,
|
|
405
431
|
});
|
|
406
432
|
}
|
|
407
433
|
catch (error) {
|
|
@@ -429,11 +455,15 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
429
455
|
logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
|
|
430
456
|
try {
|
|
431
457
|
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
458
|
+
// PRI-453: Reuse observerPainId for lineage consistency —
|
|
459
|
+
// the same id is used as canonicalPainId in recordPainEvent
|
|
460
|
+
// and as painId in the emitted event, so EvidenceChainConsoleModel
|
|
461
|
+
// can JOIN pain_events.canonical_pain_id = tasks.input_ref.
|
|
432
462
|
await emitPainDetectedEvent(wctx, {
|
|
433
463
|
ts: new Date().toISOString(),
|
|
434
464
|
type: 'pain_detected',
|
|
435
465
|
data: {
|
|
436
|
-
painId:
|
|
466
|
+
painId: observerPainId,
|
|
437
467
|
painType: 'user_frustration',
|
|
438
468
|
source: 'user_empathy',
|
|
439
469
|
reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
|
|
@@ -443,7 +473,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
443
473
|
provenance: 'openclaw_context_bound',
|
|
444
474
|
evidence,
|
|
445
475
|
},
|
|
446
|
-
});
|
|
476
|
+
}, { recordObservability: false });
|
|
447
477
|
}
|
|
448
478
|
catch (emitErr) {
|
|
449
479
|
logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.145.
|
|
5
|
+
"version": "1.145.1",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface ObservabilityBaselines {
|
|
2
|
-
/** ISO 8601 timestamp when baselines were calculated */
|
|
3
|
-
calculatedAt: string;
|
|
4
|
-
/** Principle Stock: total count of principles in the ledger */
|
|
5
|
-
principleStock: number;
|
|
6
|
-
/** Total rules across all principles */
|
|
7
|
-
totalRules: number;
|
|
8
|
-
/** Total implementations across all rules */
|
|
9
|
-
totalImplementations: number;
|
|
10
|
-
/** Structure: average rules per principle (0 if no principles) */
|
|
11
|
-
avgRulesPerPrinciple: number;
|
|
12
|
-
/** Structure: average implementations per rule (0 if no rules) */
|
|
13
|
-
avgImplementationsPerRule: number;
|
|
14
|
-
/** Total pain events from trajectory DB (0 if DB unavailable) */
|
|
15
|
-
totalPainEvents: number;
|
|
16
|
-
/** Association Rate: principles / total pain events (0 if no pain events) */
|
|
17
|
-
associationRate: number;
|
|
18
|
-
/** Count of principles with internalizationStatus = 'internalized' */
|
|
19
|
-
internalizedCount: number;
|
|
20
|
-
/** Internalization Rate: internalized / total principles (0 if no principles) */
|
|
21
|
-
internalizationRate: number;
|
|
22
|
-
/** Distribution of principle statuses */
|
|
23
|
-
statusDistribution: Record<string, number>;
|
|
24
|
-
/** Distribution of principle priorities */
|
|
25
|
-
priorityDistribution: Record<string, number>;
|
|
26
|
-
/** Distribution of internalization statuses from training store */
|
|
27
|
-
internalizationDistribution: Record<string, number>;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Calculate observability baselines for the principle evolution system.
|
|
31
|
-
*
|
|
32
|
-
* Reads the principle ledger from stateDir, computes metrics across four
|
|
33
|
-
* dimensions (Stock, Structure, Association, Internalization), logs a summary
|
|
34
|
-
* via SystemLogger, and persists results to .state/baselines.json.
|
|
35
|
-
*
|
|
36
|
-
* @param stateDir - The .state directory containing the principle ledger
|
|
37
|
-
* @param workspaceDir - Optional workspace dir for SystemLogger routing
|
|
38
|
-
* @returns The computed baselines
|
|
39
|
-
*/
|
|
40
|
-
export declare function calculateBaselines(stateDir: string, workspaceDir?: string): ObservabilityBaselines;
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observability Baselines for the Evolution SDK.
|
|
3
|
-
*
|
|
4
|
-
* Provides calculateBaselines() which measures the current state of the
|
|
5
|
-
* principle evolution system across four dimensions:
|
|
6
|
-
*
|
|
7
|
-
* 1. Principle Stock: total count of principles in the ledger
|
|
8
|
-
* 2. Structure: average sub-principles (rules) and implementations per principle
|
|
9
|
-
* 3. Association Rate: principles created / total pain events recorded
|
|
10
|
-
* 4. Internalization Rate: internalized principles / total principles
|
|
11
|
-
*
|
|
12
|
-
* Results are logged via SystemLogger and persisted to .state/baselines.json.
|
|
13
|
-
*/
|
|
14
|
-
import * as fs from 'fs';
|
|
15
|
-
import * as path from 'path';
|
|
16
|
-
import { loadLedger } from './principle-tree-ledger.js';
|
|
17
|
-
import { SystemLogger } from './system-logger.js';
|
|
18
|
-
import { atomicWriteFileSync } from '../utils/io.js';
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Constants
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
const BASELINES_FILE = 'baselines.json';
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Implementation
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
/**
|
|
27
|
-
* Calculate observability baselines for the principle evolution system.
|
|
28
|
-
*
|
|
29
|
-
* Reads the principle ledger from stateDir, computes metrics across four
|
|
30
|
-
* dimensions (Stock, Structure, Association, Internalization), logs a summary
|
|
31
|
-
* via SystemLogger, and persists results to .state/baselines.json.
|
|
32
|
-
*
|
|
33
|
-
* @param stateDir - The .state directory containing the principle ledger
|
|
34
|
-
* @param workspaceDir - Optional workspace dir for SystemLogger routing
|
|
35
|
-
* @returns The computed baselines
|
|
36
|
-
*/
|
|
37
|
-
export function calculateBaselines(stateDir, workspaceDir) {
|
|
38
|
-
const ledger = loadLedger(stateDir);
|
|
39
|
-
const { tree, trainingStore } = ledger;
|
|
40
|
-
const principles = Object.values(tree.principles);
|
|
41
|
-
const rules = Object.values(tree.rules);
|
|
42
|
-
const implementations = Object.values(tree.implementations);
|
|
43
|
-
const principleStock = principles.length;
|
|
44
|
-
const totalRules = rules.length;
|
|
45
|
-
const totalImplementations = implementations.length;
|
|
46
|
-
// Structure metrics
|
|
47
|
-
const avgRulesPerPrinciple = principleStock > 0
|
|
48
|
-
? totalRules / principleStock
|
|
49
|
-
: 0;
|
|
50
|
-
const avgImplementationsPerRule = totalRules > 0
|
|
51
|
-
? totalImplementations / totalRules
|
|
52
|
-
: 0;
|
|
53
|
-
// Count pain events from trajectory DB
|
|
54
|
-
const totalPainEvents = countPainEvents(stateDir);
|
|
55
|
-
// Association Rate: how many principles were created per pain event
|
|
56
|
-
const associationRate = totalPainEvents > 0
|
|
57
|
-
? principleStock / totalPainEvents
|
|
58
|
-
: 0;
|
|
59
|
-
// Internalization Rate from training store
|
|
60
|
-
// Filter to only entries whose principleId still exists in the ledger tree
|
|
61
|
-
// to avoid orphaned/deleted entries inflating the ratio
|
|
62
|
-
const trainingEntries = Object.values(trainingStore);
|
|
63
|
-
const activePrincipleIds = new Set(Object.keys(tree.principles));
|
|
64
|
-
const activeEntries = trainingEntries.filter((entry) => activePrincipleIds.has(entry.principleId));
|
|
65
|
-
const internalizedCount = activeEntries.filter((entry) => entry.internalizationStatus === 'internalized').length;
|
|
66
|
-
const internalizationRate = principleStock > 0
|
|
67
|
-
? internalizedCount / principleStock
|
|
68
|
-
: 0;
|
|
69
|
-
// Status distribution
|
|
70
|
-
const statusDistribution = {};
|
|
71
|
-
for (const p of principles) {
|
|
72
|
-
statusDistribution[p.status] = (statusDistribution[p.status] ?? 0) + 1;
|
|
73
|
-
}
|
|
74
|
-
// Priority distribution
|
|
75
|
-
const priorityDistribution = {};
|
|
76
|
-
for (const p of principles) {
|
|
77
|
-
priorityDistribution[p.priority] = (priorityDistribution[p.priority] ?? 0) + 1;
|
|
78
|
-
}
|
|
79
|
-
// Internalization status distribution from training store
|
|
80
|
-
const internalizationDistribution = {};
|
|
81
|
-
for (const entry of trainingEntries) {
|
|
82
|
-
internalizationDistribution[entry.internalizationStatus] =
|
|
83
|
-
(internalizationDistribution[entry.internalizationStatus] ?? 0) + 1;
|
|
84
|
-
}
|
|
85
|
-
const baselines = {
|
|
86
|
-
calculatedAt: new Date().toISOString(),
|
|
87
|
-
principleStock,
|
|
88
|
-
totalRules,
|
|
89
|
-
totalImplementations,
|
|
90
|
-
avgRulesPerPrinciple: roundTo3(avgRulesPerPrinciple),
|
|
91
|
-
avgImplementationsPerRule: roundTo3(avgImplementationsPerRule),
|
|
92
|
-
totalPainEvents,
|
|
93
|
-
associationRate: roundTo3(associationRate),
|
|
94
|
-
internalizedCount,
|
|
95
|
-
internalizationRate: roundTo3(internalizationRate),
|
|
96
|
-
statusDistribution,
|
|
97
|
-
priorityDistribution,
|
|
98
|
-
internalizationDistribution,
|
|
99
|
-
};
|
|
100
|
-
// Log summary
|
|
101
|
-
SystemLogger.log(workspaceDir, 'OBSERVABILITY_BASELINES', formatBaselineSummary(baselines));
|
|
102
|
-
// Persist to .state/baselines.json
|
|
103
|
-
persistBaselines(stateDir, baselines);
|
|
104
|
-
return baselines;
|
|
105
|
-
}
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Internal helpers
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
function roundTo3(n) {
|
|
110
|
-
return Math.round(n * 1000) / 1000;
|
|
111
|
-
}
|
|
112
|
-
function formatBaselineSummary(b) {
|
|
113
|
-
return [
|
|
114
|
-
`Principle Stock: ${b.principleStock}`,
|
|
115
|
-
`Structure: ${b.avgRulesPerPrinciple} rules/principle, ${b.avgImplementationsPerRule} impls/rule`,
|
|
116
|
-
`Association Rate: ${b.associationRate} (${b.principleStock} principles / ${b.totalPainEvents} pain events)`,
|
|
117
|
-
`Internalization Rate: ${b.internalizationRate} (${b.internalizedCount}/${b.principleStock})`,
|
|
118
|
-
].join(' | ');
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Count pain events from the trajectory SQLite database.
|
|
122
|
-
* Returns 0 if the database is unavailable or the table doesn't exist.
|
|
123
|
-
*/
|
|
124
|
-
function countPainEvents(stateDir) {
|
|
125
|
-
const dbPath = path.join(stateDir, 'trajectory.db');
|
|
126
|
-
if (!fs.existsSync(dbPath)) {
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
// Use dynamic import for better-sqlite3 to avoid hard dependency
|
|
131
|
-
// at module load time. If not available, return 0.
|
|
132
|
-
const Database = require('better-sqlite3');
|
|
133
|
-
const db = new Database(dbPath, { readonly: true });
|
|
134
|
-
try {
|
|
135
|
-
const row = db.prepare('SELECT COUNT(*) as count FROM pain_events').get();
|
|
136
|
-
return row?.count ?? 0;
|
|
137
|
-
}
|
|
138
|
-
finally {
|
|
139
|
-
db.close();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
// better-sqlite3 not available, or table doesn't exist — log and return 0
|
|
144
|
-
SystemLogger.log(stateDir, 'OBSERVABILITY_SQL_ERROR', `countPainEvents failed: ${String(err)}`);
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Persist baselines to .state/baselines.json atomically.
|
|
150
|
-
*/
|
|
151
|
-
function persistBaselines(stateDir, baselines) {
|
|
152
|
-
try {
|
|
153
|
-
const filePath = path.join(stateDir, BASELINES_FILE);
|
|
154
|
-
const dir = path.dirname(filePath);
|
|
155
|
-
if (!fs.existsSync(dir)) {
|
|
156
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
157
|
-
}
|
|
158
|
-
atomicWriteFileSync(filePath, JSON.stringify(baselines, null, 2));
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
// Baselines persistence is best-effort — don't crash the caller
|
|
162
|
-
// (the SystemLogger call above already logged the values)
|
|
163
|
-
}
|
|
164
|
-
}
|