principles-disciple 1.11.0 → 1.13.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 (233) 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 +250 -6
  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 +21 -47
  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 +110 -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 +319 -61
  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 +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -0,0 +1,350 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ computePrincipleAdherence,
4
+ computeRuleMetrics,
5
+ } from '../../../src/core/principle-internalization/lifecycle-metrics.js';
6
+ import type {
7
+ PrincipleLifecycleEvidence,
8
+ RuleLifecycleEvidence,
9
+ } from '../../../src/core/principle-internalization/lifecycle-read-model.js';
10
+ import type { ReplayReport } from '../../../src/core/replay-engine.js';
11
+ import type { ArtifactLineageRecord } from '../../../src/core/nocturnal-artifact-lineage.js';
12
+
13
+ function createReplayReport(
14
+ implementationId: string,
15
+ overrides: Partial<ReplayReport> = {},
16
+ ): ReplayReport {
17
+ return {
18
+ implementationId,
19
+ generatedAt: '2026-04-08T00:00:00.000Z',
20
+ overallDecision: 'pass',
21
+ blockers: [],
22
+ sampleFingerprints: [],
23
+ replayResults: {
24
+ painNegative: { total: 0, passed: 0, failed: 0, details: [] },
25
+ successPositive: { total: 0, passed: 0, failed: 0, details: [] },
26
+ principleAnchor: { total: 0, passed: 0, failed: 0, details: [] },
27
+ },
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function createLineageRecord(overrides: Partial<ArtifactLineageRecord> = {}): ArtifactLineageRecord {
33
+ return {
34
+ artifactKind: 'rule-implementation-candidate',
35
+ artifactId: 'artifact-1',
36
+ principleId: 'P-001',
37
+ ruleId: 'R-001',
38
+ sessionId: 'session-1',
39
+ sourceSnapshotRef: 'snapshot-1',
40
+ sourcePainIds: [],
41
+ sourceGateBlockIds: [],
42
+ storagePath: '.state/principles/implementations/IMPL-001',
43
+ implementationId: 'IMPL-001',
44
+ createdAt: '2026-04-08T00:00:00.000Z',
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ function createRuleEvidence(overrides: Partial<RuleLifecycleEvidence> = {}): RuleLifecycleEvidence {
50
+ return {
51
+ rule: {
52
+ id: 'R-001',
53
+ version: 1,
54
+ name: 'Protect deletes',
55
+ description: 'Require safe delete handling',
56
+ type: 'hook',
57
+ triggerCondition: 'tool=delete',
58
+ enforcement: 'block',
59
+ action: 'block destructive delete',
60
+ principleId: 'P-001',
61
+ status: 'enforced',
62
+ coverageRate: 0,
63
+ falsePositiveRate: 0,
64
+ createdAt: '2026-04-08T00:00:00.000Z',
65
+ updatedAt: '2026-04-08T00:00:00.000Z',
66
+ implementationIds: ['IMPL-001'],
67
+ },
68
+ implementations: [],
69
+ replayEvidence: {
70
+ reportCount: 0,
71
+ latestReports: [],
72
+ painNegative: { total: 0, passed: 0, failed: 0 },
73
+ successPositive: { total: 0, passed: 0, failed: 0 },
74
+ principleAnchor: { total: 0, passed: 0, failed: 0 },
75
+ passingImplementationIds: [],
76
+ failingImplementationIds: [],
77
+ needsReviewImplementationIds: [],
78
+ },
79
+ liveEvidence: {
80
+ activeCount: 0,
81
+ candidateCount: 0,
82
+ disabledCount: 0,
83
+ archivedCount: 0,
84
+ durablePenaltyCount: 0,
85
+ rollbackEvidenceCount: 0,
86
+ hasActiveImplementation: false,
87
+ hasPassingActiveImplementation: false,
88
+ },
89
+ lineageEvidence: {
90
+ records: [],
91
+ distinctPainSignalCount: 0,
92
+ distinctGateBlockCount: 0,
93
+ repeatedErrorSignal: 0,
94
+ },
95
+ ...overrides,
96
+ };
97
+ }
98
+
99
+ function createPrincipleEvidence(
100
+ rules: RuleLifecycleEvidence[],
101
+ overrides: Partial<PrincipleLifecycleEvidence> = {},
102
+ ): PrincipleLifecycleEvidence {
103
+ return {
104
+ principle: {
105
+ id: 'P-001',
106
+ version: 1,
107
+ text: 'Write before delete',
108
+ triggerPattern: 'delete',
109
+ action: 'write replacement first',
110
+ status: 'active',
111
+ priority: 'P1',
112
+ scope: 'general',
113
+ evaluability: 'deterministic',
114
+ valueScore: 0,
115
+ adherenceRate: 0,
116
+ painPreventedCount: 0,
117
+ derivedFromPainIds: [],
118
+ ruleIds: rules.map((rule) => rule.rule.id),
119
+ conflictsWithPrincipleIds: [],
120
+ createdAt: '2026-04-08T00:00:00.000Z',
121
+ updatedAt: '2026-04-08T00:00:00.000Z',
122
+ },
123
+ rules,
124
+ summary: {
125
+ replayReportCount: rules.reduce((sum, rule) => sum + rule.replayEvidence.reportCount, 0),
126
+ activeImplementationCount: rules.reduce((sum, rule) => sum + rule.liveEvidence.activeCount, 0),
127
+ candidateImplementationCount: rules.reduce((sum, rule) => sum + rule.liveEvidence.candidateCount, 0),
128
+ disabledImplementationCount: rules.reduce((sum, rule) => sum + rule.liveEvidence.disabledCount, 0),
129
+ archivedImplementationCount: rules.reduce((sum, rule) => sum + rule.liveEvidence.archivedCount, 0),
130
+ distinctPainSignalCount: rules.reduce((sum, rule) => sum + rule.lineageEvidence.distinctPainSignalCount, 0),
131
+ distinctGateBlockCount: rules.reduce((sum, rule) => sum + rule.lineageEvidence.distinctGateBlockCount, 0),
132
+ repeatedErrorSignal: rules.reduce((sum, rule) => sum + rule.lineageEvidence.repeatedErrorSignal, 0),
133
+ },
134
+ ...overrides,
135
+ };
136
+ }
137
+
138
+ describe('lifecycle-metrics', () => {
139
+ it('keeps sparse evidence bounded at zero instead of inferring coverage from implementation existence', () => {
140
+ const metrics = computeRuleMetrics(createRuleEvidence());
141
+
142
+ expect(metrics.coverageRate).toBe(0);
143
+ expect(metrics.falsePositiveRate).toBe(0);
144
+ expect(metrics.implementationStabilityScore).toBe(0);
145
+ });
146
+
147
+ it('computes strong rule coverage from replay hits, anchor passes, and stable active implementations', () => {
148
+ const report = createReplayReport('IMPL-001', {
149
+ replayResults: {
150
+ painNegative: { total: 4, passed: 4, failed: 0, details: [] },
151
+ successPositive: { total: 3, passed: 3, failed: 0, details: [] },
152
+ principleAnchor: { total: 2, passed: 2, failed: 0, details: [] },
153
+ },
154
+ });
155
+ const metrics = computeRuleMetrics(
156
+ createRuleEvidence({
157
+ implementations: [
158
+ {
159
+ implementation: {
160
+ id: 'IMPL-001',
161
+ ruleId: 'R-001',
162
+ type: 'code',
163
+ path: 'impl/entry.js',
164
+ version: 'v1',
165
+ coversCondition: 'delete safety',
166
+ coveragePercentage: 100,
167
+ lifecycleState: 'active',
168
+ createdAt: '2026-04-08T00:00:00.000Z',
169
+ updatedAt: '2026-04-08T00:00:00.000Z',
170
+ },
171
+ latestReplayReport: report,
172
+ replayHistoryCount: 1,
173
+ lineageRecords: [],
174
+ },
175
+ ],
176
+ replayEvidence: {
177
+ reportCount: 1,
178
+ latestReports: [report],
179
+ painNegative: { total: 4, passed: 4, failed: 0 },
180
+ successPositive: { total: 3, passed: 3, failed: 0 },
181
+ principleAnchor: { total: 2, passed: 2, failed: 0 },
182
+ passingImplementationIds: ['IMPL-001'],
183
+ failingImplementationIds: [],
184
+ needsReviewImplementationIds: [],
185
+ },
186
+ liveEvidence: {
187
+ activeCount: 1,
188
+ candidateCount: 0,
189
+ disabledCount: 0,
190
+ archivedCount: 0,
191
+ durablePenaltyCount: 0,
192
+ rollbackEvidenceCount: 0,
193
+ hasActiveImplementation: true,
194
+ hasPassingActiveImplementation: true,
195
+ },
196
+ }),
197
+ );
198
+
199
+ expect(metrics.coverageRate).toBe(98);
200
+ expect(metrics.falsePositiveRate).toBe(0);
201
+ expect(metrics.implementationStabilityScore).toBe(90);
202
+ });
203
+
204
+ it('grounds false-positive rate in success-positive replay failures and only adds a coarse live penalty', () => {
205
+ const report = createReplayReport('IMPL-001', {
206
+ overallDecision: 'needs-review',
207
+ replayResults: {
208
+ painNegative: { total: 2, passed: 2, failed: 0, details: [] },
209
+ successPositive: { total: 4, passed: 1, failed: 3, details: [] },
210
+ principleAnchor: { total: 2, passed: 2, failed: 0, details: [] },
211
+ },
212
+ });
213
+ const metrics = computeRuleMetrics(
214
+ createRuleEvidence({
215
+ replayEvidence: {
216
+ reportCount: 1,
217
+ latestReports: [report],
218
+ painNegative: { total: 2, passed: 2, failed: 0 },
219
+ successPositive: { total: 4, passed: 1, failed: 3 },
220
+ principleAnchor: { total: 2, passed: 2, failed: 0 },
221
+ passingImplementationIds: [],
222
+ failingImplementationIds: [],
223
+ needsReviewImplementationIds: ['IMPL-001'],
224
+ },
225
+ liveEvidence: {
226
+ activeCount: 1,
227
+ candidateCount: 0,
228
+ disabledCount: 1,
229
+ archivedCount: 0,
230
+ durablePenaltyCount: 1,
231
+ rollbackEvidenceCount: 0,
232
+ hasActiveImplementation: true,
233
+ hasPassingActiveImplementation: false,
234
+ },
235
+ }),
236
+ );
237
+
238
+ expect(metrics.replayFalsePositiveRate).toBe(75);
239
+ expect(metrics.livePenaltyRate).toBe(5);
240
+ expect(metrics.falsePositiveRate).toBe(80);
241
+ });
242
+
243
+ it('penalizes rollback and repeated errors when computing principle adherence', () => {
244
+ const stableRule = createRuleEvidence({
245
+ rule: {
246
+ ...createRuleEvidence().rule,
247
+ id: 'R-001',
248
+ },
249
+ replayEvidence: {
250
+ reportCount: 1,
251
+ latestReports: [createReplayReport('IMPL-001')],
252
+ painNegative: { total: 3, passed: 3, failed: 0 },
253
+ successPositive: { total: 2, passed: 2, failed: 0 },
254
+ principleAnchor: { total: 2, passed: 2, failed: 0 },
255
+ passingImplementationIds: ['IMPL-001'],
256
+ failingImplementationIds: [],
257
+ needsReviewImplementationIds: [],
258
+ },
259
+ liveEvidence: {
260
+ activeCount: 1,
261
+ candidateCount: 0,
262
+ disabledCount: 0,
263
+ archivedCount: 0,
264
+ durablePenaltyCount: 0,
265
+ rollbackEvidenceCount: 0,
266
+ hasActiveImplementation: true,
267
+ hasPassingActiveImplementation: true,
268
+ },
269
+ });
270
+ const unstableRule = createRuleEvidence({
271
+ rule: {
272
+ ...createRuleEvidence().rule,
273
+ id: 'R-002',
274
+ implementationIds: ['IMPL-002'],
275
+ },
276
+ implementations: [
277
+ {
278
+ implementation: {
279
+ id: 'IMPL-002',
280
+ ruleId: 'R-002',
281
+ type: 'code',
282
+ path: 'impl/rollback.js',
283
+ version: 'v2',
284
+ coversCondition: 'delete safety',
285
+ coveragePercentage: 70,
286
+ lifecycleState: 'disabled',
287
+ previousActive: 'IMPL-001',
288
+ disabledReason: 'Rollback after false positive spike',
289
+ createdAt: '2026-04-08T00:00:00.000Z',
290
+ updatedAt: '2026-04-08T00:00:00.000Z',
291
+ },
292
+ latestReplayReport: createReplayReport('IMPL-002', {
293
+ overallDecision: 'needs-review',
294
+ replayResults: {
295
+ painNegative: { total: 2, passed: 1, failed: 1, details: [] },
296
+ successPositive: { total: 2, passed: 1, failed: 1, details: [] },
297
+ principleAnchor: { total: 2, passed: 1, failed: 1, details: [] },
298
+ },
299
+ }),
300
+ replayHistoryCount: 1,
301
+ lineageRecords: [createLineageRecord({ sourcePainIds: ['pain-1', 'pain-2'] })],
302
+ },
303
+ ],
304
+ replayEvidence: {
305
+ reportCount: 1,
306
+ latestReports: [
307
+ createReplayReport('IMPL-002', {
308
+ overallDecision: 'needs-review',
309
+ replayResults: {
310
+ painNegative: { total: 2, passed: 1, failed: 1, details: [] },
311
+ successPositive: { total: 2, passed: 1, failed: 1, details: [] },
312
+ principleAnchor: { total: 2, passed: 1, failed: 1, details: [] },
313
+ },
314
+ }),
315
+ ],
316
+ painNegative: { total: 2, passed: 1, failed: 1 },
317
+ successPositive: { total: 2, passed: 1, failed: 1 },
318
+ principleAnchor: { total: 2, passed: 1, failed: 1 },
319
+ passingImplementationIds: [],
320
+ failingImplementationIds: [],
321
+ needsReviewImplementationIds: ['IMPL-002'],
322
+ },
323
+ liveEvidence: {
324
+ activeCount: 0,
325
+ candidateCount: 0,
326
+ disabledCount: 1,
327
+ archivedCount: 0,
328
+ durablePenaltyCount: 1,
329
+ rollbackEvidenceCount: 1,
330
+ hasActiveImplementation: false,
331
+ hasPassingActiveImplementation: false,
332
+ },
333
+ lineageEvidence: {
334
+ records: [createLineageRecord({ ruleId: 'R-002', implementationId: 'IMPL-002', sourcePainIds: ['pain-1', 'pain-2'] })],
335
+ distinctPainSignalCount: 2,
336
+ distinctGateBlockCount: 0,
337
+ repeatedErrorSignal: 2,
338
+ latestCreatedAt: '2026-04-08T00:00:00.000Z',
339
+ },
340
+ });
341
+
342
+ const adherence = computePrincipleAdherence(createPrincipleEvidence([stableRule, unstableRule]));
343
+
344
+ expect(adherence.averageRuleCoverage).toBe(69);
345
+ expect(adherence.repeatedErrorReductionScore).toBe(56.4);
346
+ expect(adherence.adherenceRate).toBe(65.22);
347
+ expect(adherence.stableRuleIds).toEqual(['R-001']);
348
+ expect(adherence.unstableRuleIds).toEqual(['R-002']);
349
+ });
350
+ });
@@ -0,0 +1,211 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { PrincipleLifecycleService } from '../../../src/core/principle-internalization/principle-lifecycle-service.js';
6
+ import { appendCandidateArtifactLineageRecord, listArtifactLineageRecords } from '../../../src/core/nocturnal-artifact-lineage.js';
7
+ import { getImplementationAssetRoot } from '../../../src/core/code-implementation-storage.js';
8
+ import { loadLedger, saveLedger, type LedgerPrinciple, type LedgerRule } from '../../../src/core/principle-tree-ledger.js';
9
+ import type { Implementation } from '../../../src/types/principle-tree-schema.js';
10
+ import type { ReplayReport } from '../../../src/core/replay-engine.js';
11
+ import { createTestContext, safeRmDir } from '../../test-utils.js';
12
+
13
+ function makePrinciple(overrides: Partial<LedgerPrinciple> = {}): LedgerPrinciple {
14
+ return {
15
+ id: 'P-001',
16
+ version: 1,
17
+ text: 'Prefer cheap safe internalization',
18
+ triggerPattern: 'write',
19
+ action: 'prefer the cheapest viable fix',
20
+ status: 'active',
21
+ priority: 'P1',
22
+ scope: 'general',
23
+ evaluability: 'weak_heuristic',
24
+ valueScore: 0,
25
+ adherenceRate: 0,
26
+ painPreventedCount: 0,
27
+ derivedFromPainIds: [],
28
+ ruleIds: ['R-001'],
29
+ conflictsWithPrincipleIds: [],
30
+ createdAt: '2026-04-08T00:00:00.000Z',
31
+ updatedAt: '2026-04-08T00:00:00.000Z',
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function makeRule(overrides: Partial<LedgerRule> = {}): LedgerRule {
37
+ return {
38
+ id: 'R-001',
39
+ version: 1,
40
+ name: 'Coach cautious writes',
41
+ description: 'Encourage safer writes before escalating to hard boundaries.',
42
+ type: 'skill',
43
+ triggerCondition: 'tool=write',
44
+ enforcement: 'warn',
45
+ action: 'warn before risky write',
46
+ principleId: 'P-001',
47
+ status: 'implemented',
48
+ coverageRate: 0,
49
+ falsePositiveRate: 0,
50
+ implementationIds: ['IMPL-001'],
51
+ createdAt: '2026-04-08T00:00:00.000Z',
52
+ updatedAt: '2026-04-08T00:00:00.000Z',
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ function makeImplementation(overrides: Partial<Implementation> = {}): Implementation {
58
+ return {
59
+ id: 'IMPL-001',
60
+ ruleId: 'R-001',
61
+ type: 'code',
62
+ path: 'implementations/IMPL-001/entry.js',
63
+ version: 'v1',
64
+ coversCondition: 'risky write',
65
+ coveragePercentage: 60,
66
+ lifecycleState: 'candidate',
67
+ createdAt: '2026-04-08T00:00:00.000Z',
68
+ updatedAt: '2026-04-08T00:00:00.000Z',
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ function makeReplayReport(overrides: Partial<ReplayReport> = {}): ReplayReport {
74
+ return {
75
+ implementationId: 'IMPL-001',
76
+ generatedAt: '2026-04-08T00:00:00.000Z',
77
+ overallDecision: 'needs-review',
78
+ blockers: [],
79
+ sampleFingerprints: ['sample-1', 'sample-2', 'sample-3'],
80
+ replayResults: {
81
+ painNegative: { total: 2, passed: 2, failed: 0, details: [] },
82
+ successPositive: { total: 2, passed: 1, failed: 1, details: [] },
83
+ principleAnchor: { total: 1, passed: 1, failed: 0, details: [] },
84
+ },
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ describe('principle-lifecycle-service', () => {
90
+ let tempDir: string;
91
+ let workspaceDir: string;
92
+ let stateDir: string;
93
+
94
+ beforeEach(() => {
95
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-lifecycle-service-'));
96
+ workspaceDir = path.join(tempDir, 'workspace');
97
+ stateDir = path.join(tempDir, 'state');
98
+ fs.mkdirSync(workspaceDir, { recursive: true });
99
+ fs.mkdirSync(stateDir, { recursive: true });
100
+ });
101
+
102
+ afterEach(() => {
103
+ safeRmDir(tempDir);
104
+ });
105
+
106
+ function seedWorkspace(): void {
107
+ saveLedger(stateDir, {
108
+ trainingStore: {
109
+ 'P-001': {
110
+ principleId: 'P-001',
111
+ evaluability: 'weak_heuristic',
112
+ applicableOpportunityCount: 5,
113
+ observedViolationCount: 2,
114
+ complianceRate: 0.6,
115
+ violationTrend: -0.2,
116
+ generatedSampleCount: 0,
117
+ approvedSampleCount: 0,
118
+ includedTrainRunIds: [],
119
+ deployedCheckpointIds: [],
120
+ internalizationStatus: 'internalized',
121
+ },
122
+ },
123
+ tree: {
124
+ principles: {
125
+ 'P-001': makePrinciple(),
126
+ },
127
+ rules: {
128
+ 'R-001': makeRule(),
129
+ },
130
+ implementations: {
131
+ 'IMPL-001': makeImplementation(),
132
+ },
133
+ metrics: {},
134
+ lastUpdated: '2026-04-08T00:00:00.000Z',
135
+ },
136
+ });
137
+
138
+ const replayDir = path.join(getImplementationAssetRoot(stateDir, 'IMPL-001'), 'replays');
139
+ fs.mkdirSync(replayDir, { recursive: true });
140
+ fs.writeFileSync(
141
+ path.join(replayDir, '2026-04-08T00-00-00-000Z.json'),
142
+ JSON.stringify(makeReplayReport(), null, 2),
143
+ 'utf-8',
144
+ );
145
+
146
+ appendCandidateArtifactLineageRecord(workspaceDir, {
147
+ artifactId: 'artifact-1',
148
+ principleId: 'P-001',
149
+ ruleId: 'R-001',
150
+ sessionId: 'session-1',
151
+ sourceSnapshotRef: 'snapshot-1',
152
+ sourcePainIds: ['pain-1'],
153
+ sourceGateBlockIds: ['gate-1'],
154
+ storagePath: getImplementationAssetRoot(stateDir, 'IMPL-001'),
155
+ implementationId: 'IMPL-001',
156
+ createdAt: '2026-04-08T00:00:00.000Z',
157
+ });
158
+ }
159
+
160
+ it('recomputes lifecycle metrics and exposes assessments plus route recommendations through one service', () => {
161
+ seedWorkspace();
162
+ const service = new PrincipleLifecycleService(workspaceDir, stateDir);
163
+
164
+ const recomputed = service.recomputeAll();
165
+ const assessments = service.listAssessments();
166
+ const recommendations = service.listRouteRecommendations();
167
+ const ledger = loadLedger(stateDir);
168
+
169
+ expect(recomputed).toHaveLength(1);
170
+ expect(recomputed[0].routeRecommendation.route).toBe('skill');
171
+ expect(assessments).toHaveLength(1);
172
+ expect(assessments[0].deprecatedReadiness.status).toBe('not-ready');
173
+ expect(recommendations).toEqual([assessments[0].routeRecommendation]);
174
+ expect(ledger.tree.rules['R-001'].coverageRate).toBeGreaterThan(0);
175
+ expect(ledger.tree.principles['P-001'].adherenceRate).toBeGreaterThan(0);
176
+ expect(ledger.tree.metrics['P-001']).toBeDefined();
177
+ });
178
+
179
+ it('surfaces the lifecycle service from WorkspaceContext without breaking existing consumers', () => {
180
+ seedWorkspace();
181
+ const context = createTestContext({ workspaceDir, stateDir });
182
+
183
+ expect(context.principleLifecycle).toBeInstanceOf(PrincipleLifecycleService);
184
+ expect(context.principleLifecycle.listAssessments()).toHaveLength(1);
185
+ expect(context.getActivePrincipleSubtrees()).toEqual([]);
186
+ });
187
+
188
+ it('keeps replay classifications behavioral-only and preserves candidate lineage semantics', () => {
189
+ seedWorkspace();
190
+ const service = new PrincipleLifecycleService(workspaceDir, stateDir);
191
+
192
+ const assessments = service.listAssessments();
193
+ const readModel = service.buildReadModel();
194
+ const lineage = listArtifactLineageRecords(workspaceDir, 'rule-implementation-candidate');
195
+ const latestReport = readModel.principles[0].rules[0].replayEvidence.latestReports[0];
196
+
197
+ expect(assessments[0].routeRecommendation.route).toBe('skill');
198
+ expect(Object.keys(latestReport.replayResults)).toEqual([
199
+ 'painNegative',
200
+ 'successPositive',
201
+ 'principleAnchor',
202
+ ]);
203
+ expect(lineage).toHaveLength(1);
204
+ expect(lineage[0]).toMatchObject({
205
+ artifactKind: 'rule-implementation-candidate',
206
+ sourcePainIds: ['pain-1'],
207
+ sourceGateBlockIds: ['gate-1'],
208
+ implementationId: 'IMPL-001',
209
+ });
210
+ });
211
+ });