principles-disciple 1.106.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/core/init.ts +3 -1
- package/src/core/workspace-dir-validation.ts +3 -3
- package/tests/core-anti-growth.test.ts +0 -13
- package/tests/hooks/prompt-characterization.test.ts +1 -11
- package/tests/hooks/prompt-diet.test.ts +3 -11
- package/tests/hooks/prompt-size-guard.test.ts +0 -10
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
- package/tests/index.test.ts +1 -1
- package/tests/runtime-v2-discovery-guard.test.ts +1 -2
- package/vitest.config.ts +2 -3
- package/vitest.unit.config.ts +12 -0
- package/src/core/evolution-hook.ts +0 -74
- package/src/core/file-storage-adapter.ts +0 -203
- package/src/core/merge-gate-audit.ts +0 -314
- package/src/core/pain-context-extractor.ts +0 -306
- package/src/core/pain-lifecycle.ts +0 -38
- package/src/core/pain-signal-adapter.ts +0 -42
- package/src/core/pain-signal.ts +0 -22
- package/src/core/principle-injector.ts +0 -84
- package/src/core/principle-tree-migration.ts +0 -196
- package/src/core/storage-adapter.ts +0 -65
- package/src/core/telemetry-event.ts +0 -109
- package/src/core/training-program.ts +0 -632
- package/src/core/workspace-dir-service.ts +0 -119
- package/src/hooks/lifecycle-routing.ts +0 -125
- package/src/service/event-log-auditor.ts +0 -284
- package/src/service/evolution-queue-lock.ts +0 -47
- package/src/service/failure-classifier.ts +0 -79
- package/src/service/internalization-trigger-adapter.ts +0 -302
- package/src/service/monitoring-query-service.ts +0 -67
- package/src/service/subagent-workflow/index.ts +0 -17
- package/src/tools/critique-prompt.ts +0 -1
- package/src/tools/model-index.ts +0 -1
- package/src/types/event-payload.ts +0 -16
- package/src/utils/glob-match.ts +0 -50
- package/src/utils/nlp.ts +0 -25
- package/src/utils/plugin-logger.ts +0 -97
- package/src/utils/subagent-probe.ts +0 -81
- package/tests/core/evolution-hook.test.ts +0 -123
- package/tests/core/file-storage-adapter.test.ts +0 -285
- package/tests/core/merge-gate-audit.test.ts +0 -117
- package/tests/core/pain-context-extractor.test.ts +0 -279
- package/tests/core/pain-lifecycle.test.ts +0 -38
- package/tests/core/pain-signal-adapter.test.ts +0 -116
- package/tests/core/pain-signal.test.ts +0 -190
- package/tests/core/principle-injector.test.ts +0 -90
- package/tests/core/principle-tree-migration.test.ts +0 -77
- package/tests/core/storage-conformance.test.ts +0 -429
- package/tests/core/telemetry-event.test.ts +0 -119
- package/tests/core/training-program.test.ts +0 -472
- package/tests/core/workspace-dir-service.test.ts +0 -68
- package/tests/core/workspace-dir-validation.test.ts +0 -143
- package/tests/integration/internalization-trigger-guard.test.ts +0 -69
- package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
- package/tests/service/failure-classifier.test.ts +0 -171
- package/tests/service/internalization-trigger-adapter.test.ts +0 -251
- package/tests/service/monitoring-query-service.test.ts +0 -67
- package/tests/utils/nlp.test.ts +0 -35
- package/tests/utils/plugin-logger.test.ts +0 -156
- package/tests/utils/subagent-probe.test.ts +0 -79
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.
|
|
5
|
+
"version": "1.108.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.108.0",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build:bundle": "node esbuild.config.js",
|
|
35
35
|
"build:production": "node esbuild.config.js --production && node scripts/verify-build.mjs",
|
|
36
36
|
"test": "vitest run",
|
|
37
|
-
"test:unit": "vitest run tests/core tests/service tests/hooks tests/commands tests/utils tests/scripts --exclude tests/commands/evolver.test.ts",
|
|
37
|
+
"test:unit": "vitest run --config vitest.unit.config.ts tests/core tests/service tests/hooks tests/commands tests/utils tests/scripts --exclude tests/commands/evolver.test.ts",
|
|
38
38
|
"test:integration": "vitest run tests/integration/",
|
|
39
39
|
"test:coverage": "vitest run --coverage",
|
|
40
40
|
"test:all": "vitest run",
|
package/src/core/init.ts
CHANGED
|
@@ -225,7 +225,9 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
|
|
|
225
225
|
addPrincipleToLedger(stateDir, ledgerPrinciple);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
const firstId = CORE_THINKING_MODELS[0]?.id ?? 'T-01';
|
|
229
|
+
const lastId = CORE_THINKING_MODELS[CORE_THINKING_MODELS.length - 1]?.id ?? 'T-10';
|
|
230
|
+
logger.info(`[PD] Initialized ${CORE_THINKING_MODELS.length} core thinking models: ${firstId} through ${lastId}`);
|
|
229
231
|
return true;
|
|
230
232
|
} catch (err) {
|
|
231
233
|
logger.error(`[PD] Failed to initialize core principles: ${String(err)}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WorkspaceDir Validation Utilities
|
|
3
3
|
*
|
|
4
|
-
* This module only validates candidate workspace directories
|
|
5
|
-
*
|
|
4
|
+
* This module only validates candidate workspace directories. Runtime hook
|
|
5
|
+
* resolution policy lives at the I/O boundary in utils/workspace-resolver.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as os from 'os';
|
|
@@ -49,4 +49,4 @@ export function validateWorkspaceDir(dir: string | undefined): string | null {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
return null;
|
|
52
|
-
}
|
|
52
|
+
}
|
|
@@ -25,10 +25,8 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
25
25
|
const ZERO_IMPORT_CANDIDATES = [
|
|
26
26
|
'trajectory-types.ts',
|
|
27
27
|
'profile.ts',
|
|
28
|
-
'pain-signal.ts',
|
|
29
28
|
'pd-task-types.ts',
|
|
30
29
|
'evolution-types.ts',
|
|
31
|
-
'telemetry-event.ts',
|
|
32
30
|
'empathy-types.ts',
|
|
33
31
|
'correction-types.ts',
|
|
34
32
|
'principle-injection.ts',
|
|
@@ -39,12 +37,10 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
39
37
|
const PLUGIN_IO_FILES = [
|
|
40
38
|
// Thin adapter candidates
|
|
41
39
|
'local-worker-routing.ts',
|
|
42
|
-
'principle-tree-migration.ts',
|
|
43
40
|
'principle-internalization/principle-lifecycle-service.ts',
|
|
44
41
|
'principle-tree-ledger-adapter.ts',
|
|
45
42
|
'principle-compiler/ledger-registrar.ts',
|
|
46
43
|
'principle-compiler/code-validator.ts',
|
|
47
|
-
'principle-injector.ts',
|
|
48
44
|
'pd-task-service.ts',
|
|
49
45
|
'principle-internalization/lifecycle-read-model.ts',
|
|
50
46
|
'principle-internalization/filesystem-lifecycle-datasource.ts',
|
|
@@ -65,14 +61,10 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
65
61
|
'pain-diagnostic-gate.ts',
|
|
66
62
|
'hygiene/tracker.ts',
|
|
67
63
|
'schema/migrations/002-init-central.ts',
|
|
68
|
-
'workspace-dir-service.ts',
|
|
69
64
|
'paths.ts',
|
|
70
65
|
'schema/migrations/004-add-thinking-and-gfi.ts',
|
|
71
|
-
'evolution-hook.ts',
|
|
72
|
-
'storage-adapter.ts',
|
|
73
66
|
'schema/migrations/003-init-workflow.ts',
|
|
74
67
|
'workspace-dir-validation.ts',
|
|
75
|
-
'pain-signal-adapter.ts',
|
|
76
68
|
'rule-implementation-runtime.ts',
|
|
77
69
|
'detection-service.ts',
|
|
78
70
|
'schema/migrations/index.ts',
|
|
@@ -89,10 +81,8 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
89
81
|
'model-training-registry.ts',
|
|
90
82
|
'focus-history.ts',
|
|
91
83
|
'model-deployment-registry.ts',
|
|
92
|
-
'training-program.ts',
|
|
93
84
|
'replay-engine.ts',
|
|
94
85
|
'external-training-contract.ts',
|
|
95
|
-
'merge-gate-audit.ts',
|
|
96
86
|
'shadow-observation-registry.ts',
|
|
97
87
|
'control-ui-db.ts',
|
|
98
88
|
'thinking-models.ts',
|
|
@@ -100,11 +90,9 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
100
90
|
'correction-cue-learner.ts',
|
|
101
91
|
'principle-compiler/compiler.ts',
|
|
102
92
|
'pain.ts',
|
|
103
|
-
'pain-context-extractor.ts',
|
|
104
93
|
'config.ts',
|
|
105
94
|
'code-implementation-storage.ts',
|
|
106
95
|
'observability.ts',
|
|
107
|
-
'file-storage-adapter.ts',
|
|
108
96
|
'workflow-funnel-loader.ts',
|
|
109
97
|
'dictionary.ts',
|
|
110
98
|
'thinking-os-parser.ts',
|
|
@@ -116,7 +104,6 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
116
104
|
'pd-task-store.ts',
|
|
117
105
|
'evolution-migration.ts',
|
|
118
106
|
'empathy-keyword-matcher.ts',
|
|
119
|
-
'pain-lifecycle.ts',
|
|
120
107
|
'session-tracker.ts',
|
|
121
108
|
'principle-tree-ledger.ts',
|
|
122
109
|
'evolution-logger.ts',
|
|
@@ -136,16 +136,6 @@ vi.mock('../../src/core/focus-history.js', () => ({
|
|
|
136
136
|
safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
|
|
137
137
|
}));
|
|
138
138
|
|
|
139
|
-
vi.mock('../../src/service/subagent-workflow/index.js', () => ({
|
|
140
|
-
EmpathyObserverWorkflowManager: vi.fn(),
|
|
141
|
-
empathyObserverWorkflowSpec: {},
|
|
142
|
-
isExpectedSubagentError: vi.fn().mockReturnValue(false),
|
|
143
|
-
}));
|
|
144
|
-
|
|
145
|
-
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
146
|
-
isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
|
|
147
|
-
}));
|
|
148
|
-
|
|
149
139
|
vi.mock('../../src/core/local-worker-routing.js', () => ({
|
|
150
140
|
classifyTask: vi.fn().mockReturnValue({
|
|
151
141
|
decision: 'stay_main',
|
|
@@ -483,4 +473,4 @@ describe('Size guard: never exceeds 9000 chars', () => {
|
|
|
483
473
|
await expect(handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ trigger: 'user' })))
|
|
484
474
|
.resolves.toBeDefined();
|
|
485
475
|
});
|
|
486
|
-
});
|
|
476
|
+
});
|
|
@@ -20,10 +20,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
20
20
|
beforeEach(() => {
|
|
21
21
|
vi.clearAllMocks();
|
|
22
22
|
process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = 'true';
|
|
23
|
+
process.env.PD_EMPATHY_API_KEY_ENV = 'PD_TEST_DISABLED_EMPATHY_API_KEY';
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
afterEach(() => {
|
|
26
27
|
process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = '';
|
|
28
|
+
delete process.env.PD_EMPATHY_API_KEY_ENV;
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
const mockGetPendingDiagnosticianTasks = vi.fn<(stateDir: string) => unknown[]>();
|
|
@@ -123,16 +125,6 @@ vi.mock('../../src/core/focus-history.js', () => ({
|
|
|
123
125
|
safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
|
|
124
126
|
}));
|
|
125
127
|
|
|
126
|
-
vi.mock('../../src/service/subagent-workflow/index.js', () => ({
|
|
127
|
-
EmpathyObserverWorkflowManager: vi.fn(),
|
|
128
|
-
empathyObserverWorkflowSpec: {},
|
|
129
|
-
isExpectedSubagentError: vi.fn().mockReturnValue(false),
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
133
|
-
isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
128
|
vi.mock('../../src/core/local-worker-routing.js', () => ({
|
|
137
129
|
classifyTask: vi.fn().mockReturnValue({
|
|
138
130
|
decision: 'stay_main',
|
|
@@ -304,7 +296,7 @@ describe('PRI-291 Prompt Diet: MVP sections preserved', () => {
|
|
|
304
296
|
resolve: (key: string) => `/fake/${key}`,
|
|
305
297
|
trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn(), recordPainEvent: vi.fn() },
|
|
306
298
|
config: { get: vi.fn().mockImplementation((k: string) => {
|
|
307
|
-
if (k === 'thresholds.pain_trigger') return
|
|
299
|
+
if (k === 'thresholds.pain_trigger') return 100;
|
|
308
300
|
if (k === 'severity_thresholds.high') return 70;
|
|
309
301
|
if (k === 'language') return 'en';
|
|
310
302
|
return undefined;
|
|
@@ -118,16 +118,6 @@ vi.mock('../../src/core/focus-history.js', () => ({
|
|
|
118
118
|
safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
|
|
119
119
|
}));
|
|
120
120
|
|
|
121
|
-
vi.mock('../../src/service/subagent-workflow/index.js', () => ({
|
|
122
|
-
EmpathyObserverWorkflowManager: vi.fn(),
|
|
123
|
-
empathyObserverWorkflowSpec: {},
|
|
124
|
-
isExpectedSubagentError: vi.fn().mockReturnValue(false),
|
|
125
|
-
}));
|
|
126
|
-
|
|
127
|
-
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
128
|
-
isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
|
|
129
|
-
}));
|
|
130
|
-
|
|
131
121
|
vi.mock('../../src/core/local-worker-routing.js', () => ({
|
|
132
122
|
classifyTask: vi.fn().mockReturnValue({
|
|
133
123
|
decision: 'stay_main',
|
|
@@ -145,16 +145,6 @@ vi.mock('../../src/core/focus-history.js', () => ({
|
|
|
145
145
|
safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
|
|
146
146
|
}));
|
|
147
147
|
|
|
148
|
-
vi.mock('../../src/service/subagent-workflow/index.js', () => ({
|
|
149
|
-
EmpathyObserverWorkflowManager: vi.fn(),
|
|
150
|
-
empathyObserverWorkflowSpec: {},
|
|
151
|
-
isExpectedSubagentError: vi.fn().mockReturnValue(false),
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
155
|
-
isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
148
|
vi.mock('../../src/core/local-worker-routing.js', () => ({
|
|
159
149
|
classifyTask: vi.fn().mockReturnValue({
|
|
160
150
|
decision: 'stay_main',
|
package/tests/index.test.ts
CHANGED
|
@@ -17,7 +17,6 @@ describe('PRI-228: Runtime V2 discovery guard', () => {
|
|
|
17
17
|
const guardedFiles = [
|
|
18
18
|
{ name: 'workspace-resolver.ts', path: path.join(PLUGIN_SRC, 'utils', 'workspace-resolver.ts') },
|
|
19
19
|
{ name: 'workspace-context.ts', path: path.join(PLUGIN_SRC, 'core', 'workspace-context.ts') },
|
|
20
|
-
{ name: 'workspace-dir-service.ts', path: path.join(PLUGIN_SRC, 'core', 'workspace-dir-service.ts') },
|
|
21
20
|
{ name: 'path-resolver.ts', path: path.join(PLUGIN_SRC, 'core', 'path-resolver.ts') },
|
|
22
21
|
];
|
|
23
22
|
|
|
@@ -151,4 +150,4 @@ describe('PRI-228: Runtime V2 discovery guard', () => {
|
|
|
151
150
|
expect(content).toContain('resolveWorkspaceDirForRuntimeV2(');
|
|
152
151
|
});
|
|
153
152
|
});
|
|
154
|
-
});
|
|
153
|
+
});
|
package/vitest.config.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { defineConfig } from 'vitest/config';
|
|
|
19
19
|
|
|
20
20
|
// Integration tests: use real SQLite database
|
|
21
21
|
// These tests require better-sqlite3 to be compiled
|
|
22
|
-
const integrationTests = [
|
|
22
|
+
export const integrationTests = [
|
|
23
23
|
// Core DB tests
|
|
24
24
|
'tests/core/control-ui-db.test.ts',
|
|
25
25
|
'tests/core/evolution-logger.test.ts',
|
|
@@ -31,7 +31,6 @@ const integrationTests = [
|
|
|
31
31
|
'tests/service/keyword-optimization-service.test.ts',
|
|
32
32
|
// Hook tests with DB dependencies
|
|
33
33
|
'tests/hooks/subagent.test.ts',
|
|
34
|
-
'tests/hooks/gate-pipeline-integration.test.ts',
|
|
35
34
|
'tests/hooks/gate-rule-host-pipeline.test.ts',
|
|
36
35
|
// Script tests with DB
|
|
37
36
|
'tests/scripts/validate-live-path.test.ts',
|
|
@@ -59,4 +58,4 @@ export default defineConfig({
|
|
|
59
58
|
},
|
|
60
59
|
},
|
|
61
60
|
},
|
|
62
|
-
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
2
|
+
import baseConfig, { integrationTests } from './vitest.config.js';
|
|
3
|
+
|
|
4
|
+
export default mergeConfig(baseConfig, defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
exclude: [
|
|
7
|
+
'**/node_modules/**',
|
|
8
|
+
'**/dist/**',
|
|
9
|
+
...integrationTests,
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EvolutionHook interface for the Evolution SDK.
|
|
3
|
-
*
|
|
4
|
-
* Provides a callback-based interface for observing evolution lifecycle
|
|
5
|
-
* events: pain detection, principle creation, and principle promotion.
|
|
6
|
-
*
|
|
7
|
-
* Per D-03, this interface contains only the 3 core event methods.
|
|
8
|
-
* Per D-04, consumers implement the interface directly (no EventEmitter).
|
|
9
|
-
* Hooks not needed can use the provided noOpEvolutionHook and override
|
|
10
|
-
* individual methods.
|
|
11
|
-
*/
|
|
12
|
-
import type { PainSignal } from './pain-signal.js';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Event Types
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/** Event payload for principle creation lifecycle events. */
|
|
19
|
-
export interface PrincipleCreatedEvent {
|
|
20
|
-
/** Unique principle identifier */
|
|
21
|
-
id: string;
|
|
22
|
-
/** Principle text ("When X, then Y.") */
|
|
23
|
-
text: string;
|
|
24
|
-
/** What triggered this principle's creation */
|
|
25
|
-
trigger: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Event payload for principle promotion lifecycle events. */
|
|
29
|
-
export interface PrinciplePromotedEvent {
|
|
30
|
-
/** Unique principle identifier */
|
|
31
|
-
id: string;
|
|
32
|
-
/** Previous status tier */
|
|
33
|
-
from: string;
|
|
34
|
-
/** New status tier */
|
|
35
|
-
to: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// EvolutionHook Interface
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Callback interface for observing evolution lifecycle events.
|
|
44
|
-
*
|
|
45
|
-
* Implement all 3 methods, or spread noOpEvolutionHook and override
|
|
46
|
-
* only the methods you need:
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```ts
|
|
50
|
-
* const myHook: EvolutionHook = {
|
|
51
|
-
* ...noOpEvolutionHook,
|
|
52
|
-
* onPainDetected(signal) { console.log(signal); },
|
|
53
|
-
* };
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export interface EvolutionHook {
|
|
57
|
-
/** Called when a pain signal is detected and recorded. */
|
|
58
|
-
onPainDetected(signal: PainSignal): void;
|
|
59
|
-
/** Called when a new principle candidate is created. */
|
|
60
|
-
onPrincipleCreated(event: PrincipleCreatedEvent): void;
|
|
61
|
-
/** Called when a principle is promoted to a higher tier. */
|
|
62
|
-
onPrinciplePromoted(event: PrinciplePromotedEvent): void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// No-op Helper
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
/** No-op implementation -- consumers can spread and override individual methods. */
|
|
70
|
-
export const noOpEvolutionHook: EvolutionHook = {
|
|
71
|
-
onPainDetected(_signal: PainSignal): void {},
|
|
72
|
-
onPrincipleCreated(_event: PrincipleCreatedEvent): void {},
|
|
73
|
-
onPrinciplePromoted(_event: PrinciplePromotedEvent): void {},
|
|
74
|
-
};
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileStorageAdapter — file-backed implementation of StorageAdapter.
|
|
3
|
-
*
|
|
4
|
-
* Wraps principle-tree-ledger functions with the async StorageAdapter
|
|
5
|
-
* contract. Uses withLockAsync for thread-safe mutateLedger with
|
|
6
|
-
* retry with exponential backoff for lock acquisition (5 retries).
|
|
7
|
-
* Write failures are logged via SystemLogger and re-thrown.
|
|
8
|
-
*
|
|
9
|
-
* Guarantees:
|
|
10
|
-
* - Atomic writes via atomicWriteFileSync (temp + rename)
|
|
11
|
-
* - Thread-safe concurrent access via file locks
|
|
12
|
-
* - Consistent read-after-write visibility
|
|
13
|
-
* - Write failures logged to SystemLogger and re-thrown
|
|
14
|
-
*/
|
|
15
|
-
import * as fs from 'fs';
|
|
16
|
-
import * as path from 'path';
|
|
17
|
-
import type { StorageAdapter } from './storage-adapter.js';
|
|
18
|
-
import type { HybridLedgerStore } from './principle-tree-ledger.js';
|
|
19
|
-
import { TREE_NAMESPACE } from './principle-tree-ledger.js';
|
|
20
|
-
import {
|
|
21
|
-
loadLedger as loadLedgerFromFile,
|
|
22
|
-
saveLedgerAsync,
|
|
23
|
-
} from './principle-tree-ledger.js';
|
|
24
|
-
import { withLockAsync, type LockOptions, LockAcquisitionError } from '../utils/file-lock.js';
|
|
25
|
-
import { atomicWriteFileSync } from '../utils/io.js';
|
|
26
|
-
import { SystemLogger } from './system-logger.js';
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Configuration
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/** Maximum retries for lock acquisition in mutateLedger. */
|
|
33
|
-
const MUTATE_RETRY_COUNT = 5;
|
|
34
|
-
|
|
35
|
-
/** Base delay in ms for exponential backoff between retries. */
|
|
36
|
-
const MUTATE_BACKOFF_BASE_MS = 50;
|
|
37
|
-
|
|
38
|
-
/** Maximum backoff delay in ms. */
|
|
39
|
-
const MUTATE_BACKOFF_MAX_MS = 500;
|
|
40
|
-
|
|
41
|
-
const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Internal helpers
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Serialize the hybrid ledger store to JSON.
|
|
49
|
-
* Mirrors the unexported serializeLedger from principle-tree-ledger.ts.
|
|
50
|
-
*/
|
|
51
|
-
function serializeStore(store: HybridLedgerStore): string {
|
|
52
|
-
return JSON.stringify(
|
|
53
|
-
{
|
|
54
|
-
...store.trainingStore,
|
|
55
|
-
[TREE_NAMESPACE]: {
|
|
56
|
-
...store.tree,
|
|
57
|
-
lastUpdated: new Date().toISOString(),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
null,
|
|
61
|
-
2,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Ensure the parent directory exists before writing. */
|
|
66
|
-
function ensureParentDir(filePath: string): void {
|
|
67
|
-
const dir = path.dirname(filePath);
|
|
68
|
-
if (!fs.existsSync(dir)) {
|
|
69
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// FileStorageAdapter
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* File-system backed storage adapter for the principle ledger.
|
|
79
|
-
*
|
|
80
|
-
* Delegates read/write operations to principle-tree-ledger while providing
|
|
81
|
-
* the async StorageAdapter interface. The mutateLedger method uses
|
|
82
|
-
* withLockAsync with exponential backoff retry for robust concurrent access.
|
|
83
|
-
*/
|
|
84
|
-
export class FileStorageAdapter implements StorageAdapter {
|
|
85
|
-
private readonly stateDir: string;
|
|
86
|
-
private readonly workspaceDir: string | undefined;
|
|
87
|
-
|
|
88
|
-
constructor(stateDir: string, workspaceDir?: string) {
|
|
89
|
-
this.stateDir = stateDir;
|
|
90
|
-
this.workspaceDir = workspaceDir;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Resolve the ledger file path for this state directory. */
|
|
94
|
-
private get filePath(): string {
|
|
95
|
-
return path.join(this.stateDir, PRINCIPLE_TRAINING_FILE);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Load the current ledger state from the file system.
|
|
100
|
-
*
|
|
101
|
-
* Returns an empty store if no persisted state exists (first run).
|
|
102
|
-
* Uses the synchronous loadLedger from principle-tree-ledger which
|
|
103
|
-
* handles missing/corrupted files gracefully.
|
|
104
|
-
*/
|
|
105
|
-
async loadLedger(): Promise<HybridLedgerStore> {
|
|
106
|
-
return loadLedgerFromFile(this.stateDir);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Persist the full ledger state atomically.
|
|
111
|
-
*
|
|
112
|
-
* Delegates to principle-tree-ledger's saveLedgerAsync which uses
|
|
113
|
-
* withLockAsync internally. Logs failures via SystemLogger.
|
|
114
|
-
*/
|
|
115
|
-
async saveLedger(store: HybridLedgerStore): Promise<void> {
|
|
116
|
-
try {
|
|
117
|
-
await saveLedgerAsync(this.stateDir, store);
|
|
118
|
-
} catch (err) {
|
|
119
|
-
SystemLogger.log(
|
|
120
|
-
this.workspaceDir,
|
|
121
|
-
'STORAGE_WRITE_FAILED',
|
|
122
|
-
`FileStorageAdapter.saveLedger failed: ${String(err)}`,
|
|
123
|
-
);
|
|
124
|
-
throw err;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Perform a read-modify-write cycle with automatic locking and retry.
|
|
130
|
-
*
|
|
131
|
-
* Uses withLockAsync to acquire a file lock, reads the current store,
|
|
132
|
-
* applies the mutate function, then writes the modified store atomically.
|
|
133
|
-
* On lock acquisition failure, retries up to MUTATE_RETRY_COUNT (5) times
|
|
134
|
-
* with exponential backoff + jitter to reduce contention.
|
|
135
|
-
*
|
|
136
|
-
* Write failures are logged to SystemLogger and re-thrown so callers
|
|
137
|
-
* can decide how to handle persistence errors.
|
|
138
|
-
*/
|
|
139
|
-
async mutateLedger<T>(mutate: (store: HybridLedgerStore) => T | Promise<T>): Promise<T> {
|
|
140
|
-
let lastError: Error | undefined;
|
|
141
|
-
|
|
142
|
-
for (let attempt = 0; attempt < MUTATE_RETRY_COUNT; attempt++) {
|
|
143
|
-
try {
|
|
144
|
-
const lockOptions: LockOptions = {
|
|
145
|
-
maxRetries: 3,
|
|
146
|
-
baseRetryDelayMs: 10,
|
|
147
|
-
maxRetryDelayMs: 200,
|
|
148
|
-
lockStaleMs: 10_000,
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const ledgerPath = this.filePath;
|
|
152
|
-
|
|
153
|
-
return await withLockAsync(ledgerPath, async () => {
|
|
154
|
-
const store = loadLedgerFromFile(this.stateDir);
|
|
155
|
-
const result = await mutate(store);
|
|
156
|
-
|
|
157
|
-
// Write directly — we already hold the lock, so we must NOT
|
|
158
|
-
// call saveLedger/saveLedgerAsync (they try to acquire the same lock).
|
|
159
|
-
try {
|
|
160
|
-
ensureParentDir(ledgerPath);
|
|
161
|
-
atomicWriteFileSync(ledgerPath, serializeStore(store));
|
|
162
|
-
} catch (writeErr) {
|
|
163
|
-
SystemLogger.log(
|
|
164
|
-
this.workspaceDir,
|
|
165
|
-
'STORAGE_WRITE_FAILED',
|
|
166
|
-
`FileStorageAdapter.mutateLedger write failed: ${String(writeErr)}`,
|
|
167
|
-
);
|
|
168
|
-
throw writeErr;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return result;
|
|
172
|
-
}, lockOptions);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
lastError = err as Error;
|
|
175
|
-
|
|
176
|
-
// Only retry on lock acquisition errors
|
|
177
|
-
if (err instanceof LockAcquisitionError && attempt < MUTATE_RETRY_COUNT - 1) {
|
|
178
|
-
const delay = Math.min(
|
|
179
|
-
MUTATE_BACKOFF_BASE_MS * Math.pow(2, attempt),
|
|
180
|
-
MUTATE_BACKOFF_MAX_MS,
|
|
181
|
-
);
|
|
182
|
-
// Add jitter (0-20%) to avoid thundering herd
|
|
183
|
-
const jitter = delay * 0.2 * Math.random();
|
|
184
|
-
const totalDelay = Math.floor(delay + jitter);
|
|
185
|
-
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Non-retryable error or exhausted retries
|
|
191
|
-
SystemLogger.log(
|
|
192
|
-
this.workspaceDir,
|
|
193
|
-
'STORAGE_MUTATE_FAILED',
|
|
194
|
-
`FileStorageAdapter.mutateLedger failed after ${attempt + 1} attempts: ${String(err)}`,
|
|
195
|
-
);
|
|
196
|
-
throw err;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Should not reach here, but satisfy the type checker
|
|
201
|
-
throw lastError ?? new Error('FileStorageAdapter.mutateLedger: unexpected state');
|
|
202
|
-
}
|
|
203
|
-
}
|