@oh-story/dsh 0.1.9 → 0.1.11

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 (529) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +209 -73
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +248 -168
  5. package/lib/drama/skills/short-drama/SKILL.md +16 -4
  6. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
  8. package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
  9. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
  10. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  11. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  12. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  13. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
  14. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
  15. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  16. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  17. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  18. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  19. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  20. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  21. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  22. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  23. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  24. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
  25. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  26. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  27. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
  28. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  29. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  30. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  31. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  32. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  33. package/lib/drama/skills/short-drama-edit/SKILL.md +65 -44
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  46. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  47. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  48. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  49. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  50. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +6 -4
  51. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +26 -67
  52. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +31 -56
  53. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +42 -78
  54. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +44 -92
  55. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +14 -16
  56. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1272 -125
  57. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +113 -9
  58. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  59. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  60. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  61. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  62. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  63. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  64. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  65. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  66. package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
  67. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
  68. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
  69. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  70. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
  71. package/lib/drama/skills/short-drama-review/SKILL.md +4 -5
  72. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
  73. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  74. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  75. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  76. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +24 -35
  77. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  78. package/lib/drama/skills/short-drama-storyboard/SKILL.md +23 -11
  79. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  80. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  81. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  82. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +139 -34
  83. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +5 -3
  84. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +15 -12
  85. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +29 -0
  86. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  87. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  88. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +52 -39
  89. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -21
  90. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +9 -23
  91. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +14 -26
  92. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  93. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
  94. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
  95. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -2
  96. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  97. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  98. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  99. package/lib/drama/skills/short-drama-write/SKILL.md +10 -7
  100. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  101. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  102. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  103. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  104. package/lib/index.js +346 -98
  105. package/lib/index.js.map +4 -4
  106. package/lib/novel-to-game/README.md +299 -59
  107. package/lib/novel-to-game/README_ZH.md +299 -56
  108. package/lib/novel-to-game/VERSION +1 -1
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  118. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  119. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  120. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  121. package/lib/novel-to-game/manifest.json +77 -87
  122. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
  123. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  124. package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
  125. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  126. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  127. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  128. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  129. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  130. package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
  131. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  132. package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
  133. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
  134. package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
  135. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  136. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  137. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  138. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
  139. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  140. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
  141. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
  142. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
  143. package/lib/oh-story/manifest.json +607 -327
  144. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  145. package/lib/oh-story/roles/character-designer.md +1 -2
  146. package/lib/oh-story/roles/consistency-checker.md +15 -13
  147. package/lib/oh-story/roles/narrative-writer.md +36 -38
  148. package/lib/oh-story/roles/story-architect.md +9 -61
  149. package/lib/oh-story/roles/story-explorer.md +20 -125
  150. package/lib/oh-story/roles/story-researcher.md +0 -1
  151. package/lib/oh-story/skills/story/SKILL.md +15 -9
  152. package/lib/oh-story/skills/story/VERSION +1 -1
  153. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  154. package/lib/oh-story/skills/story/references/author-memory.md +46 -94
  155. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  156. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  157. package/lib/oh-story/skills/story-deslop/SKILL.md +59 -73
  158. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  159. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +12 -11
  160. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  161. package/lib/oh-story/skills/story-deslop/references/author-memory.md +46 -94
  162. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  163. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +22 -5
  164. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  165. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  166. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  167. package/lib/oh-story/skills/story-import/SKILL.md +64 -584
  168. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  169. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  170. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  171. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  172. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  173. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  174. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  175. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  176. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  177. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +98 -92
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  180. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  181. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +15 -11
  182. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  183. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +132 -27
  184. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +478 -15
  185. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +182 -23
  186. package/lib/oh-story/skills/story-long-analyze/SKILL.md +81 -270
  187. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +252 -0
  188. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +3 -3
  189. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  190. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  191. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  192. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +138 -64
  193. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  194. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  195. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  196. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  197. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  198. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  199. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +55 -128
  200. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +6 -6
  201. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  202. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  203. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  204. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  205. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
  206. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  207. package/lib/oh-story/skills/story-long-scan/SKILL.md +45 -194
  208. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  209. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  210. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  211. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  212. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  213. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  214. package/lib/oh-story/skills/story-long-write/SKILL.md +49 -81
  215. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  216. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +12 -11
  217. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +24 -202
  218. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  219. package/lib/oh-story/skills/story-long-write/references/author-memory.md +46 -94
  220. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  221. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +7 -11
  222. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  223. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  224. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  225. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
  226. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  227. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  228. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  229. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  230. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +7 -15
  231. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  232. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  233. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  234. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  235. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  236. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  237. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  238. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  239. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  240. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  241. package/lib/oh-story/skills/story-long-write/references/project-files.md +19 -13
  242. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +13 -5
  243. package/lib/oh-story/skills/story-long-write/references/reference-index.md +5 -6
  244. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  245. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  246. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  247. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  248. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  249. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +15 -11
  250. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +81 -121
  251. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +41 -139
  252. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  253. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +57 -67
  254. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -253
  255. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  256. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +6 -4
  257. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +19 -65
  258. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  259. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  260. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +226 -25
  261. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  262. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +132 -27
  263. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  264. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +7 -4
  265. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +304 -46
  266. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +478 -15
  267. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +182 -23
  268. package/lib/oh-story/skills/story-review/SKILL.md +31 -302
  269. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  270. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +12 -11
  271. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  272. package/lib/oh-story/skills/story-review/references/author-memory.md +46 -94
  273. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  274. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  275. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  276. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
  277. package/lib/oh-story/skills/story-review/references/review-quality.md +6 -11
  278. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  279. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  280. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  281. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  282. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +15 -11
  283. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  284. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  285. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +478 -15
  286. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +182 -23
  287. package/lib/oh-story/skills/story-setup/SKILL.md +71 -452
  288. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  289. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +12 -11
  290. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  291. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  292. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  293. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +22 -5
  294. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
  295. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  296. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  297. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  298. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  299. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  300. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
  301. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  302. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  303. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  304. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  305. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  306. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  307. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  308. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  309. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  310. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  311. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  312. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  313. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +19 -65
  314. package/lib/oh-story/skills/story-setup/references/diagnostics.md +4 -4
  315. package/lib/oh-story/skills/story-short-analyze/SKILL.md +56 -110
  316. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  317. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  318. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  319. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  320. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  321. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  322. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  323. package/lib/oh-story/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
  324. package/lib/oh-story/skills/story-short-scan/SKILL.md +35 -65
  325. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  326. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  327. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  328. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  329. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -175
  330. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  331. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  332. package/lib/oh-story/skills/story-short-write/references/author-memory.md +46 -94
  333. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  334. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  335. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
  336. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  337. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  338. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  339. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -47
  340. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  341. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  342. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  343. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  344. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  345. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  346. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  347. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  348. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +21 -13
  349. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +116 -0
  350. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +99 -0
  351. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  352. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  353. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  354. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  355. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  356. package/lib/types/client/file-activity.d.ts +19 -1
  357. package/lib/types/client/file-activity.d.ts.map +1 -1
  358. package/lib/types/client/index.d.ts.map +1 -1
  359. package/lib/types/client/production-prompts.d.ts +7 -0
  360. package/lib/types/client/production-prompts.d.ts.map +1 -1
  361. package/lib/types/client/production-runtime.d.ts +16 -0
  362. package/lib/types/client/production-runtime.d.ts.map +1 -1
  363. package/lib/types/client/video-studio.d.ts.map +1 -1
  364. package/lib/types/drama-adapters.d.ts +5 -3
  365. package/lib/types/drama-adapters.d.ts.map +1 -1
  366. package/lib/types/native-hooks.d.ts +53 -1
  367. package/lib/types/native-hooks.d.ts.map +1 -1
  368. package/lib/types/role-identity.d.ts +17 -0
  369. package/lib/types/role-identity.d.ts.map +1 -0
  370. package/lib/types/role-tool.d.ts.map +1 -1
  371. package/lib/types/skill-provider.d.ts.map +1 -1
  372. package/lib/types/video-project.d.ts.map +1 -1
  373. package/lib/types/workspace-route.d.ts.map +1 -1
  374. package/lib/video-recap/CHANGELOG.md +60 -1
  375. package/lib/video-recap/README.en.md +348 -137
  376. package/lib/video-recap/README.md +340 -136
  377. package/lib/video-recap/manifest.json +406 -251
  378. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  379. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  380. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  381. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  382. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  383. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  384. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  385. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  386. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  387. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  388. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  389. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  390. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  391. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  392. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  393. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  394. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  395. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  396. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  397. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  398. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  399. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  400. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  401. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  402. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  403. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  404. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  405. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  406. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  407. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  408. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  409. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  410. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  411. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  412. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  413. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  414. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  415. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  416. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  417. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  418. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  419. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  420. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  421. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  422. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  423. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  424. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  425. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  426. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  427. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  428. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  429. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  430. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  431. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  432. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  433. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  434. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  435. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  436. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  437. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  438. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  439. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  440. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  441. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  442. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  443. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  444. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  445. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  446. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  447. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  448. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  449. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  450. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  451. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  452. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  453. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  454. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  455. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  456. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  457. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  458. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  459. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  460. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  461. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  462. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  463. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  464. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  465. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  466. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  467. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  468. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  469. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  470. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  471. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  472. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  473. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  474. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  475. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  476. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  477. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  478. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  479. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  480. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  481. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  482. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  483. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  484. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  485. package/lib/video-recap/skills/{video-script/scripts/agent_brief.py → video-understanding/scripts/briefing/builder.py} +38 -35
  486. package/lib/video-recap/skills/video-understanding/scripts/{brief_context.py → briefing/context.py} +4 -35
  487. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  488. package/lib/video-recap/skills/{video-script/scripts/brief_timeline.py → video-understanding/scripts/briefing/timeline.py} +5 -6
  489. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  490. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  491. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  492. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  493. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  494. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  495. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  496. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  497. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  498. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  499. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  500. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  501. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  502. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  503. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  504. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  505. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  506. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  507. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  508. package/package.json +45 -45
  509. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
  510. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  511. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
  512. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -530
  513. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -559
  514. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  515. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
  516. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +0 -678
  517. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  518. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  519. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  520. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  521. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  522. package/lib/video-recap/skills/video-script/scripts/brief_context.py +0 -348
  523. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  524. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +0 -399
  525. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  526. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +0 -327
  527. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  528. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  529. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -3,8 +3,12 @@
3
3
 
4
4
  The language model supplies compact semantic transactions. This tool validates
5
5
  and applies them in memory, renders every derived view, and writes the JSON state
6
- last as the commit point. Author memory is workspace-level and deliberately
7
- separate from each book's story-continuity tracking.
6
+ last as the commit point. Author memory lives in two kinds of store: the
7
+ project-level store under the workspace holds global / genre / workflow items
8
+ (`AP` ids); each book keeps its own book-level store under the book directory
9
+ (`BP` ids) so memory travels with the book. Both stay separate from each book's
10
+ story-continuity tracking. When the book root is the workspace itself, the
11
+ book-level store moves into a `书级/` subdirectory so the two never share a file.
8
12
  """
9
13
 
10
14
  from __future__ import annotations
@@ -29,6 +33,28 @@ PROFILE_MAX_BYTES = 12288
29
33
  PENDING_MAX_BYTES = 12288
30
34
  JOURNAL_MAX_BYTES = 24576
31
35
  QUERY_MAX_BYTES = 2048
36
+ ASSERTION_MAX_BYTES = 120 # 新建条目的断言限一句话;解释进 reason(不进 query 载荷)。
37
+ OMITTED_IDS_MAX = 20 # omitted_ids 封顶,omitted 保留真实总数——漏项列表不许把载荷本身挤炸。
38
+ LEGACY_ASSERTION_MAX_BYTES = 768 # 存量条目的读取上限;强化老条目不受新上限约束。
39
+
40
+ # query 的输出是要原样贴进执行 agent prompt 的注入载荷,QUERY_MAX_BYTES
41
+ # 是它在 prompt 里的注意力预算,不该放大。防「作者以为载入了、实际被静默
42
+ # 截断挤掉」靠三层:①新建条目的断言限 ASSERTION_MAX_BYTES,从源头短(强化
43
+ # 已有条目不受限,否则存量长断言再也无法被确认,只会派生重复条目);②写入
44
+ # 端按下列任务组合(与 references/author-memory-maintenance.md 的映射表同包跟版)估算
45
+ # 最坏查询情形——全局条目+各 scope 维度上最重的单一切片(一次查询只带一
46
+ # 个 book/genre/workflow,不同书的条目不会同现;切片按 casefold 归并,与
47
+ # same_scope_value 同一口径,轻重按 compact 字节+列表分隔符算,与真实载荷
48
+ # 同一把尺),装不下时在返回的 warnings 里点名将被略过的条目、指向「整理作
49
+ # 者记忆」,写入本身永不因注入预算失败;③查询按 重要度→本书例外→最近更新
50
+ # 排序装填,被略过的恒是重要度较低的条目,漏下的 ID 按同一优先级顺序报进
51
+ # omitted_ids。
52
+ QUERY_COMBOS: dict[str, tuple[str, ...]] = {
53
+ "正文初稿/续写": ("prose_style", "story_design"),
54
+ "去AI味/改写": ("prose_style",),
55
+ "设定/大纲": ("story_design", "workflow", "interaction"),
56
+ "审稿": ("delivery", "interaction", "prose_style"),
57
+ }
32
58
 
33
59
  KINDS = ("prose_style", "story_design", "workflow", "delivery", "interaction")
34
60
  KIND_TITLES = {
@@ -42,6 +68,11 @@ SCOPE_LEVELS = ("global", "genre", "book", "workflow")
42
68
  STATUSES = ("active", "pending", "conflict", "rejected", "superseded")
43
69
  CONFIDENCE_LEVELS = ("low", "medium", "high")
44
70
  IMPORTANCE_LEVELS = ("low", "medium", "high")
71
+ # 作者记忆只记作者明确表达的偏好。repeated_correction / inferred_pattern 两条
72
+ # 由 agent 主动推断写入的管道已经移除(#436):它们只在攒待确认清单的审阅负
73
+ # 担,不是记忆质量;文档也明说不装全量消息 hook,隐式捕获本就承诺不了完整
74
+ # 性。SOURCES 里保留这两个值只为存量 state 仍能通过校验、仍能 decide/forget,
75
+ # 新写入一律按 WRITE_SOURCES 校验。
45
76
  SOURCES = (
46
77
  "explicit_user",
47
78
  "accepted_suggestion",
@@ -49,8 +80,21 @@ SOURCES = (
49
80
  "inferred_pattern",
50
81
  "manual",
51
82
  )
83
+ WRITE_SOURCES = ("explicit_user", "accepted_suggestion", "manual")
52
84
  RANK = {"low": 0, "medium": 1, "high": 2}
53
85
 
86
+ # 两级 store(#435):项目级存 global/genre/workflow,ID 前缀 AP,位于
87
+ # {工作区}/.story/作者记忆/;书级只存该书的 book 条目,ID 前缀 BP,位于
88
+ # {书}/.story/作者记忆/,书归档、迁移时记忆随书走。ID 前缀就是路由键——
89
+ # decide/forget 看 item_id 前缀,remember/replace 看 scope.level,一份事务
90
+ # 只写一个 store。项目级 store 里升级前写入的存量 book 条目不再参与查询与估算,
91
+ # 用 migrate 搬进书目录后才回来——不做双读,双读会让迁移永远没人做。
92
+ STORE_PREFIX = {"project": "AP", "book": "BP"}
93
+ ID_PREFIXES = tuple(STORE_PREFIX.values())
94
+ # 单书布局(书根就是工作区)下书级 store 的子目录:两级 store 的默认落点在这种
95
+ # 布局里是同一个 state 文件,书级改住 {工作区}/.story/作者记忆/书级/。
96
+ SINGLE_ROOT_BOOK_DIR = "书级"
97
+
54
98
 
55
99
  class AuthorMemoryError(ValueError):
56
100
  """Expected validation or state error."""
@@ -101,13 +145,31 @@ def choice(value: object, allowed: tuple[str, ...], label: str) -> str:
101
145
  return value
102
146
 
103
147
 
148
+ def is_item_id(value: object) -> bool:
149
+ return (
150
+ isinstance(value, str)
151
+ and len(value) >= 3
152
+ and value[:2] in ID_PREFIXES
153
+ and value[2:].isdecimal()
154
+ and int(value[2:]) >= 1
155
+ )
156
+
157
+
158
+ def id_number(item_id: str) -> int:
159
+ return int(item_id[2:])
160
+
161
+
162
+ def id_store(item_id: str) -> str:
163
+ return "book" if item_id.startswith(STORE_PREFIX["book"]) else "project"
164
+
165
+
104
166
  def clean_id_list(value: object, label: str, *, maximum: int = 32) -> list[str]:
105
167
  raw = as_list(value, label)
106
168
  require(len(raw) <= maximum, f"{label} may contain at most {maximum} items")
107
169
  result: list[str] = []
108
170
  for index, item in enumerate(raw):
109
171
  item_id = clean_text(item, f"{label}[{index}]", max_bytes=32)
110
- require(item_id.startswith("AP") and item_id[2:].isdigit() and int(item_id[2:]) >= 1, f"{label}[{index}] is not an author-memory id")
172
+ require(is_item_id(item_id), f"{label}[{index}] is not an author-memory id")
111
173
  if item_id not in result:
112
174
  result.append(item_id)
113
175
  return result
@@ -158,16 +220,182 @@ def write_if_changed(path: Path, payload: str) -> None:
158
220
  atomic_write_text(path, payload)
159
221
 
160
222
 
161
- def memory_root(workspace: Path) -> Path:
162
- return workspace.resolve() / ".story" / "作者记忆"
223
+ # ---------------------------------------------------------------------------
224
+ # Stores
225
+ # ---------------------------------------------------------------------------
163
226
 
164
227
 
165
- def state_path(workspace: Path) -> Path:
166
- return memory_root(workspace) / "_author-memory-state.json"
228
+ class Store:
229
+ """一个 state 文件的落点:项目级(工作区)或书级(书目录)。"""
167
230
 
231
+ __slots__ = ("kind", "root", "book")
168
232
 
169
- def empty_state() -> dict[str, Any]:
170
- return {
233
+ def __init__(self, kind: str, root: Path, book: str | None) -> None:
234
+ self.kind = kind
235
+ self.root = root
236
+ self.book = book
237
+
238
+ @property
239
+ def prefix(self) -> str:
240
+ return STORE_PREFIX[self.kind]
241
+
242
+ @property
243
+ def state_path(self) -> Path:
244
+ return self.root / "_author-memory-state.json"
245
+
246
+ @property
247
+ def label(self) -> str:
248
+ return "项目级" if self.kind == "project" else f"书级({self.book})"
249
+
250
+
251
+ def project_store(workspace: Path) -> Store:
252
+ return Store("project", workspace.resolve() / ".story" / "作者记忆", None)
253
+
254
+
255
+ def peek_book_name(state_path: Path) -> str | None:
256
+ """不校验整份 state,只取书名——书级 store 一旦建立,书名以 state 为准,
257
+ 书目录改名不影响。"""
258
+ if not state_path.exists():
259
+ return None
260
+ document = read_json(state_path)
261
+ name = document.get("book") if isinstance(document, dict) else None
262
+ return name if isinstance(name, str) and name.strip() else None
263
+
264
+
265
+ def enclosing_workspace(workspace: Path) -> Path | None:
266
+ """--workspace 指到的其实是多书工作区里的一本书时,返回真正的创作工作区。
267
+
268
+ 认三种迹象,任一成立即是:①工作区的上一层目录叫 长篇 / 短篇(story-setup 的书
269
+ 目录约定,返回再上一层);②某个祖先目录有 .active-book;③某个祖先目录有项目级
270
+ store(不带 book 字段的 state)——但工作区自己已有项目级 store 时不认这一条,最
271
+ 近的项目级 store 就是它自己,免得主目录里一份误建的 store 挡住正常的单书工作区。
272
+ 读不出来的祖先 state 不算迹象。"""
273
+ resolved = workspace.resolve()
274
+ if resolved.parent.name in {"长篇", "短篇"}:
275
+ return resolved.parent.parent
276
+ own = project_store(resolved).state_path
277
+ owns_project_store = own.exists() and peek_book_name(own) is None
278
+ for ancestor in resolved.parents:
279
+ if (ancestor / ".active-book").exists():
280
+ return ancestor
281
+ if owns_project_store:
282
+ continue
283
+ state_path = project_store(ancestor).state_path
284
+ try:
285
+ document = read_json(state_path) if state_path.is_file() else None
286
+ except AuthorMemoryError:
287
+ document = None
288
+ if isinstance(document, dict) and "book" not in document:
289
+ return ancestor
290
+ return None
291
+
292
+
293
+ def same_directory(first: Path, second: Path) -> bool:
294
+ # samefile 认得大小写不敏感文件系统(macOS APFS 默认)上只差大小写的同一目录。
295
+ if first.exists() and second.exists():
296
+ return os.path.samefile(first, second)
297
+ return first.resolve() == second.resolve()
298
+
299
+
300
+ def is_single_root(workspace: Path, book_root: Path | None) -> bool:
301
+ """单书布局:书根就是工作区(正文/、大纲/、追踪/ 直接在工作区根)。
302
+
303
+ 同一目录还可能是多书工作区里的一本书被误传成了 --workspace(书目录自己也含
304
+ .story/作者记忆/)。那时按单书布局处理会把书级 state 挪进 书级/、在原处补一份
305
+ 空的项目级 state,此后正确的调用全部失败,所以一律报错、零写入。"""
306
+ if book_root is None or not same_directory(book_root, workspace):
307
+ return False
308
+ outer = enclosing_workspace(workspace)
309
+ require(
310
+ outer is None,
311
+ f"{workspace} 是创作工作区 {outer} 里的一本书,不是单书工作区:--workspace 应传 {outer},"
312
+ f"这个目录放 --book-root",
313
+ )
314
+ return True
315
+
316
+
317
+ def book_memory_root(workspace: Path, book_root: Path) -> Path:
318
+ root = book_root.resolve() / ".story" / "作者记忆"
319
+ # 单书布局下两级 store 的默认落点是同一个 state 文件;书级改住子目录,
320
+ # 两份 state、两套派生视图、两条修订线仍各自独立。
321
+ return root / SINGLE_ROOT_BOOK_DIR if is_single_root(workspace, book_root) else root
322
+
323
+
324
+ def sole_legacy_book_name(project: Store) -> str | None:
325
+ """项目级 store 里存量 book 条目只指向一本书时返回该书名(单书布局的升级默认)。"""
326
+ if not project.state_path.exists():
327
+ return None
328
+ document = read_json(project.state_path)
329
+ items = document.get("items") if isinstance(document, dict) else None
330
+ names: dict[str, str] = {}
331
+ for item in (items.values() if isinstance(items, dict) else ()):
332
+ scope = item.get("scope") if isinstance(item, dict) else None
333
+ if (
334
+ isinstance(scope, dict) and scope.get("level") == "book"
335
+ and isinstance(scope.get("value"), str) and scope["value"].strip()
336
+ and item.get("status") in {"active", "pending", "conflict"}
337
+ ):
338
+ names.setdefault(scope["value"].casefold(), scope["value"])
339
+ return next(iter(names.values())) if len(names) == 1 else None
340
+
341
+
342
+ def book_store(workspace: Path, book_root: Path, book: str | None) -> Store:
343
+ require(book_root.exists() and book_root.is_dir(), f"book root does not exist: {book_root}")
344
+ resolved = book_root.resolve()
345
+ root = book_memory_root(workspace, book_root)
346
+ name = optional_text(book, "book", max_bytes=180)
347
+ stored = peek_book_name(root / "_author-memory-state.json")
348
+ if name is None:
349
+ name = stored
350
+ elif stored is not None:
351
+ require(
352
+ name.casefold() == stored.casefold(),
353
+ f"--book「{name}」与 {root} 里记录的书「{stored}」不一致",
354
+ )
355
+ if name is None and is_single_root(workspace, book_root):
356
+ # 单书工作区的目录名常常不是书名;升级前的本书条目只指向一本书时,以它为准,
357
+ # 否则 migrate 会按目录名找不到存量、静默迁移零条。
358
+ name = sole_legacy_book_name(project_store(workspace))
359
+ if name is None:
360
+ name = clean_text(resolved.name, "book root name", max_bytes=180)
361
+ return Store("book", root, name)
362
+
363
+
364
+ def relocate_misplaced_book_state(workspace: Path, book_root: Path | None) -> None:
365
+ """单书布局的自愈:旧版把书级 state(带 state.book)写在了项目级位置,此后项目级
366
+ 读写一律失败。带 --book-root {工作区} 运行任一命令时,把它原子移进书级子目录,
367
+ 再在项目级位置补一份空 state 重建视图。state 内容不变,不推进任何修订。"""
368
+ if not is_single_root(workspace, book_root):
369
+ return
370
+ project = project_store(workspace)
371
+ misplaced = peek_book_name(project.state_path)
372
+ if misplaced is None:
373
+ return
374
+ target = Store("book", book_memory_root(workspace, book_root), misplaced)
375
+ require(
376
+ not target.state_path.exists(),
377
+ f"{project.state_path} 与 {target.state_path} 都是书级 state,无法自动归位;"
378
+ f"保留修订较新的一份移到 {target.state_path},另一份备份后移走再重跑",
379
+ )
380
+ state = validate_state(read_json(project.state_path), store=target)
381
+ target.root.mkdir(parents=True, exist_ok=True)
382
+ os.replace(project.state_path, target.state_path)
383
+ write_snapshot(target, state)
384
+ write_snapshot(project, empty_state())
385
+
386
+
387
+ def resolve_target_store(kind: str, workspace: Path, book_root: Path | None, book: str | None) -> Store:
388
+ if kind == "project":
389
+ return project_store(workspace)
390
+ require(
391
+ book_root is not None,
392
+ "book 级条目须传 --book-root {书目录}——记忆随书存放在 {书}/.story/作者记忆/,不再写进工作区的项目级 store",
393
+ )
394
+ return book_store(workspace, book_root, book)
395
+
396
+
397
+ def empty_state(book: str | None = None) -> dict[str, Any]:
398
+ state: dict[str, Any] = {
171
399
  "schema_version": STATE_SCHEMA_VERSION,
172
400
  "state_revision": 0,
173
401
  "next_item_number": 1,
@@ -175,6 +403,14 @@ def empty_state() -> dict[str, Any]:
175
403
  "journal": [],
176
404
  "applied_transactions": {},
177
405
  }
406
+ if book is not None:
407
+ state["book"] = book
408
+ return state
409
+
410
+
411
+ # ---------------------------------------------------------------------------
412
+ # Validation
413
+ # ---------------------------------------------------------------------------
178
414
 
179
415
 
180
416
  def normalize_scope(value: object, label: str) -> dict[str, str | None]:
@@ -208,19 +444,19 @@ def normalize_item(value: object, label: str) -> dict[str, Any]:
208
444
  }
209
445
  require_known_keys(item, allowed, label)
210
446
  item_id = clean_text(item.get("id"), f"{label}.id", max_bytes=32)
211
- require(item_id.startswith("AP") and item_id[2:].isdigit() and int(item_id[2:]) >= 1, f"{label}.id is invalid")
447
+ require(is_item_id(item_id), f"{label}.id is invalid")
212
448
  evidence = [normalize_evidence(entry, f"{label}.evidence[{index}]") for index, entry in enumerate(as_list(item.get("evidence"), f"{label}.evidence"))]
213
449
  require(bool(evidence), f"{label}.evidence must not be empty")
214
450
  status = choice(item.get("status"), STATUSES, f"{label}.status")
215
451
  conflicts = clean_id_list(item.get("conflicts_with"), f"{label}.conflicts_with")
216
452
  superseded_by = optional_text(item.get("superseded_by"), f"{label}.superseded_by", max_bytes=32)
217
453
  if superseded_by is not None:
218
- require(superseded_by.startswith("AP") and superseded_by[2:].isdigit(), f"{label}.superseded_by is invalid")
454
+ require(is_item_id(superseded_by), f"{label}.superseded_by is invalid")
219
455
  return {
220
456
  "id": item_id,
221
457
  "kind": choice(item.get("kind"), KINDS, f"{label}.kind"),
222
458
  "scope": normalize_scope(item.get("scope"), f"{label}.scope"),
223
- "assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=768),
459
+ "assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=LEGACY_ASSERTION_MAX_BYTES),
224
460
  "confidence": choice(item.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
225
461
  "importance": choice(item.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
226
462
  "status": status,
@@ -235,11 +471,28 @@ def normalize_item(value: object, label: str) -> dict[str, Any]:
235
471
  }
236
472
 
237
473
 
238
- def validate_state(value: object) -> dict[str, Any]:
474
+ def scope_fits_store(scope: dict[str, str | None], book: str | None) -> bool:
475
+ if book is None:
476
+ return True # 项目级:存量 book 条目仍合法,只为老库能通过校验并被 migrate 搬走
477
+ return scope["level"] == "book" and (scope["value"] or "").casefold() == book.casefold()
478
+
479
+
480
+ def validate_state(value: object, *, store: Store) -> dict[str, Any]:
239
481
  state = as_mapping(value, "state")
240
- allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions"}
482
+ allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions", "book"}
241
483
  require_known_keys(state, allowed, "state")
242
484
  require(state.get("schema_version") == STATE_SCHEMA_VERSION, f"state.schema_version must be {STATE_SCHEMA_VERSION}")
485
+ if store.kind == "book":
486
+ book = clean_text(state.get("book"), "state.book", max_bytes=180)
487
+ require(store.book is not None and book.casefold() == store.book.casefold(), f"state.book「{book}」与目标书「{store.book}」不一致")
488
+ else:
489
+ require(
490
+ "book" not in state,
491
+ "project-level state must not carry state.book:这份其实是书级 state。--workspace 指到了"
492
+ "长篇/、短篇/ 下的书目录时,改传创作工作区根、书目录放 --book-root;只有书根就是工作区"
493
+ "(单书布局)时,带 --book-root {工作区} 重跑任一命令即自动移进 .story/作者记忆/书级/",
494
+ )
495
+ book = None
243
496
  revision = as_int(state.get("state_revision"), "state.state_revision")
244
497
  next_number = as_int(state.get("next_item_number"), "state.next_item_number", minimum=1)
245
498
  raw_items = as_mapping(state.get("items"), "state.items")
@@ -248,7 +501,9 @@ def validate_state(value: object) -> dict[str, Any]:
248
501
  for raw_id, raw_item in raw_items.items():
249
502
  normalized = normalize_item(raw_item, f"state.items.{raw_id}")
250
503
  require(raw_id == normalized["id"], f"state.items key {raw_id} does not match item id")
251
- max_number = max(max_number, int(raw_id[2:]))
504
+ require(raw_id.startswith(store.prefix), f"state.items.{raw_id} does not belong to the {store.label} store(前缀应为 {store.prefix})")
505
+ require(scope_fits_store(normalized["scope"], book), f"state.items.{raw_id} scope does not belong to book「{book}」")
506
+ max_number = max(max_number, id_number(raw_id))
252
507
  require(normalized["created_revision"] <= normalized["updated_revision"] <= revision, f"state.items.{raw_id} revision is ahead of state")
253
508
  items[raw_id] = normalized
254
509
  require(next_number > max_number, "state.next_item_number must be greater than every allocated item id")
@@ -295,7 +550,7 @@ def validate_state(value: object) -> dict[str, Any]:
295
550
  item_ids = clean_id_list(mapping.get("item_ids"), f"state.applied_transactions.{transaction_id}.item_ids")
296
551
  require(bool(item_ids), f"state.applied_transactions.{transaction_id}.item_ids must not be empty")
297
552
  require(all(item_id in items for item_id in item_ids), f"state.applied_transactions.{transaction_id}.item_ids references an unknown item")
298
- return {
553
+ result = {
299
554
  "schema_version": STATE_SCHEMA_VERSION,
300
555
  "state_revision": revision,
301
556
  "next_item_number": next_number,
@@ -303,6 +558,15 @@ def validate_state(value: object) -> dict[str, Any]:
303
558
  "journal": copy.deepcopy(journal),
304
559
  "applied_transactions": copy.deepcopy(transactions),
305
560
  }
561
+ if book is not None:
562
+ result["book"] = book
563
+ return result
564
+
565
+
566
+ def load_state(store: Store) -> dict[str, Any] | None:
567
+ if not store.state_path.exists():
568
+ return None
569
+ return validate_state(read_json(store.state_path), store=store)
306
570
 
307
571
 
308
572
  def normalize_preference(value: object, label: str, *, allow_status: bool) -> dict[str, Any]:
@@ -311,12 +575,17 @@ def normalize_preference(value: object, label: str, *, allow_status: bool) -> di
311
575
  if allow_status:
312
576
  allowed |= {"status", "conflicts_with"}
313
577
  require_known_keys(preference, allowed, label)
314
- source = choice(preference.get("source"), SOURCES, f"{label}.source")
578
+ raw_source = preference.get("source")
579
+ require(
580
+ raw_source not in {"repeated_correction", "inferred_pattern"},
581
+ f"{label}.source「{raw_source}」已不再写入:作者记忆只记作者明确表达的偏好,"
582
+ f"不从重复修改或成稿轨迹推断;范围含糊的原话用 explicit_user 并置 status=pending",
583
+ )
584
+ source = choice(raw_source, WRITE_SOURCES, f"{label}.source")
315
585
  status = choice(preference.get("status"), ("active", "pending", "conflict"), f"{label}.status") if allow_status else "active"
316
586
  conflicts = clean_id_list(preference.get("conflicts_with", []), f"{label}.conflicts_with") if allow_status else []
317
587
  if status == "active":
318
588
  require(not conflicts, f"{label}.conflicts_with must be empty for active status")
319
- require(source not in {"repeated_correction", "inferred_pattern"}, f"{label} inferred evidence must remain pending")
320
589
  elif status == "conflict":
321
590
  require(bool(conflicts), f"{label}.conflicts_with is required for conflict status")
322
591
  else:
@@ -324,7 +593,9 @@ def normalize_preference(value: object, label: str, *, allow_status: bool) -> di
324
593
  return {
325
594
  "kind": choice(preference.get("kind"), KINDS, f"{label}.kind"),
326
595
  "scope": normalize_scope(preference.get("scope"), f"{label}.scope"),
327
- "assertion": clean_text(preference.get("assertion"), f"{label}.assertion", max_bytes=768),
596
+ # 这里按存量上限收;ASSERTION_MAX_BYTES 只在真正新建条目时校验
597
+ # (require_new_item_assertion),好让存量长断言仍能被强化。
598
+ "assertion": clean_text(preference.get("assertion"), f"{label}.assertion", max_bytes=LEGACY_ASSERTION_MAX_BYTES),
328
599
  "quote": clean_text(preference.get("quote"), f"{label}.quote", max_bytes=768),
329
600
  "source_ref": optional_text(preference.get("source_ref"), f"{label}.source_ref", max_bytes=240),
330
601
  "source": source,
@@ -397,25 +668,115 @@ def normalize_record_event(value: object) -> dict[str, Any]:
397
668
  return {"event_id": event_id, "operation": normalized["operations"][0]}
398
669
 
399
670
 
671
+ # ---------------------------------------------------------------------------
672
+ # Routing: which store does an operation belong to?
673
+ # ---------------------------------------------------------------------------
674
+
675
+
676
+ def scope_store_kind(scope: dict[str, str | None]) -> str:
677
+ return "book" if scope["level"] == "book" else "project"
678
+
679
+
680
+ def store_kind_label(kind: str) -> str:
681
+ return "书级(BP)" if kind == "book" else "项目级(AP)"
682
+
683
+
684
+ def operation_store_kind(operation: dict[str, Any], label: str) -> str:
685
+ action = operation["action"]
686
+ if action in {"decide", "forget"}:
687
+ return id_store(operation["item_id"])
688
+ preference = operation["preference"]
689
+ kind = scope_store_kind(preference["scope"])
690
+ if action == "remember":
691
+ for conflict_id in preference["conflicts_with"]:
692
+ require(
693
+ id_store(conflict_id) == kind,
694
+ f"{label}.conflicts_with 只能引用同一 store 的条目({conflict_id} 在{store_kind_label(id_store(conflict_id))}):"
695
+ f"本书例外不算与全局规则冲突,直接 remember 为 book 条目即可",
696
+ )
697
+ return kind
698
+ for old_id in operation["old_ids"]:
699
+ require(
700
+ id_store(old_id) == kind,
701
+ f"{label} replace 不能跨 store:{old_id} 在{store_kind_label(id_store(old_id))},新条目范围属于{store_kind_label(kind)};"
702
+ f"跨 store 改版拆成 forget+remember,存量 book 条目先 migrate",
703
+ )
704
+ return kind
705
+
706
+
707
+ def transaction_store_kind(transaction: dict[str, Any]) -> str:
708
+ kinds = {
709
+ operation_store_kind(operation, f"transaction.operations[{index}]")
710
+ for index, operation in enumerate(transaction["operations"])
711
+ }
712
+ require(
713
+ len(kinds) == 1,
714
+ "一份事务只能写一个 store:项目级(global/genre/workflow 范围或 AP 编号)与书级(book 范围或 BP 编号)的操作要分开提交",
715
+ )
716
+ return kinds.pop()
717
+
718
+
719
+ # ---------------------------------------------------------------------------
720
+ # Transactions
721
+ # ---------------------------------------------------------------------------
722
+
723
+
400
724
  def transaction_digest(transaction: dict[str, Any]) -> str:
401
725
  canonical = json.dumps(transaction, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
402
726
  return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
403
727
 
404
728
 
405
729
  def fingerprint(preference: dict[str, Any]) -> str:
730
+ """同一条偏好的身份:kind+scope+断言,scope.value 与 same_scope_value 同样按
731
+ casefold 比——否则「Urban」「urban」会各建一条,两条同断言一起挤进 prompt。"""
732
+ scope = preference["scope"]
406
733
  value = {
407
734
  "kind": preference["kind"],
408
- "scope": preference["scope"],
735
+ "scope": {"level": scope["level"], "value": None if scope["value"] is None else scope["value"].casefold()},
409
736
  "assertion": preference["assertion"].casefold(),
410
737
  }
411
738
  return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
412
739
 
413
740
 
414
- def allocate_item(state: dict[str, Any], preference: dict[str, Any], revision: int) -> dict[str, Any]:
415
- item_id = f"AP{state['next_item_number']:03d}"
741
+ def require_new_item_assertion(preference: dict[str, Any]) -> None:
742
+ """新建条目的断言限一句话。强化已有条目走不到这里——存量长断言必须还能
743
+ 被确认,否则作者重申老偏好只会派生一条重复条目,库反而更挤。"""
744
+ size = len(preference["assertion"].encode("utf-8"))
745
+ require(
746
+ size <= ASSERTION_MAX_BYTES,
747
+ f"新条目的 assertion {size} 字节,超出 {ASSERTION_MAX_BYTES} 字节上限——"
748
+ f"断言限一句话,需要解释的背景写进 reason。若这是对已有条目的重申,"
749
+ f"原样使用该条目的 assertion 即可强化(不受本上限约束);"
750
+ f"若确实是几条互不依赖的偏好,才拆成几条分别记录。",
751
+ )
752
+
753
+
754
+ def require_scope_fits_store(state: dict[str, Any], scope: dict[str, str | None]) -> None:
755
+ book = state.get("book")
756
+ if book is None:
757
+ require(
758
+ scope["level"] != "book",
759
+ "book 级条目须传 --book-root {书目录}——记忆随书存放在 {书}/.story/作者记忆/,不再写进工作区的项目级 store",
760
+ )
761
+ else:
762
+ require(
763
+ scope_fits_store(scope, book),
764
+ f"book 条目的范围「{scope['value']}」与本书 store「{book}」不一致——只有这本书的条目才住在这个书目录",
765
+ )
766
+
767
+
768
+ def next_item_id(state: dict[str, Any]) -> str:
769
+ prefix = STORE_PREFIX["book"] if state.get("book") is not None else STORE_PREFIX["project"]
770
+ item_id = f"{prefix}{state['next_item_number']:03d}"
416
771
  state["next_item_number"] += 1
772
+ return item_id
773
+
774
+
775
+ def allocate_item(state: dict[str, Any], preference: dict[str, Any], revision: int) -> dict[str, Any]:
776
+ require_new_item_assertion(preference)
777
+ require_scope_fits_store(state, preference["scope"])
417
778
  return {
418
- "id": item_id,
779
+ "id": next_item_id(state),
419
780
  "kind": preference["kind"],
420
781
  "scope": copy.deepcopy(preference["scope"]),
421
782
  "assertion": preference["assertion"],
@@ -449,6 +810,7 @@ def require_item(state: dict[str, Any], item_id: str, label: str) -> dict[str, A
449
810
 
450
811
 
451
812
  def apply_remember(state: dict[str, Any], preference: dict[str, Any], revision: int) -> str:
813
+ require_scope_fits_store(state, preference["scope"])
452
814
  for conflict_id in preference["conflicts_with"]:
453
815
  conflict = require_item(state, conflict_id, "remember")
454
816
  require(conflict["status"] == "active", f"remember conflict {conflict_id} must be active")
@@ -493,6 +855,23 @@ def apply_decide(state: dict[str, Any], operation: dict[str, Any], revision: int
493
855
  return f"{verb} {item['id']}:{item['assertion']}"
494
856
 
495
857
 
858
+ def release_conflicts(state: dict[str, Any], removed_ids: set[str], revision: int) -> int:
859
+ """被撤下的 active 条目不再是任何候选的冲突对象;冲突对象清空的候选退回 pending。"""
860
+ released = 0
861
+ for candidate in state["items"].values():
862
+ if candidate["status"] != "conflict":
863
+ continue
864
+ retained = [item_id for item_id in candidate["conflicts_with"] if item_id not in removed_ids]
865
+ if retained == candidate["conflicts_with"]:
866
+ continue
867
+ candidate["conflicts_with"] = retained
868
+ candidate["updated_revision"] = revision
869
+ if not retained:
870
+ candidate["status"] = "pending"
871
+ released += 1
872
+ return released
873
+
874
+
496
875
  def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
497
876
  old_items = [require_item(state, item_id, "replace") for item_id in operation["old_ids"]]
498
877
  for item in old_items:
@@ -505,19 +884,7 @@ def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: in
505
884
  item["status"] = "superseded"
506
885
  item["superseded_by"] = replacement["id"]
507
886
  item["updated_revision"] = revision
508
- old_ids = {item["id"] for item in old_items}
509
- released = 0
510
- for candidate in state["items"].values():
511
- if candidate["status"] != "conflict":
512
- continue
513
- retained = [item_id for item_id in candidate["conflicts_with"] if item_id not in old_ids]
514
- if retained == candidate["conflicts_with"]:
515
- continue
516
- candidate["conflicts_with"] = retained
517
- candidate["updated_revision"] = revision
518
- if not retained:
519
- candidate["status"] = "pending"
520
- released += 1
887
+ released = release_conflicts(state, {item["id"] for item in old_items}, revision)
521
888
  replaced = ", ".join(item["id"] for item in old_items)
522
889
  suffix = f";{released} 个其他冲突候选退回待确认" if released else ""
523
890
  return f"用 {replacement['id']} 替代 {replaced}:{replacement['assertion']}{suffix}"
@@ -531,20 +898,46 @@ def apply_forget(state: dict[str, Any], operation: dict[str, Any], revision: int
531
898
  item["reason"] = operation["reason"]
532
899
  item["updated_revision"] = revision
533
900
  add_evidence(item, operation["quote"], None)
534
- released = 0
535
- for candidate in state["items"].values():
536
- if candidate["status"] != "conflict" or item["id"] not in candidate["conflicts_with"]:
537
- continue
538
- candidate["conflicts_with"] = [conflict_id for conflict_id in candidate["conflicts_with"] if conflict_id != item["id"]]
539
- candidate["updated_revision"] = revision
540
- if not candidate["conflicts_with"]:
541
- candidate["status"] = "pending"
542
- released += 1
901
+ released = release_conflicts(state, {item["id"]}, revision)
543
902
  suffix = f";{released} 个冲突候选退回待确认" if released else ""
544
903
  return f"忘记 {item['id']}:{item['assertion']}{suffix}"
545
904
 
546
905
 
547
- def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest: str) -> tuple[dict[str, Any], list[str]]:
906
+ def committed_now() -> str:
907
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
908
+
909
+
910
+ def seal_transaction(
911
+ updated: dict[str, Any],
912
+ transaction_id: str,
913
+ digest: str,
914
+ summaries: list[str],
915
+ *,
916
+ store: Store,
917
+ ) -> dict[str, Any]:
918
+ """把本次修订写进 journal / applied_transactions,并整份校验。"""
919
+ revision = updated["state_revision"] + 1
920
+ updated["state_revision"] = revision
921
+ updated["journal"].append({
922
+ "revision": revision,
923
+ "transaction_id": transaction_id,
924
+ "committed_at": committed_now(),
925
+ "summaries": summaries,
926
+ })
927
+ item_ids = sorted(
928
+ (item_id for item_id, item in updated["items"].items() if item["updated_revision"] == revision),
929
+ key=id_number,
930
+ )
931
+ require(bool(item_ids), "transaction did not update any author-memory item")
932
+ updated["applied_transactions"][transaction_id] = {
933
+ "revision": revision,
934
+ "digest": digest,
935
+ "item_ids": item_ids,
936
+ }
937
+ return validate_state(updated, store=store)
938
+
939
+
940
+ def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest: str, *, store: Store) -> tuple[dict[str, Any], list[str]]:
548
941
  applied = state["applied_transactions"].get(transaction["transaction_id"])
549
942
  if applied is not None:
550
943
  require(applied["digest"] == digest, "transaction_id was already used with different content")
@@ -562,25 +955,12 @@ def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest
562
955
  summaries.append(apply_replace(updated, operation, revision))
563
956
  else:
564
957
  summaries.append(apply_forget(updated, operation, revision))
565
- committed_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
566
- updated["state_revision"] = revision
567
- updated["journal"].append({
568
- "revision": revision,
569
- "transaction_id": transaction["transaction_id"],
570
- "committed_at": committed_at,
571
- "summaries": summaries,
572
- })
573
- item_ids = sorted(
574
- (item_id for item_id, item in updated["items"].items() if item["updated_revision"] == revision),
575
- key=lambda item_id: int(item_id[2:]),
576
- )
577
- require(bool(item_ids), "transaction did not update any author-memory item")
578
- updated["applied_transactions"][transaction["transaction_id"]] = {
579
- "revision": revision,
580
- "digest": digest,
581
- "item_ids": item_ids,
582
- }
583
- return validate_state(updated), summaries
958
+ return seal_transaction(updated, transaction["transaction_id"], digest, summaries, store=store), summaries
959
+
960
+
961
+ # ---------------------------------------------------------------------------
962
+ # Derived views
963
+ # ---------------------------------------------------------------------------
584
964
 
585
965
 
586
966
  def scope_label(scope: dict[str, str | None]) -> str:
@@ -590,24 +970,36 @@ def scope_label(scope: dict[str, str | None]) -> str:
590
970
  return f"{labels[scope['level']]}:{scope['value']}"
591
971
 
592
972
 
973
+ def store_caption(state: dict[str, Any]) -> str:
974
+ book = state.get("book")
975
+ if book is None:
976
+ return "项目级记忆(全局、题材、流程);各书的书级记忆住在各自书目录的 .story/作者记忆/。"
977
+ return f"本书「{book}」的书级记忆;全局、题材、流程记忆住在工作区的 .story/作者记忆/。"
978
+
979
+
593
980
  def render_profile(state: dict[str, Any]) -> str:
594
981
  lines = [
595
982
  "# 作者画像",
596
983
  "",
597
984
  "<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
598
985
  "",
599
- f"> 状态修订:{state['state_revision']}。仅列出已确认偏好;当前明确要求、本书设定与硬性门禁优先。",
986
+ f"> 状态修订:{state['state_revision']}。{store_caption(state)}仅列出已确认偏好;当前明确要求、本书设定与硬性门禁优先。",
600
987
  "",
601
988
  ]
602
989
  active = [item for item in state["items"].values() if item["status"] == "active"]
603
990
  for kind in KINDS:
604
991
  lines.extend([f"## {KIND_TITLES[kind]}", ""])
605
- items = sorted((item for item in active if item["kind"] == kind), key=lambda item: int(item["id"][2:]))
992
+ items = sorted((item for item in active if item["kind"] == kind), key=lambda item: id_number(item["id"]))
606
993
  if not items:
607
994
  lines.extend(["- 暂无", ""])
608
995
  continue
609
996
  for item in items:
610
- lines.append(f"- **{item['id']}**〔{scope_label(item['scope'])}|{item['confidence']}|确认 {item['confirmation_count']} 次〕{item['assertion']}")
997
+ # 必须显示 importance:它决定超编时谁留在 prompt 里,而「整理作者
998
+ # 记忆」只以本文件为输入——不显示就无从判断该退役哪条。
999
+ lines.append(
1000
+ f"- **{item['id']}**〔{scope_label(item['scope'])}|重要 {item['importance']}"
1001
+ f"|把握 {item['confidence']}|确认 {item['confirmation_count']} 次〕{item['assertion']}"
1002
+ )
611
1003
  lines.append("")
612
1004
  return "\n".join(lines).rstrip() + "\n"
613
1005
 
@@ -618,10 +1010,10 @@ def render_pending(state: dict[str, Any]) -> str:
618
1010
  "",
619
1011
  "<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
620
1012
  "",
621
- f"> 状态修订:{state['state_revision']}。待确认项不参与创作约束,也不应打断当前任务。",
1013
+ f"> 状态修订:{state['state_revision']}。{store_caption(state)}待确认项不参与创作约束,也不应打断当前任务。",
622
1014
  "",
623
1015
  ]
624
- items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item: int(item["id"][2:]))
1016
+ items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item: id_number(item["id"]))
625
1017
  if not items:
626
1018
  lines.extend(["暂无待确认项。", ""])
627
1019
  for item in items:
@@ -630,7 +1022,7 @@ def render_pending(state: dict[str, Any]) -> str:
630
1022
  "",
631
1023
  f"- 候选习惯:{item['assertion']}",
632
1024
  f"- 范围:{scope_label(item['scope'])}",
633
- f"- 原话:\u201c{item['evidence'][-1]['quote']}\u201d",
1025
+ f"- 原话:“{item['evidence'][-1]['quote']}”",
634
1026
  f"- 依据:{item['reason']}",
635
1027
  f"- 置信度 / 重要度:{item['confidence']} / {item['importance']}",
636
1028
  ])
@@ -646,6 +1038,8 @@ def render_journal(state: dict[str, Any]) -> str:
646
1038
  "",
647
1039
  "<!-- 由 author_memory_commit.py 生成,请勿手改;最近记录在前。 -->",
648
1040
  "",
1041
+ f"> {store_caption(state)}",
1042
+ "",
649
1043
  ]
650
1044
  if not state["journal"]:
651
1045
  lines.extend(["暂无变更。", ""])
@@ -668,40 +1062,295 @@ def render_views(state: dict[str, Any]) -> dict[str, str]:
668
1062
  return views
669
1063
 
670
1064
 
671
- def write_snapshot(workspace: Path, state: dict[str, Any]) -> None:
672
- root = memory_root(workspace)
1065
+ def write_snapshot(store: Store, state: dict[str, Any]) -> None:
673
1066
  views = render_views(state)
674
1067
  state_payload = json_payload(state)
675
1068
  require(len(state_payload.encode("utf-8")) <= STATE_MAX_BYTES, f"_author-memory-state.json exceeds {STATE_MAX_BYTES} bytes")
676
1069
  for name, payload in views.items():
677
- write_if_changed(root / name, payload)
1070
+ write_if_changed(store.root / name, payload)
678
1071
  # State is the authority and therefore the last commit point.
679
- write_if_changed(state_path(workspace), state_payload)
1072
+ write_if_changed(store.state_path, state_payload)
1073
+
1074
+
1075
+ # ---------------------------------------------------------------------------
1076
+ # Query & budget
1077
+ # ---------------------------------------------------------------------------
1078
+
1079
+
1080
+ def same_scope_value(item_value: str | None, requested: str | None) -> bool:
1081
+ return requested is not None and item_value is not None and item_value.casefold() == requested.casefold()
1082
+
1083
+
1084
+ def summarize_assertion(assertion: str, *, limit: int = 14) -> str:
1085
+ return assertion if len(assertion) <= limit else assertion[:limit] + "…"
680
1086
 
681
1087
 
682
- def command_init(workspace: Path) -> dict[str, Any]:
1088
+ def compact_item(item: dict[str, Any]) -> dict[str, Any]:
1089
+ """query 载荷只带这四个字段——估算与真实输出必须同一把尺。"""
1090
+ return {"id": item["id"], "kind": item["kind"], "scope": item["scope"], "assertion": item["assertion"]}
1091
+
1092
+
1093
+ def compact_bytes(item: dict[str, Any]) -> int:
1094
+ return len(json.dumps(compact_item(item), ensure_ascii=False).encode("utf-8"))
1095
+
1096
+
1097
+ SCOPE_RANK = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
1098
+
1099
+
1100
+ def query_sort_key(item: dict[str, Any]) -> tuple[int, int, int, int, int]:
1101
+ """重要度→本书例外→最近更新→确认次数→编号。
1102
+
1103
+ 重要度必须排在 scope 之前:超编时先丢的应当是不重要的条目,而不是「凡
1104
+ 全局一律先丢」。scope 在前会让任意数量的 low 本书琐事挤掉 high 的全局
1105
+ 铁律——那恰恰是作者最不愿意丢的那一类。同重要度之内才按本书例外优先。
1106
+ 两个 store 的修订号互不可比,但 book 条目只来自书级 store、其余只来自项目
1107
+ 级,同 scope 必同 store,所以按修订号比「最近更新」在合并后仍成立。
1108
+ """
1109
+ return (
1110
+ -RANK[item["importance"]],
1111
+ SCOPE_RANK[item["scope"]["level"]],
1112
+ -item["updated_revision"],
1113
+ -item["confirmation_count"],
1114
+ id_number(item["id"]),
1115
+ )
1116
+
1117
+
1118
+ def fit_items(
1119
+ sorted_items: list[dict[str, Any]],
1120
+ revision: int,
1121
+ *,
1122
+ extra: dict[str, Any] | None = None,
1123
+ ) -> tuple[dict[str, Any], list[str]]:
1124
+ """按 query 输出信封把条目装进 QUERY_MAX_BYTES:装不下的跳过而不中断
1125
+ (一条长的不挡后面的短条),漏下的 ID 报进 omitted_ids(封顶
1126
+ OMITTED_IDS_MAX 条,omitted 保留真实总数)。返回 (结果文档, 全部漏下 ID)。
1127
+
1128
+ 漏项恒按候选优先级排序,不按被丢弃的先后:收尾回吐的条目优先级高于循环
1129
+ 里跳过的,若按追加顺序排,omitted_ids 的封顶正好会把最该报的那条切掉。
1130
+ `extra` 是同样计入信封的附加字段(book_revision 等)。
1131
+ """
1132
+ result: dict[str, Any] = {
1133
+ "ok": True,
1134
+ "command": "query",
1135
+ "initialized": True,
1136
+ "revision": revision,
1137
+ "items": [],
1138
+ "omitted": 0,
1139
+ "omitted_ids": [],
1140
+ }
1141
+ result.update(extra or {})
1142
+ order = {item["id"]: index for index, item in enumerate(sorted_items)}
1143
+ kept: list[dict[str, Any]] = []
1144
+ dropped: set[str] = set()
1145
+
1146
+ def ordered_omitted() -> list[str]:
1147
+ return sorted(dropped, key=order.__getitem__)
1148
+
1149
+ def envelope_bytes() -> int:
1150
+ omitted = ordered_omitted()
1151
+ result["items"] = [compact_item(item) for item in kept]
1152
+ result["omitted"] = len(omitted)
1153
+ result["omitted_ids"] = omitted[:OMITTED_IDS_MAX]
1154
+ return len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8"))
1155
+
1156
+ for item in sorted_items:
1157
+ kept.append(item)
1158
+ if envelope_bytes() > QUERY_MAX_BYTES:
1159
+ kept.pop()
1160
+ dropped.add(item["id"])
1161
+ # omitted 计数落定后包可能恰好贴边超出一两个字节,回吐条目直到装下。
1162
+ while kept and envelope_bytes() > QUERY_MAX_BYTES:
1163
+ dropped.add(kept.pop()["id"])
1164
+ envelope_bytes()
1165
+ return result, ordered_omitted()
1166
+
1167
+
1168
+ def build_query_result(
1169
+ candidates: list[dict[str, Any]],
1170
+ project_state: dict[str, Any] | None,
1171
+ book_state: dict[str, Any] | None,
1172
+ ) -> tuple[dict[str, Any], list[str]]:
1173
+ """真实查询与写入端估算共用的唯一信封构造:候选已按优先级排好,这里补上
1174
+ book_revision 等附加字段再装填。两处信封差一个字段就是一次假阴性——回执
1175
+ 说没事、query 照样丢条。"""
1176
+ extra: dict[str, Any] = {}
1177
+ if book_state is not None:
1178
+ extra["book_revision"] = book_state["state_revision"]
1179
+ revision = project_state["state_revision"] if project_state is not None else 0
1180
+ return fit_items(candidates, revision, extra=extra)
1181
+
1182
+
1183
+ def merged_query(
1184
+ project_state: dict[str, Any] | None,
1185
+ book_state: dict[str, Any] | None,
1186
+ kinds: set[str],
1187
+ requested_scopes: dict[str, str | None],
1188
+ ) -> tuple[dict[str, Any], list[str]]:
1189
+ """真实查询的纯函数部分:项目级按 kind/scope 过滤(存量 book 条目不参与,
1190
+ migrate 后才回来),书级整个 store 就是这本书的,只按 status/kind 过滤;
1191
+ 合并后按 重要度→本书例外→最近更新 装填。"""
1192
+
1193
+ def relevant(item: dict[str, Any]) -> bool:
1194
+ if item["status"] != "active" or item["kind"] not in kinds:
1195
+ return False
1196
+ level = item["scope"]["level"]
1197
+ if level == "global":
1198
+ return True
1199
+ return level != "book" and same_scope_value(item["scope"]["value"], requested_scopes[level])
1200
+
1201
+ candidates: list[dict[str, Any]] = []
1202
+ if project_state is not None:
1203
+ candidates.extend(item for item in project_state["items"].values() if relevant(item))
1204
+ if book_state is not None:
1205
+ candidates.extend(active_of_kinds(book_state, tuple(kinds)))
1206
+ candidates.sort(key=query_sort_key)
1207
+ return build_query_result(candidates, project_state, book_state)
1208
+
1209
+
1210
+ def slice_weight(items: list[dict[str, Any]]) -> int:
1211
+ """切片在真实载荷里的占位:compact 字节+每条在 JSON 数组里的分隔符。
1212
+
1213
+ 只比 compact 字节会挑错切片——条目多、单条短的切片字节和更小,实际占位
1214
+ 却更大,于是估算判「装得下」而真实查询溢出(warnings 假阴性)。
1215
+ """
1216
+ return sum(compact_bytes(item) + 2 for item in items)
1217
+
1218
+
1219
+ def active_of_kinds(state: dict[str, Any] | None, kinds: tuple[str, ...]) -> list[dict[str, Any]]:
1220
+ if state is None:
1221
+ return []
1222
+ return [item for item in state["items"].values() if item["status"] == "active" and item["kind"] in kinds]
1223
+
1224
+
1225
+ def worst_case_items(
1226
+ project_state: dict[str, Any] | None,
1227
+ book_state: dict[str, Any] | None,
1228
+ kinds: tuple[str, ...],
1229
+ ) -> list[dict[str, Any]]:
1230
+ """某个任务组合的最坏查询候选:全局条目+各 scope 维度上最重的单一切片。
1231
+
1232
+ 一次查询只带一个 book/genre/workflow,不同书的条目不会同现,所以按切片
1233
+ 取最重而不是全加起来,多书工作区才不会被粗算误伤。切片按 casefold 归并,
1234
+ 与 same_scope_value 同一口径——否则只差大小写的同名书在这里算两个切片、
1235
+ 在真实查询里却合成一个,估算就成了下界。
1236
+
1237
+ book 维度就是当前可见的书级 store(整个 store 是这本书的一片),所以写书级
1238
+ 条目时「本书+全局」是精确的;写项目级条目而没传 --book-root 时看不到任何
1239
+ 书级 store,估算只覆盖项目级。项目级里的存量 book 条目不参与查询,也不参与
1240
+ 估算。
1241
+ """
1242
+ pool = [item for item in active_of_kinds(project_state, kinds) if item["scope"]["level"] != "book"]
1243
+ worst = [item for item in pool if item["scope"]["level"] == "global"]
1244
+ slices: dict[str, dict[str, list[dict[str, Any]]]] = {"genre": {}, "workflow": {}}
1245
+ for item in pool:
1246
+ level = item["scope"]["level"]
1247
+ if level != "global":
1248
+ slices[level].setdefault((item["scope"]["value"] or "").casefold(), []).append(item)
1249
+ for level in ("genre", "workflow"):
1250
+ if slices[level]:
1251
+ worst.extend(max(slices[level].values(), key=slice_weight))
1252
+ worst.extend(active_of_kinds(book_state, kinds))
1253
+ worst.sort(key=query_sort_key)
1254
+ return worst
1255
+
1256
+
1257
+ def legacy_book_items(project_state: dict[str, Any] | None) -> list[dict[str, Any]]:
1258
+ """项目级 store 里升级前写入、还活着的 book 条目——只有 migrate 会碰它们。"""
1259
+ if project_state is None:
1260
+ return []
1261
+ return [
1262
+ item for item in project_state["items"].values()
1263
+ if item["scope"]["level"] == "book" and item["status"] in {"active", "pending", "conflict"}
1264
+ ]
1265
+
1266
+
1267
+ def query_budget_warnings(project_state: dict[str, Any] | None, book_state: dict[str, Any] | None) -> list[str]:
1268
+ """写入回执的预算提醒:每个任务组合按最坏查询情形试装,装不下的点名。
1269
+
1270
+ 写入永不因注入预算失败;提醒指向「整理作者记忆」。点名带断言首句——只给
1271
+ 编号的话,作者不打开 作者画像.md 就无从判断丢的是什么。
1272
+ """
1273
+ warnings: list[str] = []
1274
+ for task, kinds in QUERY_COMBOS.items():
1275
+ worst = worst_case_items(project_state, book_state, kinds)
1276
+ _, omitted = build_query_result(worst, project_state, book_state)
1277
+ if not omitted:
1278
+ continue
1279
+ assertions = {item["id"]: item["assertion"] for item in worst}
1280
+ shown = ";".join(f"{item_id}「{summarize_assertion(assertions[item_id])}」" for item_id in omitted[:3])
1281
+ more = f" 等 {len(omitted)} 条" if len(omitted) > 3 else ""
1282
+ warnings.append(
1283
+ f"「{task}」最坏查询装不下 {len(omitted)} 条,它们不会进入 prompt:"
1284
+ f"{shown}{more}——超出 {QUERY_MAX_BYTES} 字节注入预算。"
1285
+ f"说「整理作者记忆」可合并同义条、退役过时条。"
1286
+ )
1287
+ return warnings
1288
+
1289
+
1290
+ def visible_states(
1291
+ store: Store,
1292
+ updated: dict[str, Any],
1293
+ workspace: Path,
1294
+ book_root: Path | None,
1295
+ book: str | None,
1296
+ ) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[str]]:
1297
+ """写入后可见的两级状态:刚写的那份用内存里的,另一份只读加载(没有就 None)。
1298
+
1299
+ 此时写入已经落盘,另一份读不出来(书目录不存在、--book 与书级记录不符、state
1300
+ 损坏)只能降级为提醒:若在这里报错,调用方会告诉作者「没记住」,换个 event_id
1301
+ 重试就派生重复条目。"""
1302
+ try:
1303
+ if store.kind == "project":
1304
+ other = load_state(book_store(workspace, book_root, book)) if book_root is not None else None
1305
+ return updated, other, []
1306
+ return load_state(project_store(workspace)), updated, []
1307
+ except AuthorMemoryError as exc:
1308
+ other_label = "书级" if store.kind == "project" else "项目级"
1309
+ notice = f"已写入{store.label} store;但{other_label} store 读取失败,本次预算提醒没算上它:{exc}"
1310
+ return (updated, None, [notice]) if store.kind == "project" else (None, updated, [notice])
1311
+
1312
+
1313
+ # ---------------------------------------------------------------------------
1314
+ # Commands
1315
+ # ---------------------------------------------------------------------------
1316
+
1317
+
1318
+ def prepare_workspace(workspace: Path, book_root: Path | None = None) -> None:
1319
+ """每个命令的入口:校验工作区,并在单书布局下先把误放的书级 state 归位——
1320
+ 归位之前项目级 store 读不出来,放在任何读取之前才能让只读查询也自愈。"""
683
1321
  require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
684
- path = state_path(workspace)
685
- if path.exists():
686
- state = validate_state(read_json(path))
687
- else:
688
- state = empty_state()
689
- write_snapshot(workspace, state)
690
- return {"ok": True, "command": "init", "revision": state["state_revision"], "root": str(memory_root(workspace))}
1322
+ relocate_misplaced_book_state(workspace, book_root)
1323
+
1324
+
1325
+ def store_fields(store: Store) -> dict[str, Any]:
1326
+ fields: dict[str, Any] = {"store": store.kind, "root": str(store.root)}
1327
+ if store.book is not None:
1328
+ fields["book"] = store.book
1329
+ return fields
691
1330
 
692
1331
 
693
- def command_commit(workspace: Path, input_path: Path) -> dict[str, Any]:
694
- require(state_path(workspace).exists(), "author memory is not initialized; run init first")
695
- state = validate_state(read_json(state_path(workspace)))
1332
+ def command_init(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
1333
+ prepare_workspace(workspace, book_root)
1334
+ store = book_store(workspace, book_root, book) if book_root is not None else project_store(workspace)
1335
+ state = load_state(store)
1336
+ if state is None:
1337
+ state = empty_state(store.book)
1338
+ write_snapshot(store, state)
1339
+ return {"ok": True, "command": "init", "revision": state["state_revision"], **store_fields(store)}
1340
+
1341
+
1342
+ def command_commit(workspace: Path, book_root: Path | None, book: str | None, input_path: Path) -> dict[str, Any]:
1343
+ prepare_workspace(workspace, book_root)
696
1344
  transaction = normalize_transaction(read_json(input_path))
1345
+ store = resolve_target_store(transaction_store_kind(transaction), workspace, book_root, book)
1346
+ require(store.state_path.exists(), f"{store.label} author memory is not initialized; run init first")
1347
+ state = load_state(store)
697
1348
  digest = transaction_digest(transaction)
698
- updated, summaries = apply_transaction(state, transaction, digest)
1349
+ updated, summaries = apply_transaction(state, transaction, digest, store=store)
699
1350
  replayed = updated is state
700
- if not replayed:
701
- write_snapshot(workspace, updated)
702
- else:
703
- # Repair missing or stale views during an idempotent retry.
704
- write_snapshot(workspace, state)
1351
+ # 幂等重放时也重写快照,修复缺失或过期的派生视图。
1352
+ write_snapshot(store, updated)
1353
+ project_state, book_state, load_warnings = visible_states(store, updated, workspace, book_root, book)
705
1354
  return {
706
1355
  "ok": True,
707
1356
  "command": "commit",
@@ -710,14 +1359,18 @@ def command_commit(workspace: Path, input_path: Path) -> dict[str, Any]:
710
1359
  "replayed": replayed,
711
1360
  "item_ids": updated["applied_transactions"][transaction["transaction_id"]]["item_ids"],
712
1361
  "summaries": summaries,
1362
+ "warnings": load_warnings + query_budget_warnings(project_state, book_state),
1363
+ **store_fields(store),
713
1364
  }
714
1365
 
715
1366
 
716
- def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
717
- require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
1367
+ def command_record(workspace: Path, book_root: Path | None, book: str | None, input_path: Path) -> dict[str, Any]:
1368
+ prepare_workspace(workspace, book_root)
718
1369
  event = normalize_record_event(read_json(input_path))
719
- path = state_path(workspace)
720
- state = validate_state(read_json(path)) if path.exists() else empty_state()
1370
+ store = resolve_target_store(operation_store_kind(event["operation"], "event.operation"), workspace, book_root, book)
1371
+ state = load_state(store)
1372
+ if state is None:
1373
+ state = empty_state(store.book)
721
1374
  transaction_id = f"record:{event['event_id']}"
722
1375
  applied = state["applied_transactions"].get(transaction_id)
723
1376
  expected_revision = applied["revision"] - 1 if applied is not None else state["state_revision"]
@@ -728,12 +1381,13 @@ def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
728
1381
  "operations": [event["operation"]],
729
1382
  }
730
1383
  digest = transaction_digest(transaction)
731
- updated, summaries = apply_transaction(state, transaction, digest)
1384
+ updated, summaries = apply_transaction(state, transaction, digest, store=store)
732
1385
  replayed = updated is state
733
- write_snapshot(workspace, updated)
1386
+ write_snapshot(store, updated)
734
1387
  record = updated["applied_transactions"][transaction_id]
735
1388
  item_ids = record["item_ids"]
736
1389
  receipt = f"Author Memory Receipt: r{record['revision']} · {', '.join(item_ids)}"
1390
+ project_state, book_state, load_warnings = visible_states(store, updated, workspace, book_root, book)
737
1391
  return {
738
1392
  "ok": True,
739
1393
  "command": "record",
@@ -744,104 +1398,212 @@ def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
744
1398
  "item_ids": item_ids,
745
1399
  "receipt": receipt,
746
1400
  "summaries": summaries,
1401
+ "warnings": load_warnings + query_budget_warnings(project_state, book_state),
1402
+ **store_fields(store),
747
1403
  }
748
1404
 
749
1405
 
750
- def same_scope_value(item_value: str | None, requested: str | None) -> bool:
751
- return requested is not None and item_value is not None and item_value.casefold() == requested.casefold()
752
-
753
-
754
1406
  def command_query(
755
1407
  workspace: Path,
1408
+ book_root: Path | None,
756
1409
  kinds: list[str] | None,
757
1410
  book: str | None,
758
1411
  genre: str | None,
759
1412
  workflow: str | None,
760
1413
  ) -> dict[str, Any]:
761
- require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
762
- path = state_path(workspace)
763
- if not path.exists():
764
- return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "omitted": 0}
765
- state = validate_state(read_json(path))
766
- requested_kinds = set(kinds or KINDS)
1414
+ prepare_workspace(workspace, book_root)
1415
+ require(
1416
+ bool(kinds),
1417
+ "query 必须显式传 --kind(按调用方 skill 写好的 query 命令选类型,映射表见 references/author-memory-maintenance.md),不再默认返回全部类型",
1418
+ )
1419
+ project_state = load_state(project_store(workspace))
1420
+ book_state = load_state(book_store(workspace, book_root, book)) if book_root is not None else None
1421
+ if project_state is None and book_state is None:
1422
+ return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "omitted": 0, "omitted_ids": []}
767
1423
  requested_scopes = {
768
- "book": optional_text(book, "query.book", max_bytes=180),
769
1424
  "genre": optional_text(genre, "query.genre", max_bytes=180),
770
1425
  "workflow": optional_text(workflow, "query.workflow", max_bytes=180),
771
1426
  }
772
-
773
- def relevant(item: dict[str, Any]) -> bool:
774
- if item["status"] != "active" or item["kind"] not in requested_kinds:
775
- return False
776
- level = item["scope"]["level"]
777
- return level == "global" or same_scope_value(item["scope"]["value"], requested_scopes[level])
778
-
779
- scope_rank = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
780
- candidates = sorted(
781
- (item for item in state["items"].values() if relevant(item)),
782
- key=lambda item: (
783
- scope_rank[item["scope"]["level"]],
784
- -RANK[item["importance"]],
785
- -item["confirmation_count"],
786
- int(item["id"][2:]),
787
- ),
788
- )
789
- result: dict[str, Any] = {
790
- "ok": True,
791
- "command": "query",
792
- "initialized": True,
793
- "revision": state["state_revision"],
794
- "items": [],
795
- "omitted": len(candidates),
796
- }
797
- for item in candidates:
798
- compact = {
799
- "id": item["id"],
800
- "kind": item["kind"],
801
- "scope": item["scope"],
802
- "assertion": item["assertion"],
803
- }
804
- result["items"].append(compact)
805
- result["omitted"] = len(candidates) - len(result["items"])
806
- payload = json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n"
807
- if len(payload.encode("utf-8")) > QUERY_MAX_BYTES:
808
- result["items"].pop()
809
- result["omitted"] += 1
810
- break
1427
+ # 装不下的**跳过而不中断**(一条长的不挡后面的短条),漏下的 ID 报进
1428
+ # omitted_ids:非空=记忆超编该整理了,不是「没有更多了」。写入端的
1429
+ # warnings 与这里共用 build_query_result 同一个信封,估算即实况。
1430
+ result, _ = merged_query(project_state, book_state, set(kinds), requested_scopes)
811
1431
  require(len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")) <= QUERY_MAX_BYTES, "query result exceeds its fixed byte budget")
812
1432
  return result
813
1433
 
814
1434
 
815
- def command_check(workspace: Path) -> dict[str, Any]:
816
- path = state_path(workspace)
817
- require(path.exists(), "author memory is not initialized")
818
- state = validate_state(read_json(path))
1435
+ def check_store(store: Store) -> dict[str, Any]:
1436
+ state = load_state(store)
1437
+ require(state is not None, f"{store.label} author memory is not initialized")
819
1438
  views = render_views(state)
820
- root = memory_root(workspace)
821
1439
  for name, expected in views.items():
822
- view_path = root / name
1440
+ view_path = store.root / name
823
1441
  require(view_path.exists(), f"missing derived view: {view_path}")
824
1442
  require(view_path.read_text(encoding="utf-8") == expected, f"derived view is stale or edited: {view_path}")
825
1443
  counts = {status: sum(1 for item in state["items"].values() if item["status"] == status) for status in STATUSES}
826
- return {"ok": True, "command": "check", "revision": state["state_revision"], "counts": counts}
1444
+ return {"revision": state["state_revision"], "counts": counts}
1445
+
1446
+
1447
+ def command_check(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
1448
+ prepare_workspace(workspace, book_root)
1449
+ project = project_store(workspace)
1450
+ result: dict[str, Any] = {"ok": True, "command": "check"}
1451
+ if book_root is not None:
1452
+ store = book_store(workspace, book_root, book)
1453
+ result["book"] = {"name": store.book, "root": str(store.root), **check_store(store)}
1454
+ if project.state_path.exists():
1455
+ result["project"] = {"root": str(project.root), **check_store(project)}
1456
+ else:
1457
+ result["project"] = {"root": str(project.root), **check_store(project)}
1458
+ # 兼容旧调用:顶层 revision / counts 指项目级 store。
1459
+ primary = result.get("project") or result["book"]
1460
+ result["revision"] = primary["revision"]
1461
+ result["counts"] = primary["counts"]
1462
+ return result
1463
+
1464
+
1465
+ MIGRATE_PREFIX = "migrate:"
1466
+
1467
+
1468
+ def migration_digest(item: dict[str, Any]) -> str:
1469
+ """迁移事务的摘要只看条目不可变的身份:编号、类型、范围、断言。证据会随强化
1470
+ 增长、状态会随 decide/forget 变化,它们若进摘要,中途失败后作者动过条目就会
1471
+ 永远「already used with different content」。"""
1472
+ return transaction_digest({"id": item["id"], "kind": item["kind"], "scope": item["scope"], "assertion": item["assertion"]})
1473
+
1474
+
1475
+ def migrated_item(source: dict[str, Any], new_id: str, revision: int, id_map: dict[str, str]) -> dict[str, Any]:
1476
+ """把项目级的存量 book 条目原样搬进书级 store:断言、证据、确认次数、重要度
1477
+ 全部保留,只换编号与修订。冲突对象只保留同书已迁移的(重映射为 BP),
1478
+ 引用全局条目的冲突关系不再成立——本书例外按优先级覆盖全局,不算冲突——
1479
+ 对象清空的候选退回 pending。"""
1480
+ item = copy.deepcopy(source)
1481
+ item["id"] = new_id
1482
+ item["created_revision"] = revision
1483
+ item["updated_revision"] = revision
1484
+ item["superseded_by"] = None
1485
+ item["conflicts_with"] = [id_map[old] for old in source["conflicts_with"] if old in id_map]
1486
+ if item["status"] == "conflict" and not item["conflicts_with"]:
1487
+ item["status"] = "pending"
1488
+ return item
1489
+
1490
+
1491
+ def migration_records(state: dict[str, Any]) -> dict[str, str]:
1492
+ """书级 store 里「源 AP 编号 → 已建 BP 编号」;每个源条目一笔 migrate:APxxx 事务。"""
1493
+ records: dict[str, str] = {}
1494
+ for transaction_id, record in state["applied_transactions"].items():
1495
+ if transaction_id.startswith(MIGRATE_PREFIX) and is_item_id(transaction_id[len(MIGRATE_PREFIX):]):
1496
+ records[transaction_id[len(MIGRATE_PREFIX):]] = record["item_ids"][0]
1497
+ return records
1498
+
1499
+
1500
+ def apply_single(state: dict[str, Any], transaction_id: str, digest: str, mutate, summaries: list[str], *, store: Store) -> dict[str, Any]:
1501
+ updated = copy.deepcopy(state)
1502
+ mutate(updated, updated["state_revision"] + 1)
1503
+ return seal_transaction(updated, transaction_id, digest, summaries, store=store)
1504
+
1505
+
1506
+ def command_migrate(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
1507
+ """把项目级 store 里某本书的存量 book 条目搬进该书目录的书级 store。
1508
+
1509
+ 书级每个源条目一笔事务(migrate:APxxx,摘要只看不可变字段),所以每个源条目
1510
+ 至多迁一次,与本次批次里还有哪些条目无关、源条目状态怎么变都不会锁死;项目级
1511
+ 随后一笔事务把这批源条目标 superseded 并注明去向。先写书级再写项目级,中途
1512
+ 失败直接重跑:书级已有记录的直接复用编号,只补项目级。
1513
+ """
1514
+ prepare_workspace(workspace, book_root)
1515
+ require(book_root is not None, "migrate 须传 --book-root {书目录}:书名到书目录的映射由调用方给出")
1516
+ project = project_store(workspace)
1517
+ store = book_store(workspace, book_root, book)
1518
+ assert store.book is not None
1519
+ project_state = load_state(project)
1520
+ book_state = load_state(store)
1521
+
1522
+ def result(migrated: list[dict[str, str]]) -> dict[str, Any]:
1523
+ return {
1524
+ "ok": True,
1525
+ "command": "migrate",
1526
+ "migrated": migrated,
1527
+ "project_revision": project_state["state_revision"] if project_state is not None else 0,
1528
+ "revision": book_state["state_revision"] if book_state is not None else 0,
1529
+ "warnings": query_budget_warnings(project_state, book_state),
1530
+ **store_fields(store),
1531
+ }
1532
+
1533
+ if project_state is None:
1534
+ return result([])
1535
+ legacy = sorted(
1536
+ (item for item in legacy_book_items(project_state) if same_scope_value(item["scope"]["value"], store.book)),
1537
+ key=lambda item: id_number(item["id"]),
1538
+ )
1539
+ if book_state is None:
1540
+ if not legacy:
1541
+ return result([])
1542
+ book_state = empty_state(store.book)
1543
+ id_map = migration_records(book_state)
1544
+
1545
+ # 1) 书级:还没迁的源条目各建一条 BP;已迁的复用编号。
1546
+ for item in legacy:
1547
+ if item["id"] in id_map:
1548
+ continue
1549
+ transaction_id = f"{MIGRATE_PREFIX}{item['id']}"
1550
+ new_id = f"{STORE_PREFIX['book']}{book_state['next_item_number']:03d}"
1551
+
1552
+ preview = migrated_item(item, new_id, 1, id_map)
1553
+
1554
+ def create(updated: dict[str, Any], revision: int, source=item, new_id=new_id) -> None:
1555
+ updated["next_item_number"] += 1
1556
+ updated["items"][new_id] = migrated_item(source, new_id, revision, id_map)
1557
+
1558
+ book_state = apply_single(
1559
+ book_state, transaction_id, migration_digest(item), create,
1560
+ [f"迁移 {item['id']} → {new_id}({preview['status']}):{item['assertion']}"],
1561
+ store=store,
1562
+ )
1563
+ id_map[item["id"]] = new_id
1564
+ # 幂等重跑也重写快照,修复缺失或过期的派生视图。
1565
+ write_snapshot(store, book_state)
1566
+
1567
+ # 2) 项目级:这批源条目标 superseded 并注明去向。
1568
+ migrated = [{"from": item["id"], "to": id_map[item["id"]]} for item in legacy]
1569
+ if legacy:
1570
+ transaction_id = f"{MIGRATE_PREFIX}{store.book.casefold()}:r{project_state['state_revision'] + 1}"
1571
+ digest = transaction_digest({"transaction_id": transaction_id, "migrated": migrated})
1572
+
1573
+ def move_out(updated: dict[str, Any], revision: int) -> None:
1574
+ for item in legacy:
1575
+ target = updated["items"][item["id"]]
1576
+ target["status"] = "superseded"
1577
+ target["superseded_by"] = None
1578
+ target["conflicts_with"] = []
1579
+ target["reason"] = f"已迁移到书级 store「{store.book}」:{id_map[item['id']]}"
1580
+ target["updated_revision"] = revision
1581
+ release_conflicts(updated, set(id_map), revision)
1582
+
1583
+ summaries = [f"迁出 {pair['from']} → {pair['to']}:{project_state['items'][pair['from']]['assertion']}" for pair in migrated]
1584
+ project_state = apply_single(project_state, transaction_id, digest, move_out, summaries, store=project)
1585
+ write_snapshot(project, project_state)
1586
+ return result(migrated)
827
1587
 
828
1588
 
829
1589
  def build_parser() -> argparse.ArgumentParser:
830
1590
  parser = argparse.ArgumentParser(description=__doc__)
831
1591
  subparsers = parser.add_subparsers(dest="command", required=True)
832
- for command in ("init", "check"):
833
- child = subparsers.add_parser(command)
1592
+
1593
+ def add_location(child: argparse.ArgumentParser) -> None:
834
1594
  child.add_argument("--workspace", required=True, type=Path)
835
- commit = subparsers.add_parser("commit")
836
- commit.add_argument("--workspace", required=True, type=Path)
837
- commit.add_argument("--input", required=True, type=Path)
838
- record = subparsers.add_parser("record")
839
- record.add_argument("--workspace", required=True, type=Path)
840
- record.add_argument("--input", required=True, type=Path)
1595
+ child.add_argument("--book-root", type=Path, help="书目录;书级条目(scope.level=book / BP 编号)的落点")
1596
+ child.add_argument("--book", help="书名;传了 --book-root 时默认取 store 记录的书名或目录名")
1597
+
1598
+ for command in ("init", "check", "migrate"):
1599
+ add_location(subparsers.add_parser(command))
1600
+ for command in ("commit", "record"):
1601
+ child = subparsers.add_parser(command)
1602
+ add_location(child)
1603
+ child.add_argument("--input", required=True, type=Path)
841
1604
  query = subparsers.add_parser("query")
842
- query.add_argument("--workspace", required=True, type=Path)
1605
+ add_location(query)
843
1606
  query.add_argument("--kind", action="append", choices=KINDS)
844
- query.add_argument("--book")
845
1607
  query.add_argument("--genre")
846
1608
  query.add_argument("--workflow")
847
1609
  return parser
@@ -851,15 +1613,17 @@ def main() -> int:
851
1613
  args = build_parser().parse_args()
852
1614
  try:
853
1615
  if args.command == "init":
854
- result = command_init(args.workspace)
1616
+ result = command_init(args.workspace, args.book_root, args.book)
855
1617
  elif args.command == "commit":
856
- result = command_commit(args.workspace, args.input)
1618
+ result = command_commit(args.workspace, args.book_root, args.book, args.input)
857
1619
  elif args.command == "record":
858
- result = command_record(args.workspace, args.input)
1620
+ result = command_record(args.workspace, args.book_root, args.book, args.input)
859
1621
  elif args.command == "query":
860
- result = command_query(args.workspace, args.kind, args.book, args.genre, args.workflow)
1622
+ result = command_query(args.workspace, args.book_root, args.kind, args.book, args.genre, args.workflow)
1623
+ elif args.command == "migrate":
1624
+ result = command_migrate(args.workspace, args.book_root, args.book)
861
1625
  else:
862
- result = command_check(args.workspace)
1626
+ result = command_check(args.workspace, args.book_root, args.book)
863
1627
  emit(result)
864
1628
  return 0
865
1629
  except AuthorMemoryError as exc: