principles-disciple 1.52.0 → 1.54.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.
@@ -0,0 +1,113 @@
1
+ ---
2
+ phase: "01-basic-visualization"
3
+ plan: "01-GAP-CLOSURE"
4
+ verified: 2026-04-17T15:30:00Z
5
+ status: passed
6
+ score: "4/4 must-haves verified"
7
+ overrides_applied: 0
8
+ re_verification: false
9
+ gaps: []
10
+ deferred: []
11
+ human_verification: []
12
+ requirement_mismatch:
13
+ note: "Requirement IDs provided (SDK-CORE-03, SDK-ADP-07, SDK-ADP-08, SDK-TEST-02, SDK-TEST-03, SDK-MGMT-01, SDK-MGMT-02) are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. This phase (01-basic-visualization) addresses VIZ-04 (Empty state optimization) - a different domain. Requirement coverage cannot be established for IDs that do not belong to this phase."
14
+ ---
15
+
16
+ # Phase 01-basic-visualization: GAP-CLOSURE Verification Report
17
+
18
+ **Phase Goal:** Close verification gaps from Phase 01 by implementing missing i18n fixes for LineChart component and coverage trend empty state.
19
+ **Verified:** 2026-04-17T15:30:00Z
20
+ **Status:** passed
21
+ **Re-verification:** No — initial verification
22
+
23
+ ## Goal Achievement
24
+
25
+ ### Observable Truths
26
+
27
+ | # | Truth | Status | Evidence |
28
+ | --- | ----- | ------ | -------- |
29
+ | 1 | LineChart interface has emptyText prop for i18n | VERIFIED | `emptyText?: string;` at line 864 in charts.tsx |
30
+ | 2 | LineChart renders emptyText prop instead of hardcoded '暂无数据' | VERIFIED | Conditional render at lines 878-885: `if (!emptyText) return null;` then `{emptyText}` |
31
+ | 3 | Coverage trend section shows EmptyState when no data | VERIFIED | Ternary at line 259: `data.coverageTrend.length >= 1 ? (...) : (<EmptyState ...>)` |
32
+ | 4 | All LineChart usages pass emptyText prop | VERIFIED | Lines 272, 454, 521 all have `emptyText={t('common.noData')}` |
33
+
34
+ **Score:** 4/4 truths verified
35
+
36
+ ### Deferred Items
37
+
38
+ None
39
+
40
+ ### Required Artifacts
41
+
42
+ | Artifact | Expected | Status | Details |
43
+ | -------- | -------- | ------ | ------- |
44
+ | `charts.tsx:864` | `emptyText?: string` in LineChartProps | VERIFIED | Prop exists at correct line |
45
+ | `charts.tsx:876` | Default value `emptyText = ''` | VERIFIED | Default empty string assigned |
46
+ | `charts.tsx:878-885` | Conditional render using emptyText | VERIFIED | Returns null if no emptyText, renders div with {emptyText} otherwise |
47
+ | `ThinkingModelsPage.tsx:259` | Ternary operator for coverage trend | VERIFIED | `data.coverageTrend.length >= 1 ? (...) : (<EmptyState ...>)` |
48
+ | `ThinkingModelsPage.tsx:272` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
49
+ | `ThinkingModelsPage.tsx:454` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
50
+ | `ThinkingModelsPage.tsx:521` | LineChart with emptyText prop | VERIFIED | `emptyText={t('common.noData')}` |
51
+
52
+ ### Key Link Verification
53
+
54
+ | From | To | Via | Status | Details |
55
+ | ---- | --- | --- | ------ | ------- |
56
+ | LineChart | i18n system | emptyText prop | WIRED | emptyText prop accepts i18n string and renders it |
57
+ | Coverage trend | EmptyState | ternary operator | WIRED | Ternary correctly switches between LineChart and EmptyState |
58
+ | ThinkingModelsPage | common.noData | t() function | WIRED | All LineChart usages pass i18n key |
59
+ | EmptyState | i18n keys | t() function | WIRED | `t('thinkingModels.emptyCoverageTrend')` and `t('thinkingModels.emptyCoverageTrendDesc')` used |
60
+
61
+ ### Data-Flow Trace (Level 4)
62
+
63
+ | Artifact | Data Variable | Source | Produces Real Data | Status |
64
+ | -------- | ------------- | ------ | ------------------ | ------ |
65
+ | LineChart emptyText | emptyText prop | Parent component via t('common.noData') | N/A | STATIC — i18n key is static, not dynamic |
66
+
67
+ ### Behavioral Spot-Checks
68
+
69
+ | Behavior | Command | Result | Status |
70
+ | -------- | ------- | ------ | ------ |
71
+ | emptyText in interface | `grep -n "emptyText?: string" charts.tsx` | `864: emptyText?: string;` | PASS |
72
+ | No hardcoded Chinese | `grep -n "暂无数据" charts.tsx` | No matches | PASS |
73
+ | Ternary for coverage | `grep -n "coverageTrend.length.*?" ThinkingModelsPage.tsx` | `259: {data.coverageTrend.length >= 1 ?` | PASS |
74
+ | EmptyState for coverage | `grep -n "EmptyState" ThinkingModelsPage.tsx \| head -3` | Lines 276-279 with i18n keys | PASS |
75
+ | All LineChart have emptyText | `grep -B5 -A10 "<LineChart" ThinkingModelsPage.tsx \| grep -c "emptyText"` | 3 | PASS |
76
+
77
+ ### Requirements Coverage
78
+
79
+ **IMPORTANT - Requirement Mismatch Found:**
80
+
81
+ | Requirement | Source | Description | Status | Evidence |
82
+ | ----------- | ------ | ----------- | ------ | -------- |
83
+ | SDK-CORE-03 | User-provided | Implement universal PainSignal interface logic | N/A | Does not belong to 01-basic-visualization phase |
84
+ | SDK-ADP-07 | User-provided | Implement Coding domain adapter | N/A | Does not belong to 01-basic-visualization phase |
85
+ | SDK-ADP-08 | User-provided | Implement second domain adapter | N/A | Does not belong to 01-basic-visualization phase |
86
+ | SDK-TEST-02 | User-provided | Implement full Adapter conformance test suite | N/A | Does not belong to 01-basic-visualization phase |
87
+ | SDK-TEST-03 | User-provided | Execute and publish performance benchmarks | N/A | Does not belong to 01-basic-visualization phase |
88
+ | SDK-MGMT-01 | User-provided | Package SDK as @principles/core npm package | N/A | Does not belong to 01-basic-visualization phase |
89
+ | SDK-MGMT-02 | User-provided | Establish Semver versioning and migration guides | N/A | Does not belong to 01-basic-visualization phase |
90
+ | VIZ-04 | GAP-CLOSURE-PLAN | Empty state optimization | VERIFIED | All 4 must_haves from GAP-CLOSURE-PLAN verified |
91
+
92
+ **The 7 requirement IDs provided are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. They do not map to the 01-basic-visualization phase. The phase's actual scope is VIZ-04 (Empty state optimization), which has been verified through must_haves.**
93
+
94
+ ### Anti-Patterns Found
95
+
96
+ | File | Line | Pattern | Severity | Impact |
97
+ | ---- | ---- | ------- | -------- | ------ |
98
+ | None | - | No anti-patterns detected | - | - |
99
+
100
+ ### Human Verification Required
101
+
102
+ None required — all truths verifiable via static analysis.
103
+
104
+ ### Gaps Summary
105
+
106
+ **None.** All 4 must_haves verified. Phase goal achieved.
107
+
108
+ **Notable finding:** The requirement IDs provided by user (SDK-CORE-03, SDK-ADP-07, SDK-ADP-08, SDK-TEST-02, SDK-TEST-03, SDK-MGMT-01, SDK-MGMT-02) are SDK Core Implementation Phase 1 requirements per REQUIREMENTS.md. This phase (01-basic-visualization) addresses VIZ-04 (Empty state optimization for visualization) - a different scope. Requirement coverage cannot be established for IDs that do not belong to this phase.
109
+
110
+ ---
111
+
112
+ _Verified: 2026-04-17T15:30:00Z_
113
+ _Verifier: Claude (gsd-verifier)_
@@ -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.52.0",
5
+ "version": "1.54.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.52.0",
3
+ "version": "1.54.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -13,8 +13,10 @@
13
13
  * npm run bootstrap-rules (production)
14
14
  */
15
15
 
16
- import { loadLedger, createRule, updatePrinciple } from './principle-tree-ledger.js';
16
+ import { loadLedger, createRule, updatePrinciple, addPrincipleToLedger } from './principle-tree-ledger.js';
17
+ import type { LedgerPrinciple } from './principle-tree-ledger.js';
17
18
  import { loadStore } from './principle-training-state.js';
19
+ import { CORE_THINKING_MODELS } from './init.js';
18
20
 
19
21
  export interface BootstrapResult {
20
22
  principleId: string;
@@ -77,12 +79,49 @@ export function selectPrinciplesForBootstrap(stateDir: string, limit = 3): strin
77
79
  * @throws Error if no deterministic principles found
78
80
  */
79
81
  export function bootstrapRules(stateDir: string, limit = 3): BootstrapResult[] {
82
+ // Migration: if T-01..T-10 exist in Training Store but not in Ledger Tree, backfill.
83
+ // This handles workspaces initialized before Ledger Tree was added.
84
+ const store = loadStore(stateDir);
85
+ let ledger = loadLedger(stateDir);
86
+ const hasTrainingT = Object.keys(store).some((id) => id.startsWith('T-'));
87
+ const hasAnyLedgerT = Object.keys(ledger.tree.principles).some((id) => id.startsWith('T-'));
88
+ if (hasTrainingT && !hasAnyLedgerT) {
89
+ console.warn('[bootstrap] Migrating T-01..T-10 from Training Store to Ledger Tree');
90
+ const now = new Date().toISOString();
91
+ for (const [id, entry] of Object.entries(store)) {
92
+ if (!id.startsWith('T-')) continue;
93
+ const model = CORE_THINKING_MODELS.find((m) => m.id === id);
94
+ if (!model) continue;
95
+ const lp: LedgerPrinciple = {
96
+ id,
97
+ version: 1,
98
+ text: model.description,
99
+ coreAxiomId: id,
100
+ triggerPattern: '',
101
+ action: '',
102
+ status: 'active',
103
+ priority: 'P1',
104
+ scope: 'general',
105
+ evaluability: entry.evaluability,
106
+ valueScore: 0,
107
+ adherenceRate: 0,
108
+ painPreventedCount: 0,
109
+ derivedFromPainIds: [],
110
+ ruleIds: [],
111
+ conflictsWithPrincipleIds: [],
112
+ createdAt: now,
113
+ updatedAt: now,
114
+ suggestedRules: [],
115
+ };
116
+ addPrincipleToLedger(stateDir, lp);
117
+ }
118
+ // Reload ledger after migration so subsequent reads see the new data.
119
+ ledger = loadLedger(stateDir);
120
+ }
121
+
80
122
  // Select principles for bootstrap
81
123
  const selectedPrincipleIds = selectPrinciplesForBootstrap(stateDir, limit);
82
124
 
83
- // Load current ledger state
84
- const ledger = loadLedger(stateDir);
85
-
86
125
  const results: BootstrapResult[] = [];
87
126
 
88
127
  for (const principleId of selectedPrincipleIds) {
@@ -0,0 +1,74 @@
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
+ };
@@ -0,0 +1,203 @@
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
+ }
package/src/core/init.ts CHANGED
@@ -5,6 +5,8 @@ import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
5
5
  import { PD_DIRS } from './paths.js';
6
6
  import { defaultContextConfig } from '../types.js';
7
7
  import { loadStore, setPrincipleState, type PrincipleTrainingState } from './principle-training-state.js';
8
+ import { addPrincipleToLedger } from './principle-tree-ledger.js';
9
+ import type { LedgerPrinciple } from './principle-tree-ledger.js';
8
10
  import { atomicWriteFileSync } from '../utils/io.js';
9
11
  import { createDefaultKeywordStore, saveKeywordStore } from './empathy-keyword-matcher.js';
10
12
 
@@ -150,7 +152,7 @@ function copyRecursiveSync(srcDir: string, destDir: string, api: OpenClawPluginA
150
152
  * Core thinking model definitions (T-01 through T-10).
151
153
  * These are the built-in cognitive patterns that every workspace should have.
152
154
  */
153
- const CORE_THINKING_MODELS: Array<{
155
+ export const CORE_THINKING_MODELS: Array<{
154
156
  id: string;
155
157
  name: string;
156
158
  description: string;
@@ -190,7 +192,7 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
190
192
  for (const model of CORE_THINKING_MODELS) {
191
193
  const state: PrincipleTrainingState = {
192
194
  principleId: model.id,
193
- evaluability: 'deterministic',
195
+ evaluability: 'manual_only',
194
196
  applicableOpportunityCount: 0,
195
197
  observedViolationCount: 0,
196
198
  complianceRate: 0,
@@ -202,6 +204,31 @@ export function ensureCorePrinciples(stateDir: string, logger: PluginLogger): bo
202
204
  internalizationStatus: 'needs_training',
203
205
  };
204
206
  setPrincipleState(stateDir, state);
207
+
208
+ // Also write to Ledger Tree so bootstrapRules() can find them
209
+ const now = new Date().toISOString();
210
+ const ledgerPrinciple: LedgerPrinciple = {
211
+ id: model.id,
212
+ version: 1,
213
+ text: model.description,
214
+ coreAxiomId: model.id,
215
+ triggerPattern: '',
216
+ action: '',
217
+ status: 'active',
218
+ priority: 'P1',
219
+ scope: 'general',
220
+ evaluability: 'manual_only',
221
+ valueScore: 0,
222
+ adherenceRate: 0,
223
+ painPreventedCount: 0,
224
+ derivedFromPainIds: [],
225
+ ruleIds: [],
226
+ conflictsWithPrincipleIds: [],
227
+ createdAt: now,
228
+ updatedAt: now,
229
+ suggestedRules: [],
230
+ };
231
+ addPrincipleToLedger(stateDir, ledgerPrinciple);
205
232
  }
206
233
 
207
234
  logger.info(`[PD] Initialized ${CORE_THINKING_MODELS.length} core thinking models: T-01 through T-10`);