principles-disciple 1.72.0 → 1.74.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 (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { autoCompressFocus, cleanupStaleInfo } from '../focus-history.js';
6
+
7
+ function makeFocusContent(artifactRows: string[], extraLines = 0): string {
8
+ const lines: string[] = [
9
+ '# CURRENT_FOCUS',
10
+ '',
11
+ '**版本**: v1',
12
+ '**更新**: 2026-05-09',
13
+ '',
14
+ '## 📍 状态快照',
15
+ '',
16
+ '当前聚焦于核心功能开发。',
17
+ '',
18
+ '## 🔄 当前任务',
19
+ '',
20
+ '- [ ] 实现新功能',
21
+ '- [ ] 修复已知问题',
22
+ '- [ ] 完善测试覆盖',
23
+ '',
24
+ '## ➡️ 下一步',
25
+ '',
26
+ '1. 完成 PRI-82 E2E 测试',
27
+ '2. 提交代码审查',
28
+ '',
29
+ '## 🧠 Working Memory',
30
+ '',
31
+ '> Last updated: 2026-05-09T12:00:00Z',
32
+ '',
33
+ '### 📁 文件输出记录',
34
+ '',
35
+ '| 文件路径 | 操作 | 描述 |',
36
+ '|----------|------|------|',
37
+ ];
38
+
39
+ for (const row of artifactRows) {
40
+ lines.push(row);
41
+ }
42
+
43
+ lines.push('');
44
+ lines.push('### ⚠️ 活动问题');
45
+ lines.push('- 测试覆盖不足 → 增加E2E测试');
46
+ lines.push('');
47
+ lines.push('### ➡️ 下一步行动');
48
+ lines.push('1. 完成回归测试');
49
+ lines.push('2. 提交PR');
50
+ lines.push('');
51
+
52
+ for (let i = 0; i < extraLines; i++) {
53
+ lines.push(`<!-- padding line ${i} -->`);
54
+ }
55
+
56
+ return lines.join('\n');
57
+ }
58
+
59
+ describe('autoCompressFocus E2E regression (PRI-82)', () => {
60
+ let tmpDir: string;
61
+ let workspaceDir: string;
62
+ let stateDir: string;
63
+ let focusPath: string;
64
+
65
+ beforeEach(() => {
66
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-focus-e2e-'));
67
+ workspaceDir = tmpDir;
68
+ stateDir = path.join(tmpDir, '.state');
69
+ fs.mkdirSync(stateDir, { recursive: true });
70
+
71
+ const focusDir = path.join(tmpDir, 'okr');
72
+ fs.mkdirSync(focusDir, { recursive: true });
73
+ focusPath = path.join(focusDir, 'CURRENT_FOCUS.md');
74
+ });
75
+
76
+ afterEach(() => {
77
+ fs.rmSync(tmpDir, { recursive: true, force: true });
78
+ });
79
+
80
+ it('removes missing-file artifact rows via filesystem filtering before core compression', () => {
81
+ const existingFile = path.join(workspaceDir, 'src', 'existing-module.ts');
82
+
83
+ fs.mkdirSync(path.join(workspaceDir, 'src'), { recursive: true });
84
+ fs.writeFileSync(existingFile, 'export const x = 1;');
85
+
86
+ const existingRow = '| `src/existing-module.ts` | modified | 核心模块 |';
87
+ const missingRow = '| `src/deleted-module.ts` | modified | 已删除模块 |';
88
+
89
+ const content = makeFocusContent([existingRow, missingRow], 90);
90
+
91
+ fs.writeFileSync(focusPath, content);
92
+
93
+ const result = autoCompressFocus(focusPath, workspaceDir, stateDir);
94
+
95
+ expect(result.compressed).toBe(true);
96
+ expect(result.newContent).toBeDefined();
97
+
98
+ expect(result.newContent!).not.toContain('deleted-module.ts');
99
+
100
+ expect(result.newLines).toBeLessThan(result.oldLines);
101
+
102
+ expect(result.reason).toContain('Auto-compressed');
103
+ });
104
+
105
+ it('goes through core compressFocusContent path when threshold is exceeded', () => {
106
+ const content = makeFocusContent([
107
+ '| `src/utils.ts` | created | 工具函数 |',
108
+ ], 90);
109
+
110
+ fs.writeFileSync(focusPath, content);
111
+
112
+ const result = autoCompressFocus(focusPath, workspaceDir, stateDir);
113
+
114
+ expect(result.compressed).toBe(true);
115
+
116
+ expect(result.reason).toContain('Auto-compressed');
117
+
118
+ const writtenContent = fs.readFileSync(focusPath, 'utf-8');
119
+ expect(writtenContent).not.toBe(content);
120
+ expect(writtenContent.split('\n').length).toBeLessThan(content.split('\n').length);
121
+ });
122
+
123
+ it('does not compress when below threshold', () => {
124
+ const content = makeFocusContent([
125
+ '| `src/utils.ts` | created | 工具函数 |',
126
+ ], 0);
127
+
128
+ fs.writeFileSync(focusPath, content);
129
+
130
+ const result = autoCompressFocus(focusPath, workspaceDir, stateDir);
131
+
132
+ expect(result.compressed).toBe(false);
133
+ expect(result.reason).toBe('Below threshold');
134
+ });
135
+
136
+ it('creates backup in history when compressing', () => {
137
+ const content = makeFocusContent([
138
+ '| `src/utils.ts` | created | 工具函数 |',
139
+ ], 90);
140
+
141
+ fs.writeFileSync(focusPath, content);
142
+
143
+ const result = autoCompressFocus(focusPath, workspaceDir, stateDir);
144
+
145
+ expect(result.compressed).toBe(true);
146
+ expect(result.backupPath).not.toBeNull();
147
+
148
+ const historyDir = path.join(path.dirname(focusPath), '.history');
149
+ expect(fs.existsSync(historyDir)).toBe(true);
150
+
151
+ const historyFiles = fs.readdirSync(historyDir).filter(f => f.startsWith('CURRENT_FOCUS.v'));
152
+ expect(historyFiles.length).toBeGreaterThanOrEqual(1);
153
+ });
154
+
155
+ it('handles workspace without existing files gracefully', () => {
156
+ const content = makeFocusContent([
157
+ '| `src/ghost-file.ts` | modified | 不存在的文件 |',
158
+ ], 90);
159
+
160
+ fs.writeFileSync(focusPath, content);
161
+
162
+ const result = autoCompressFocus(focusPath, workspaceDir, stateDir);
163
+
164
+ expect(result.compressed).toBe(true);
165
+
166
+ expect(result.newContent!).not.toContain('ghost-file.ts');
167
+ });
168
+ });
169
+
170
+ describe('cleanupStaleInfo filesystem filtering (PRI-82)', () => {
171
+ let tmpDir: string;
172
+ let workspaceDir: string;
173
+
174
+ beforeEach(() => {
175
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-cleanup-'));
176
+ workspaceDir = tmpDir;
177
+ });
178
+
179
+ afterEach(() => {
180
+ fs.rmSync(tmpDir, { recursive: true, force: true });
181
+ });
182
+
183
+ it('preserves artifact row for existing file and removes row for missing file', () => {
184
+ const existingFile = path.join(workspaceDir, 'src', 'real.ts');
185
+ fs.mkdirSync(path.join(workspaceDir, 'src'), { recursive: true });
186
+ fs.writeFileSync(existingFile, 'export const a = 1;');
187
+
188
+ const content = makeFocusContent([
189
+ '| `src/real.ts` | modified | 真实文件 |',
190
+ '| `src/phantom.ts` | modified | 幽灵文件 |',
191
+ ], 0);
192
+
193
+ const result = cleanupStaleInfo(content, workspaceDir);
194
+
195
+ expect(result).toContain('real.ts');
196
+ expect(result).not.toContain('phantom.ts');
197
+ });
198
+
199
+ it('preserves all artifact rows when no workspaceDir provided', () => {
200
+ const content = makeFocusContent([
201
+ '| `src/real.ts` | modified | 真实文件 |',
202
+ '| `src/phantom.ts` | modified | 幽灵文件 |',
203
+ ], 0);
204
+
205
+ const result = cleanupStaleInfo(content);
206
+
207
+ expect(result).toContain('real.ts');
208
+ expect(result).toContain('phantom.ts');
209
+ });
210
+ });
@@ -123,7 +123,7 @@ export const DEFAULT_SETTINGS: PainSettings = {
123
123
  promotion_similarity_threshold: 0.8
124
124
  },
125
125
  scores: {
126
- paralysis: 30, // Reduced from 40
126
+ paralysis: 45, // Must be >= pain_trigger (40) so llm_paralysis can trigger diagnosis (PRI-274)
127
127
  default_confusion: 30,
128
128
  default_loop: 40,
129
129
  tool_failure_friction: 15, // Reduced from 30. A failing tool shouldn't instantly cripple the AI
@@ -21,44 +21,19 @@ import {
21
21
  CORRECTION_SEED_KEYWORDS,
22
22
  MAX_CORRECTION_KEYWORDS,
23
23
  } from './correction-types.js';
24
- import { checkKeywordOptCooldown, recordKeywordOptRun } from '../service/nocturnal-runtime.js';
25
24
  import { atomicWriteFileSync } from '../utils/io.js';
26
25
 
27
26
  const KEYWORD_STORE_FILE = 'correction_keywords.json';
28
27
 
29
- // CORR-08: Daily optimization throttle (uses checkCooldown in nocturnal-runtime.ts)
30
- // Note: throttle state is stored in nocturnal-runtime.json, not a separate file.
31
-
32
- // Weight bounds for correction keywords (D-39-03, D-39-15)
33
28
  const MIN_KEYWORD_WEIGHT = 0.1;
34
29
  const MAX_KEYWORD_WEIGHT = 0.9;
35
30
 
36
- // =========================================================================
37
- // Module-level cache (D-04, D-05)
38
- // =========================================================================
39
-
40
- /**
41
- * Invalidated on every successful save so the next load re-reads from disk.
42
- * Set to null intentionally — never assume disk and memory are in sync after a write.
43
- */
44
31
  let _correctionCueCache: CorrectionKeywordStore | null = null;
45
32
 
46
- /**
47
- * Resets the module-level cache (for testing only).
48
- * @internal
49
- */
50
33
  export function _resetCorrectionCueCache(): void {
51
34
  _correctionCueCache = null;
52
35
  }
53
36
 
54
- // =========================================================================
55
- // Default store factory
56
- // =========================================================================
57
-
58
- /**
59
- * Creates a fresh store populated with the 16 seed keywords (D-08, D-09).
60
- * addedAt is stamped with the current ISO timestamp.
61
- */
62
37
  function createDefaultStore(): CorrectionKeywordStore {
63
38
  const now = new Date().toISOString();
64
39
  const keywords: CorrectionKeyword[] = CORRECTION_SEED_KEYWORDS.map((k) => ({
@@ -69,14 +44,6 @@ function createDefaultStore(): CorrectionKeywordStore {
69
44
  return { keywords, version: 1, lastOptimizedAt: now };
70
45
  }
71
46
 
72
- // =========================================================================
73
- // Load / save
74
- // =========================================================================
75
-
76
- /**
77
- * Loads the keyword store from disk.
78
- * On first run (file absent) or parse failure, creates and persists the default store.
79
- */
80
47
  export function loadCorrectionKeywordStore(stateDir: string): CorrectionKeywordStore {
81
48
  if (_correctionCueCache) return _correctionCueCache;
82
49
 
@@ -88,23 +55,16 @@ export function loadCorrectionKeywordStore(stateDir: string): CorrectionKeywordS
88
55
  _correctionCueCache = JSON.parse(raw) as CorrectionKeywordStore;
89
56
  return _correctionCueCache;
90
57
  } catch {
91
- // Parse failure — fall through to default
58
+ void 0;
92
59
  }
93
60
  }
94
61
 
95
- // File absent or corrupt: seed the store and persist it (D-01)
96
62
  const defaultStore = createDefaultStore();
97
63
  saveCorrectionKeywordStore(stateDir, defaultStore);
98
64
  _correctionCueCache = defaultStore;
99
65
  return _correctionCueCache;
100
66
  }
101
67
 
102
- /**
103
- * Atomically saves the keyword store to disk (D-03, T-38-02).
104
- * Uses temp-file-then-rename to ensure the file is always valid JSON or
105
- * the previous valid state if a crash occurs mid-write.
106
- * MUST invalidate the cache after the rename (D-05).
107
- */
108
68
  export function saveCorrectionKeywordStore(
109
69
  stateDir: string,
110
70
  store: CorrectionKeywordStore
@@ -114,29 +74,17 @@ export function saveCorrectionKeywordStore(
114
74
  fs.mkdirSync(stateDir, { recursive: true });
115
75
  atomicWriteFileSync(filePath, JSON.stringify(store, null, 2));
116
76
 
117
- // Invalidate cache so the next read re-loads from disk (D-05)
118
77
  _correctionCueCache = null;
119
78
  }
120
79
 
121
- // =========================================================================
122
- // Throttle helpers (CORR-08)
123
- // =========================================================================
124
- // Singleton state
125
- // =========================================================================
126
-
127
80
  let _instance: CorrectionCueLearner | null = null;
128
81
  let _lastStateDir: string | null = null;
129
82
 
130
- /** Resets singleton state (for testing only). @internal */
131
83
  export function _resetCorrectionCueLearnerInstance(): void {
132
84
  _instance = null;
133
85
  _lastStateDir = null;
134
86
  }
135
87
 
136
- // =========================================================================
137
- // CorrectionCueLearner class
138
- // =========================================================================
139
-
140
88
  export class CorrectionCueLearner {
141
89
  private readonly store: CorrectionKeywordStore;
142
90
  private readonly stateDir: string;
@@ -146,17 +94,6 @@ export class CorrectionCueLearner {
146
94
  this.store = loadCorrectionKeywordStore(stateDir);
147
95
  }
148
96
 
149
- // ── Public API ──────────────────────────────────────────────────────────
150
-
151
- /**
152
- * Checks whether text contains a correction cue (D-11).
153
- * Pure read-only — does NOT modify the store.
154
- * Normalisation is equivalent to the original detectCorrectionCue():
155
- * trim → lowercase → strip punctuation
156
- * Returns weighted score based on keyword accuracy (D-39-03, D-39-04).
157
- *
158
- * To record hits/TPs, call recordHit() and recordTruePositive() separately.
159
- */
160
97
  match(text: string): CorrectionMatchResult {
161
98
  const normalized = text
162
99
  .trim()
@@ -168,9 +105,6 @@ export class CorrectionCueLearner {
168
105
 
169
106
  for (const keyword of this.store.keywords) {
170
107
  if (normalized.includes(keyword.term.toLowerCase())) {
171
- // D-39-03, D-39-04: Weighted score formula
172
- // No history (tp=0, fp=0) → accuracy = 1 (trust raw weight)
173
- // Has history → accuracy = tp / (tp + fp) (proportional to true positive rate)
174
108
  const tp = keyword.truePositiveCount ?? 0;
175
109
  const fp = keyword.falsePositiveCount ?? 0;
176
110
  const accuracy = (tp + fp) > 0 ? tp / (tp + fp) : 1;
@@ -184,7 +118,6 @@ export class CorrectionCueLearner {
184
118
  const cappedScore = Math.min(1, totalScore);
185
119
  const isMatched = matchedTerms.length > 0;
186
120
 
187
- // D-39-04: Confidence derived from multiple signals
188
121
  const termConfidence = Math.min(1, matchedTerms.length / 3);
189
122
  const scoreConfidence = Math.min(1, cappedScore / 0.8);
190
123
  const confidence = Math.max(termConfidence, scoreConfidence);
@@ -197,12 +130,6 @@ export class CorrectionCueLearner {
197
130
  };
198
131
  }
199
132
 
200
- /**
201
- * Records a keyword hit (for hitCount/FPR tracking).
202
- * Increments hitCount and updates lastHitAt for all matched terms.
203
- * Intentionally does NOT flush — hitCount is best-effort analytics,
204
- * persisted by the next recordTruePositive() or flush() call.
205
- */
206
133
  recordHits(terms: string[]): void {
207
134
  for (const term of terms) {
208
135
  const keywordIndex = this.store.keywords.findIndex(k => k.term.toLowerCase() === term.toLowerCase());
@@ -216,17 +143,12 @@ export class CorrectionCueLearner {
216
143
  }
217
144
  }
218
145
 
219
- /**
220
- * Records a confirmed true positive for the given keyword term.
221
- * Increments truePositiveCount atomically.
222
- */
223
146
  recordTruePositive(term: string): void {
224
147
  const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
225
148
  if (!keyword) return;
226
149
 
227
150
  keyword.truePositiveCount = (keyword.truePositiveCount ?? 0) + 1;
228
151
 
229
- // Update in-store reference
230
152
  const keywordIndex = this.store.keywords.findIndex(k => k.term.toLowerCase() === term.toLowerCase());
231
153
  if (keywordIndex >= 0) {
232
154
  this.store.keywords[keywordIndex] = { ...keyword };
@@ -235,61 +157,23 @@ export class CorrectionCueLearner {
235
157
  this.flush();
236
158
  }
237
159
 
238
- /**
239
- * Records a confirmed false positive for the given keyword term.
240
- * CORR-10: Decreases keyword weight by 20% (x0.8 multiplicative factor).
241
- * D-39-17: Keywords at very low weight (<0.1) still match but contribute minimally.
242
- */
243
160
  recordFalsePositive(term: string): void {
244
161
  const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
245
162
  if (!keyword) return;
246
163
 
247
164
  keyword.falsePositiveCount = (keyword.falsePositiveCount ?? 0) + 1;
248
165
 
249
- // D-39-15: Multiplicative weight decay x0.8 on confirmed FP
250
166
  keyword.weight = Math.max(MIN_KEYWORD_WEIGHT, keyword.weight * 0.8);
251
167
  keyword.lastHitAt = new Date().toISOString();
252
168
 
253
- // Update in-store reference
254
169
  const keywordIndex = this.store.keywords.findIndex(k => k.term.toLowerCase() === term.toLowerCase());
255
170
  if (keywordIndex >= 0) {
256
171
  this.store.keywords[keywordIndex] = { ...keyword };
257
172
  }
258
173
 
259
- // D-39-16: Apply decay BEFORE flush to disk
260
174
  this.flush();
261
175
  }
262
176
 
263
- /**
264
- * Returns true if optimization is allowed (within daily throttle limit).
265
- * CORR-08: Max 4 optimizations per day across all triggers.
266
- */
267
- canRunKeywordOptimization(): boolean {
268
- // D-39-12, D-39-13: Per-workspace throttle, 4 calls/day
269
- // Uses dedicated keywordOptRunTimestamps array to avoid pollution from regular nocturnal runs (#321)
270
- const cooldown = checkKeywordOptCooldown(this.stateDir, {
271
- maxRunsPerWindow: 4,
272
- quotaWindowMs: 24 * 60 * 60 * 1000,
273
- });
274
- return !cooldown.quotaExhausted;
275
- }
276
-
277
- /**
278
- * Records that an optimization was performed.
279
- * Updates lastOptimizedAt for the store and records the run in the
280
- * keyword-optimization quota (dedicated from regular nocturnal quota).
281
- * @throws Error if quota recording fails — caller should propagate
282
- */
283
- async recordOptimizationPerformed(): Promise<void> {
284
- this.store.lastOptimizedAt = new Date().toISOString();
285
- this.flush();
286
- await recordKeywordOptRun(this.stateDir);
287
- }
288
-
289
- /**
290
- * Adds a new keyword to the store and immediately flushes (D-06, D-07).
291
- * Throws if the 200-term limit would be exceeded.
292
- */
293
177
  add(keyword: Omit<CorrectionKeyword, 'addedAt'>): void {
294
178
  if (this.store.keywords.length >= MAX_CORRECTION_KEYWORDS) {
295
179
  throw new Error('Correction keyword store limit reached (200 terms)');
@@ -304,11 +188,6 @@ export class CorrectionCueLearner {
304
188
  this.flush();
305
189
  }
306
190
 
307
- /**
308
- * Updates the weight of an existing keyword.
309
- * Weight is clamped to 0.1-0.9 range.
310
- * Throws if keyword not found.
311
- */
312
191
  updateWeight(term: string, weight: number): void {
313
192
  const keyword = this.store.keywords.find(
314
193
  k => k.term.toLowerCase() === term.toLowerCase()
@@ -317,7 +196,7 @@ export class CorrectionCueLearner {
317
196
  throw new Error(`Keyword not found: ${term}`);
318
197
  }
319
198
 
320
- keyword.weight = Math.max(MIN_KEYWORD_WEIGHT, Math.min(MAX_KEYWORD_WEIGHT, weight)); // Clamp to MIN-MAX_KEYWORD_WEIGHT
199
+ keyword.weight = Math.max(MIN_KEYWORD_WEIGHT, Math.min(MAX_KEYWORD_WEIGHT, weight));
321
200
  const idx = this.store.keywords.findIndex(
322
201
  k => k.term.toLowerCase() === term.toLowerCase()
323
202
  );
@@ -327,10 +206,6 @@ export class CorrectionCueLearner {
327
206
  this.flush();
328
207
  }
329
208
 
330
- /**
331
- * Removes a keyword from the store by term.
332
- * Throws if keyword not found.
333
- */
334
209
  remove(term: string): void {
335
210
  const idx = this.store.keywords.findIndex(
336
211
  k => k.term.toLowerCase() === term.toLowerCase()
@@ -342,27 +217,18 @@ export class CorrectionCueLearner {
342
217
  this.flush();
343
218
  }
344
219
 
345
- /** Returns a reference to the in-memory store. */
346
220
  getStore(): CorrectionKeywordStore {
347
221
  return this.store;
348
222
  }
349
223
 
350
- /** Returns the lastOptimizedAt timestamp. */
351
224
  getLastOptimizedAt(): string {
352
225
  return this.store.lastOptimizedAt;
353
226
  }
354
227
 
355
- /** Persists the current in-memory store to disk atomically. */
356
228
  flush(): void {
357
229
  saveCorrectionKeywordStore(this.stateDir, this.store);
358
230
  }
359
231
 
360
- // ── Singleton factory ───────────────────────────────────────────────────
361
-
362
- /**
363
- * Returns the shared CorrectionCueLearner instance for a given stateDir.
364
- * Re-creates the instance if stateDir changes (e.g. workspace switch).
365
- */
366
232
  static get(stateDir: string): CorrectionCueLearner {
367
233
  if (!_instance || _lastStateDir !== stateDir) {
368
234
  _instance = new CorrectionCueLearner(stateDir);
@@ -1,88 +1,16 @@
1
- /**
2
- * Correction Cue Keyword Types
3
- *
4
- * Types for the dynamic correction cue detection system.
5
- * Replaces the previous hardcoded cue list in detectCorrectionCue()
6
- * with a persistent, learnable keyword store.
7
- */
8
-
9
- // =========================================================================
10
- // Keyword Store
11
- // =========================================================================
12
-
13
- export interface CorrectionKeyword {
14
- /** The keyword term to match against normalized user text */
15
- term: string;
16
- /** Contribution weight (0-1) */
17
- weight: number;
18
- /** How this keyword was introduced */
19
- source: 'seed' | 'llm' | 'user';
20
- /** ISO 8601 timestamp of when this keyword was added */
21
- addedAt: string;
22
- /** Total times this keyword has matched (default: 0) */
23
- hitCount?: number;
24
- /** Confirmed correct matches (default: 0) */
25
- truePositiveCount?: number;
26
- /** Confirmed incorrect matches (default: 0) */
27
- falsePositiveCount?: number;
28
- /** Last time this keyword matched (ISO timestamp) */
29
- lastHitAt?: string;
30
- }
31
-
32
- export interface CorrectionKeywordStore {
33
- /** All correction keywords */
34
- keywords: CorrectionKeyword[];
35
- /** Schema version */
36
- version: number;
37
- /** Last time keyword optimization was performed (ISO timestamp) */
38
- lastOptimizedAt: string;
39
- }
40
-
41
- // =========================================================================
42
- // Match Result
43
- // =========================================================================
44
-
45
- export interface CorrectionMatchResult {
46
- /** Whether any keyword matched */
47
- matched: boolean;
48
- /** Matched terms (empty array when no match; may be truncated to first N items) */
49
- matchedTerms: string[];
50
- /** Weighted score (0-1) based on keyword weight and accuracy */
51
- score: number;
52
- /** Confidence in the match result (0-1) */
53
- confidence: number;
54
- }
55
-
56
- // =========================================================================
57
- // Seed Keywords (16 terms — sourced from detectCorrectionCue)
58
- // =========================================================================
59
-
60
- /** Maximum number of keywords the store may hold (D-06). */
61
- export const MAX_CORRECTION_KEYWORDS = 200;
62
-
63
- /**
64
- * Preset seed keywords for correction cue detection.
65
- * Mirrors the hardcoded list in detectCorrectionCue() exactly (D-08).
66
- * addedAt is intentionally empty here — it is filled in at runtime by
67
- * createDefaultStore() when the store is first persisted to disk.
68
- */
69
- export const CORRECTION_SEED_KEYWORDS: CorrectionKeyword[] = [
70
- // Chinese (8)
71
- { term: '不是这个', weight: 0.6, source: 'seed', addedAt: '' },
72
- { term: '不对', weight: 0.5, source: 'seed', addedAt: '' },
73
- { term: '错了', weight: 0.5, source: 'seed', addedAt: '' },
74
- { term: '搞错了', weight: 0.7, source: 'seed', addedAt: '' },
75
- { term: '理解错了', weight: 0.7, source: 'seed', addedAt: '' },
76
- { term: '你理解错了', weight: 0.8, source: 'seed', addedAt: '' },
77
- { term: '重新来', weight: 0.6, source: 'seed', addedAt: '' },
78
- { term: '再试一次', weight: 0.4, source: 'seed', addedAt: '' },
79
- // English (8)
80
- { term: 'you are wrong', weight: 0.7, source: 'seed', addedAt: '' },
81
- { term: 'wrong file', weight: 0.6, source: 'seed', addedAt: '' },
82
- { term: 'not this', weight: 0.4, source: 'seed', addedAt: '' },
83
- { term: 'redo', weight: 0.6, source: 'seed', addedAt: '' },
84
- { term: 'try again', weight: 0.4, source: 'seed', addedAt: '' },
85
- { term: 'again', weight: 0.3, source: 'seed', addedAt: '' },
86
- { term: 'please redo', weight: 0.6, source: 'seed', addedAt: '' },
87
- { term: 'please try again', weight: 0.5, source: 'seed', addedAt: '' },
88
- ];
1
+ import type {
2
+ CorrectionKeyword,
3
+ CorrectionKeywordStore,
4
+ CorrectionMatchResult,
5
+ } from '@principles/core/runtime-v2';
6
+
7
+ export type {
8
+ CorrectionKeyword,
9
+ CorrectionKeywordStore,
10
+ CorrectionMatchResult,
11
+ };
12
+
13
+ export {
14
+ MAX_CORRECTION_KEYWORDS,
15
+ CORRECTION_SEED_KEYWORDS,
16
+ } from '@principles/core/runtime-v2';
@@ -1,7 +1,7 @@
1
1
 
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
- import { atomicWriteFileSync } from '../utils/io.js';
4
+ import { JsonFileStore } from './file-store.js';
5
5
 
6
6
  export type RuleType = 'regex' | 'exact_match';
7
7
 
@@ -64,25 +64,31 @@ const DEFAULT_RULES: Record<string, PainRule> = {
64
64
 
65
65
  export class PainDictionary {
66
66
  private data: PainDictionaryData = { rules: {} };
67
- private readonly filePath: string;
67
+ private readonly store: JsonFileStore<PainDictionaryData>;
68
68
  private readonly compiledRegex: Map<string, RegExp> = new Map();
69
69
 
70
70
  constructor(private readonly stateDir: string) {
71
- this.filePath = path.join(stateDir, 'pain_dictionary.json');
71
+ const filePath = path.join(stateDir, 'pain_dictionary.json');
72
+ this.store = new JsonFileStore<PainDictionaryData>(filePath, () => ({ rules: { ...DEFAULT_RULES } }));
72
73
  }
73
74
 
74
75
  load(): void {
75
- if (fs.existsSync(this.filePath)) {
76
- try {
77
- this.data = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
78
- } catch {
79
- console.error('[PD] Failed to parse pain_dictionary.json, using defaults.');
80
- this.data = { rules: { ...DEFAULT_RULES } };
81
- }
76
+ const filePath = path.join(this.stateDir, 'pain_dictionary.json');
77
+ const fileExisted = fs.existsSync(filePath);
78
+ const loaded = this.store.load();
79
+ // Check if we got real data (has at least one rule from file) or just defaults
80
+ const hasRules = loaded.rules && Object.keys(loaded.rules).length > 0;
81
+ if (hasRules) {
82
+ this.data = loaded;
82
83
  } else {
83
84
  this.data = { rules: { ...DEFAULT_RULES } };
84
- console.log(`[PD:Dictionary] Dictionary not found at ${this.filePath}, creating with default rules`);
85
- this.flush();
85
+ // Only overwrite if file didn't previously exist preserve corrupt files
86
+ if (!fileExisted) {
87
+ console.log(`[PD:Dictionary] Dictionary not found, creating with default rules`);
88
+ this.flush();
89
+ } else {
90
+ console.warn(`[PD:Dictionary] Dictionary corrupt or empty, preserving file and using defaults`);
91
+ }
86
92
  }
87
93
  this.compile();
88
94
  }
@@ -152,14 +158,7 @@ export class PainDictionary {
152
158
  }
153
159
 
154
160
  flush(): void {
155
- try {
156
- if (!fs.existsSync(this.stateDir)) {
157
- fs.mkdirSync(this.stateDir, { recursive: true });
158
- }
159
- atomicWriteFileSync(this.filePath, JSON.stringify(this.data, null, 2));
160
- } catch (e) {
161
- console.error('[PD] Failed to flush pain_dictionary.json:', e);
162
- }
161
+ this.store.save(this.data);
163
162
  }
164
163
 
165
164
  getStats(): { totalRules: number; totalHits: number } {