@zwbigi/ink-xy 0.1.2 → 0.1.4

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 (598) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/server/app/_global-error.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/api/inkos/route.js.nft.json +1 -1
  21. package/.next/server/app/api/skills/install/route.js.nft.json +1 -1
  22. package/.next/server/app/api/skills/search/route.js.nft.json +1 -1
  23. package/.next/server/app/index.html +1 -1
  24. package/.next/server/app/index.rsc +1 -1
  25. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  26. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  27. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  28. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  29. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  30. package/.next/server/app-paths-manifest.json +2 -2
  31. package/.next/server/chunks/162.js +1 -1
  32. package/.next/server/middleware-build-manifest.js +1 -1
  33. package/.next/server/pages/404.html +1 -1
  34. package/.next/server/pages/500.html +1 -1
  35. package/.next/trace +4 -4
  36. package/.next/trace-build +1 -1
  37. package/bin/pi-web.js +4 -1
  38. package/inkos/.env.example +20 -0
  39. package/inkos/.node-version +1 -0
  40. package/inkos/.nvmrc +1 -0
  41. package/inkos/CHANGELOG.md +787 -0
  42. package/inkos/CONTRIBUTING.md +89 -0
  43. package/inkos/LICENSE +661 -0
  44. package/inkos/README.en.md +483 -0
  45. package/inkos/README.ja.md +461 -0
  46. package/inkos/README.md +272 -0
  47. package/inkos/assets/15qun.jpg +0 -0
  48. package/inkos/assets/41777702961_.pic.jpg +0 -0
  49. package/inkos/assets/inkos-short-demo-cover.png +0 -0
  50. package/inkos/assets/inkos-text.svg +40 -0
  51. package/inkos/assets/logo.svg +47 -0
  52. package/inkos/assets/screenshot-chapters.png +0 -0
  53. package/inkos/assets/screenshot-pipeline.png +0 -0
  54. package/inkos/assets/screenshot-state.png +0 -0
  55. package/inkos/assets/screenshot-terminal.png +0 -0
  56. package/inkos/assets/wechat-group-v8.jpg +0 -0
  57. package/inkos/package.json +42 -0
  58. package/inkos/packages/cli/package.json +74 -0
  59. package/inkos/packages/cli/src/__tests__/analytics.test.ts +154 -0
  60. package/inkos/packages/cli/src/__tests__/cli-integration.test.ts +1031 -0
  61. package/inkos/packages/cli/src/__tests__/daemon.test.ts +93 -0
  62. package/inkos/packages/cli/src/__tests__/doctor.test.ts +36 -0
  63. package/inkos/packages/cli/src/__tests__/interact-command.test.ts +142 -0
  64. package/inkos/packages/cli/src/__tests__/interaction-tools.test.ts +107 -0
  65. package/inkos/packages/cli/src/__tests__/llm-overrides.test.ts +25 -0
  66. package/inkos/packages/cli/src/__tests__/localization.test.ts +121 -0
  67. package/inkos/packages/cli/src/__tests__/progress-text.test.ts +92 -0
  68. package/inkos/packages/cli/src/__tests__/project-bootstrap.test.ts +71 -0
  69. package/inkos/packages/cli/src/__tests__/publish-package.test.ts +272 -0
  70. package/inkos/packages/cli/src/__tests__/revision-command.test.ts +82 -0
  71. package/inkos/packages/cli/src/__tests__/runtime-requirements.test.ts +89 -0
  72. package/inkos/packages/cli/src/__tests__/short-fiction-command.test.ts +48 -0
  73. package/inkos/packages/cli/src/__tests__/studio-runtime.test.ts +142 -0
  74. package/inkos/packages/cli/src/__tests__/studio.test.ts +87 -0
  75. package/inkos/packages/cli/src/__tests__/tui-activity-state.test.ts +20 -0
  76. package/inkos/packages/cli/src/__tests__/tui-agent-session.test.ts +213 -0
  77. package/inkos/packages/cli/src/__tests__/tui-chat-depth.test.ts +27 -0
  78. package/inkos/packages/cli/src/__tests__/tui-chat-draft.test.ts +44 -0
  79. package/inkos/packages/cli/src/__tests__/tui-command.test.ts +86 -0
  80. package/inkos/packages/cli/src/__tests__/tui-composer-caret.test.ts +46 -0
  81. package/inkos/packages/cli/src/__tests__/tui-composer-display.test.ts +40 -0
  82. package/inkos/packages/cli/src/__tests__/tui-dashboard.test.tsx +219 -0
  83. package/inkos/packages/cli/src/__tests__/tui-effects-i18n.test.ts +29 -0
  84. package/inkos/packages/cli/src/__tests__/tui-i18n.test.ts +22 -0
  85. package/inkos/packages/cli/src/__tests__/tui-input-chrome.test.ts +10 -0
  86. package/inkos/packages/cli/src/__tests__/tui-input-history.test.ts +40 -0
  87. package/inkos/packages/cli/src/__tests__/tui-layout.test.ts +55 -0
  88. package/inkos/packages/cli/src/__tests__/tui-local-commands.test.ts +47 -0
  89. package/inkos/packages/cli/src/__tests__/tui-session-store.test.ts +81 -0
  90. package/inkos/packages/cli/src/__tests__/tui-setup-i18n.test.ts +31 -0
  91. package/inkos/packages/cli/src/__tests__/tui-slash-autocomplete.test.ts +33 -0
  92. package/inkos/packages/cli/src/commands/agent.ts +65 -0
  93. package/inkos/packages/cli/src/commands/analytics.ts +77 -0
  94. package/inkos/packages/cli/src/commands/audit.ts +52 -0
  95. package/inkos/packages/cli/src/commands/book.ts +260 -0
  96. package/inkos/packages/cli/src/commands/compose.ts +50 -0
  97. package/inkos/packages/cli/src/commands/config.ts +328 -0
  98. package/inkos/packages/cli/src/commands/consolidate.ts +50 -0
  99. package/inkos/packages/cli/src/commands/daemon.ts +121 -0
  100. package/inkos/packages/cli/src/commands/detect.ts +125 -0
  101. package/inkos/packages/cli/src/commands/doctor.ts +391 -0
  102. package/inkos/packages/cli/src/commands/draft.ts +43 -0
  103. package/inkos/packages/cli/src/commands/eval.ts +217 -0
  104. package/inkos/packages/cli/src/commands/export.ts +45 -0
  105. package/inkos/packages/cli/src/commands/fanfic.ts +183 -0
  106. package/inkos/packages/cli/src/commands/genre.ts +160 -0
  107. package/inkos/packages/cli/src/commands/import.ts +158 -0
  108. package/inkos/packages/cli/src/commands/init.ts +47 -0
  109. package/inkos/packages/cli/src/commands/interact.ts +109 -0
  110. package/inkos/packages/cli/src/commands/plan.ts +54 -0
  111. package/inkos/packages/cli/src/commands/radar.ts +60 -0
  112. package/inkos/packages/cli/src/commands/review.ts +253 -0
  113. package/inkos/packages/cli/src/commands/revise.ts +58 -0
  114. package/inkos/packages/cli/src/commands/short-fiction.ts +294 -0
  115. package/inkos/packages/cli/src/commands/status.ts +138 -0
  116. package/inkos/packages/cli/src/commands/studio.ts +194 -0
  117. package/inkos/packages/cli/src/commands/style.ts +99 -0
  118. package/inkos/packages/cli/src/commands/tui.ts +18 -0
  119. package/inkos/packages/cli/src/commands/update.ts +45 -0
  120. package/inkos/packages/cli/src/commands/write.ts +324 -0
  121. package/inkos/packages/cli/src/index.ts +5 -0
  122. package/inkos/packages/cli/src/interaction/tools.ts +49 -0
  123. package/inkos/packages/cli/src/localization.ts +215 -0
  124. package/inkos/packages/cli/src/program.ts +106 -0
  125. package/inkos/packages/cli/src/progress-text.ts +85 -0
  126. package/inkos/packages/cli/src/project-bootstrap.ts +175 -0
  127. package/inkos/packages/cli/src/runtime-requirements.ts +135 -0
  128. package/inkos/packages/cli/src/tui/__tests__/markdown.test.ts +64 -0
  129. package/inkos/packages/cli/src/tui/activity-state.ts +41 -0
  130. package/inkos/packages/cli/src/tui/agent-input.ts +264 -0
  131. package/inkos/packages/cli/src/tui/ansi.ts +72 -0
  132. package/inkos/packages/cli/src/tui/app.ts +130 -0
  133. package/inkos/packages/cli/src/tui/chat-depth.ts +22 -0
  134. package/inkos/packages/cli/src/tui/chat-draft.ts +42 -0
  135. package/inkos/packages/cli/src/tui/composer-caret.ts +22 -0
  136. package/inkos/packages/cli/src/tui/composer-display.ts +26 -0
  137. package/inkos/packages/cli/src/tui/dashboard-model.ts +164 -0
  138. package/inkos/packages/cli/src/tui/dashboard.tsx +544 -0
  139. package/inkos/packages/cli/src/tui/effects.ts +542 -0
  140. package/inkos/packages/cli/src/tui/i18n.ts +278 -0
  141. package/inkos/packages/cli/src/tui/input-history.ts +69 -0
  142. package/inkos/packages/cli/src/tui/local-commands.ts +55 -0
  143. package/inkos/packages/cli/src/tui/markdown.ts +64 -0
  144. package/inkos/packages/cli/src/tui/session-store.ts +6 -0
  145. package/inkos/packages/cli/src/tui/setup.ts +397 -0
  146. package/inkos/packages/cli/src/tui/slash-autocomplete.ts +62 -0
  147. package/inkos/packages/cli/src/tui/theme.ts +17 -0
  148. package/inkos/packages/cli/src/utils.ts +222 -0
  149. package/inkos/packages/cli/tsconfig.json +9 -0
  150. package/inkos/packages/core/genres/cozy.md +43 -0
  151. package/inkos/packages/core/genres/cultivation.md +42 -0
  152. package/inkos/packages/core/genres/dungeon-core.md +40 -0
  153. package/inkos/packages/core/genres/horror.md +51 -0
  154. package/inkos/packages/core/genres/isekai.md +43 -0
  155. package/inkos/packages/core/genres/litrpg.md +43 -0
  156. package/inkos/packages/core/genres/other.md +24 -0
  157. package/inkos/packages/core/genres/progression.md +41 -0
  158. package/inkos/packages/core/genres/romantasy.md +45 -0
  159. package/inkos/packages/core/genres/sci-fi.md +42 -0
  160. package/inkos/packages/core/genres/system-apocalypse.md +40 -0
  161. package/inkos/packages/core/genres/tower-climber.md +41 -0
  162. package/inkos/packages/core/genres/urban.md +53 -0
  163. package/inkos/packages/core/genres/xianxia.md +46 -0
  164. package/inkos/packages/core/genres/xuanhuan.md +64 -0
  165. package/inkos/packages/core/package.json +61 -0
  166. package/inkos/packages/core/src/__tests__/agent-max-tokens-policy.test.ts +29 -0
  167. package/inkos/packages/core/src/__tests__/agent-session.test.ts +866 -0
  168. package/inkos/packages/core/src/__tests__/agent-system-prompt.test.ts +167 -0
  169. package/inkos/packages/core/src/__tests__/agent-tools-params.test.ts +197 -0
  170. package/inkos/packages/core/src/__tests__/agent-tools.test.ts +421 -0
  171. package/inkos/packages/core/src/__tests__/ai-tells.test.ts +90 -0
  172. package/inkos/packages/core/src/__tests__/architect-phase5-consolidated.test.ts +445 -0
  173. package/inkos/packages/core/src/__tests__/architect-phase5.test.ts +455 -0
  174. package/inkos/packages/core/src/__tests__/architect-phase7.test.ts +210 -0
  175. package/inkos/packages/core/src/__tests__/architect.test.ts +859 -0
  176. package/inkos/packages/core/src/__tests__/audit-parse.test.ts +78 -0
  177. package/inkos/packages/core/src/__tests__/book-id.test.ts +26 -0
  178. package/inkos/packages/core/src/__tests__/book-session-store.test.ts +447 -0
  179. package/inkos/packages/core/src/__tests__/book-session.test.ts +113 -0
  180. package/inkos/packages/core/src/__tests__/chapter-analyzer.test.ts +574 -0
  181. package/inkos/packages/core/src/__tests__/chapter-memo-parser.test.ts +247 -0
  182. package/inkos/packages/core/src/__tests__/chapter-persistence.test.ts +198 -0
  183. package/inkos/packages/core/src/__tests__/chapter-review-cycle.test.ts +294 -0
  184. package/inkos/packages/core/src/__tests__/chapter-splitter.test.ts +156 -0
  185. package/inkos/packages/core/src/__tests__/chapter-state-recovery.test.ts +235 -0
  186. package/inkos/packages/core/src/__tests__/chapter-truth-validation.test.ts +253 -0
  187. package/inkos/packages/core/src/__tests__/composer.test.ts +627 -0
  188. package/inkos/packages/core/src/__tests__/config-loader.test.ts +325 -0
  189. package/inkos/packages/core/src/__tests__/config-migration.test.ts +102 -0
  190. package/inkos/packages/core/src/__tests__/consolidator.test.ts +32 -0
  191. package/inkos/packages/core/src/__tests__/context-filter.test.ts +60 -0
  192. package/inkos/packages/core/src/__tests__/context-transform.test.ts +108 -0
  193. package/inkos/packages/core/src/__tests__/continuity.test.ts +391 -0
  194. package/inkos/packages/core/src/__tests__/detection-insights.test.ts +59 -0
  195. package/inkos/packages/core/src/__tests__/detector.test.ts +86 -0
  196. package/inkos/packages/core/src/__tests__/draft-directive-parser.test.ts +386 -0
  197. package/inkos/packages/core/src/__tests__/edit-controller.test.ts +190 -0
  198. package/inkos/packages/core/src/__tests__/effective-llm-config.test.ts +486 -0
  199. package/inkos/packages/core/src/__tests__/fanfic-dimensions.test.ts +58 -0
  200. package/inkos/packages/core/src/__tests__/fanfic-models.test.ts +69 -0
  201. package/inkos/packages/core/src/__tests__/governed-working-set.test.ts +155 -0
  202. package/inkos/packages/core/src/__tests__/hook-arbiter.test.ts +124 -0
  203. package/inkos/packages/core/src/__tests__/hook-governance.test.ts +228 -0
  204. package/inkos/packages/core/src/__tests__/hook-health.test.ts +166 -0
  205. package/inkos/packages/core/src/__tests__/hook-ledger-validator.test.ts +236 -0
  206. package/inkos/packages/core/src/__tests__/hook-promotion.test.ts +192 -0
  207. package/inkos/packages/core/src/__tests__/hook-stale-detection.test.ts +136 -0
  208. package/inkos/packages/core/src/__tests__/index-notify-lazy.test.ts +20 -0
  209. package/inkos/packages/core/src/__tests__/interaction-chat-tokens.test.ts +170 -0
  210. package/inkos/packages/core/src/__tests__/interaction-models.test.ts +155 -0
  211. package/inkos/packages/core/src/__tests__/interaction-nl-router.test.ts +223 -0
  212. package/inkos/packages/core/src/__tests__/interaction-runtime.test.ts +633 -0
  213. package/inkos/packages/core/src/__tests__/interaction-tools.test.ts +343 -0
  214. package/inkos/packages/core/src/__tests__/length-metrics.test.ts +82 -0
  215. package/inkos/packages/core/src/__tests__/length-normalizer.test.ts +331 -0
  216. package/inkos/packages/core/src/__tests__/list-models.test.ts +109 -0
  217. package/inkos/packages/core/src/__tests__/llm-env.test.ts +31 -0
  218. package/inkos/packages/core/src/__tests__/logger.test.ts +175 -0
  219. package/inkos/packages/core/src/__tests__/long-span-fatigue.test.ts +160 -0
  220. package/inkos/packages/core/src/__tests__/memory-retrieval.test.ts +1303 -0
  221. package/inkos/packages/core/src/__tests__/models.test.ts +918 -0
  222. package/inkos/packages/core/src/__tests__/outline-paths.test.ts +97 -0
  223. package/inkos/packages/core/src/__tests__/path-safety.test.ts +22 -0
  224. package/inkos/packages/core/src/__tests__/persisted-governed-plan.test.ts +134 -0
  225. package/inkos/packages/core/src/__tests__/phase5-cleanup.test.ts +393 -0
  226. package/inkos/packages/core/src/__tests__/phase5-hotfix.test.ts +288 -0
  227. package/inkos/packages/core/src/__tests__/phase7-hotfix.test.ts +614 -0
  228. package/inkos/packages/core/src/__tests__/pipeline-agent.test.ts +354 -0
  229. package/inkos/packages/core/src/__tests__/pipeline-runner-memory-sync.test.ts +317 -0
  230. package/inkos/packages/core/src/__tests__/pipeline-runner.test.ts +5200 -0
  231. package/inkos/packages/core/src/__tests__/planner-context.test.ts +137 -0
  232. package/inkos/packages/core/src/__tests__/planner-prompts-ratio.test.ts +11 -0
  233. package/inkos/packages/core/src/__tests__/planner-prompts.test.ts +171 -0
  234. package/inkos/packages/core/src/__tests__/planner.test.ts +362 -0
  235. package/inkos/packages/core/src/__tests__/planning-materials.test.ts +90 -0
  236. package/inkos/packages/core/src/__tests__/polisher.test.ts +189 -0
  237. package/inkos/packages/core/src/__tests__/post-write-validator.test.ts +291 -0
  238. package/inkos/packages/core/src/__tests__/probe.test.ts +77 -0
  239. package/inkos/packages/core/src/__tests__/project-interaction.test.ts +241 -0
  240. package/inkos/packages/core/src/__tests__/provider.test.ts +953 -0
  241. package/inkos/packages/core/src/__tests__/providers-group.test.ts +34 -0
  242. package/inkos/packages/core/src/__tests__/providers-lookup.test.ts +81 -0
  243. package/inkos/packages/core/src/__tests__/providers-schema.test.ts +158 -0
  244. package/inkos/packages/core/src/__tests__/proxy-fetch.test.ts +75 -0
  245. package/inkos/packages/core/src/__tests__/revise-foundation.test.ts +514 -0
  246. package/inkos/packages/core/src/__tests__/reviser.test.ts +859 -0
  247. package/inkos/packages/core/src/__tests__/runtime-state-store.test.ts +388 -0
  248. package/inkos/packages/core/src/__tests__/scheduler.test.ts +123 -0
  249. package/inkos/packages/core/src/__tests__/secrets-migration.test.ts +71 -0
  250. package/inkos/packages/core/src/__tests__/secrets.test.ts +95 -0
  251. package/inkos/packages/core/src/__tests__/sensitive-words.test.ts +88 -0
  252. package/inkos/packages/core/src/__tests__/service-presets-regression.test.ts +73 -0
  253. package/inkos/packages/core/src/__tests__/service-resolver-regression.test.ts +75 -0
  254. package/inkos/packages/core/src/__tests__/service-resolver.test.ts +228 -0
  255. package/inkos/packages/core/src/__tests__/session-transcript-restore.test.ts +1311 -0
  256. package/inkos/packages/core/src/__tests__/session-transcript.test.ts +195 -0
  257. package/inkos/packages/core/src/__tests__/settler-delta-parser.test.ts +133 -0
  258. package/inkos/packages/core/src/__tests__/short-fiction-public.test.ts +241 -0
  259. package/inkos/packages/core/src/__tests__/spot-fix-patches.test.ts +104 -0
  260. package/inkos/packages/core/src/__tests__/state-manager.test.ts +1298 -0
  261. package/inkos/packages/core/src/__tests__/state-projections.test.ts +130 -0
  262. package/inkos/packages/core/src/__tests__/state-reducer.test.ts +372 -0
  263. package/inkos/packages/core/src/__tests__/state-validator-agent.test.ts +165 -0
  264. package/inkos/packages/core/src/__tests__/state-validator.test.ts +122 -0
  265. package/inkos/packages/core/src/__tests__/style-analyzer.test.ts +61 -0
  266. package/inkos/packages/core/src/__tests__/temperature-constraints.test.ts +57 -0
  267. package/inkos/packages/core/src/__tests__/v13-hotfix-round4.test.ts +343 -0
  268. package/inkos/packages/core/src/__tests__/verify-service.test.ts +77 -0
  269. package/inkos/packages/core/src/__tests__/webhook.test.ts +91 -0
  270. package/inkos/packages/core/src/__tests__/writer-parser.test.ts +348 -0
  271. package/inkos/packages/core/src/__tests__/writer-prompts.test.ts +269 -0
  272. package/inkos/packages/core/src/__tests__/writer.test.ts +1360 -0
  273. package/inkos/packages/core/src/agent/agent-session.ts +737 -0
  274. package/inkos/packages/core/src/agent/agent-system-prompt.ts +199 -0
  275. package/inkos/packages/core/src/agent/agent-tools.ts +835 -0
  276. package/inkos/packages/core/src/agent/context-transform.ts +85 -0
  277. package/inkos/packages/core/src/agent/index.ts +14 -0
  278. package/inkos/packages/core/src/agents/ai-tells.ts +161 -0
  279. package/inkos/packages/core/src/agents/architect.ts +1291 -0
  280. package/inkos/packages/core/src/agents/base.ts +100 -0
  281. package/inkos/packages/core/src/agents/chapter-analyzer.ts +634 -0
  282. package/inkos/packages/core/src/agents/composer.ts +469 -0
  283. package/inkos/packages/core/src/agents/consolidator.ts +218 -0
  284. package/inkos/packages/core/src/agents/continuity.ts +824 -0
  285. package/inkos/packages/core/src/agents/detection-insights.ts +72 -0
  286. package/inkos/packages/core/src/agents/detector.ts +224 -0
  287. package/inkos/packages/core/src/agents/en-prompt-sections.ts +129 -0
  288. package/inkos/packages/core/src/agents/fanfic-canon-importer.ts +146 -0
  289. package/inkos/packages/core/src/agents/fanfic-dimensions.ts +87 -0
  290. package/inkos/packages/core/src/agents/fanfic-prompt-sections.ts +109 -0
  291. package/inkos/packages/core/src/agents/foundation-reviewer.ts +204 -0
  292. package/inkos/packages/core/src/agents/length-normalizer.ts +218 -0
  293. package/inkos/packages/core/src/agents/observer-prompts.ts +127 -0
  294. package/inkos/packages/core/src/agents/planner-context.ts +297 -0
  295. package/inkos/packages/core/src/agents/planner-prompts.ts +404 -0
  296. package/inkos/packages/core/src/agents/planner.ts +783 -0
  297. package/inkos/packages/core/src/agents/polisher.ts +153 -0
  298. package/inkos/packages/core/src/agents/post-write-validator.ts +873 -0
  299. package/inkos/packages/core/src/agents/radar-source.ts +123 -0
  300. package/inkos/packages/core/src/agents/radar.ts +120 -0
  301. package/inkos/packages/core/src/agents/reviser.ts +701 -0
  302. package/inkos/packages/core/src/agents/rules-reader.ts +155 -0
  303. package/inkos/packages/core/src/agents/sensitive-words.ts +142 -0
  304. package/inkos/packages/core/src/agents/settler-delta-parser.ts +53 -0
  305. package/inkos/packages/core/src/agents/settler-parser.ts +38 -0
  306. package/inkos/packages/core/src/agents/settler-prompts.ts +230 -0
  307. package/inkos/packages/core/src/agents/short-fiction.ts +429 -0
  308. package/inkos/packages/core/src/agents/state-validator.ts +322 -0
  309. package/inkos/packages/core/src/agents/style-analyzer.ts +93 -0
  310. package/inkos/packages/core/src/agents/writer-parser.ts +178 -0
  311. package/inkos/packages/core/src/agents/writer-prompts.ts +899 -0
  312. package/inkos/packages/core/src/agents/writer.ts +1450 -0
  313. package/inkos/packages/core/src/index.ts +392 -0
  314. package/inkos/packages/core/src/interaction/book-session-store.ts +226 -0
  315. package/inkos/packages/core/src/interaction/draft-directive-parser.ts +266 -0
  316. package/inkos/packages/core/src/interaction/edit-controller.ts +270 -0
  317. package/inkos/packages/core/src/interaction/events.ts +41 -0
  318. package/inkos/packages/core/src/interaction/export-artifact.ts +151 -0
  319. package/inkos/packages/core/src/interaction/intents.ts +63 -0
  320. package/inkos/packages/core/src/interaction/modes.ts +13 -0
  321. package/inkos/packages/core/src/interaction/nl-router.ts +258 -0
  322. package/inkos/packages/core/src/interaction/project-control.ts +150 -0
  323. package/inkos/packages/core/src/interaction/project-session-store.ts +81 -0
  324. package/inkos/packages/core/src/interaction/project-tools.ts +704 -0
  325. package/inkos/packages/core/src/interaction/request-router.ts +5 -0
  326. package/inkos/packages/core/src/interaction/runtime.ts +1167 -0
  327. package/inkos/packages/core/src/interaction/session-transcript-legacy.ts +113 -0
  328. package/inkos/packages/core/src/interaction/session-transcript-restore.ts +607 -0
  329. package/inkos/packages/core/src/interaction/session-transcript-schema.ts +76 -0
  330. package/inkos/packages/core/src/interaction/session-transcript.ts +189 -0
  331. package/inkos/packages/core/src/interaction/session.ts +226 -0
  332. package/inkos/packages/core/src/interaction/truth-authority.ts +45 -0
  333. package/inkos/packages/core/src/llm/config-migration.ts +58 -0
  334. package/inkos/packages/core/src/llm/cover-providers.ts +45 -0
  335. package/inkos/packages/core/src/llm/provider.ts +1331 -0
  336. package/inkos/packages/core/src/llm/providers/endpoints/ai360.ts +42 -0
  337. package/inkos/packages/core/src/llm/providers/endpoints/anthropic.ts +82 -0
  338. package/inkos/packages/core/src/llm/providers/endpoints/astronCodingPlan.ts +30 -0
  339. package/inkos/packages/core/src/llm/providers/endpoints/baichuan.ts +28 -0
  340. package/inkos/packages/core/src/llm/providers/endpoints/bailian.ts +65 -0
  341. package/inkos/packages/core/src/llm/providers/endpoints/bailianCodingPlan.ts +30 -0
  342. package/inkos/packages/core/src/llm/providers/endpoints/custom.ts +22 -0
  343. package/inkos/packages/core/src/llm/providers/endpoints/deepseek.ts +35 -0
  344. package/inkos/packages/core/src/llm/providers/endpoints/giteeai.ts +41 -0
  345. package/inkos/packages/core/src/llm/providers/endpoints/githubCopilot.ts +43 -0
  346. package/inkos/packages/core/src/llm/providers/endpoints/glmCodingPlan.ts +28 -0
  347. package/inkos/packages/core/src/llm/providers/endpoints/google.ts +51 -0
  348. package/inkos/packages/core/src/llm/providers/endpoints/hunyuan.ts +42 -0
  349. package/inkos/packages/core/src/llm/providers/endpoints/infiniai.ts +72 -0
  350. package/inkos/packages/core/src/llm/providers/endpoints/internlm.ts +28 -0
  351. package/inkos/packages/core/src/llm/providers/endpoints/kimiCode.ts +23 -0
  352. package/inkos/packages/core/src/llm/providers/endpoints/kimiCodingPlan.ts +24 -0
  353. package/inkos/packages/core/src/llm/providers/endpoints/kkaiapi.ts +56 -0
  354. package/inkos/packages/core/src/llm/providers/endpoints/longcat.ts +25 -0
  355. package/inkos/packages/core/src/llm/providers/endpoints/minimax.ts +39 -0
  356. package/inkos/packages/core/src/llm/providers/endpoints/minimaxCodingPlan.ts +28 -0
  357. package/inkos/packages/core/src/llm/providers/endpoints/mistral.ts +40 -0
  358. package/inkos/packages/core/src/llm/providers/endpoints/modelscope.ts +30 -0
  359. package/inkos/packages/core/src/llm/providers/endpoints/moonshot.ts +39 -0
  360. package/inkos/packages/core/src/llm/providers/endpoints/newapi.ts +21 -0
  361. package/inkos/packages/core/src/llm/providers/endpoints/ollama.ts +73 -0
  362. package/inkos/packages/core/src/llm/providers/endpoints/openai.ts +77 -0
  363. package/inkos/packages/core/src/llm/providers/endpoints/opencodeCodingPlan.ts +30 -0
  364. package/inkos/packages/core/src/llm/providers/endpoints/openrouter.ts +87 -0
  365. package/inkos/packages/core/src/llm/providers/endpoints/ppio.ts +86 -0
  366. package/inkos/packages/core/src/llm/providers/endpoints/qiniu.ts +32 -0
  367. package/inkos/packages/core/src/llm/providers/endpoints/sensenova.ts +45 -0
  368. package/inkos/packages/core/src/llm/providers/endpoints/siliconcloud.ts +126 -0
  369. package/inkos/packages/core/src/llm/providers/endpoints/spark.ts +33 -0
  370. package/inkos/packages/core/src/llm/providers/endpoints/stepfun.ts +35 -0
  371. package/inkos/packages/core/src/llm/providers/endpoints/tencentcloud.ts +25 -0
  372. package/inkos/packages/core/src/llm/providers/endpoints/volcengine.ts +52 -0
  373. package/inkos/packages/core/src/llm/providers/endpoints/volcengineCodingPlan.ts +42 -0
  374. package/inkos/packages/core/src/llm/providers/endpoints/wenxin.ts +106 -0
  375. package/inkos/packages/core/src/llm/providers/endpoints/xai.ts +34 -0
  376. package/inkos/packages/core/src/llm/providers/endpoints/xiaomimimo.ts +26 -0
  377. package/inkos/packages/core/src/llm/providers/endpoints/zeroone.ts +34 -0
  378. package/inkos/packages/core/src/llm/providers/endpoints/zhipu.ts +61 -0
  379. package/inkos/packages/core/src/llm/providers/index.ts +71 -0
  380. package/inkos/packages/core/src/llm/providers/lookup.ts +70 -0
  381. package/inkos/packages/core/src/llm/providers/probe.ts +35 -0
  382. package/inkos/packages/core/src/llm/providers/types.ts +89 -0
  383. package/inkos/packages/core/src/llm/providers/verify.ts +104 -0
  384. package/inkos/packages/core/src/llm/secrets.ts +77 -0
  385. package/inkos/packages/core/src/llm/service-presets.ts +215 -0
  386. package/inkos/packages/core/src/llm/service-resolver.ts +91 -0
  387. package/inkos/packages/core/src/models/book-rules.ts +126 -0
  388. package/inkos/packages/core/src/models/book.ts +70 -0
  389. package/inkos/packages/core/src/models/chapter.ts +42 -0
  390. package/inkos/packages/core/src/models/detection.ts +25 -0
  391. package/inkos/packages/core/src/models/genre-profile.ts +36 -0
  392. package/inkos/packages/core/src/models/input-governance.ts +99 -0
  393. package/inkos/packages/core/src/models/length-governance.ts +46 -0
  394. package/inkos/packages/core/src/models/project.ts +161 -0
  395. package/inkos/packages/core/src/models/runtime-state.ts +144 -0
  396. package/inkos/packages/core/src/models/state.ts +52 -0
  397. package/inkos/packages/core/src/models/style-profile.ts +15 -0
  398. package/inkos/packages/core/src/notify/dispatcher.ts +96 -0
  399. package/inkos/packages/core/src/notify/feishu.ts +34 -0
  400. package/inkos/packages/core/src/notify/telegram.ts +25 -0
  401. package/inkos/packages/core/src/notify/webhook.ts +58 -0
  402. package/inkos/packages/core/src/notify/wechat-work.ts +22 -0
  403. package/inkos/packages/core/src/pipeline/agent.ts +691 -0
  404. package/inkos/packages/core/src/pipeline/chapter-persistence.ts +79 -0
  405. package/inkos/packages/core/src/pipeline/chapter-review-cycle.ts +324 -0
  406. package/inkos/packages/core/src/pipeline/chapter-state-recovery.ts +236 -0
  407. package/inkos/packages/core/src/pipeline/chapter-truth-validation.ts +145 -0
  408. package/inkos/packages/core/src/pipeline/detection-runner.ts +164 -0
  409. package/inkos/packages/core/src/pipeline/persisted-governed-plan.ts +216 -0
  410. package/inkos/packages/core/src/pipeline/runner.ts +3438 -0
  411. package/inkos/packages/core/src/pipeline/scheduler.ts +411 -0
  412. package/inkos/packages/core/src/pipeline/short-fiction-runner.ts +801 -0
  413. package/inkos/packages/core/src/prompts/index.ts +1 -0
  414. package/inkos/packages/core/src/prompts/short-fiction.ts +273 -0
  415. package/inkos/packages/core/src/state/manager.ts +560 -0
  416. package/inkos/packages/core/src/state/memory-db.ts +359 -0
  417. package/inkos/packages/core/src/state/runtime-state-store.ts +164 -0
  418. package/inkos/packages/core/src/state/state-bootstrap.ts +657 -0
  419. package/inkos/packages/core/src/state/state-projections.ts +255 -0
  420. package/inkos/packages/core/src/state/state-reducer.ts +260 -0
  421. package/inkos/packages/core/src/state/state-validator.ts +117 -0
  422. package/inkos/packages/core/src/utils/analytics.ts +92 -0
  423. package/inkos/packages/core/src/utils/book-id.ts +31 -0
  424. package/inkos/packages/core/src/utils/cadence-policy.ts +46 -0
  425. package/inkos/packages/core/src/utils/chapter-cadence.ts +211 -0
  426. package/inkos/packages/core/src/utils/chapter-memo-parser.ts +157 -0
  427. package/inkos/packages/core/src/utils/chapter-splitter.ts +80 -0
  428. package/inkos/packages/core/src/utils/config-loader.ts +29 -0
  429. package/inkos/packages/core/src/utils/context-assembly.ts +98 -0
  430. package/inkos/packages/core/src/utils/context-filter.ts +190 -0
  431. package/inkos/packages/core/src/utils/effective-llm-config.ts +529 -0
  432. package/inkos/packages/core/src/utils/governed-context.ts +101 -0
  433. package/inkos/packages/core/src/utils/governed-working-set.ts +395 -0
  434. package/inkos/packages/core/src/utils/hook-arbiter.ts +332 -0
  435. package/inkos/packages/core/src/utils/hook-governance.ts +199 -0
  436. package/inkos/packages/core/src/utils/hook-health.ts +189 -0
  437. package/inkos/packages/core/src/utils/hook-ledger-validator.ts +277 -0
  438. package/inkos/packages/core/src/utils/hook-lifecycle.ts +224 -0
  439. package/inkos/packages/core/src/utils/hook-policy.ts +115 -0
  440. package/inkos/packages/core/src/utils/hook-promotion.ts +313 -0
  441. package/inkos/packages/core/src/utils/hook-stale-detection.ts +168 -0
  442. package/inkos/packages/core/src/utils/length-metrics.ts +123 -0
  443. package/inkos/packages/core/src/utils/llm-endpoint-auth.ts +40 -0
  444. package/inkos/packages/core/src/utils/llm-env.ts +74 -0
  445. package/inkos/packages/core/src/utils/logger.ts +123 -0
  446. package/inkos/packages/core/src/utils/long-span-fatigue.ts +545 -0
  447. package/inkos/packages/core/src/utils/memory-retrieval.ts +527 -0
  448. package/inkos/packages/core/src/utils/narrative-control.ts +177 -0
  449. package/inkos/packages/core/src/utils/outline-paths.ts +275 -0
  450. package/inkos/packages/core/src/utils/path-safety.ts +11 -0
  451. package/inkos/packages/core/src/utils/planning-materials.ts +185 -0
  452. package/inkos/packages/core/src/utils/pov-filter.ts +149 -0
  453. package/inkos/packages/core/src/utils/proxy-fetch.ts +44 -0
  454. package/inkos/packages/core/src/utils/runtime-writer.ts +41 -0
  455. package/inkos/packages/core/src/utils/spot-fix-patches.ts +189 -0
  456. package/inkos/packages/core/src/utils/story-markdown.ts +346 -0
  457. package/inkos/packages/core/src/utils/web-search.ts +82 -0
  458. package/inkos/packages/core/src/utils/writing-methodology.ts +164 -0
  459. package/inkos/packages/core/tsconfig.json +8 -0
  460. package/inkos/packages/core/vitest.config.ts +7 -0
  461. package/inkos/packages/studio/components.json +25 -0
  462. package/inkos/packages/studio/index.html +13 -0
  463. package/inkos/packages/studio/package.json +72 -0
  464. package/inkos/packages/studio/postcss.config.js +3 -0
  465. package/inkos/packages/studio/src/App.test.ts +25 -0
  466. package/inkos/packages/studio/src/App.tsx +280 -0
  467. package/inkos/packages/studio/src/api/__tests__/normalize-base-url.test.ts +40 -0
  468. package/inkos/packages/studio/src/api/book-create.test.ts +104 -0
  469. package/inkos/packages/studio/src/api/book-create.ts +94 -0
  470. package/inkos/packages/studio/src/api/errors.ts +17 -0
  471. package/inkos/packages/studio/src/api/index.ts +30 -0
  472. package/inkos/packages/studio/src/api/lib/run-store.ts +177 -0
  473. package/inkos/packages/studio/src/api/lib/sse.ts +50 -0
  474. package/inkos/packages/studio/src/api/phase5-hotfix.test.ts +335 -0
  475. package/inkos/packages/studio/src/api/safety.ts +6 -0
  476. package/inkos/packages/studio/src/api/server.test.ts +3162 -0
  477. package/inkos/packages/studio/src/api/server.ts +3666 -0
  478. package/inkos/packages/studio/src/api/v13-hotfix-round4.test.ts +226 -0
  479. package/inkos/packages/studio/src/app-state.test.ts +8 -0
  480. package/inkos/packages/studio/src/app-state.ts +1 -0
  481. package/inkos/packages/studio/src/components/ConfirmDialog.tsx +95 -0
  482. package/inkos/packages/studio/src/components/ServiceConfigSourceCard.tsx +139 -0
  483. package/inkos/packages/studio/src/components/ServiceQuickLinks.tsx +65 -0
  484. package/inkos/packages/studio/src/components/Sidebar.tsx +652 -0
  485. package/inkos/packages/studio/src/components/ai-elements/code-block.tsx +562 -0
  486. package/inkos/packages/studio/src/components/ai-elements/confirmation.tsx +174 -0
  487. package/inkos/packages/studio/src/components/ai-elements/message.tsx +360 -0
  488. package/inkos/packages/studio/src/components/ai-elements/prompt-input.tsx +1457 -0
  489. package/inkos/packages/studio/src/components/ai-elements/reasoning.tsx +226 -0
  490. package/inkos/packages/studio/src/components/ai-elements/shimmer.tsx +77 -0
  491. package/inkos/packages/studio/src/components/ai-elements/tool.tsx +173 -0
  492. package/inkos/packages/studio/src/components/chat/BookSidebar.tsx +291 -0
  493. package/inkos/packages/studio/src/components/chat/ChatMessage.tsx +39 -0
  494. package/inkos/packages/studio/src/components/chat/QuickActions.tsx +73 -0
  495. package/inkos/packages/studio/src/components/chat/ToolExecutionSteps.tsx +320 -0
  496. package/inkos/packages/studio/src/components/chat/__tests__/ToolExecutionSteps.test.ts +114 -0
  497. package/inkos/packages/studio/src/components/chat-utils.ts +56 -0
  498. package/inkos/packages/studio/src/components/chatbar-state.test.ts +69 -0
  499. package/inkos/packages/studio/src/components/sidebar/ChaptersSection.tsx +66 -0
  500. package/inkos/packages/studio/src/components/sidebar/CharacterSection.tsx +129 -0
  501. package/inkos/packages/studio/src/components/sidebar/FoundationSection.tsx +61 -0
  502. package/inkos/packages/studio/src/components/sidebar/ProgressSection.tsx +124 -0
  503. package/inkos/packages/studio/src/components/sidebar/SidebarCard.tsx +30 -0
  504. package/inkos/packages/studio/src/components/sidebar/SummarySection.tsx +89 -0
  505. package/inkos/packages/studio/src/components/ui/alert.tsx +76 -0
  506. package/inkos/packages/studio/src/components/ui/badge.tsx +52 -0
  507. package/inkos/packages/studio/src/components/ui/button-group.tsx +87 -0
  508. package/inkos/packages/studio/src/components/ui/button.tsx +58 -0
  509. package/inkos/packages/studio/src/components/ui/collapsible.tsx +19 -0
  510. package/inkos/packages/studio/src/components/ui/command.tsx +194 -0
  511. package/inkos/packages/studio/src/components/ui/dialog.tsx +158 -0
  512. package/inkos/packages/studio/src/components/ui/dropdown-menu.tsx +266 -0
  513. package/inkos/packages/studio/src/components/ui/hover-card.tsx +51 -0
  514. package/inkos/packages/studio/src/components/ui/input-group.tsx +158 -0
  515. package/inkos/packages/studio/src/components/ui/input.tsx +20 -0
  516. package/inkos/packages/studio/src/components/ui/select.tsx +199 -0
  517. package/inkos/packages/studio/src/components/ui/separator.tsx +23 -0
  518. package/inkos/packages/studio/src/components/ui/spinner.tsx +10 -0
  519. package/inkos/packages/studio/src/components/ui/textarea.tsx +18 -0
  520. package/inkos/packages/studio/src/components/ui/tooltip.tsx +66 -0
  521. package/inkos/packages/studio/src/constants/service-groups.ts +29 -0
  522. package/inkos/packages/studio/src/hooks/use-api.test.ts +93 -0
  523. package/inkos/packages/studio/src/hooks/use-api.ts +189 -0
  524. package/inkos/packages/studio/src/hooks/use-book-activity.test.ts +129 -0
  525. package/inkos/packages/studio/src/hooks/use-book-activity.ts +180 -0
  526. package/inkos/packages/studio/src/hooks/use-colors.ts +27 -0
  527. package/inkos/packages/studio/src/hooks/use-hash-route.test.ts +101 -0
  528. package/inkos/packages/studio/src/hooks/use-hash-route.ts +89 -0
  529. package/inkos/packages/studio/src/hooks/use-i18n.ts +289 -0
  530. package/inkos/packages/studio/src/hooks/use-session-events.ts +64 -0
  531. package/inkos/packages/studio/src/hooks/use-sse.test.ts +52 -0
  532. package/inkos/packages/studio/src/hooks/use-sse.ts +92 -0
  533. package/inkos/packages/studio/src/hooks/use-theme.test.ts +31 -0
  534. package/inkos/packages/studio/src/hooks/use-theme.ts +75 -0
  535. package/inkos/packages/studio/src/index.css +323 -0
  536. package/inkos/packages/studio/src/lib/error-copy.test.ts +34 -0
  537. package/inkos/packages/studio/src/lib/error-copy.ts +37 -0
  538. package/inkos/packages/studio/src/lib/utils.ts +6 -0
  539. package/inkos/packages/studio/src/main.tsx +10 -0
  540. package/inkos/packages/studio/src/pages/Analytics.tsx +80 -0
  541. package/inkos/packages/studio/src/pages/BookCreate.tsx +895 -0
  542. package/inkos/packages/studio/src/pages/BookDetail.tsx +652 -0
  543. package/inkos/packages/studio/src/pages/ChapterReader.tsx +266 -0
  544. package/inkos/packages/studio/src/pages/ChatPage.tsx +521 -0
  545. package/inkos/packages/studio/src/pages/DaemonControl.tsx +116 -0
  546. package/inkos/packages/studio/src/pages/Dashboard.tsx +379 -0
  547. package/inkos/packages/studio/src/pages/DoctorView.tsx +82 -0
  548. package/inkos/packages/studio/src/pages/GenreManager.tsx +464 -0
  549. package/inkos/packages/studio/src/pages/ImportManager.tsx +216 -0
  550. package/inkos/packages/studio/src/pages/LanguageSelector.tsx +74 -0
  551. package/inkos/packages/studio/src/pages/LogViewer.tsx +82 -0
  552. package/inkos/packages/studio/src/pages/RadarView.tsx +157 -0
  553. package/inkos/packages/studio/src/pages/ServiceDetailPage.tsx +393 -0
  554. package/inkos/packages/studio/src/pages/ServiceListPage.tsx +463 -0
  555. package/inkos/packages/studio/src/pages/StyleManager.tsx +225 -0
  556. package/inkos/packages/studio/src/pages/TruthFiles.tsx +194 -0
  557. package/inkos/packages/studio/src/pages/chat-page-state.test.ts +206 -0
  558. package/inkos/packages/studio/src/pages/chat-page-state.ts +112 -0
  559. package/inkos/packages/studio/src/pages/page-state.test.ts +258 -0
  560. package/inkos/packages/studio/src/pages/service-detail-state.test.ts +294 -0
  561. package/inkos/packages/studio/src/pages/service-detail-state.ts +234 -0
  562. package/inkos/packages/studio/src/pages/style-manager-state.test.ts +22 -0
  563. package/inkos/packages/studio/src/pages/truth-files-state.test.ts +61 -0
  564. package/inkos/packages/studio/src/shared/contracts.ts +143 -0
  565. package/inkos/packages/studio/src/store/chat/__tests__/message-parts.test.ts +172 -0
  566. package/inkos/packages/studio/src/store/chat/index.ts +3 -0
  567. package/inkos/packages/studio/src/store/chat/initialState.ts +8 -0
  568. package/inkos/packages/studio/src/store/chat/message-policy.test.ts +16 -0
  569. package/inkos/packages/studio/src/store/chat/message-policy.ts +5 -0
  570. package/inkos/packages/studio/src/store/chat/parts-builder.ts +187 -0
  571. package/inkos/packages/studio/src/store/chat/selectors.ts +13 -0
  572. package/inkos/packages/studio/src/store/chat/slices/create/action.ts +10 -0
  573. package/inkos/packages/studio/src/store/chat/slices/create/initialState.ts +9 -0
  574. package/inkos/packages/studio/src/store/chat/slices/message/action.ts +417 -0
  575. package/inkos/packages/studio/src/store/chat/slices/message/initialState.ts +10 -0
  576. package/inkos/packages/studio/src/store/chat/slices/message/runtime.test.ts +21 -0
  577. package/inkos/packages/studio/src/store/chat/slices/message/runtime.ts +233 -0
  578. package/inkos/packages/studio/src/store/chat/slices/message/stream-events.ts +272 -0
  579. package/inkos/packages/studio/src/store/chat/store.ts +11 -0
  580. package/inkos/packages/studio/src/store/chat/types.ts +169 -0
  581. package/inkos/packages/studio/src/store/service/index.ts +2 -0
  582. package/inkos/packages/studio/src/store/service/store.ts +123 -0
  583. package/inkos/packages/studio/src/store/service/types.ts +50 -0
  584. package/inkos/packages/studio/tsconfig.json +24 -0
  585. package/inkos/packages/studio/tsconfig.server.json +11 -0
  586. package/inkos/packages/studio/vite.config.ts +34 -0
  587. package/inkos/packages/studio/vitest.config.ts +14 -0
  588. package/inkos/pnpm-lock.yaml +9569 -0
  589. package/inkos/pnpm-workspace.yaml +2 -0
  590. package/inkos/scripts/prepare-package-for-publish.mjs +135 -0
  591. package/inkos/scripts/restore-package-json.mjs +31 -0
  592. package/inkos/scripts/set-package-versions.mjs +74 -0
  593. package/inkos/scripts/verify-no-workspace-protocol.mjs +140 -0
  594. package/inkos/skills/SKILL.md +654 -0
  595. package/inkos/tsconfig.json +19 -0
  596. package/package.json +4 -3
  597. /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_buildManifest.js +0 -0
  598. /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_ssgManifest.js +0 -0
@@ -0,0 +1,1303 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createRequire } from "node:module";
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import * as memoryRetrieval from "../utils/memory-retrieval.js";
7
+ import { retrieveMemorySelection } from "../utils/memory-retrieval.js";
8
+ import { MemoryDB } from "../state/memory-db.js";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const hasNodeSqlite = (() => {
12
+ try {
13
+ require("node:sqlite");
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ })();
19
+ const sqliteIt = hasNodeSqlite ? it : it.skip;
20
+
21
+ describe("retrieveMemorySelection", () => {
22
+ let root = "";
23
+
24
+ afterEach(async () => {
25
+ if (root) {
26
+ await rm(root, { recursive: true, force: true });
27
+ root = "";
28
+ }
29
+ vi.resetModules();
30
+ vi.doUnmock("../state/memory-db.js");
31
+ });
32
+
33
+ it("indexes current state facts into sqlite-backed memory selection", async () => {
34
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-test-"));
35
+ const bookDir = join(root, "book");
36
+ const storyDir = join(bookDir, "story");
37
+ await mkdir(storyDir, { recursive: true });
38
+
39
+ await Promise.all([
40
+ writeFile(
41
+ join(storyDir, "current_state.md"),
42
+ [
43
+ "# Current State",
44
+ "",
45
+ "| Field | Value |",
46
+ "| --- | --- |",
47
+ "| Current Chapter | 9 |",
48
+ "| Current Location | Ashen ferry crossing |",
49
+ "| Protagonist State | Lin Yue hides the broken oath token and the old wound has reopened. |",
50
+ "| Current Goal | Find the vanished mentor before the guild covers its tracks. |",
51
+ "| Current Conflict | Mentor debt with the vanished teacher blocks every choice. |",
52
+ "",
53
+ ].join("\n"),
54
+ "utf-8",
55
+ ),
56
+ writeFile(join(storyDir, "chapter_summaries.md"), "# Chapter Summaries\n", "utf-8"),
57
+ writeFile(join(storyDir, "pending_hooks.md"), "# Pending Hooks\n", "utf-8"),
58
+ ]);
59
+
60
+ const result = await retrieveMemorySelection({
61
+ bookDir,
62
+ chapterNumber: 10,
63
+ goal: "Bring the focus back to the vanished mentor conflict.",
64
+ mustKeep: ["Lin Yue hides the broken oath token and the old wound has reopened."],
65
+ });
66
+
67
+ expect(result.facts.length).toBeGreaterThan(0);
68
+ expect(result.facts).toEqual(
69
+ expect.arrayContaining([
70
+ expect.objectContaining({
71
+ predicate: "Current Conflict",
72
+ object: "Mentor debt with the vanished teacher blocks every choice.",
73
+ validFromChapter: 9,
74
+ sourceChapter: 9,
75
+ }),
76
+ ]),
77
+ );
78
+ if (hasNodeSqlite) {
79
+ expect(result.dbPath).toContain("memory.db");
80
+ } else {
81
+ expect(result.dbPath).toBeUndefined();
82
+ }
83
+ });
84
+
85
+ it("extracts mentor-focused query terms without pulling guild-route negatives into English retrieval", () => {
86
+ const extractQueryTerms = (memoryRetrieval as Record<string, unknown>).extractQueryTerms as
87
+ | ((goal: string, outlineNode: string | undefined, mustKeep: ReadonlyArray<string>) => ReadonlyArray<string>)
88
+ | undefined;
89
+
90
+ expect(extractQueryTerms).toBeDefined();
91
+ const terms = extractQueryTerms?.(
92
+ "Pull focus back to the mentor debt and do not open a new frontier in this chapter.",
93
+ "Handle guild noise without letting the guild route overtake the mentor-debt mainline.",
94
+ ["Lin Yue does not abandon the mentor debt."],
95
+ ) ?? [];
96
+
97
+ expect(terms).toContain("mentor");
98
+ expect(terms).toContain("debt");
99
+ expect(terms).not.toContain("guild");
100
+ expect(terms).not.toContain("route");
101
+ });
102
+
103
+ it("extracts 师债-focused query terms without pulling 商会路线 negatives into Chinese retrieval", () => {
104
+ const extractQueryTerms = (memoryRetrieval as Record<string, unknown>).extractQueryTerms as
105
+ | ((goal: string, outlineNode: string | undefined, mustKeep: ReadonlyArray<string>) => ReadonlyArray<string>)
106
+ | undefined;
107
+
108
+ expect(extractQueryTerms).toBeDefined();
109
+ const terms = extractQueryTerms?.(
110
+ "第51章把注意力拉回师债,不让商会路线盖过主线。",
111
+ "处理商会噪音,但不允许商会路线盖过师债主线。",
112
+ ["林月不会放弃师债。"],
113
+ ) ?? [];
114
+
115
+ expect(terms).toContain("师债");
116
+ expect(terms).not.toContain("商会");
117
+ expect(terms).not.toContain("商会路线");
118
+ });
119
+
120
+ it("prefers the mentor-debt recap chapter over nearby guild-noise chapters in English retrieval", async () => {
121
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-en-test-"));
122
+ const bookDir = join(root, "book");
123
+ const storyDir = join(bookDir, "story");
124
+ await mkdir(storyDir, { recursive: true });
125
+
126
+ await Promise.all([
127
+ writeFile(
128
+ join(storyDir, "current_state.md"),
129
+ [
130
+ "| Field | Value |",
131
+ "| --- | --- |",
132
+ "| Current Chapter | 10 |",
133
+ "| Current Goal | Continue tracing the mentor debt |",
134
+ "| Current Conflict | Mentor debt mainline vs guild safe route |",
135
+ "",
136
+ ].join("\n"),
137
+ "utf-8",
138
+ ),
139
+ writeFile(
140
+ join(storyDir, "pending_hooks.md"),
141
+ [
142
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
143
+ "| --- | --- | --- | --- | --- | --- | --- |",
144
+ "| mentor-debt | 1 | relationship | open | 10 | 16 | The mentor debt remains unresolved |",
145
+ "| guild-route | 1 | mystery | open | 9 | 12 | The guild keeps offering a safer road |",
146
+ "",
147
+ ].join("\n"),
148
+ "utf-8",
149
+ ),
150
+ writeFile(
151
+ join(storyDir, "chapter_summaries.md"),
152
+ [
153
+ "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
154
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
155
+ "| 6 | Guild Pressure 6 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
156
+ "| 7 | Guild Pressure 7 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
157
+ "| 8 | Guild Pressure 8 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
158
+ "| 9 | Guild Pressure 9 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
159
+ "| 10 | Mentor Debt Echo 10 | Lin Yue | Lin Yue returns to the mentor debt trail and checks the oath token again | Commitment to the mentor debt hardens | mentor-debt advanced | tense | mainline |",
160
+ "",
161
+ ].join("\n"),
162
+ "utf-8",
163
+ ),
164
+ ]);
165
+
166
+ const result = await retrieveMemorySelection({
167
+ bookDir,
168
+ chapterNumber: 11,
169
+ goal: "Pull focus back to the mentor debt and do not let the guild route overtake the mainline.",
170
+ outlineNode: "Handle guild noise without letting the guild route overtake the mentor-debt mainline.",
171
+ mustKeep: ["Lin Yue does not abandon the mentor debt."],
172
+ });
173
+
174
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(10);
175
+ });
176
+
177
+ it("prefers the explicit 师债回响 chapter over nearby 商会噪音 chapters in Chinese retrieval", async () => {
178
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-zh-test-"));
179
+ const bookDir = join(root, "book");
180
+ const storyDir = join(bookDir, "story");
181
+ await mkdir(storyDir, { recursive: true });
182
+
183
+ await Promise.all([
184
+ writeFile(
185
+ join(storyDir, "current_state.md"),
186
+ [
187
+ "| 字段 | 值 |",
188
+ "| --- | --- |",
189
+ "| 当前章节 | 50 |",
190
+ "| 当前目标 | 继续追查师债 |",
191
+ "| 当前冲突 | 师债主线 vs 商会安全路线 |",
192
+ "",
193
+ ].join("\n"),
194
+ "utf-8",
195
+ ),
196
+ writeFile(
197
+ join(storyDir, "pending_hooks.md"),
198
+ [
199
+ "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
200
+ "| --- | --- | --- | --- | --- | --- | --- |",
201
+ "| mentor-debt | 1 | relationship | open | 50 | 60 | 师债真相与誓令碎片持续绑定 |",
202
+ "| guild-route | 1 | mystery | open | 49 | 55 | 商会安全路线仍在诱导主角偏航 |",
203
+ "",
204
+ ].join("\n"),
205
+ "utf-8",
206
+ ),
207
+ writeFile(
208
+ join(storyDir, "chapter_summaries.md"),
209
+ [
210
+ "| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |",
211
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
212
+ "| 46 | 商会余波46 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
213
+ "| 47 | 商会余波47 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
214
+ "| 48 | 商会余波48 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
215
+ "| 49 | 商会余波49 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
216
+ "| 50 | 师债回响50 | 林月 | 林月再次追查师债线索,并核对誓令碎片痕迹 | 对师债真相的执念更强 | mentor-debt 推进 | 紧绷 | 主线推进 |",
217
+ "",
218
+ ].join("\n"),
219
+ "utf-8",
220
+ ),
221
+ ]);
222
+
223
+ const result = await retrieveMemorySelection({
224
+ bookDir,
225
+ chapterNumber: 51,
226
+ goal: "第51章把注意力拉回师债,不让商会路线盖过主线。",
227
+ outlineNode: "处理商会噪音,但不允许商会路线盖过师债主线。",
228
+ mustKeep: ["林月不会放弃师债。"],
229
+ });
230
+
231
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(50);
232
+ });
233
+
234
+ it("keeps the mentor-debt recap chapter in markdown fallback mode for English books", async () => {
235
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-en-fallback-test-"));
236
+ const bookDir = join(root, "book");
237
+ const storyDir = join(bookDir, "story");
238
+ await mkdir(storyDir, { recursive: true });
239
+
240
+ await Promise.all([
241
+ writeFile(
242
+ join(storyDir, "current_state.md"),
243
+ [
244
+ "| Field | Value |",
245
+ "| --- | --- |",
246
+ "| Current Chapter | 10 |",
247
+ "| Current Goal | Continue tracing the mentor debt |",
248
+ "| Current Conflict | Mentor debt mainline vs guild safe route |",
249
+ "",
250
+ ].join("\n"),
251
+ "utf-8",
252
+ ),
253
+ writeFile(
254
+ join(storyDir, "pending_hooks.md"),
255
+ [
256
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
257
+ "| --- | --- | --- | --- | --- | --- | --- |",
258
+ "| mentor-debt | 1 | relationship | open | 10 | 16 | The mentor debt remains unresolved |",
259
+ "| guild-route | 1 | mystery | open | 9 | 12 | The guild keeps offering a safer road |",
260
+ "",
261
+ ].join("\n"),
262
+ "utf-8",
263
+ ),
264
+ writeFile(
265
+ join(storyDir, "chapter_summaries.md"),
266
+ [
267
+ "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
268
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
269
+ "| 6 | Guild Pressure 6 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
270
+ "| 7 | Guild Pressure 7 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
271
+ "| 8 | Guild Pressure 8 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
272
+ "| 9 | Guild Pressure 9 | Lin Yue | Guild pressure keeps building around the safe route | Guild route remains noisy | guild-route probed | restrained | holding-pattern |",
273
+ "| 10 | Mentor Debt Echo 10 | Lin Yue | Lin Yue returns to the mentor debt trail and checks the oath token again | Commitment to the mentor debt hardens | mentor-debt advanced | tense | mainline |",
274
+ "",
275
+ ].join("\n"),
276
+ "utf-8",
277
+ ),
278
+ ]);
279
+
280
+ vi.resetModules();
281
+ vi.doMock("../state/memory-db.js", () => ({
282
+ MemoryDB: class {
283
+ constructor() {
284
+ throw new Error("sqlite unavailable");
285
+ }
286
+ },
287
+ }));
288
+ const { retrieveMemorySelection: retrieveFallback } = await import("../utils/memory-retrieval.js");
289
+
290
+ const result = await retrieveFallback({
291
+ bookDir,
292
+ chapterNumber: 11,
293
+ goal: "Pull focus back to the mentor debt and do not let the guild route overtake the mainline.",
294
+ outlineNode: "Handle guild noise without letting the guild route overtake the mentor-debt mainline.",
295
+ mustKeep: ["Lin Yue does not abandon the mentor debt."],
296
+ });
297
+
298
+ expect(result.dbPath).toBeUndefined();
299
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(10);
300
+ expect(result.summaries.at(-1)?.chapter).toBe(10);
301
+ });
302
+
303
+ it("keeps the 师债回响 chapter in markdown fallback mode for Chinese books", async () => {
304
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-zh-fallback-test-"));
305
+ const bookDir = join(root, "book");
306
+ const storyDir = join(bookDir, "story");
307
+ await mkdir(storyDir, { recursive: true });
308
+
309
+ await Promise.all([
310
+ writeFile(
311
+ join(storyDir, "current_state.md"),
312
+ [
313
+ "| 字段 | 值 |",
314
+ "| --- | --- |",
315
+ "| 当前章节 | 50 |",
316
+ "| 当前目标 | 继续追查师债 |",
317
+ "| 当前冲突 | 师债主线 vs 商会安全路线 |",
318
+ "",
319
+ ].join("\n"),
320
+ "utf-8",
321
+ ),
322
+ writeFile(
323
+ join(storyDir, "pending_hooks.md"),
324
+ [
325
+ "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
326
+ "| --- | --- | --- | --- | --- | --- | --- |",
327
+ "| mentor-debt | 1 | relationship | open | 50 | 60 | 师债真相与誓令碎片持续绑定 |",
328
+ "| guild-route | 1 | mystery | open | 49 | 55 | 商会安全路线仍在诱导主角偏航 |",
329
+ "",
330
+ ].join("\n"),
331
+ "utf-8",
332
+ ),
333
+ writeFile(
334
+ join(storyDir, "chapter_summaries.md"),
335
+ [
336
+ "| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |",
337
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
338
+ "| 46 | 商会余波46 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
339
+ "| 47 | 商会余波47 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
340
+ "| 48 | 商会余波48 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
341
+ "| 49 | 商会余波49 | 林月 | 林月处理商会杂务与路引试探 | 继续压住商会支线 | guild-route 试探 | 克制 | 过渡牵制 |",
342
+ "| 50 | 师债回响50 | 林月 | 林月再次追查师债线索,并核对誓令碎片痕迹 | 对师债真相的执念更强 | mentor-debt 推进 | 紧绷 | 主线推进 |",
343
+ "",
344
+ ].join("\n"),
345
+ "utf-8",
346
+ ),
347
+ ]);
348
+
349
+ vi.resetModules();
350
+ vi.doMock("../state/memory-db.js", () => ({
351
+ MemoryDB: class {
352
+ constructor() {
353
+ throw new Error("sqlite unavailable");
354
+ }
355
+ },
356
+ }));
357
+ const { retrieveMemorySelection: retrieveFallback } = await import("../utils/memory-retrieval.js");
358
+
359
+ const result = await retrieveFallback({
360
+ bookDir,
361
+ chapterNumber: 51,
362
+ goal: "第51章把注意力拉回师债,不让商会路线盖过主线。",
363
+ outlineNode: "处理商会噪音,但不允许商会路线盖过师债主线。",
364
+ mustKeep: ["林月不会放弃师债。"],
365
+ });
366
+
367
+ expect(result.dbPath).toBeUndefined();
368
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(50);
369
+ expect(result.summaries.at(-1)?.chapter).toBe(50);
370
+ });
371
+
372
+ sqliteIt("uses existing sqlite summaries and hooks without requiring markdown truth files", async () => {
373
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-db-test-"));
374
+ const bookDir = join(root, "book");
375
+ const storyDir = join(bookDir, "story");
376
+ await mkdir(storyDir, { recursive: true });
377
+
378
+ await writeFile(
379
+ join(storyDir, "current_state.md"),
380
+ [
381
+ "| Field | Value |",
382
+ "| --- | --- |",
383
+ "| Current Chapter | 9 |",
384
+ "| Current Conflict | Mentor debt mainline vs guild safe route |",
385
+ "",
386
+ ].join("\n"),
387
+ "utf-8",
388
+ );
389
+
390
+ const memoryDb = new MemoryDB(bookDir);
391
+ try {
392
+ memoryDb.upsertSummary({
393
+ chapter: 9,
394
+ title: "Mentor Debt Echo",
395
+ characters: "Lin Yue",
396
+ events: "Lin Yue returns to the mentor debt trail",
397
+ stateChanges: "Commitment hardens",
398
+ hookActivity: "mentor-debt advanced",
399
+ mood: "tense",
400
+ chapterType: "mainline",
401
+ });
402
+ memoryDb.upsertHook({
403
+ hookId: "mentor-debt",
404
+ startChapter: 1,
405
+ type: "relationship",
406
+ status: "open",
407
+ lastAdvancedChapter: 9,
408
+ expectedPayoff: "16",
409
+ notes: "Mentor debt remains unresolved",
410
+ });
411
+ } finally {
412
+ memoryDb.close();
413
+ }
414
+
415
+ const result = await retrieveMemorySelection({
416
+ bookDir,
417
+ chapterNumber: 10,
418
+ goal: "Pull focus back to the mentor debt.",
419
+ mustKeep: ["Lin Yue does not abandon the mentor debt."],
420
+ });
421
+
422
+ expect(result.dbPath).toContain("memory.db");
423
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(9);
424
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("mentor-debt");
425
+ });
426
+
427
+ sqliteIt("backfills sqlite memory from structured state instead of stale markdown truth files", async () => {
428
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-db-structured-test-"));
429
+ const bookDir = join(root, "book");
430
+ const storyDir = join(bookDir, "story");
431
+ const stateDir = join(storyDir, "state");
432
+ await mkdir(stateDir, { recursive: true });
433
+
434
+ await Promise.all([
435
+ writeFile(
436
+ join(storyDir, "current_state.md"),
437
+ [
438
+ "| Field | Value |",
439
+ "| --- | --- |",
440
+ "| Current Chapter | 9 |",
441
+ "| Current Conflict | Old markdown conflict |",
442
+ "",
443
+ ].join("\n"),
444
+ "utf-8",
445
+ ),
446
+ writeFile(
447
+ join(storyDir, "pending_hooks.md"),
448
+ [
449
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
450
+ "| --- | --- | --- | --- | --- | --- | --- |",
451
+ "| markdown-hook | 1 | mystery | open | 9 | 12 | Old markdown hook |",
452
+ "",
453
+ ].join("\n"),
454
+ "utf-8",
455
+ ),
456
+ writeFile(
457
+ join(storyDir, "chapter_summaries.md"),
458
+ [
459
+ "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
460
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
461
+ "| 9 | Markdown Summary | Lin Yue | Old markdown events | Old markdown state | markdown-hook advanced | tense | fallback |",
462
+ "",
463
+ ].join("\n"),
464
+ "utf-8",
465
+ ),
466
+ writeFile(
467
+ join(stateDir, "manifest.json"),
468
+ JSON.stringify({
469
+ schemaVersion: 2,
470
+ language: "en",
471
+ lastAppliedChapter: 12,
472
+ projectionVersion: 1,
473
+ migrationWarnings: [],
474
+ }, null, 2),
475
+ "utf-8",
476
+ ),
477
+ writeFile(
478
+ join(stateDir, "current_state.json"),
479
+ JSON.stringify({
480
+ chapter: 12,
481
+ facts: [
482
+ {
483
+ subject: "protagonist",
484
+ predicate: "Current Conflict",
485
+ object: "Structured conflict should win.",
486
+ validFromChapter: 12,
487
+ validUntilChapter: null,
488
+ sourceChapter: 12,
489
+ },
490
+ ],
491
+ }, null, 2),
492
+ "utf-8",
493
+ ),
494
+ writeFile(
495
+ join(stateDir, "hooks.json"),
496
+ JSON.stringify({
497
+ hooks: [
498
+ {
499
+ hookId: "structured-hook",
500
+ startChapter: 10,
501
+ type: "relationship",
502
+ status: "progressing",
503
+ lastAdvancedChapter: 12,
504
+ expectedPayoff: "Structured payoff",
505
+ notes: "Structured hook should win.",
506
+ },
507
+ ],
508
+ }, null, 2),
509
+ "utf-8",
510
+ ),
511
+ writeFile(
512
+ join(stateDir, "chapter_summaries.json"),
513
+ JSON.stringify({
514
+ rows: [
515
+ {
516
+ chapter: 12,
517
+ title: "Structured Summary",
518
+ characters: "Lin Yue",
519
+ events: "Structured events should win.",
520
+ stateChanges: "Structured state should win.",
521
+ hookActivity: "structured-hook advanced",
522
+ mood: "tight",
523
+ chapterType: "mainline",
524
+ },
525
+ ],
526
+ }, null, 2),
527
+ "utf-8",
528
+ ),
529
+ ]);
530
+
531
+ const result = await retrieveMemorySelection({
532
+ bookDir,
533
+ chapterNumber: 13,
534
+ goal: "Bring the focus back to the structured hook.",
535
+ mustKeep: ["Structured conflict should win."],
536
+ });
537
+
538
+ expect(result.facts).toEqual(
539
+ expect.arrayContaining([
540
+ expect.objectContaining({
541
+ object: "Structured conflict should win.",
542
+ sourceChapter: 12,
543
+ }),
544
+ ]),
545
+ );
546
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("structured-hook");
547
+ expect(result.hooks.map((hook) => hook.hookId)).not.toContain("markdown-hook");
548
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(12);
549
+ expect(result.summaries.map((summary) => summary.title)).toContain("Structured Summary");
550
+ });
551
+
552
+ it("bootstraps structured runtime state from legacy markdown truth files during retrieval", async () => {
553
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-bootstrap-test-"));
554
+ const bookDir = join(root, "book");
555
+ const storyDir = join(bookDir, "story");
556
+ const stateDir = join(storyDir, "state");
557
+ await mkdir(storyDir, { recursive: true });
558
+
559
+ await Promise.all([
560
+ writeFile(
561
+ join(storyDir, "current_state.md"),
562
+ [
563
+ "| Field | Value |",
564
+ "| --- | --- |",
565
+ "| Current Chapter | 12 |",
566
+ "| Current Conflict | Mentor debt mainline vs guild safe route |",
567
+ "",
568
+ ].join("\n"),
569
+ "utf-8",
570
+ ),
571
+ writeFile(
572
+ join(storyDir, "pending_hooks.md"),
573
+ [
574
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
575
+ "| --- | --- | --- | --- | --- | --- | --- |",
576
+ "| mentor-debt | 1 | relationship | open | 12 | 16 | The mentor debt remains unresolved |",
577
+ "",
578
+ ].join("\n"),
579
+ "utf-8",
580
+ ),
581
+ writeFile(
582
+ join(storyDir, "chapter_summaries.md"),
583
+ [
584
+ "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
585
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
586
+ "| 12 | Mentor Debt Echo | Lin Yue | Lin Yue returns to the mentor debt trail | Commitment hardens | mentor-debt advanced | tense | mainline |",
587
+ "",
588
+ ].join("\n"),
589
+ "utf-8",
590
+ ),
591
+ ]);
592
+
593
+ const result = await retrieveMemorySelection({
594
+ bookDir,
595
+ chapterNumber: 13,
596
+ goal: "Pull focus back to the mentor debt.",
597
+ mustKeep: ["Lin Yue does not abandon the mentor debt."],
598
+ });
599
+
600
+ const manifest = JSON.parse(await readFile(join(stateDir, "manifest.json"), "utf-8"));
601
+ const currentState = JSON.parse(await readFile(join(stateDir, "current_state.json"), "utf-8"));
602
+ const hooks = JSON.parse(await readFile(join(stateDir, "hooks.json"), "utf-8"));
603
+ const summaries = JSON.parse(await readFile(join(stateDir, "chapter_summaries.json"), "utf-8"));
604
+
605
+ expect(manifest.schemaVersion).toBe(2);
606
+ expect(currentState.chapter).toBe(12);
607
+ expect(hooks.hooks[0]?.hookId).toBe("mentor-debt");
608
+ expect(summaries.rows[0]?.title).toBe("Mentor Debt Echo");
609
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("mentor-debt");
610
+ });
611
+
612
+ it("prefers structured state files over legacy markdown truth files when both exist", async () => {
613
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-structured-preferred-test-"));
614
+ const bookDir = join(root, "book");
615
+ const storyDir = join(bookDir, "story");
616
+ const stateDir = join(storyDir, "state");
617
+ await mkdir(stateDir, { recursive: true });
618
+
619
+ await Promise.all([
620
+ writeFile(
621
+ join(storyDir, "current_state.md"),
622
+ [
623
+ "| Field | Value |",
624
+ "| --- | --- |",
625
+ "| Current Chapter | 9 |",
626
+ "| Current Conflict | Old markdown conflict |",
627
+ "",
628
+ ].join("\n"),
629
+ "utf-8",
630
+ ),
631
+ writeFile(
632
+ join(storyDir, "pending_hooks.md"),
633
+ [
634
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
635
+ "| --- | --- | --- | --- | --- | --- | --- |",
636
+ "| markdown-hook | 1 | mystery | open | 9 | 12 | Old markdown hook |",
637
+ "",
638
+ ].join("\n"),
639
+ "utf-8",
640
+ ),
641
+ writeFile(
642
+ join(storyDir, "chapter_summaries.md"),
643
+ [
644
+ "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
645
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
646
+ "| 9 | Markdown Summary | Lin Yue | Old markdown event | Old markdown state | markdown-hook advanced | tense | fallback |",
647
+ "",
648
+ ].join("\n"),
649
+ "utf-8",
650
+ ),
651
+ writeFile(join(stateDir, "manifest.json"), JSON.stringify({
652
+ schemaVersion: 2,
653
+ language: "en",
654
+ lastAppliedChapter: 12,
655
+ projectionVersion: 1,
656
+ migrationWarnings: [],
657
+ }, null, 2), "utf-8"),
658
+ writeFile(join(stateDir, "current_state.json"), JSON.stringify({
659
+ chapter: 12,
660
+ facts: [
661
+ {
662
+ subject: "protagonist",
663
+ predicate: "Current Conflict",
664
+ object: "Structured conflict should win.",
665
+ validFromChapter: 12,
666
+ validUntilChapter: null,
667
+ sourceChapter: 12,
668
+ },
669
+ ],
670
+ }, null, 2), "utf-8"),
671
+ writeFile(join(stateDir, "hooks.json"), JSON.stringify({
672
+ hooks: [
673
+ {
674
+ hookId: "structured-hook",
675
+ startChapter: 10,
676
+ type: "relationship",
677
+ status: "progressing",
678
+ lastAdvancedChapter: 12,
679
+ expectedPayoff: "Structured payoff",
680
+ notes: "Structured hook should win.",
681
+ },
682
+ ],
683
+ }, null, 2), "utf-8"),
684
+ writeFile(join(stateDir, "chapter_summaries.json"), JSON.stringify({
685
+ rows: [
686
+ {
687
+ chapter: 12,
688
+ title: "Structured Summary",
689
+ characters: "Lin Yue",
690
+ events: "Structured events should win.",
691
+ stateChanges: "Structured state should win.",
692
+ hookActivity: "structured-hook advanced",
693
+ mood: "tight",
694
+ chapterType: "mainline",
695
+ },
696
+ ],
697
+ }, null, 2), "utf-8"),
698
+ ]);
699
+
700
+ const result = await retrieveMemorySelection({
701
+ bookDir,
702
+ chapterNumber: 13,
703
+ goal: "Bring the focus back to the structured hook.",
704
+ mustKeep: ["Structured conflict should win."],
705
+ });
706
+
707
+ expect(result.facts).toEqual(
708
+ expect.arrayContaining([
709
+ expect.objectContaining({
710
+ object: "Structured conflict should win.",
711
+ sourceChapter: 12,
712
+ }),
713
+ ]),
714
+ );
715
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("structured-hook");
716
+ expect(result.hooks.map((hook) => hook.hookId)).not.toContain("markdown-hook");
717
+ expect(result.summaries.map((summary) => summary.chapter)).toContain(12);
718
+ expect(result.summaries.map((summary) => summary.title)).toContain("Structured Summary");
719
+ });
720
+
721
+ it("recalls stale open hooks alongside recent governed memory selections", async () => {
722
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-stale-hook-test-"));
723
+ const bookDir = join(root, "book");
724
+ const storyDir = join(bookDir, "story");
725
+ const stateDir = join(storyDir, "state");
726
+ await mkdir(stateDir, { recursive: true });
727
+
728
+ await Promise.all([
729
+ writeFile(
730
+ join(stateDir, "manifest.json"),
731
+ JSON.stringify({
732
+ schemaVersion: 2,
733
+ language: "en",
734
+ lastAppliedChapter: 25,
735
+ projectionVersion: 1,
736
+ migrationWarnings: [],
737
+ }, null, 2),
738
+ "utf-8",
739
+ ),
740
+ writeFile(
741
+ join(stateDir, "current_state.json"),
742
+ JSON.stringify({
743
+ chapter: 25,
744
+ facts: [],
745
+ }, null, 2),
746
+ "utf-8",
747
+ ),
748
+ writeFile(
749
+ join(stateDir, "chapter_summaries.json"),
750
+ JSON.stringify({
751
+ rows: [],
752
+ }, null, 2),
753
+ "utf-8",
754
+ ),
755
+ writeFile(
756
+ join(stateDir, "hooks.json"),
757
+ JSON.stringify({
758
+ hooks: [
759
+ {
760
+ hookId: "recent-route",
761
+ startChapter: 22,
762
+ type: "route",
763
+ status: "open",
764
+ lastAdvancedChapter: 24,
765
+ expectedPayoff: "Recent route payoff",
766
+ notes: "Recent but not critical.",
767
+ },
768
+ {
769
+ hookId: "stale-debt",
770
+ startChapter: 3,
771
+ type: "relationship",
772
+ status: "open",
773
+ lastAdvancedChapter: 8,
774
+ expectedPayoff: "Mentor debt payoff",
775
+ notes: "Long-stale but still unresolved.",
776
+ },
777
+ ],
778
+ }, null, 2),
779
+ "utf-8",
780
+ ),
781
+ ]);
782
+
783
+ const result = await retrieveMemorySelection({
784
+ bookDir,
785
+ chapterNumber: 26,
786
+ goal: "Keep the chapter on the mainline debt conflict.",
787
+ mustKeep: ["The mentor debt is still unresolved."],
788
+ });
789
+
790
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("recent-route");
791
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("stale-debt");
792
+ });
793
+
794
+ it("surfaces one stale unresolved hook beyond the primary quota while excluding stale resolved hooks", async () => {
795
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-stale-quota-test-"));
796
+ const bookDir = join(root, "book");
797
+ const storyDir = join(bookDir, "story");
798
+ const stateDir = join(storyDir, "state");
799
+ await mkdir(stateDir, { recursive: true });
800
+
801
+ await Promise.all([
802
+ writeFile(
803
+ join(stateDir, "manifest.json"),
804
+ JSON.stringify({
805
+ schemaVersion: 2,
806
+ language: "en",
807
+ lastAppliedChapter: 40,
808
+ projectionVersion: 1,
809
+ migrationWarnings: [],
810
+ }, null, 2),
811
+ "utf-8",
812
+ ),
813
+ writeFile(
814
+ join(stateDir, "current_state.json"),
815
+ JSON.stringify({
816
+ chapter: 40,
817
+ facts: [],
818
+ }, null, 2),
819
+ "utf-8",
820
+ ),
821
+ writeFile(
822
+ join(stateDir, "chapter_summaries.json"),
823
+ JSON.stringify({
824
+ rows: [],
825
+ }, null, 2),
826
+ "utf-8",
827
+ ),
828
+ writeFile(
829
+ join(stateDir, "hooks.json"),
830
+ JSON.stringify({
831
+ hooks: [
832
+ {
833
+ hookId: "recent-route",
834
+ startChapter: 37,
835
+ type: "route",
836
+ status: "open",
837
+ lastAdvancedChapter: 39,
838
+ expectedPayoff: "Recent route payoff",
839
+ notes: "Recent route remains active.",
840
+ },
841
+ {
842
+ hookId: "recent-guild",
843
+ startChapter: 36,
844
+ type: "politics",
845
+ status: "progressing",
846
+ lastAdvancedChapter: 38,
847
+ expectedPayoff: "Guild payoff",
848
+ notes: "Recent guild pressure remains active.",
849
+ },
850
+ {
851
+ hookId: "recent-token",
852
+ startChapter: 35,
853
+ type: "artifact",
854
+ status: "open",
855
+ lastAdvancedChapter: 37,
856
+ expectedPayoff: "Token payoff",
857
+ notes: "Recent token route remains active.",
858
+ },
859
+ {
860
+ hookId: "stale-omega",
861
+ startChapter: 3,
862
+ type: "relationship",
863
+ status: "open",
864
+ lastAdvancedChapter: 8,
865
+ expectedPayoff: "Old relic payoff",
866
+ notes: "Dormant unresolved line.",
867
+ },
868
+ {
869
+ hookId: "stale-resolved",
870
+ startChapter: 2,
871
+ type: "mystery",
872
+ status: "resolved",
873
+ lastAdvancedChapter: 7,
874
+ expectedPayoff: "Already closed",
875
+ notes: "Should not be resurfaced.",
876
+ },
877
+ ],
878
+ }, null, 2),
879
+ "utf-8",
880
+ ),
881
+ ]);
882
+
883
+ const result = await retrieveMemorySelection({
884
+ bookDir,
885
+ chapterNumber: 41,
886
+ goal: "Keep the chapter on the harbor confrontation.",
887
+ mustKeep: ["The harbor confrontation must stay central."],
888
+ });
889
+
890
+ expect(result.hooks.map((hook) => hook.hookId)).toEqual([
891
+ "recent-route",
892
+ "recent-guild",
893
+ "recent-token",
894
+ "stale-omega",
895
+ ]);
896
+ expect(result.hooks.map((hook) => hook.hookId)).not.toContain("stale-resolved");
897
+ });
898
+
899
+ it("surfaces multiple stale hook families when debt pressure clusters instead of only one stale extra", async () => {
900
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-stale-cluster-test-"));
901
+ const bookDir = join(root, "book");
902
+ const storyDir = join(bookDir, "story");
903
+ const stateDir = join(storyDir, "state");
904
+ await mkdir(stateDir, { recursive: true });
905
+
906
+ await Promise.all([
907
+ writeFile(
908
+ join(stateDir, "manifest.json"),
909
+ JSON.stringify({
910
+ schemaVersion: 2,
911
+ language: "en",
912
+ lastAppliedChapter: 50,
913
+ projectionVersion: 1,
914
+ migrationWarnings: [],
915
+ }, null, 2),
916
+ "utf-8",
917
+ ),
918
+ writeFile(
919
+ join(stateDir, "current_state.json"),
920
+ JSON.stringify({
921
+ chapter: 50,
922
+ facts: [],
923
+ }, null, 2),
924
+ "utf-8",
925
+ ),
926
+ writeFile(
927
+ join(stateDir, "chapter_summaries.json"),
928
+ JSON.stringify({
929
+ rows: [],
930
+ }, null, 2),
931
+ "utf-8",
932
+ ),
933
+ writeFile(
934
+ join(stateDir, "hooks.json"),
935
+ JSON.stringify({
936
+ hooks: [
937
+ {
938
+ hookId: "recent-route",
939
+ startChapter: 47,
940
+ type: "route",
941
+ status: "open",
942
+ lastAdvancedChapter: 49,
943
+ expectedPayoff: "Recent route payoff",
944
+ notes: "Recent route remains active.",
945
+ },
946
+ {
947
+ hookId: "recent-guild",
948
+ startChapter: 46,
949
+ type: "politics",
950
+ status: "progressing",
951
+ lastAdvancedChapter: 48,
952
+ expectedPayoff: "Guild payoff",
953
+ notes: "Recent guild pressure remains active.",
954
+ },
955
+ {
956
+ hookId: "recent-token",
957
+ startChapter: 45,
958
+ type: "artifact",
959
+ status: "open",
960
+ lastAdvancedChapter: 47,
961
+ expectedPayoff: "Token payoff",
962
+ notes: "Recent token route remains active.",
963
+ },
964
+ {
965
+ hookId: "stale-omega",
966
+ startChapter: 6,
967
+ type: "relationship",
968
+ status: "open",
969
+ lastAdvancedChapter: 12,
970
+ expectedPayoff: "Old relic payoff",
971
+ notes: "Dormant unresolved relationship line.",
972
+ },
973
+ {
974
+ hookId: "stale-sable",
975
+ startChapter: 8,
976
+ type: "mystery",
977
+ status: "open",
978
+ lastAdvancedChapter: 14,
979
+ expectedPayoff: "Archive payoff",
980
+ notes: "Dormant unresolved mystery line.",
981
+ },
982
+ ],
983
+ }, null, 2),
984
+ "utf-8",
985
+ ),
986
+ ]);
987
+
988
+ const result = await retrieveMemorySelection({
989
+ bookDir,
990
+ chapterNumber: 51,
991
+ goal: "Keep the chapter on the debt cluster and route pressure together.",
992
+ mustKeep: ["The old debt cluster must stay legible."],
993
+ });
994
+
995
+ expect(result.hooks.map((hook) => hook.hookId)).toEqual(expect.arrayContaining([
996
+ "stale-omega",
997
+ "stale-sable",
998
+ ]));
999
+ });
1000
+
1001
+ it("does not surface far-future unstarted hooks in early chapter retrieval", async () => {
1002
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-future-hook-gate-test-"));
1003
+ const bookDir = join(root, "book");
1004
+ const storyDir = join(bookDir, "story");
1005
+ const stateDir = join(storyDir, "state");
1006
+ await mkdir(stateDir, { recursive: true });
1007
+
1008
+ await Promise.all([
1009
+ writeFile(
1010
+ join(stateDir, "manifest.json"),
1011
+ JSON.stringify({
1012
+ schemaVersion: 2,
1013
+ language: "zh",
1014
+ lastAppliedChapter: 0,
1015
+ projectionVersion: 1,
1016
+ migrationWarnings: [],
1017
+ }, null, 2),
1018
+ "utf-8",
1019
+ ),
1020
+ writeFile(
1021
+ join(stateDir, "current_state.json"),
1022
+ JSON.stringify({
1023
+ chapter: 0,
1024
+ facts: [],
1025
+ }, null, 2),
1026
+ "utf-8",
1027
+ ),
1028
+ writeFile(
1029
+ join(stateDir, "chapter_summaries.json"),
1030
+ JSON.stringify({
1031
+ rows: [],
1032
+ }, null, 2),
1033
+ "utf-8",
1034
+ ),
1035
+ writeFile(
1036
+ join(stateDir, "hooks.json"),
1037
+ JSON.stringify({
1038
+ hooks: [
1039
+ {
1040
+ hookId: "future-gault",
1041
+ startChapter: 54,
1042
+ type: "threat",
1043
+ status: "open",
1044
+ lastAdvancedChapter: 0,
1045
+ expectedPayoff: "Late assembly loss",
1046
+ notes: "Far-future disruption only.",
1047
+ },
1048
+ {
1049
+ hookId: "future-ledger-trial",
1050
+ startChapter: 22,
1051
+ type: "institutional",
1052
+ status: "open",
1053
+ lastAdvancedChapter: 0,
1054
+ expectedPayoff: "Late court hearing",
1055
+ notes: "Far-future institutional clash.",
1056
+ },
1057
+ {
1058
+ hookId: "opening-call",
1059
+ startChapter: 1,
1060
+ type: "mystery",
1061
+ status: "open",
1062
+ lastAdvancedChapter: 0,
1063
+ expectedPayoff: "Trace the anonymous caller",
1064
+ notes: "Opening anonymous call.",
1065
+ },
1066
+ {
1067
+ hookId: "nearby-ledger",
1068
+ startChapter: 4,
1069
+ type: "evidence",
1070
+ status: "open",
1071
+ lastAdvancedChapter: 0,
1072
+ expectedPayoff: "Find the first ledger fragment",
1073
+ notes: "Near-future evidence reveal.",
1074
+ },
1075
+ {
1076
+ hookId: "future-final-choice",
1077
+ startChapter: 71,
1078
+ type: "climax",
1079
+ status: "open",
1080
+ lastAdvancedChapter: 0,
1081
+ expectedPayoff: "Final disclosure choice",
1082
+ notes: "Endgame only.",
1083
+ },
1084
+ ],
1085
+ }, null, 2),
1086
+ "utf-8",
1087
+ ),
1088
+ ]);
1089
+
1090
+ const result = await retrieveMemorySelection({
1091
+ bookDir,
1092
+ chapterNumber: 1,
1093
+ goal: "稳住开篇压力,不提前展开远期线。",
1094
+ mustKeep: ["匿名来电必须留在开篇。"],
1095
+ });
1096
+
1097
+ expect(result.hooks.map((hook) => hook.hookId).sort()).toEqual([
1098
+ "nearby-ledger",
1099
+ "opening-call",
1100
+ ]);
1101
+ });
1102
+
1103
+ it("does not resurface a resolved hook just because mustKeep shares an artifact term", async () => {
1104
+ root = await mkdtemp(join(tmpdir(), "inkos-memory-retrieval-resolved-artifact-test-"));
1105
+ const bookDir = join(root, "book");
1106
+ const storyDir = join(bookDir, "story");
1107
+ const stateDir = join(storyDir, "state");
1108
+ await mkdir(stateDir, { recursive: true });
1109
+
1110
+ await Promise.all([
1111
+ writeFile(
1112
+ join(stateDir, "manifest.json"),
1113
+ JSON.stringify({
1114
+ schemaVersion: 2,
1115
+ language: "en",
1116
+ lastAppliedChapter: 10,
1117
+ projectionVersion: 1,
1118
+ migrationWarnings: [],
1119
+ }, null, 2),
1120
+ "utf-8",
1121
+ ),
1122
+ writeFile(
1123
+ join(stateDir, "current_state.json"),
1124
+ JSON.stringify({
1125
+ chapter: 10,
1126
+ facts: [],
1127
+ }, null, 2),
1128
+ "utf-8",
1129
+ ),
1130
+ writeFile(
1131
+ join(stateDir, "chapter_summaries.json"),
1132
+ JSON.stringify({
1133
+ rows: [],
1134
+ }, null, 2),
1135
+ "utf-8",
1136
+ ),
1137
+ writeFile(
1138
+ join(stateDir, "hooks.json"),
1139
+ JSON.stringify({
1140
+ hooks: [
1141
+ {
1142
+ hookId: "mentor-oath",
1143
+ startChapter: 8,
1144
+ type: "relationship",
1145
+ status: "open",
1146
+ lastAdvancedChapter: 9,
1147
+ expectedPayoff: "Mentor oath payoff",
1148
+ notes: "Mentor oath debt with Lin Yue",
1149
+ },
1150
+ {
1151
+ hookId: "old-seal",
1152
+ startChapter: 3,
1153
+ type: "artifact",
1154
+ status: "resolved",
1155
+ lastAdvancedChapter: 3,
1156
+ expectedPayoff: "Seal already recovered",
1157
+ notes: "Jade seal already recovered.",
1158
+ },
1159
+ ],
1160
+ }, null, 2),
1161
+ "utf-8",
1162
+ ),
1163
+ ]);
1164
+
1165
+ const result = await retrieveMemorySelection({
1166
+ bookDir,
1167
+ chapterNumber: 11,
1168
+ goal: "Bring the focus back to the mentor oath conflict with Lin Yue.",
1169
+ outlineNode: "Track the merchant guild's escape route.",
1170
+ mustKeep: ["The jade seal cannot be destroyed."],
1171
+ });
1172
+
1173
+ expect(result.hooks.map((hook) => hook.hookId)).toContain("mentor-oath");
1174
+ expect(result.hooks.map((hook) => hook.hookId)).not.toContain("old-seal");
1175
+ });
1176
+ });
1177
+
1178
+ describe("parsePendingHooksMarkdown", () => {
1179
+ it("strips markdown emphasis from hook ids in pending hooks tables", () => {
1180
+ const hooks = memoryRetrieval.parsePendingHooksMarkdown([
1181
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
1182
+ "| --- | --- | --- | --- | --- | --- | --- |",
1183
+ "| **H009** | 3 | mystery | open | 3 | 9 | Bold markdown leaked into hook id |",
1184
+ "| **H010** | 3 | threat | open | 3 | 6 | Another emphasized hook id |",
1185
+ "",
1186
+ ].join("\n"));
1187
+
1188
+ expect(hooks.map((hook) => hook.hookId)).toEqual(["H009", "H010"]);
1189
+ });
1190
+
1191
+ it("parses semantic payoff timing from extended pending hooks tables", () => {
1192
+ const hooks = memoryRetrieval.parsePendingHooksMarkdown([
1193
+ "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | payoff_timing | notes |",
1194
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
1195
+ "| oath-debt | 8 | relationship | open | 12 | Reveal why the mentor broke the oath | slow-burn | Long-buried debt stays unresolved |",
1196
+ "| kiln-key | 15 | mystery | open | 15 | Find out what the kiln key opens next chapter | immediate | Fresh key with a fast local payoff |",
1197
+ "",
1198
+ ].join("\n"));
1199
+
1200
+ expect(hooks).toEqual([
1201
+ expect.objectContaining({
1202
+ hookId: "oath-debt",
1203
+ payoffTiming: "slow-burn",
1204
+ notes: "Long-buried debt stays unresolved",
1205
+ }),
1206
+ expect.objectContaining({
1207
+ hookId: "kiln-key",
1208
+ payoffTiming: "immediate",
1209
+ notes: "Fresh key with a fast local payoff",
1210
+ }),
1211
+ ]);
1212
+ });
1213
+
1214
+ });
1215
+
1216
+ // ---------------------------------------------------------------------------
1217
+ // Phase 9-2 — computeRecyclableHooks unit tests
1218
+ // ---------------------------------------------------------------------------
1219
+
1220
+ import { computeRecyclableHooks } from "../utils/memory-retrieval.js";
1221
+ import type { StoredHook } from "../state/memory-db.js";
1222
+
1223
+ function makeHook(overrides: Partial<StoredHook> & Pick<StoredHook, "hookId">): StoredHook {
1224
+ return {
1225
+ startChapter: 1,
1226
+ type: "foreshadow",
1227
+ status: "open",
1228
+ lastAdvancedChapter: 0,
1229
+ expectedPayoff: "",
1230
+ notes: "",
1231
+ ...overrides,
1232
+ };
1233
+ }
1234
+
1235
+ describe("computeRecyclableHooks", () => {
1236
+ it("returns empty array when no hooks are stale", () => {
1237
+ const hooks = [
1238
+ makeHook({ hookId: "H1", startChapter: 8, lastAdvancedChapter: 9, status: "pressured" }),
1239
+ makeHook({ hookId: "H2", startChapter: 9, lastAdvancedChapter: 0, status: "open" }),
1240
+ ];
1241
+ expect(computeRecyclableHooks(hooks, 10)).toEqual([]);
1242
+ });
1243
+
1244
+ it("flags pressured hooks silent ≥ 5 chapters", () => {
1245
+ const hooks = [
1246
+ makeHook({ hookId: "H1", startChapter: 3, lastAdvancedChapter: 4, status: "pressured" }),
1247
+ makeHook({ hookId: "H2", startChapter: 9, lastAdvancedChapter: 9, status: "pressured" }),
1248
+ ];
1249
+ const result = computeRecyclableHooks(hooks, 10);
1250
+ expect(result.map((h) => h.hookId)).toEqual(["H1"]);
1251
+ });
1252
+
1253
+ it("flags near_payoff hooks silent ≥ 5 chapters", () => {
1254
+ const hooks = [
1255
+ makeHook({ hookId: "H1", startChapter: 3, lastAdvancedChapter: 4, status: "near_payoff" }),
1256
+ ];
1257
+ const result = computeRecyclableHooks(hooks, 10);
1258
+ expect(result.map((h) => h.hookId)).toEqual(["H1"]);
1259
+ });
1260
+
1261
+ it("flags core hooks silent ≥ 8 chapters (not 10)", () => {
1262
+ const hooks = [
1263
+ makeHook({ hookId: "H-core", startChapter: 2, lastAdvancedChapter: 2, status: "open", coreHook: true }),
1264
+ makeHook({ hookId: "H-regular", startChapter: 2, lastAdvancedChapter: 2, status: "open" }),
1265
+ ];
1266
+ // silence = 10 - 2 = 8. core: qualifies (>=8). regular: does not (<10).
1267
+ const result = computeRecyclableHooks(hooks, 10);
1268
+ expect(result.map((h) => h.hookId)).toEqual(["H-core"]);
1269
+ });
1270
+
1271
+ it("flags plain open hooks only when silent ≥ 10 chapters", () => {
1272
+ const hooks = [
1273
+ makeHook({ hookId: "H1", startChapter: 1, lastAdvancedChapter: 0, status: "open" }),
1274
+ ];
1275
+ expect(computeRecyclableHooks(hooks, 10).map((h) => h.hookId)).toEqual([]);
1276
+ expect(computeRecyclableHooks(hooks, 11).map((h) => h.hookId)).toEqual(["H1"]);
1277
+ });
1278
+
1279
+ it("excludes resolved / deferred hooks regardless of silence", () => {
1280
+ const hooks = [
1281
+ makeHook({ hookId: "H1", startChapter: 1, lastAdvancedChapter: 1, status: "resolved" }),
1282
+ makeHook({ hookId: "H2", startChapter: 1, lastAdvancedChapter: 1, status: "deferred" }),
1283
+ ];
1284
+ expect(computeRecyclableHooks(hooks, 20)).toEqual([]);
1285
+ });
1286
+
1287
+ it("excludes future-planted hooks that have not yet landed", () => {
1288
+ const hooks = [
1289
+ makeHook({ hookId: "H1", startChapter: 30, lastAdvancedChapter: 0, status: "open" }),
1290
+ ];
1291
+ expect(computeRecyclableHooks(hooks, 10)).toEqual([]);
1292
+ });
1293
+
1294
+ it("sorts by silence DESC — most overdue hook first", () => {
1295
+ const hooks = [
1296
+ makeHook({ hookId: "H-mid", startChapter: 2, lastAdvancedChapter: 4, status: "pressured" }),
1297
+ makeHook({ hookId: "H-worst", startChapter: 1, lastAdvancedChapter: 1, status: "pressured" }),
1298
+ makeHook({ hookId: "H-mild", startChapter: 3, lastAdvancedChapter: 5, status: "pressured" }),
1299
+ ];
1300
+ const result = computeRecyclableHooks(hooks, 10);
1301
+ expect(result.map((h) => h.hookId)).toEqual(["H-worst", "H-mid", "H-mild"]);
1302
+ });
1303
+ });