principles-disciple 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +94 -5
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +17 -42
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +83 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +300 -57
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +713 -0
  77. package/src/core/profile.ts +3 -1
  78. package/src/core/promotion-gate.ts +14 -18
  79. package/src/core/replay-engine.ts +562 -0
  80. package/src/core/risk-calculator.ts +6 -4
  81. package/src/core/rule-host-helpers.ts +39 -0
  82. package/src/core/rule-host-types.ts +82 -0
  83. package/src/core/rule-host.ts +245 -0
  84. package/src/core/rule-implementation-runtime.ts +38 -0
  85. package/src/core/schema/db-types.ts +16 -0
  86. package/src/core/schema/index.ts +26 -0
  87. package/src/core/schema/migration-runner.ts +207 -0
  88. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  89. package/src/core/schema/migrations/002-init-central.ts +122 -0
  90. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  91. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  92. package/src/core/schema/migrations/index.ts +31 -0
  93. package/src/core/schema/schema-definitions.ts +650 -0
  94. package/src/core/session-tracker.ts +6 -4
  95. package/src/core/shadow-observation-registry.ts +6 -3
  96. package/src/core/system-logger.ts +2 -2
  97. package/src/core/thinking-models.ts +182 -46
  98. package/src/core/thinking-os-parser.ts +164 -0
  99. package/src/core/training-program.ts +7 -7
  100. package/src/core/trajectory.ts +42 -36
  101. package/src/core/workspace-context.ts +77 -11
  102. package/src/core/workspace-dir-validation.ts +152 -0
  103. package/src/hooks/AGENTS.md +31 -0
  104. package/src/hooks/bash-risk.ts +3 -1
  105. package/src/hooks/edit-verification.ts +9 -5
  106. package/src/hooks/gate-block-helper.ts +5 -1
  107. package/src/hooks/gate.ts +152 -5
  108. package/src/hooks/gfi-gate.ts +9 -2
  109. package/src/hooks/lifecycle-routing.ts +124 -0
  110. package/src/hooks/lifecycle.ts +12 -12
  111. package/src/hooks/llm.ts +17 -109
  112. package/src/hooks/message-sanitize.ts +5 -3
  113. package/src/hooks/pain.ts +19 -15
  114. package/src/hooks/progressive-trust-gate.ts +7 -1
  115. package/src/hooks/prompt.ts +169 -60
  116. package/src/hooks/subagent.ts +5 -4
  117. package/src/hooks/thinking-checkpoint.ts +2 -0
  118. package/src/hooks/trajectory-collector.ts +15 -12
  119. package/src/http/principles-console-route.ts +31 -68
  120. package/src/i18n/commands.ts +2 -2
  121. package/src/index.ts +126 -40
  122. package/src/service/central-database.ts +131 -43
  123. package/src/service/central-health-service.ts +47 -0
  124. package/src/service/central-overview-service.ts +135 -0
  125. package/src/service/central-sync-service.ts +87 -0
  126. package/src/service/control-ui-query-service.ts +46 -36
  127. package/src/service/event-log-auditor.ts +261 -0
  128. package/src/service/evolution-query-service.ts +23 -22
  129. package/src/service/evolution-worker.ts +565 -261
  130. package/src/service/health-query-service.ts +213 -36
  131. package/src/service/nocturnal-runtime.ts +8 -4
  132. package/src/service/nocturnal-service.ts +499 -59
  133. package/src/service/nocturnal-target-selector.ts +5 -7
  134. package/src/service/runtime-summary-service.ts +2 -1
  135. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  136. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  137. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  138. package/src/service/subagent-workflow/index.ts +2 -0
  139. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +155 -285
  140. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  141. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  142. package/src/service/subagent-workflow/types.ts +9 -4
  143. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  144. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  145. package/src/service/trajectory-service.ts +2 -1
  146. package/src/tools/critique-prompt.ts +1 -1
  147. package/src/tools/deep-reflect.ts +175 -209
  148. package/src/tools/model-index.ts +2 -1
  149. package/src/types/event-types.ts +2 -2
  150. package/src/types/principle-tree-schema.ts +29 -23
  151. package/src/utils/file-lock.ts +5 -3
  152. package/src/utils/io.ts +5 -2
  153. package/src/utils/nlp.ts +5 -46
  154. package/src/utils/node-vm-polyfill.ts +11 -0
  155. package/src/utils/plugin-logger.ts +2 -0
  156. package/src/utils/retry.ts +572 -0
  157. package/src/utils/subagent-probe.ts +1 -1
  158. package/templates/langs/en/core/AGENTS.md +0 -13
  159. package/templates/langs/en/core/SOUL.md +1 -31
  160. package/templates/langs/en/core/TOOLS.md +0 -4
  161. package/templates/langs/en/principles/THINKING_OS.md +64 -0
  162. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  163. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  164. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  165. package/templates/langs/zh/core/AGENTS.md +0 -22
  166. package/templates/langs/zh/core/SOUL.md +1 -31
  167. package/templates/langs/zh/core/TOOLS.md +0 -4
  168. package/templates/langs/zh/principles/THINKING_OS.md +64 -0
  169. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  170. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  171. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  172. package/tests/commands/evolution-status.test.ts +119 -0
  173. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  174. package/tests/core/code-implementation-storage.test.ts +398 -0
  175. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  176. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  177. package/tests/core/nocturnal-artificer.test.ts +241 -0
  178. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  179. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  180. package/tests/core/pd-task-store.test.ts +126 -0
  181. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  182. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  183. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  184. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  185. package/tests/core/principle-training-state.test.ts +228 -1
  186. package/tests/core/principle-tree-ledger.test.ts +423 -0
  187. package/tests/core/regression-v1-9-1.test.ts +265 -0
  188. package/tests/core/replay-engine.test.ts +234 -0
  189. package/tests/core/rule-host-helpers.test.ts +120 -0
  190. package/tests/core/rule-host.test.ts +389 -0
  191. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  192. package/tests/core/workspace-context.test.ts +53 -0
  193. package/tests/core/workspace-dir-validation.test.ts +272 -0
  194. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  195. package/tests/hooks/pain.test.ts +74 -10
  196. package/tests/hooks/prompt.test.ts +63 -1
  197. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  198. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  199. package/tests/service/data-endpoints-regression.test.ts +834 -0
  200. package/tests/service/evolution-worker.test.ts +0 -123
  201. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  202. package/tests/utils/nlp.test.ts +1 -19
  203. package/tests/utils/retry.test.ts +327 -0
  204. package/ui/src/App.tsx +1 -1
  205. package/ui/src/api.ts +4 -0
  206. package/ui/src/charts.tsx +366 -0
  207. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  208. package/ui/src/i18n/ui.ts +55 -22
  209. package/ui/src/pages/OverviewPage.tsx +441 -81
  210. package/ui/src/styles.css +43 -0
  211. package/ui/src/types.ts +17 -1
  212. package/src/agents/nocturnal-dreamer.md +0 -152
  213. package/src/agents/nocturnal-philosopher.md +0 -138
  214. package/src/agents/nocturnal-reflector.md +0 -126
  215. package/src/agents/nocturnal-scribe.md +0 -164
  216. package/templates/workspace/.principles/00-kernel.md +0 -51
  217. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  218. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  219. package/templates/workspace/.principles/PROFILE.json +0 -54
  220. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  221. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  222. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  223. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  224. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  225. package/templates/workspace/.principles/models/first_principles.md +0 -62
  226. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  227. package/templates/workspace/.principles/models/porter_five.md +0 -63
  228. package/templates/workspace/.principles/models/swot.md +0 -60
  229. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  230. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -0,0 +1,713 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { withLock, withLockAsync } from '../utils/file-lock.js';
4
+ import type {
5
+ Implementation,
6
+ ImplementationLifecycleState,
7
+ Principle,
8
+ PrincipleTreeStore,
9
+ PrincipleValueMetrics,
10
+ Rule,
11
+ } from '../types/principle-tree-schema.js';
12
+
13
+ const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
14
+
15
+ export const TREE_NAMESPACE = '_tree';
16
+
17
+ export interface LegacyPrincipleTrainingState {
18
+ principleId: string;
19
+ evaluability: 'deterministic' | 'weak_heuristic' | 'manual_only';
20
+ applicableOpportunityCount: number;
21
+ observedViolationCount: number;
22
+ complianceRate: number;
23
+ violationTrend: number;
24
+ generatedSampleCount: number;
25
+ approvedSampleCount: number;
26
+ includedTrainRunIds: string[];
27
+ deployedCheckpointIds: string[];
28
+ lastEvalScore?: number;
29
+ internalizationStatus:
30
+ | 'prompt_only'
31
+ | 'needs_training'
32
+ | 'in_training'
33
+ | 'deployed_pending_eval'
34
+ | 'internalized'
35
+ | 'regressed';
36
+ }
37
+
38
+ export type LegacyPrincipleTrainingStore = Record<string, LegacyPrincipleTrainingState>;
39
+
40
+ export interface LedgerPrinciple extends Principle {
41
+ suggestedRules?: string[];
42
+ }
43
+
44
+ export interface LedgerRule extends Rule {
45
+ implementationIds: string[];
46
+ }
47
+
48
+ export interface LedgerTreeStore extends Omit<PrincipleTreeStore, 'principles' | 'rules'> {
49
+ principles: Record<string, LedgerPrinciple>;
50
+ rules: Record<string, LedgerRule>;
51
+ }
52
+
53
+ export interface HybridLedgerStore {
54
+ trainingStore: LegacyPrincipleTrainingStore;
55
+ tree: LedgerTreeStore;
56
+ }
57
+
58
+ export interface PrincipleSubtree {
59
+ principle: LedgerPrinciple;
60
+ rules: {
61
+ rule: LedgerRule;
62
+ implementations: Implementation[];
63
+ }[];
64
+ }
65
+
66
+ const VALID_EVALUABILITIES = ['deterministic', 'weak_heuristic', 'manual_only'] as const;
67
+ const VALID_INTERNALIZATION_STATUSES = [
68
+ 'prompt_only',
69
+ 'needs_training',
70
+ 'in_training',
71
+ 'deployed_pending_eval',
72
+ 'internalized',
73
+ 'regressed',
74
+ ] as const;
75
+
76
+ function isRecord(value: unknown): value is Record<string, unknown> {
77
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
78
+ }
79
+
80
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
81
+ function clampFloat(value: unknown, min: number, max: number, fallback: number): number {
82
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
83
+ return fallback;
84
+ }
85
+ return Math.max(min, Math.min(max, value));
86
+ }
87
+
88
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
89
+ function clampInt(value: unknown, min: number, max: number, fallback: number): number {
90
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
91
+ return fallback;
92
+ }
93
+ return Math.max(min, Math.min(max, Math.round(value)));
94
+ }
95
+
96
+ function stringArray(value: unknown): string[] {
97
+ return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
98
+ }
99
+
100
+ function uniqueStrings(values: string[]): string[] {
101
+ return Array.from(new Set(values));
102
+ }
103
+
104
+ function createEmptyTree(): LedgerTreeStore {
105
+ return {
106
+ principles: {},
107
+ rules: {},
108
+ implementations: {},
109
+ metrics: {},
110
+ lastUpdated: new Date(0).toISOString(),
111
+ };
112
+ }
113
+
114
+ function parseLegacyTrainingStore(raw: unknown): LegacyPrincipleTrainingStore {
115
+ if (!isRecord(raw)) {
116
+ return {};
117
+ }
118
+
119
+ const result: LegacyPrincipleTrainingStore = {};
120
+
121
+ for (const [principleId, candidate] of Object.entries(raw)) {
122
+ if (principleId === TREE_NAMESPACE || !isRecord(candidate)) {
123
+ continue;
124
+ }
125
+
126
+ if (candidate.principleId !== principleId) {
127
+ continue;
128
+ }
129
+
130
+ const evaluability = VALID_EVALUABILITIES.includes(candidate.evaluability as (typeof VALID_EVALUABILITIES)[number])
131
+ ? candidate.evaluability as LegacyPrincipleTrainingState['evaluability']
132
+ : 'manual_only';
133
+ const internalizationStatus = VALID_INTERNALIZATION_STATUSES.includes(
134
+ candidate.internalizationStatus as (typeof VALID_INTERNALIZATION_STATUSES)[number],
135
+ )
136
+ ? candidate.internalizationStatus as LegacyPrincipleTrainingState['internalizationStatus']
137
+ : 'prompt_only';
138
+ const rawLastEvalScore = candidate.lastEvalScore;
139
+
140
+ result[principleId] = {
141
+ principleId,
142
+ evaluability,
143
+ applicableOpportunityCount: clampInt(candidate.applicableOpportunityCount, 0, Number.POSITIVE_INFINITY, 0),
144
+ observedViolationCount: clampInt(candidate.observedViolationCount, 0, Number.POSITIVE_INFINITY, 0),
145
+ complianceRate: clampFloat(candidate.complianceRate, 0, 1, 0),
146
+ violationTrend: clampFloat(candidate.violationTrend, -1, 1, 0),
147
+ generatedSampleCount: clampInt(candidate.generatedSampleCount, 0, Number.POSITIVE_INFINITY, 0),
148
+ approvedSampleCount: clampInt(candidate.approvedSampleCount, 0, Number.POSITIVE_INFINITY, 0),
149
+ includedTrainRunIds: stringArray(candidate.includedTrainRunIds),
150
+ deployedCheckpointIds: stringArray(candidate.deployedCheckpointIds),
151
+ lastEvalScore:
152
+ typeof rawLastEvalScore === 'number' && Number.isFinite(rawLastEvalScore)
153
+ ? clampFloat(rawLastEvalScore, 0, 1, 0)
154
+ : undefined,
155
+ internalizationStatus,
156
+ };
157
+ }
158
+
159
+ return result;
160
+ }
161
+
162
+ function parsePrinciples(raw: unknown): Record<string, LedgerPrinciple> {
163
+ if (!isRecord(raw)) {
164
+ return {};
165
+ }
166
+
167
+ const principles: Record<string, LedgerPrinciple> = {};
168
+ for (const [id, value] of Object.entries(raw)) {
169
+ if (!isRecord(value)) {
170
+ continue;
171
+ }
172
+
173
+ const suggestedRules = stringArray(value.suggestedRules);
174
+ principles[id] = {
175
+ ...value,
176
+ id,
177
+ ruleIds: stringArray(value.ruleIds),
178
+ conflictsWithPrincipleIds: stringArray(value.conflictsWithPrincipleIds),
179
+ derivedFromPainIds: stringArray(value.derivedFromPainIds),
180
+ ...(Object.prototype.hasOwnProperty.call(value, 'suggestedRules') ? { suggestedRules } : {}),
181
+ } as LedgerPrinciple;
182
+ }
183
+
184
+ return principles;
185
+ }
186
+
187
+ function parseRules(raw: unknown): Record<string, LedgerRule> {
188
+ if (!isRecord(raw)) {
189
+ return {};
190
+ }
191
+
192
+ const rules: Record<string, LedgerRule> = {};
193
+ for (const [id, value] of Object.entries(raw)) {
194
+ if (!isRecord(value)) {
195
+ continue;
196
+ }
197
+
198
+ rules[id] = {
199
+ ...value,
200
+ id,
201
+ principleId: typeof value.principleId === 'string' ? value.principleId : '',
202
+ implementationIds: stringArray(value.implementationIds),
203
+ } as LedgerRule;
204
+ }
205
+
206
+ return rules;
207
+ }
208
+
209
+ function parseImplementations(raw: unknown): Record<string, Implementation> {
210
+ if (!isRecord(raw)) {
211
+ return {};
212
+ }
213
+
214
+ const implementations: Record<string, Implementation> = {};
215
+ for (const [id, value] of Object.entries(raw)) {
216
+ if (!isRecord(value) || typeof value.ruleId !== 'string') {
217
+ continue;
218
+ }
219
+
220
+ implementations[id] = {
221
+ ...value,
222
+ id,
223
+ ruleId: value.ruleId,
224
+ } as Implementation;
225
+ }
226
+
227
+ return implementations;
228
+ }
229
+
230
+ function parseMetrics(raw: unknown): Record<string, PrincipleValueMetrics> {
231
+ if (!isRecord(raw)) {
232
+ return {};
233
+ }
234
+
235
+ const metrics: Record<string, PrincipleValueMetrics> = {};
236
+ for (const [id, value] of Object.entries(raw)) {
237
+ if (!isRecord(value)) {
238
+ continue;
239
+ }
240
+
241
+ metrics[id] = {
242
+ ...value,
243
+ principleId: typeof value.principleId === 'string' ? value.principleId : id,
244
+ } as PrincipleValueMetrics;
245
+ }
246
+
247
+ return metrics;
248
+ }
249
+
250
+ function parseTree(raw: unknown): LedgerTreeStore {
251
+ if (!isRecord(raw)) {
252
+ return createEmptyTree();
253
+ }
254
+
255
+ return {
256
+ principles: parsePrinciples(raw.principles),
257
+ rules: parseRules(raw.rules),
258
+ implementations: parseImplementations(raw.implementations),
259
+ metrics: parseMetrics(raw.metrics),
260
+ lastUpdated: typeof raw.lastUpdated === 'string' ? raw.lastUpdated : new Date(0).toISOString(),
261
+ };
262
+ }
263
+
264
+ function serializeLedger(store: HybridLedgerStore): string {
265
+ return JSON.stringify(
266
+ {
267
+ ...store.trainingStore,
268
+ [TREE_NAMESPACE]: {
269
+ ...store.tree,
270
+ lastUpdated: new Date().toISOString(),
271
+ },
272
+ },
273
+ null,
274
+ 2,
275
+ );
276
+ }
277
+
278
+ function ensureParentDir(filePath: string): void {
279
+ const dir = path.dirname(filePath);
280
+ if (!fs.existsSync(dir)) {
281
+ fs.mkdirSync(dir, { recursive: true });
282
+ }
283
+ }
284
+
285
+ function readLedgerFromFile(filePath: string): HybridLedgerStore {
286
+ if (!fs.existsSync(filePath)) {
287
+ return {
288
+ trainingStore: {},
289
+ tree: createEmptyTree(),
290
+ };
291
+ }
292
+
293
+ try {
294
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as unknown;
295
+ const raw = isRecord(parsed) ? parsed : {};
296
+ // #219: Handle both formats:
297
+ // - New format: { trainingStore: {...}, tree: {...} }
298
+ // - Legacy format: { P_xxx: {...}, _tree: {...} }
299
+ const trainingStoreRaw = raw.trainingStore ?? raw;
300
+ const treeRaw = raw[TREE_NAMESPACE] ?? raw.tree;
301
+ return {
302
+ trainingStore: parseLegacyTrainingStore(trainingStoreRaw),
303
+ tree: parseTree(treeRaw),
304
+ };
305
+ } catch {
306
+ return {
307
+ trainingStore: {},
308
+ tree: createEmptyTree(),
309
+ };
310
+ }
311
+ }
312
+
313
+ function writeLedgerUnlocked(filePath: string, store: HybridLedgerStore): void {
314
+ ensureParentDir(filePath);
315
+ fs.writeFileSync(filePath, serializeLedger(store), 'utf-8');
316
+ }
317
+
318
+ /* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
319
+ function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) => T): T {
320
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
321
+ const filePath = getLedgerFilePath(stateDir);
322
+ return withLock(filePath, () => {
323
+ const store = readLedgerFromFile(filePath);
324
+ const result = mutate(store);
325
+ writeLedgerUnlocked(filePath, store);
326
+ return result;
327
+ });
328
+ }
329
+
330
+ /* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
331
+ async function mutateLedgerAsync<T>(stateDir: string, mutate: (store: HybridLedgerStore) => Promise<T>): Promise<T> {
332
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
333
+ const filePath = getLedgerFilePath(stateDir);
334
+ return withLockAsync(filePath, async () => {
335
+ const store = readLedgerFromFile(filePath);
336
+ const result = await mutate(store);
337
+ writeLedgerUnlocked(filePath, store);
338
+ return result;
339
+ });
340
+ }
341
+
342
+ export function getLedgerFilePath(stateDir: string): string {
343
+ return path.join(stateDir, PRINCIPLE_TRAINING_FILE);
344
+ }
345
+
346
+ export function loadLedger(stateDir: string): HybridLedgerStore {
347
+ return readLedgerFromFile(getLedgerFilePath(stateDir));
348
+ }
349
+
350
+ export function saveLedger(stateDir: string, store: HybridLedgerStore): void {
351
+ mutateLedger(stateDir, (current) => {
352
+ current.trainingStore = store.trainingStore;
353
+ current.tree = store.tree;
354
+ });
355
+ }
356
+
357
+ export async function saveLedgerAsync(stateDir: string, store: HybridLedgerStore): Promise<void> {
358
+ await mutateLedgerAsync(stateDir, async (current) => {
359
+ current.trainingStore = store.trainingStore;
360
+ current.tree = store.tree;
361
+ });
362
+ }
363
+
364
+ export function updateTrainingStore(
365
+ stateDir: string,
366
+ /* eslint-disable no-unused-vars -- Reason: callback parameter is forwarded to mutateLedger callback */
367
+ mutate: (store: LegacyPrincipleTrainingStore) => void,
368
+ ): void {
369
+ mutateLedger(stateDir, (store) => {
370
+ mutate(store.trainingStore);
371
+ });
372
+ }
373
+
374
+ export function createRule(stateDir: string, rule: LedgerRule): LedgerRule {
375
+ return mutateLedger(stateDir, (store) => {
376
+ const principle = store.tree.principles[rule.principleId];
377
+ if (!principle) {
378
+ throw new Error(`Cannot create rule "${rule.id}" for missing principle "${rule.principleId}".`);
379
+ }
380
+
381
+ const nextRule: LedgerRule = {
382
+ ...rule,
383
+ implementationIds: uniqueStrings(rule.implementationIds),
384
+ };
385
+ store.tree.rules[nextRule.id] = nextRule;
386
+ principle.ruleIds = uniqueStrings([...principle.ruleIds, nextRule.id]);
387
+ return nextRule;
388
+ });
389
+ }
390
+
391
+ export function createImplementation(stateDir: string, implementation: Implementation): Implementation {
392
+ return mutateLedger(stateDir, (store) => {
393
+ const rule = store.tree.rules[implementation.ruleId];
394
+ if (!rule) {
395
+ throw new Error(`Cannot create implementation "${implementation.id}" for missing rule "${implementation.ruleId}".`);
396
+ }
397
+
398
+ store.tree.implementations[implementation.id] = implementation;
399
+ rule.implementationIds = uniqueStrings([...rule.implementationIds, implementation.id]);
400
+ return implementation;
401
+ });
402
+ }
403
+
404
+ export function updatePrinciple(
405
+ stateDir: string,
406
+ principleId: string,
407
+ updates: Partial<LedgerPrinciple>,
408
+ ): LedgerPrinciple {
409
+ return mutateLedger(stateDir, (store) => {
410
+ const existingPrinciple = store.tree.principles[principleId];
411
+ if (!existingPrinciple) {
412
+ throw new Error(`Cannot update missing principle "${principleId}".`);
413
+ }
414
+
415
+ const nextPrinciple: LedgerPrinciple = {
416
+ ...existingPrinciple,
417
+ ...updates,
418
+ id: principleId,
419
+ ruleIds: updates.ruleIds ? uniqueStrings(updates.ruleIds) : existingPrinciple.ruleIds,
420
+ conflictsWithPrincipleIds: updates.conflictsWithPrincipleIds
421
+ ? uniqueStrings(updates.conflictsWithPrincipleIds)
422
+ : existingPrinciple.conflictsWithPrincipleIds,
423
+ derivedFromPainIds: updates.derivedFromPainIds
424
+ ? uniqueStrings(updates.derivedFromPainIds)
425
+ : existingPrinciple.derivedFromPainIds,
426
+ ...(Object.prototype.hasOwnProperty.call(updates, 'suggestedRules')
427
+ ? { suggestedRules: uniqueStrings(updates.suggestedRules ?? []) }
428
+ : Object.prototype.hasOwnProperty.call(existingPrinciple, 'suggestedRules')
429
+ ? { suggestedRules: existingPrinciple.suggestedRules }
430
+ : {}),
431
+ };
432
+
433
+ store.tree.principles[principleId] = nextPrinciple;
434
+ return nextPrinciple;
435
+ });
436
+ }
437
+
438
+ export function updateRule(stateDir: string, ruleId: string, updates: Partial<LedgerRule>): LedgerRule {
439
+ return mutateLedger(stateDir, (store) => {
440
+ const existingRule = store.tree.rules[ruleId];
441
+ if (!existingRule) {
442
+ throw new Error(`Cannot update missing rule "${ruleId}".`);
443
+ }
444
+
445
+ const nextPrincipleId = updates.principleId ?? existingRule.principleId;
446
+ const nextPrinciple = store.tree.principles[nextPrincipleId];
447
+ if (!nextPrinciple) {
448
+ throw new Error(`Cannot move rule "${ruleId}" to missing principle "${nextPrincipleId}".`);
449
+ }
450
+
451
+ const nextRule: LedgerRule = {
452
+ ...existingRule,
453
+ ...updates,
454
+ id: ruleId,
455
+ principleId: nextPrincipleId,
456
+ implementationIds: updates.implementationIds
457
+ ? uniqueStrings(updates.implementationIds)
458
+ : existingRule.implementationIds,
459
+ };
460
+
461
+ if (existingRule.principleId !== nextPrincipleId) {
462
+ const previousPrinciple = store.tree.principles[existingRule.principleId];
463
+ if (previousPrinciple) {
464
+ previousPrinciple.ruleIds = previousPrinciple.ruleIds.filter((candidateId) => candidateId !== ruleId);
465
+ }
466
+ nextPrinciple.ruleIds = uniqueStrings([...nextPrinciple.ruleIds, ruleId]);
467
+ }
468
+
469
+ store.tree.rules[ruleId] = nextRule;
470
+ return nextRule;
471
+ });
472
+ }
473
+
474
+ export function deleteRule(stateDir: string, ruleId: string): LedgerRule | undefined {
475
+ return mutateLedger(stateDir, (store) => {
476
+ const existingRule = store.tree.rules[ruleId];
477
+ if (!existingRule) {
478
+ return undefined;
479
+ }
480
+
481
+ const parentPrinciple = store.tree.principles[existingRule.principleId];
482
+ if (parentPrinciple) {
483
+ parentPrinciple.ruleIds = parentPrinciple.ruleIds.filter((candidateId) => candidateId !== ruleId);
484
+ }
485
+
486
+ const implementationIds = uniqueStrings([
487
+ ...existingRule.implementationIds,
488
+ ...Object.values(store.tree.implementations)
489
+ .filter((implementation) => implementation.ruleId === ruleId)
490
+ .map((implementation) => implementation.id),
491
+ ]);
492
+ for (const implementationId of implementationIds) {
493
+ delete store.tree.implementations[implementationId];
494
+ }
495
+
496
+ delete store.tree.rules[ruleId];
497
+ return existingRule;
498
+ });
499
+ }
500
+
501
+ export function updateImplementation(
502
+ stateDir: string,
503
+ implementationId: string,
504
+ updates: Partial<Implementation>,
505
+ ): Implementation {
506
+ return mutateLedger(stateDir, (store) => {
507
+ const existingImplementation = store.tree.implementations[implementationId];
508
+ if (!existingImplementation) {
509
+ throw new Error(`Cannot update missing implementation "${implementationId}".`);
510
+ }
511
+
512
+ const nextRuleId = updates.ruleId ?? existingImplementation.ruleId;
513
+ const nextRule = store.tree.rules[nextRuleId];
514
+ if (!nextRule) {
515
+ throw new Error(`Cannot move implementation "${implementationId}" to missing rule "${nextRuleId}".`);
516
+ }
517
+
518
+ const nextImplementation: Implementation = {
519
+ ...existingImplementation,
520
+ ...updates,
521
+ id: implementationId,
522
+ ruleId: nextRuleId,
523
+ };
524
+
525
+ if (existingImplementation.ruleId !== nextRuleId) {
526
+ const previousRule = store.tree.rules[existingImplementation.ruleId];
527
+ if (previousRule) {
528
+ previousRule.implementationIds = previousRule.implementationIds.filter(
529
+ (candidateId) => candidateId !== implementationId,
530
+ );
531
+ }
532
+ nextRule.implementationIds = uniqueStrings([...nextRule.implementationIds, implementationId]);
533
+ }
534
+
535
+ store.tree.implementations[implementationId] = nextImplementation;
536
+ return nextImplementation;
537
+ });
538
+ }
539
+
540
+ export function deleteImplementation(stateDir: string, implementationId: string): Implementation | undefined {
541
+ return mutateLedger(stateDir, (store) => {
542
+ const existingImplementation = store.tree.implementations[implementationId];
543
+ if (!existingImplementation) {
544
+ return undefined;
545
+ }
546
+
547
+ const parentRule = store.tree.rules[existingImplementation.ruleId];
548
+ if (parentRule) {
549
+ parentRule.implementationIds = parentRule.implementationIds.filter(
550
+ (candidateId) => candidateId !== implementationId,
551
+ );
552
+ }
553
+
554
+ delete store.tree.implementations[implementationId];
555
+ return existingImplementation;
556
+ });
557
+ }
558
+
559
+ export function listImplementationsForRule(stateDir: string, ruleId: string): Implementation[] {
560
+ const ledger = loadLedger(stateDir);
561
+ const rule = ledger.tree.rules[ruleId];
562
+ if (!rule) {
563
+ return [];
564
+ }
565
+
566
+ return rule.implementationIds
567
+ .map((implementationId) => ledger.tree.implementations[implementationId])
568
+ .filter((implementation): implementation is Implementation => implementation !== undefined);
569
+ }
570
+
571
+ export function getPrincipleSubtree(stateDir: string, principleId: string): PrincipleSubtree | undefined {
572
+ const ledger = loadLedger(stateDir);
573
+ const principle = ledger.tree.principles[principleId];
574
+ if (!principle) {
575
+ return undefined;
576
+ }
577
+
578
+ return {
579
+ principle,
580
+ rules: principle.ruleIds
581
+ .map((ruleId) => ledger.tree.rules[ruleId])
582
+ .filter((rule): rule is LedgerRule => rule !== undefined)
583
+ .map((rule) => ({
584
+ rule,
585
+ implementations: rule.implementationIds
586
+ .map((implementationId) => ledger.tree.implementations[implementationId])
587
+ .filter((implementation): implementation is Implementation => implementation !== undefined),
588
+ })),
589
+ };
590
+ }
591
+
592
+ export function updatePrincipleValueMetrics(
593
+ stateDir: string,
594
+ principleId: string,
595
+ metrics: PrincipleValueMetrics,
596
+ ): PrincipleValueMetrics {
597
+ return mutateLedger(stateDir, (store) => {
598
+ const nextMetrics: PrincipleValueMetrics = {
599
+ ...metrics,
600
+ principleId,
601
+ };
602
+ store.tree.metrics[principleId] = nextMetrics;
603
+ return nextMetrics;
604
+ });
605
+ }
606
+
607
+ // ---------------------------------------------------------------------------
608
+ // Implementation Lifecycle State Transitions
609
+ // ---------------------------------------------------------------------------
610
+
611
+ /**
612
+ * Valid lifecycle state transitions (per Phase 13 context D-15):
613
+ * candidate -> active (promote)
614
+ * active -> disabled (disable)
615
+ * disabled -> active (re-enable via promote)
616
+ * disabled -> archived (permanent disable)
617
+ * active -> archived (direct archive)
618
+ * candidate -> archived (rejected candidate cleanup)
619
+ */
620
+ const VALID_LIFECYCLE_TRANSITIONS: Record<ImplementationLifecycleState, ImplementationLifecycleState[]> = {
621
+ candidate: ['active', 'archived'],
622
+ active: ['disabled', 'archived'],
623
+ disabled: ['active', 'archived'],
624
+ archived: [],
625
+ };
626
+
627
+ /**
628
+ * Validate a lifecycle state transition.
629
+ * Returns true if the transition is valid, false otherwise.
630
+ */
631
+ export function isValidLifecycleTransition(
632
+ from: ImplementationLifecycleState,
633
+ to: ImplementationLifecycleState
634
+ ): boolean {
635
+ return VALID_LIFECYCLE_TRANSITIONS[from]?.includes(to) ?? false;
636
+ }
637
+
638
+ /**
639
+ * Get allowed transitions for a given lifecycle state.
640
+ */
641
+ export function getAllowedTransitions(from: ImplementationLifecycleState): ImplementationLifecycleState[] {
642
+ return VALID_LIFECYCLE_TRANSITIONS[from] ?? [];
643
+ }
644
+
645
+ /**
646
+ * Transition an implementation's lifecycle state with validation.
647
+ * Throws on invalid transitions */
648
+ export function transitionImplementationState(
649
+ stateDir: string,
650
+ implementationId: string,
651
+ newState: ImplementationLifecycleState
652
+ ): Implementation {
653
+ return mutateLedger(stateDir, (store) => {
654
+ const impl = store.tree.implementations[implementationId];
655
+ if (!impl) {
656
+ throw new Error(`Implementation not found: ${implementationId}`);
657
+ }
658
+
659
+ const currentState = impl.lifecycleState ?? 'candidate';
660
+ if (!isValidLifecycleTransition(currentState, newState)) {
661
+ const allowed = getAllowedTransitions(currentState);
662
+ throw new Error(
663
+ `Invalid lifecycle transition: ${currentState} -> ${newState}. ` +
664
+ `Allowed: ${allowed.length > 0 ? allowed.join(', ') : 'none (terminal state)'}`
665
+ );
666
+ }
667
+
668
+ const updated: Implementation = {
669
+ ...impl,
670
+ lifecycleState: newState,
671
+ updatedAt: new Date().toISOString(),
672
+ };
673
+
674
+ store.tree.implementations[implementationId] = updated;
675
+ return updated;
676
+ });
677
+ }
678
+
679
+ /**
680
+ * Get all implementations for a specific lifecycle state across all rules.
681
+ */
682
+ export function listImplementationsByLifecycleState(
683
+ stateDir: string,
684
+ state: ImplementationLifecycleState
685
+ ): Implementation[] {
686
+ const ledger = loadLedger(stateDir);
687
+ return Object.values(ledger.tree.implementations).filter(
688
+ (impl) => impl.lifecycleState === state
689
+ );
690
+ }
691
+
692
+ /**
693
+ * Get implementations in a specific lifecycle state for a given rule.
694
+ */
695
+ export function listRuleImplementationsByState(
696
+ stateDir: string,
697
+ ruleId: string,
698
+ state: ImplementationLifecycleState
699
+ ): Implementation[] {
700
+ const implementations = listImplementationsForRule(stateDir, ruleId);
701
+ return implementations.filter((impl) => impl.lifecycleState === state);
702
+ }
703
+
704
+ /**
705
+ * Find active implementation for a rule (helper for rule host lookup).
706
+ */
707
+ export function findActiveImplementation(
708
+ stateDir: string,
709
+ ruleId: string
710
+ ): Implementation | null {
711
+ const implementations = listImplementationsForRule(stateDir, ruleId);
712
+ return implementations.find((impl) => impl.lifecycleState === 'active') ?? null;
713
+ }