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,398 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import {
6
+ deleteImplementationAssetDir,
7
+ getImplementationAssetRoot,
8
+ loadManifest,
9
+ writeManifest,
10
+ loadEntrySource,
11
+ createImplementationAssetDir,
12
+ writeEntrySource,
13
+ type CodeImplementationManifest,
14
+ } from '../../src/core/code-implementation-storage.js';
15
+ import { safeRmDir } from '../test-utils.js';
16
+
17
+ describe('CodeImplementationStorage', () => {
18
+ let tempDir: string;
19
+ let stateDir: string;
20
+
21
+ beforeEach(() => {
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-cis-'));
23
+ stateDir = path.join(tempDir, '.state');
24
+ fs.mkdirSync(stateDir, { recursive: true });
25
+ });
26
+
27
+ afterEach(() => {
28
+ safeRmDir(tempDir);
29
+ });
30
+
31
+ // -------------------------------------------------------------------------
32
+ // getImplementationAssetRoot
33
+ // -------------------------------------------------------------------------
34
+
35
+ describe('getImplementationAssetRoot', () => {
36
+ it('returns correct path under .state/principles/implementations/{implId}', () => {
37
+ const result = getImplementationAssetRoot(stateDir, 'IMPL_001');
38
+ expect(result).toBe(path.join(stateDir, 'principles', 'implementations', 'IMPL_001'));
39
+ });
40
+
41
+ it('throws on implId with forward slash', () => {
42
+ expect(() => getImplementationAssetRoot(stateDir, 'foo/bar')).toThrow(
43
+ 'Invalid implementation ID',
44
+ );
45
+ });
46
+
47
+ it('throws on implId with backslash', () => {
48
+ expect(() => getImplementationAssetRoot(stateDir, 'foo\\bar')).toThrow(
49
+ 'Invalid implementation ID',
50
+ );
51
+ });
52
+
53
+ it('throws on implId with double-dot traversal', () => {
54
+ expect(() => getImplementationAssetRoot(stateDir, '..')).toThrow(
55
+ 'Invalid implementation ID',
56
+ );
57
+ });
58
+
59
+ it('throws on empty implId', () => {
60
+ expect(() => getImplementationAssetRoot(stateDir, '')).toThrow(
61
+ 'Implementation ID must not be empty',
62
+ );
63
+ });
64
+ });
65
+
66
+ // -------------------------------------------------------------------------
67
+ // loadManifest
68
+ // -------------------------------------------------------------------------
69
+
70
+ describe('loadManifest', () => {
71
+ it('returns null when no manifest file exists', () => {
72
+ const result = loadManifest(stateDir, 'nonexistent');
73
+ expect(result).toBeNull();
74
+ });
75
+
76
+ it('returns parsed manifest when manifest file exists', () => {
77
+ const implId = 'IMPL_002';
78
+ const manifest: CodeImplementationManifest = {
79
+ version: '1.0.0',
80
+ entryFile: 'entry.js',
81
+ createdAt: '2026-01-01T00:00:00.000Z',
82
+ updatedAt: '2026-01-01T00:00:00.000Z',
83
+ replaySampleRefs: [],
84
+ lastEvalReportRef: null,
85
+ };
86
+
87
+ // Write directly (bypass writeManifest to test load independently)
88
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
89
+ fs.mkdirSync(assetRoot, { recursive: true });
90
+ fs.writeFileSync(path.join(assetRoot, 'manifest.json'), JSON.stringify(manifest), 'utf-8');
91
+
92
+ const result = loadManifest(stateDir, implId);
93
+ expect(result).toEqual(manifest);
94
+ });
95
+
96
+ it('returns null for corrupted manifest JSON', () => {
97
+ const implId = 'IMPL_003';
98
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
99
+ fs.mkdirSync(assetRoot, { recursive: true });
100
+ fs.writeFileSync(path.join(assetRoot, 'manifest.json'), 'not-valid-json{{{', 'utf-8');
101
+
102
+ const result = loadManifest(stateDir, implId);
103
+ expect(result).toBeNull();
104
+ });
105
+ });
106
+
107
+ // -------------------------------------------------------------------------
108
+ // writeManifest
109
+ // -------------------------------------------------------------------------
110
+
111
+ describe('writeManifest', () => {
112
+ it('creates manifest file with correct JSON content', () => {
113
+ const implId = 'IMPL_004';
114
+ const manifest: CodeImplementationManifest = {
115
+ version: '2.0.0',
116
+ entryFile: 'entry.js',
117
+ createdAt: '2026-01-01T00:00:00.000Z',
118
+ updatedAt: '2026-01-02T00:00:00.000Z',
119
+ replaySampleRefs: ['fp-001', 'fp-002'],
120
+ lastEvalReportRef: 'replays/2026-01-02.json',
121
+ };
122
+
123
+ writeManifest(stateDir, implId, manifest);
124
+
125
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
126
+ const manifestPath = path.join(assetRoot, 'manifest.json');
127
+ expect(fs.existsSync(manifestPath)).toBe(true);
128
+
129
+ const written = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
130
+ expect(written).toEqual(manifest);
131
+ });
132
+
133
+ it('creates asset directory if it does not exist', () => {
134
+ const implId = 'IMPL_005';
135
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
136
+ expect(fs.existsSync(assetRoot)).toBe(false);
137
+
138
+ writeManifest(stateDir, implId, {
139
+ version: '1.0.0',
140
+ entryFile: 'entry.js',
141
+ createdAt: '2026-01-01T00:00:00.000Z',
142
+ updatedAt: '2026-01-01T00:00:00.000Z',
143
+ replaySampleRefs: [],
144
+ lastEvalReportRef: null,
145
+ });
146
+
147
+ expect(fs.existsSync(assetRoot)).toBe(true);
148
+ });
149
+
150
+ it('uses withLock for atomic writes (manifest file is written atomically)', () => {
151
+ const implId = 'IMPL_006';
152
+
153
+ // Write twice rapidly - withLock should prevent corruption
154
+ const manifest1: CodeImplementationManifest = {
155
+ version: '1.0.0',
156
+ entryFile: 'entry.js',
157
+ createdAt: '2026-01-01T00:00:00.000Z',
158
+ updatedAt: '2026-01-01T00:00:00.000Z',
159
+ replaySampleRefs: [],
160
+ lastEvalReportRef: null,
161
+ };
162
+
163
+ writeManifest(stateDir, implId, manifest1);
164
+
165
+ const manifest2: CodeImplementationManifest = {
166
+ ...manifest1,
167
+ version: '2.0.0',
168
+ updatedAt: '2026-01-02T00:00:00.000Z',
169
+ };
170
+
171
+ writeManifest(stateDir, implId, manifest2);
172
+
173
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
174
+ const written = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
175
+ expect(written.version).toBe('2.0.0');
176
+ expect(written.updatedAt).toBe('2026-01-02T00:00:00.000Z');
177
+ });
178
+ });
179
+
180
+ // -------------------------------------------------------------------------
181
+ // loadEntrySource
182
+ // -------------------------------------------------------------------------
183
+
184
+ describe('loadEntrySource', () => {
185
+ it('returns null when no manifest exists', () => {
186
+ const result = loadEntrySource(stateDir, 'nonexistent');
187
+ expect(result).toBeNull();
188
+ });
189
+
190
+ it('returns null when manifest exists but entry file does not', () => {
191
+ const implId = 'IMPL_007';
192
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
193
+ fs.mkdirSync(assetRoot, { recursive: true });
194
+
195
+ // Write manifest pointing to a non-existent entry file
196
+ const manifest: CodeImplementationManifest = {
197
+ version: '1.0.0',
198
+ entryFile: 'nonexistent.js',
199
+ createdAt: '2026-01-01T00:00:00.000Z',
200
+ updatedAt: '2026-01-01T00:00:00.000Z',
201
+ replaySampleRefs: [],
202
+ lastEvalReportRef: null,
203
+ };
204
+ fs.writeFileSync(
205
+ path.join(assetRoot, 'manifest.json'),
206
+ JSON.stringify(manifest),
207
+ 'utf-8',
208
+ );
209
+
210
+ const result = loadEntrySource(stateDir, implId);
211
+ expect(result).toBeNull();
212
+ });
213
+
214
+ it('returns file content when entry file exists', () => {
215
+ const implId = 'IMPL_008';
216
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
217
+ fs.mkdirSync(assetRoot, { recursive: true });
218
+
219
+ const entryContent = 'export function evaluate() { return true; }';
220
+ fs.writeFileSync(path.join(assetRoot, 'entry.js'), entryContent, 'utf-8');
221
+
222
+ const manifest: CodeImplementationManifest = {
223
+ version: '1.0.0',
224
+ entryFile: 'entry.js',
225
+ createdAt: '2026-01-01T00:00:00.000Z',
226
+ updatedAt: '2026-01-01T00:00:00.000Z',
227
+ replaySampleRefs: [],
228
+ lastEvalReportRef: null,
229
+ };
230
+ fs.writeFileSync(
231
+ path.join(assetRoot, 'manifest.json'),
232
+ JSON.stringify(manifest),
233
+ 'utf-8',
234
+ );
235
+
236
+ const result = loadEntrySource(stateDir, implId);
237
+ expect(result).toBe(entryContent);
238
+ });
239
+ });
240
+
241
+ // -------------------------------------------------------------------------
242
+ // createImplementationAssetDir
243
+ // -------------------------------------------------------------------------
244
+
245
+ describe('createImplementationAssetDir', () => {
246
+ it('creates directory structure with manifest.json, entry.js, replays/ dir', () => {
247
+ const implId = 'IMPL_009';
248
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
249
+
250
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
251
+ expect(fs.existsSync(path.join(assetRoot, 'manifest.json'))).toBe(true);
252
+ expect(fs.existsSync(path.join(assetRoot, 'entry.js'))).toBe(true);
253
+ expect(fs.existsSync(path.join(assetRoot, 'replays'))).toBe(true);
254
+ expect(fs.statSync(path.join(assetRoot, 'replays')).isDirectory()).toBe(true);
255
+ });
256
+
257
+ it('manifest has correct fields', () => {
258
+ const implId = 'IMPL_010';
259
+ const manifest = createImplementationAssetDir(stateDir, implId, '3.5.0');
260
+
261
+ expect(manifest.version).toBe('3.5.0');
262
+ expect(manifest.entryFile).toBe('entry.js');
263
+ expect(typeof manifest.createdAt).toBe('string');
264
+ expect(typeof manifest.updatedAt).toBe('string');
265
+ expect(manifest.replaySampleRefs).toEqual([]);
266
+ expect(manifest.lastEvalReportRef).toBeNull();
267
+ });
268
+
269
+ it('manifest does NOT contain lifecycleState field', () => {
270
+ const implId = 'IMPL_011';
271
+ const manifest = createImplementationAssetDir(stateDir, implId, '1.0.0');
272
+
273
+ expect((manifest as any).lifecycleState).toBeUndefined();
274
+ expect('lifecycleState' in manifest).toBe(false);
275
+
276
+ // Also verify persisted manifest lacks lifecycleState
277
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
278
+ const raw = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
279
+ expect('lifecycleState' in raw).toBe(false);
280
+ });
281
+
282
+ it('persists lineage metadata when provided for a generated candidate', () => {
283
+ const implId = 'IMPL_011A';
284
+ const manifest = createImplementationAssetDir(stateDir, implId, '1.0.0', {
285
+ lineage: {
286
+ principleId: 'P-001',
287
+ ruleId: 'R-001',
288
+ sourceSnapshotRef: 'snapshot-session-1',
289
+ sourcePainIds: ['pain:gate:1'],
290
+ sourceGateBlockIds: ['gate:write:1'],
291
+ sourceSessionId: 'session-1',
292
+ artificerArtifactId: 'artifact-1',
293
+ },
294
+ });
295
+
296
+ expect(manifest.lineage).toMatchObject({
297
+ principleId: 'P-001',
298
+ ruleId: 'R-001',
299
+ sourceSessionId: 'session-1',
300
+ artificerArtifactId: 'artifact-1',
301
+ });
302
+
303
+ const loaded = loadManifest(stateDir, implId);
304
+ expect(loaded?.lineage).toMatchObject({
305
+ sourcePainIds: ['pain:gate:1'],
306
+ sourceGateBlockIds: ['gate:write:1'],
307
+ });
308
+ });
309
+
310
+ it('is idempotent: repeated calls do not overwrite existing entry.js', () => {
311
+ const implId = 'IMPL_012';
312
+
313
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
314
+
315
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
316
+ const entryPath = path.join(assetRoot, 'entry.js');
317
+
318
+ // Overwrite entry.js with custom content
319
+ const customContent = '// CUSTOM CONTENT - should survive';
320
+ fs.writeFileSync(entryPath, customContent, 'utf-8');
321
+
322
+ // Call again
323
+ createImplementationAssetDir(stateDir, implId, '2.0.0');
324
+
325
+ // Custom content should still be there
326
+ const content = fs.readFileSync(entryPath, 'utf-8');
327
+ expect(content).toBe(customContent);
328
+ });
329
+
330
+ it('manifest path matches Phase 13 replay-engine path convention', () => {
331
+ const implId = 'IMPL_013';
332
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
333
+
334
+ const assetRoot = getImplementationAssetRoot(stateDir, implId);
335
+
336
+ // Phase 13 replay-engine persists reports under:
337
+ // {stateDir}/.state/principles/implementations/{implId}/replays/{timestamp}.json
338
+ const replaysDir = path.join(assetRoot, 'replays');
339
+ expect(fs.existsSync(replaysDir)).toBe(true);
340
+
341
+ // Verify the full convention path is constructable
342
+ const reportPath = path.join(replaysDir, '2026-01-01T00-00-00.000Z.json');
343
+ fs.writeFileSync(reportPath, '{"test": true}', 'utf-8');
344
+ expect(fs.existsSync(reportPath)).toBe(true);
345
+ });
346
+
347
+ it('manifest is loadable via loadManifest after creation', () => {
348
+ const implId = 'IMPL_014';
349
+ createImplementationAssetDir(stateDir, implId, '4.0.0');
350
+
351
+ const loaded = loadManifest(stateDir, implId);
352
+ expect(loaded).not.toBeNull();
353
+ expect(loaded!.version).toBe('4.0.0');
354
+ expect(loaded!.entryFile).toBe('entry.js');
355
+ });
356
+
357
+ it('entry source is loadable via loadEntrySource after creation', () => {
358
+ const implId = 'IMPL_015';
359
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
360
+
361
+ const source = loadEntrySource(stateDir, implId);
362
+ expect(source).not.toBeNull();
363
+ expect(source).toContain('export const meta');
364
+ expect(source).toContain('export function evaluate');
365
+ });
366
+
367
+ it('writes generated entry source when provided', () => {
368
+ const implId = 'IMPL_016';
369
+ createImplementationAssetDir(stateDir, implId, '1.0.0', {
370
+ entrySource: 'export const meta = { name: "x", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "ok" }; }',
371
+ });
372
+
373
+ const source = loadEntrySource(stateDir, implId);
374
+ expect(source).toContain('export const meta');
375
+ expect(source).not.toContain('placeholder');
376
+ });
377
+ });
378
+
379
+ describe('entry and cleanup helpers', () => {
380
+ it('overwrites entry source through writeEntrySource', () => {
381
+ const implId = 'IMPL_017';
382
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
383
+
384
+ writeEntrySource(stateDir, implId, 'export const meta = { name: "updated", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "updated" }; }');
385
+
386
+ expect(loadEntrySource(stateDir, implId)).toContain('updated');
387
+ });
388
+
389
+ it('removes the asset directory through deleteImplementationAssetDir', () => {
390
+ const implId = 'IMPL_018';
391
+ createImplementationAssetDir(stateDir, implId, '1.0.0');
392
+
393
+ deleteImplementationAssetDir(stateDir, implId);
394
+
395
+ expect(fs.existsSync(getImplementationAssetRoot(stateDir, implId))).toBe(false);
396
+ });
397
+ });
398
+ });
@@ -52,11 +52,12 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
52
52
  });
53
53
 
54
54
  const p = reducer.getPrincipleById(id!);
55
- expect(p?.evaluability).toBe('manual_only');
55
+ // #212: Default is weak_heuristic (not manual_only) — allows basic pattern-matching evaluation
56
+ expect(p?.evaluability).toBe('weak_heuristic');
56
57
  expect(p?.detectorMetadata).toBeUndefined();
57
58
  });
58
59
 
59
- it('accepts deterministic evaluability when provided', () => {
60
+ it('downgrades deterministic to weak_heuristic when no detectorMetadata provided', () => {
60
61
  const workspace = makeTempDir();
61
62
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
62
63
 
@@ -70,9 +71,8 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
70
71
  });
71
72
 
72
73
  const p = reducer.getPrincipleById(id!);
73
- // Implementation contract: deterministic/weak_heuristic without detectorMetadata
74
- // is automatically downgraded to manual_only (defense in depth).
75
- expect(p?.evaluability).toBe('manual_only');
74
+ // #212: deterministic without detectorMetadata → downgraded to weak_heuristic (not manual_only)
75
+ expect(p?.evaluability).toBe('weak_heuristic');
76
76
  expect(p?.detectorMetadata).toBeUndefined();
77
77
  });
78
78
 
@@ -90,9 +90,8 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
90
90
  });
91
91
 
92
92
  const p = reducer.getPrincipleById(id!);
93
- // Implementation contract: deterministic/weak_heuristic without detectorMetadata
94
- // is automatically downgraded to manual_only.
95
- expect(p?.evaluability).toBe('manual_only');
93
+ // #212: weak_heuristic does not require detectorMetadata — no downgrade
94
+ expect(p?.evaluability).toBe('weak_heuristic');
96
95
  });
97
96
 
98
97
  it('stores detectorMetadata when provided', () => {
@@ -131,14 +130,14 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
131
130
  triggerPattern: 'file write fails',
132
131
  action: 'check permissions first',
133
132
  source: 'diagnostician',
134
- // evaluability omitted — defaults to manual_only
133
+ // evaluability omitted — defaults to weak_heuristic (#212)
135
134
  detectorMetadata: VALID_DETECTOR_METADATA,
136
135
  });
137
136
 
138
137
  const p = reducer.getPrincipleById(id!);
139
- // Without explicit evaluability, detectorMetadata alone is stored but evaluability defaults
140
- expect(p?.evaluability).toBe('manual_only'); // default — detectorMetadata is stored
141
- expect(p?.detectorMetadata).toEqual(VALID_DETECTOR_METADATA); // but stored for reference
138
+ // #212: Default is weak_heuristic detectorMetadata stored and used
139
+ expect(p?.evaluability).toBe('weak_heuristic');
140
+ expect(p?.detectorMetadata).toEqual(VALID_DETECTOR_METADATA);
142
141
  });
143
142
  });
144
143
 
@@ -334,14 +333,14 @@ describe('candidate_created event — evaluability in event data', () => {
334
333
  triggerPattern: 'file write fails',
335
334
  action: 'check permissions',
336
335
  source: 'diagnostician',
337
- // no evaluability
336
+ // no evaluability — defaults to weak_heuristic (#212)
338
337
  });
339
338
 
340
339
  const events = reducer.getEventLog();
341
340
  const candidateEvent = events.find(e => e.type === 'candidate_created');
342
341
  expect(candidateEvent).toBeDefined();
343
- // Defaults to 'manual_only' in event data
344
- expect((candidateEvent!.data as any).evaluability).toBe('manual_only');
342
+ // #212: Defaults to 'weak_heuristic' (not manual_only)
343
+ expect((candidateEvent!.data as any).evaluability).toBe('weak_heuristic');
345
344
  });
346
345
 
347
346
  it('emits candidate_created event with detectorMetadata when provided', () => {
@@ -370,7 +369,7 @@ describe('candidate_created event — evaluability in event data', () => {
370
369
  // ---------------------------------------------------------------------------
371
370
 
372
371
  describe('manual_only classification — no automatic targeting', () => {
373
- it('principle without detectorMetadata has manual_only evaluability', () => {
372
+ it('principle without detectorMetadata has weak_heuristic evaluability', () => {
374
373
  const workspace = makeTempDir();
375
374
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
376
375
 
@@ -380,12 +379,13 @@ describe('manual_only classification — no automatic targeting', () => {
380
379
  triggerPattern: 'generic error',
381
380
  action: 'be careful',
382
381
  source: 'diagnostician',
383
- // no evaluability, no detectorMetadata
382
+ // no evaluability, no detectorMetadata — defaults to weak_heuristic (#212)
384
383
  });
385
384
 
386
385
  // Principle auto-promotes to probation (not active)
387
386
  const p = reducer.getProbationPrinciples()[0];
388
- expect(p?.evaluability).toBe('manual_only');
387
+ // #212: Default is weak_heuristic (not manual_only)
388
+ expect(p?.evaluability).toBe('weak_heuristic');
389
389
  expect(p?.detectorMetadata).toBeUndefined();
390
390
  });
391
391
 
@@ -463,7 +463,7 @@ describe('getActivePrinciples / getProbationPrinciples — evaluability field',
463
463
  // ---------------------------------------------------------------------------
464
464
 
465
465
  describe('Malformed detectorMetadata — defense in depth', () => {
466
- it('downgrades to manual_only when confidence is not a valid enum value', () => {
466
+ it('downgrades to weak_heuristic when confidence is not a valid enum value', () => {
467
467
  const workspace = makeTempDir();
468
468
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
469
469
 
@@ -485,12 +485,11 @@ describe('Malformed detectorMetadata — defense in depth', () => {
485
485
  });
486
486
 
487
487
  const p = reducer.getPrincipleById(id!);
488
- // Subagent should not pass invalid confidence, but reducer also defends:
489
- // invalid confidence = no real auto-trainability
490
- expect(p?.evaluability).toBe('manual_only');
488
+ // #212: deterministic with invalid metadata downgraded to weak_heuristic (not manual_only)
489
+ expect(p?.evaluability).toBe('weak_heuristic');
491
490
  });
492
491
 
493
- it('downgrades to manual_only when applicabilityTags is empty', () => {
492
+ it('weak_heuristic accepts malformed metadata without downgrade', () => {
494
493
  const workspace = makeTempDir();
495
494
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
496
495
 
@@ -511,10 +510,11 @@ describe('Malformed detectorMetadata — defense in depth', () => {
511
510
  });
512
511
 
513
512
  const p = reducer.getPrincipleById(id!);
514
- expect(p?.evaluability).toBe('manual_only');
513
+ // #212: weak_heuristic does not require detectorMetadata — no downgrade needed
514
+ expect(p?.evaluability).toBe('weak_heuristic');
515
515
  });
516
516
 
517
- it('downgrades to manual_only when positiveSignals is empty', () => {
517
+ it('weak_heuristic accepts empty positiveSignals without downgrade', () => {
518
518
  const workspace = makeTempDir();
519
519
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
520
520
 
@@ -535,10 +535,10 @@ describe('Malformed detectorMetadata — defense in depth', () => {
535
535
  });
536
536
 
537
537
  const p = reducer.getPrincipleById(id!);
538
- expect(p?.evaluability).toBe('manual_only');
538
+ expect(p?.evaluability).toBe('weak_heuristic');
539
539
  });
540
540
 
541
- it('downgrades to manual_only when negativeSignals is empty', () => {
541
+ it('weak_heuristic accepts empty negativeSignals without downgrade', () => {
542
542
  const workspace = makeTempDir();
543
543
  const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
544
544
 
@@ -559,7 +559,7 @@ describe('Malformed detectorMetadata — defense in depth', () => {
559
559
  });
560
560
 
561
561
  const p = reducer.getPrincipleById(id!);
562
- expect(p?.evaluability).toBe('manual_only');
562
+ expect(p?.evaluability).toBe('weak_heuristic');
563
563
  });
564
564
 
565
565
  it('accepts valid detectorMetadata with all three signal arrays non-empty', () => {
@@ -0,0 +1,53 @@
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 {
6
+ appendCandidateArtifactLineageRecord,
7
+ listArtifactLineageRecords,
8
+ } from '../../src/core/nocturnal-artifact-lineage.js';
9
+ import { safeRmDir } from '../test-utils.js';
10
+
11
+ describe('nocturnal-artifact-lineage', () => {
12
+ let workspaceDir: string;
13
+
14
+ beforeEach(() => {
15
+ workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-lineage-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ safeRmDir(workspaceDir);
20
+ });
21
+
22
+ it('appends a candidate lineage record with explicit pain and gate refs', () => {
23
+ appendCandidateArtifactLineageRecord(workspaceDir, {
24
+ artifactId: 'artifact-1',
25
+ principleId: 'P-001',
26
+ ruleId: 'R-001',
27
+ sessionId: 'session-1',
28
+ sourceSnapshotRef: 'snapshot-1',
29
+ sourcePainIds: ['pain:gate:1', 'pain:tool:2'],
30
+ sourceGateBlockIds: ['gate:write:1'],
31
+ storagePath: path.join(workspaceDir, '.state', 'principles', 'implementations', 'IMPL-1'),
32
+ implementationId: 'IMPL-1',
33
+ createdAt: '2026-04-08T00:00:00.000Z',
34
+ });
35
+
36
+ const records = listArtifactLineageRecords(
37
+ workspaceDir,
38
+ 'rule-implementation-candidate'
39
+ );
40
+
41
+ expect(records).toHaveLength(1);
42
+ expect(records[0]).toMatchObject({
43
+ artifactKind: 'rule-implementation-candidate',
44
+ artifactId: 'artifact-1',
45
+ principleId: 'P-001',
46
+ ruleId: 'R-001',
47
+ sourceSnapshotRef: 'snapshot-1',
48
+ sourcePainIds: ['pain:gate:1', 'pain:tool:2'],
49
+ sourceGateBlockIds: ['gate:write:1'],
50
+ implementationId: 'IMPL-1',
51
+ });
52
+ });
53
+ });