@oh-story/dsh 0.1.8 → 0.1.10

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 (360) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +237 -84
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +183 -77
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +140 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +28 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +8 -2
  10. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  11. package/lib/drama/skills/short-drama-edit/SKILL.md +146 -0
  12. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  21. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  22. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  23. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +38 -0
  24. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +29 -0
  25. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +54 -0
  26. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
  27. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +51 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
  29. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +520 -0
  30. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  31. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  32. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  33. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  34. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  35. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  36. package/lib/drama/skills/short-drama-produce/SKILL.md +29 -4
  37. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +22 -0
  38. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +13 -12
  39. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  40. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  41. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  42. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  43. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +199 -3
  44. package/lib/drama/skills/short-drama-review/SKILL.md +2 -0
  45. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +7 -0
  46. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +10 -0
  47. package/lib/drama/skills/short-drama-storyboard/SKILL.md +12 -1
  48. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  49. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  50. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +78 -2
  51. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +12 -5
  53. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
  54. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +76 -7
  55. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +5 -2
  56. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +21 -0
  57. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -17
  58. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +12 -3
  59. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +61 -5
  60. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -3
  61. package/lib/drama/skills/short-drama-write/SKILL.md +11 -2
  62. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  63. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  64. package/lib/index.js +290 -90
  65. package/lib/index.js.map +4 -4
  66. package/lib/novel-to-game/README.md +299 -59
  67. package/lib/novel-to-game/README_ZH.md +299 -56
  68. package/lib/novel-to-game/VERSION +1 -1
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  78. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  79. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  80. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  81. package/lib/novel-to-game/manifest.json +77 -87
  82. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +9 -35
  83. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  84. package/lib/novel-to-game/skills/game-build/SKILL.md +17 -35
  85. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  86. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  87. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  88. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  89. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  90. package/lib/novel-to-game/skills/game-concept/SKILL.md +16 -34
  91. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  92. package/lib/novel-to-game/skills/game-qa/SKILL.md +9 -20
  93. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +10 -6
  94. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -32
  95. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  96. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  98. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +9 -18
  99. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  100. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +10 -27
  101. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -6
  102. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +6 -11
  103. package/lib/oh-story/manifest.json +419 -254
  104. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  105. package/lib/oh-story/roles/character-designer.md +1 -1
  106. package/lib/oh-story/roles/consistency-checker.md +4 -4
  107. package/lib/oh-story/roles/narrative-writer.md +56 -243
  108. package/lib/oh-story/roles/story-architect.md +6 -6
  109. package/lib/oh-story/roles/story-explorer.md +11 -11
  110. package/lib/oh-story/skills/story/SKILL.md +13 -7
  111. package/lib/oh-story/skills/story/VERSION +1 -1
  112. package/lib/oh-story/skills/story/references/author-memory.md +61 -29
  113. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  114. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  115. package/lib/oh-story/skills/story-deslop/SKILL.md +54 -229
  116. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +29 -56
  117. package/lib/oh-story/skills/story-deslop/references/author-memory.md +61 -29
  118. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  119. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  120. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  121. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  122. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  123. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  124. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  125. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  126. package/lib/oh-story/skills/story-import/SKILL.md +78 -82
  127. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  128. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  129. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  130. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
  131. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  132. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +9 -34
  133. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +197 -15
  134. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
  135. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
  136. package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
  137. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
  139. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
  141. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
  142. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
  143. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  144. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
  145. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
  146. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  147. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  148. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  149. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
  150. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  151. package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
  152. package/lib/oh-story/skills/story-long-write/SKILL.md +52 -216
  153. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +29 -56
  154. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +72 -12
  155. package/lib/oh-story/skills/story-long-write/references/author-memory.md +61 -29
  156. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  157. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  158. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  159. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +10 -4
  160. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  161. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +6 -15
  162. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  163. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  164. package/lib/oh-story/skills/story-long-write/references/project-files.md +110 -0
  165. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
  166. package/lib/oh-story/skills/story-long-write/references/reference-index.md +86 -0
  167. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  168. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  169. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  170. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  171. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  172. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +9 -34
  173. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +88 -118
  174. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -119
  175. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
  176. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +73 -54
  177. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  178. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +63 -4
  179. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  180. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +531 -0
  181. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  182. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  183. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +197 -15
  184. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  185. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  186. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +285 -0
  187. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
  188. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  189. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
  190. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
  191. package/lib/oh-story/skills/story-review/SKILL.md +80 -87
  192. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +29 -56
  193. package/lib/oh-story/skills/story-review/references/author-memory.md +61 -29
  194. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  195. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +10 -4
  196. package/lib/oh-story/skills/story-review/references/review-quality.md +6 -12
  197. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  198. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  199. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +9 -34
  200. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  201. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  202. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  203. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  204. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  205. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
  206. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
  207. package/lib/oh-story/skills/story-setup/SKILL.md +58 -43
  208. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +29 -56
  209. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  210. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  211. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +10 -4
  212. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  213. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  214. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
  215. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  216. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  217. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  218. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  219. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +63 -4
  220. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  221. package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
  222. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  223. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
  224. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  225. package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
  226. package/lib/oh-story/skills/story-short-write/SKILL.md +15 -324
  227. package/lib/oh-story/skills/story-short-write/references/author-memory.md +61 -29
  228. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  229. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  230. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +10 -4
  231. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  232. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  233. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  234. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  235. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  236. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  237. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  238. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  239. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  240. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  241. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  242. package/lib/oh-story/skills/story-short-write/references/reference-index.md +55 -0
  243. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  244. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  245. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  246. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  247. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  248. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +78 -0
  249. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
  250. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
  251. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  252. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  253. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  254. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  255. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  256. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  257. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  258. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  259. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  260. package/lib/types/client/file-activity.d.ts +19 -1
  261. package/lib/types/client/file-activity.d.ts.map +1 -1
  262. package/lib/types/client/index.d.ts.map +1 -1
  263. package/lib/types/client/production-prompts.d.ts +7 -0
  264. package/lib/types/client/production-prompts.d.ts.map +1 -1
  265. package/lib/types/client/production-runtime.d.ts +33 -0
  266. package/lib/types/client/production-runtime.d.ts.map +1 -1
  267. package/lib/types/drama-adapters.d.ts +5 -3
  268. package/lib/types/drama-adapters.d.ts.map +1 -1
  269. package/lib/types/native-hooks.d.ts +52 -0
  270. package/lib/types/native-hooks.d.ts.map +1 -1
  271. package/lib/types/role-identity.d.ts +17 -0
  272. package/lib/types/role-identity.d.ts.map +1 -0
  273. package/lib/types/role-tool.d.ts.map +1 -1
  274. package/lib/types/skill-provider.d.ts.map +1 -1
  275. package/lib/video-recap/CHANGELOG.md +31 -1
  276. package/lib/video-recap/manifest.json +162 -162
  277. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  278. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  279. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  280. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  281. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  282. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  283. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  284. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  285. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  286. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  287. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  288. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  289. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  290. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  291. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  292. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  293. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  294. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  295. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  296. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  297. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  298. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  299. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  300. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  301. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  302. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  303. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  304. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  305. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  306. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  307. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  308. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  309. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  310. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  311. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  312. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  313. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  314. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  315. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  316. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  317. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  318. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  319. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  320. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  321. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  322. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  323. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  324. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  325. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  326. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  327. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  328. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  329. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  330. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  331. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  332. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  333. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  334. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  335. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  336. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  337. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  338. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  339. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  340. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  341. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  342. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  343. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  344. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  345. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  346. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  347. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  348. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  349. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  350. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  351. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  352. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  353. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  354. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  355. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  356. package/package.json +45 -45
  357. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -59
  358. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  359. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -42
  360. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const fs = require('fs');
5
+ const { loadStyleWhitelist, maskStyleText } = require('./style-whitelist.js');
5
6
  const path = require('path');
6
7
 
7
8
  const USAGE = `Usage: node check-ai-patterns.js [--check] [--json] [--fail-on=blocking|all] <file...>
@@ -30,6 +31,7 @@ Detect high-risk AI-flavor prose patterns that need human rewrite:
30
31
  - 章尾状态总结体 (文末窗口 这一夜注定/这一切都结束了/新的人生才刚刚开始/命运的齿轮)
31
32
  - 引号强调滥用 (叙述里 1-4 字短词加引号强调,密度型)
32
33
 
34
+ Book-local .deslop-whitelist literal spans are excluded from style scanning (no regex or ancestor inheritance).
33
35
  Each finding carries severity: blocking by default for generation/deslop cleanup (not-is-comparison / em-dash / voice-contrast / negation-parade / reverse-not-is / trailer-ending / trailer-summary). This is a local style/readability gate, not an AIGC detector score; functional human text can be marked for review instead of hard-edited for a detector.
34
36
  或 advisory (period-stutter / long-paragraph / micro-action-tic / stock-reaction-tic / action-list-tic / abstract-summary-tic / cliche-density-tic / metaphor-density-tic / reasoning-chain-tic / system-notice-formality-tic / overcompressed-prose-tic / low-connective-density-tic / quote-emphasis-tic / formulaic-parallelism,是提示,justified 的长推理/氛围段可保留)。
35
37
  --fail-on=blocking 只在出现 blocking finding 时退出 1;默认 --fail-on=all 有任何 finding 即退出 1。
@@ -308,7 +310,10 @@ for (const file of options.files) {
308
310
  continue;
309
311
  }
310
312
 
311
- const findings = scanDocument(input).map((finding) => ({ file, ...finding }));
313
+ let whitelist;
314
+ try { whitelist = loadStyleWhitelist(fullPath); }
315
+ catch (error) { die(`${file}: unable to read .deslop-whitelist (${error.message})`); }
316
+ const findings = scanDocument(maskStyleText(input, whitelist)).map((finding) => ({ file, ...finding }));
312
317
  allFindings.push(...findings);
313
318
  }
314
319
 
@@ -43,6 +43,17 @@ const PLACEHOLDER_PATTERNS = [
43
43
  { re: /我(无法|不能)(继续(写|创作|生成|下去)|生成(内容|文本|正文)?|创作|续写|完成(这个|本)?(章|篇|创作|请求))/, label: '元信息泄漏(生成拒绝语)', hard: false },
44
44
  ];
45
45
 
46
+ const QUOTED_SPAN_PATTERNS = [
47
+ /「[^」]*」/g,
48
+ /『[^』]*』/g,
49
+ /【[^】]*】/g,
50
+ /“[^”]*”/g,
51
+ // 单引号须成对;词内撇号(don't、O’Connor)不作为开闭引号。
52
+ /(?<![A-Za-z0-9_])‘(?:[^’]|(?<=[A-Za-z0-9_])’(?=[A-Za-z0-9_]))*(?!(?<=[A-Za-z0-9_])’[A-Za-z0-9_])’/g,
53
+ /"[^"]*"/g,
54
+ /(?<![A-Za-z0-9_])'(?:[^']|(?<=[A-Za-z0-9_])'(?=[A-Za-z0-9_]))*(?!(?<=[A-Za-z0-9_])'[A-Za-z0-9_])'/g,
55
+ ];
56
+
46
57
  // 工程词泄漏(正文元信息扫描的确定性版):弱模型把写作工程词漏进正文,破坏代入感
47
58
  // (DeepSeek-v4 这类会在对话里冒「该到下一章了」)。漏词的模型自己发现不了,靠脚本兜。
48
59
  // tier1 = 纯写作流水线术语,正文里几乎永不合法;tier2 = 章节结构/歧义词,角色在故事内
@@ -150,21 +161,23 @@ function isContent(trimmed) {
150
161
  return trimmed && !trimmed.startsWith('#') && !/^-{3,}$/.test(trimmed);
151
162
  }
152
163
 
153
- function isDialogueLike(trimmed) {
154
- return /[“”"'‘’「」『』【】]/.test(trimmed);
164
+ // 等长遮住成对引号内的内容以保留列号;未闭合引号不豁免。
165
+ function maskQuotedSpans(text) {
166
+ let masked = text;
167
+ for (const pattern of QUOTED_SPAN_PATTERNS) {
168
+ masked = masked.replace(pattern, (match) => ' '.repeat(match.length));
169
+ }
170
+ return masked;
155
171
  }
156
172
 
157
173
  // 去掉成对引号内的片段(台词/系统词/引用物件),只留引号外叙述。复读判定用:重复台词是体裁
158
174
  // 手法(豁免),但「叙述 + 引号内物件/短台词」混合行里引号外叙述的复读仍是退化,不能整行豁免。
159
175
  function stripQuoted(text) {
160
- return text
161
- .replace(/「[^」]*」/g, '')
162
- .replace(/『[^』]*』/g, '')
163
- .replace(/【[^】]*】/g, '')
164
- .replace(/“[^”]*”/g, '')
165
- .replace(/‘[^’]*’/g, '')
166
- .replace(/"[^"]*"/g, '')
167
- .replace(/'[^']*'/g, '');
176
+ let stripped = text;
177
+ for (const pattern of QUOTED_SPAN_PATTERNS) {
178
+ stripped = stripped.replace(pattern, '');
179
+ }
180
+ return stripped;
168
181
  }
169
182
 
170
183
  function visibleLength(text) {
@@ -253,18 +266,19 @@ function findPlaceholders(content) {
253
266
  const findings = [];
254
267
  for (const { trimmed, lineNo } of content) {
255
268
  if (!isContent(trimmed)) continue;
256
- const dialogue = isDialogueLike(trimmed);
269
+ const outsideQuotes = maskQuotedSpans(trimmed);
257
270
  for (const { re, label, hard } of PLACEHOLDER_PATTERNS) {
258
- if (!hard && dialogue) continue; // soft 拒绝语在对话行里可能是正常台词,豁免
259
- const m = re.exec(trimmed);
271
+ // hard 信号仍扫描整行;soft 自指/拒绝语只扫描成对引号外,且遮罩等长以保留列号。
272
+ const m = re.exec(hard ? trimmed : outsideQuotes);
260
273
  if (m) {
274
+ const index = m.index || 0;
261
275
  findings.push({
262
276
  line: lineNo,
263
- column: (m.index || 0) + 1,
277
+ column: index + 1,
264
278
  type: 'placeholder-leak',
265
279
  severity: 'blocking',
266
280
  message: `${label}:正文混入元信息/拒绝语/占位符,重写本段干净落地。`,
267
- excerpt: compact(trimmed.slice(Math.max(0, (m.index || 0) - 4), (m.index || 0) + 20)),
281
+ excerpt: compact(trimmed.slice(Math.max(0, index - 4), index + 20)),
268
282
  });
269
283
  break; // one finding per line is enough
270
284
  }
@@ -283,17 +297,22 @@ function findMetaLeak(content) {
283
297
  // 标题行(第N章 章名,无 ## 前缀时也算)属「标题行以外的正文」之外,排除
284
298
  if (/^第[一二三四五六七八九十百千万两0-9]+章/.test(trimmed)) continue;
285
299
  }
286
- const dialogue = isDialogueLike(trimmed);
287
- let m = META_TIER1_RE.exec(trimmed);
300
+ const outsideQuotes = maskQuotedSpans(trimmed);
301
+ let m = META_TIER1_RE.exec(outsideQuotes);
302
+ let quotedTier1 = false;
303
+ if (!m) {
304
+ m = META_TIER1_RE.exec(trimmed);
305
+ quotedTier1 = Boolean(m);
306
+ }
288
307
  if (m) {
289
308
  // tier1 纯工程词正文里几乎永不合法→blocking;但写手/编剧题材里角色在故事内真讨论创作,
290
- // 台词(对话行)里可能合法,降级为 advisory(仍报告,交人/LLM 判,不强制回炉)。
309
+ // 且命中实际位于成对引号内时可能合法,降级为 advisory。同一行别处出现引号不影响判定。
291
310
  findings.push({
292
311
  line: lineNo,
293
312
  column: m.index + 1,
294
313
  type: 'meta-leak',
295
- severity: dialogue ? 'advisory' : 'blocking',
296
- message: `工程词泄漏:「${m[0]}」是写作流水线术语,正文里不该出现;改成角色/场景内表达。${dialogue ? '例外:角色为作者/编剧、在故事内真实讨论创作时,台词里可能合法。' : ''}`,
314
+ severity: quotedTier1 ? 'advisory' : 'blocking',
315
+ message: `工程词泄漏:「${m[0]}」是写作流水线术语,正文里不该出现;改成角色/场景内表达。${quotedTier1 ? '例外:角色为作者/编剧、在故事内真实讨论创作时,台词里可能合法。' : ''}`,
297
316
  excerpt: compact(trimmed.slice(Math.max(0, m.index - 6), m.index + 18)),
298
317
  });
299
318
  continue; // tier1 命中即可,不再叠 tier2
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const fs = require('fs');
5
+ const { loadStyleWhitelist, styleSpans } = require('./style-whitelist.js');
5
6
  const path = require('path');
6
7
 
7
8
  const USAGE = `Usage: node normalize-punctuation.js [--check] [--quote-mode keep|ascii|yan] <file...>
@@ -9,6 +10,7 @@ const USAGE = `Usage: node normalize-punctuation.js [--check] [--quote-mode keep
9
10
  Normalize正文 punctuation deterministically:
10
11
  - replace ellipses, em dashes, and double hyphens with Chinese punctuation
11
12
  - remove markdown divider lines (---) from正文
13
+ - preserve pause punctuation covered by book-local .deslop-whitelist literals
12
14
  - keep quote style by default; convert quotes only when explicitly requested
13
15
  `;
14
16
 
@@ -61,7 +63,10 @@ for (const file of options.files) {
61
63
  continue;
62
64
  }
63
65
 
64
- const result = normalizeDocument(input, options.quoteMode);
66
+ let whitelist;
67
+ try { whitelist = loadStyleWhitelist(fullPath); }
68
+ catch (error) { die(`${file}: unable to read .deslop-whitelist (${error.message})`); }
69
+ const result = normalizeDocument(input, options.quoteMode, whitelist);
65
70
  totalFindings += result.findings.length;
66
71
 
67
72
  if (options.check) {
@@ -94,7 +99,7 @@ function die(message) {
94
99
  process.exit(2);
95
100
  }
96
101
 
97
- function normalizeDocument(input, quoteMode) {
102
+ function normalizeDocument(input, quoteMode, whitelist) {
98
103
  const { lines, endings } = splitLinesKeepingEndings(input);
99
104
 
100
105
  const findings = [];
@@ -159,7 +164,7 @@ function normalizeDocument(input, quoteMode) {
159
164
  }
160
165
 
161
166
  const commentOpenBefore = commentOpen;
162
- const punctuationResult = normalizePausePunctuation(line, lineNo, commentOpen);
167
+ const punctuationResult = normalizePausePunctuation(line, lineNo, commentOpen, whitelist);
163
168
  findings.push(...punctuationResult.findings);
164
169
  line = punctuationResult.line;
165
170
  commentOpen = punctuationResult.commentOpen;
@@ -237,7 +242,7 @@ function isClosingFence(line, fence) {
237
242
  // 一遍归一化留不干净,再跑一遍还会改已定稿的正文;所以反复归一化到不动点。
238
243
  // 每遍至少把一个 `…/./—/-` 换成非停顿字符,字符数严格递减,必然收敛。
239
244
  // findings 只留第一遍:同一处不重复计数,column 也仍然是原行的偏移。
240
- function normalizePausePunctuation(line, lineNo, commentOpen) {
245
+ function normalizePausePunctuation(line, lineNo, commentOpen, whitelist) {
241
246
  let current = line;
242
247
  let findings = null;
243
248
  let commentOpenAfter = commentOpen;
@@ -245,7 +250,7 @@ function normalizePausePunctuation(line, lineNo, commentOpen) {
245
250
  for (;;) {
246
251
  const comments = htmlCommentSpans(current, commentOpen);
247
252
  commentOpenAfter = comments.open;
248
- const pass = normalizePausePunctuationPass(current, lineNo, comments.spans);
253
+ const pass = normalizePausePunctuationPass(current, lineNo, comments.spans.concat(styleSpans(current, whitelist)));
249
254
  if (findings === null) findings = pass.findings;
250
255
  if (pass.line === current) break;
251
256
  current = pass.line;
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Book-local only: never inherit an unrelated workspace or another book's choices.
7
+ function loadStyleWhitelist(file) {
8
+ const parent = path.dirname(path.resolve(file));
9
+ const book = path.basename(parent) === '正文' ? path.dirname(parent) : parent;
10
+ try {
11
+ return fs.readFileSync(path.join(book, '.deslop-whitelist'), 'utf8')
12
+ .split(/\r?\n/).map(line => line.trim())
13
+ .filter(line => line && !line.startsWith('#'))
14
+ .sort((a, b) => b.length - a.length);
15
+ } catch (error) {
16
+ if (error.code === 'ENOENT') return [];
17
+ throw error;
18
+ }
19
+ }
20
+
21
+ function styleSpans(text, whitelist) {
22
+ const spans = [];
23
+ for (const literal of whitelist) {
24
+ for (let at = text.indexOf(literal); at !== -1; at = text.indexOf(literal, at + 1)) {
25
+ spans.push([at, at + literal.length]);
26
+ }
27
+ }
28
+ return spans;
29
+ }
30
+
31
+ function maskStyleText(text, whitelist) {
32
+ const chars = text.split('');
33
+ for (const [start, end] of styleSpans(text, whitelist)) {
34
+ chars.fill('?', start, end);
35
+ }
36
+ return chars.join('');
37
+ }
38
+
39
+ module.exports = { loadStyleWhitelist, styleSpans, maskStyleText };
@@ -117,11 +117,26 @@ def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -
117
117
  require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}")
118
118
 
119
119
 
120
+ # 事务校验期间收集全部超长字段,一次报完;None 表示逐条立即报错(初始化、状态读取等路径)。
121
+ _LENGTH_ISSUES: list[str] | None = None
122
+
123
+
124
+ def length_hint(label: str, text: str, max_bytes: int) -> str:
125
+ """把字节上限换算成字数:模型写的是中文,只看得懂「要删几个字」。"""
126
+ size = len(text.encode("utf-8"))
127
+ allowed = max(1, max_bytes * len(text) // size)
128
+ return (f"{label} exceeds {max_bytes} bytes(现 {len(text)} 字,上限约 {allowed} 字,"
129
+ f"至少删 {len(text) - allowed} 字)")
130
+
131
+
120
132
  def clean_text(value: object, label: str, *, allow_empty: bool = False, max_bytes: int = 768) -> str:
121
133
  require(isinstance(value, str), f"{label} must be a string")
122
134
  cleaned = " ".join(value.replace("|", "|").split())
123
135
  require(allow_empty or bool(cleaned), f"{label} must not be empty")
124
- require(len(cleaned.encode("utf-8")) <= max_bytes, f"{label} exceeds {max_bytes} bytes")
136
+ if len(cleaned.encode("utf-8")) > max_bytes:
137
+ if _LENGTH_ISSUES is None:
138
+ raise TrackingError(length_hint(label, cleaned, max_bytes))
139
+ _LENGTH_ISSUES.append(length_hint(label, cleaned, max_bytes))
125
140
  return cleaned
126
141
 
127
142
 
@@ -277,9 +292,15 @@ def archive_retired_tracking_paths(tracking: Path) -> list[str]:
277
292
  return retired
278
293
 
279
294
 
295
+ def _both_blank(position: dict[str, Any]) -> bool:
296
+ return all(isinstance(position.get(key), str) and not position[key].strip() for key in ("story_time", "scene"))
297
+
298
+
280
299
  def validate_position(value: object, label: str = "context.position") -> dict[str, Any]:
281
300
  position = as_mapping(value, label)
282
301
  require_known_keys(position, {"volume", "volume_start_chapter", "story_time", "scene"}, label)
302
+ require(not _both_blank(position),
303
+ f"{label}.story_time and {label}.scene must not be empty: fill where this chapter ends")
283
304
  return {
284
305
  "volume": safe_file_component(position.get("volume"), f"{label}.volume"),
285
306
  "volume_start_chapter": as_int(
@@ -781,7 +802,13 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
781
802
  lines.extend(f"- {item}" for item in retired)
782
803
  payload = "\n".join(lines) + "\n"
783
804
  size = byte_size(payload)
784
- require(size <= DELTA_MAX_BYTES, f"chapter delta is {size} bytes; hard cap is {DELTA_MAX_BYTES}")
805
+ if size > DELTA_MAX_BYTES:
806
+ longest = sorted((line for line in lines if line.startswith("- ")), key=len, reverse=True)[:3]
807
+ allowed = DELTA_MAX_BYTES * len(payload) // size
808
+ raise TrackingError(
809
+ f"chapter delta is {size} bytes; hard cap is {DELTA_MAX_BYTES}(逐章记录现 {len(payload)} 字,"
810
+ f"上限约 {allowed} 字,至少压缩 {len(payload) - allowed} 字;最长几项:"
811
+ + ";".join(f"{line[2:18]}…({len(line) - 2} 字)" for line in longest) + ")")
785
812
  return payload
786
813
 
787
814
 
@@ -901,6 +928,17 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
901
928
 
902
929
 
903
930
  def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
931
+ global _LENGTH_ISSUES
932
+ _LENGTH_ISSUES = []
933
+ try:
934
+ transaction = _normalize_transaction(project, state, document)
935
+ finally:
936
+ issues, _LENGTH_ISSUES = _LENGTH_ISSUES, None
937
+ require(not issues, f"{len(issues)} 个字段超长,一次改完再提交:" + ";".join(issues))
938
+ return transaction
939
+
940
+
941
+ def _normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
904
942
  root = as_mapping(document, "transaction")
905
943
  require_known_keys(
906
944
  root,
@@ -999,6 +1037,13 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
999
1037
  "a revision must resubmit every current context item; retire them in an append transaction instead: "
1000
1038
  + ";".join(sorted(dropped)),
1001
1039
  )
1040
+ kept = set(next_context["long_term_constraints"]) | set(next_context["continuity_risks"])
1041
+ still_listed = sorted(set(transaction["delta"]["retired_context_items"]) & kept)
1042
+ require(
1043
+ not still_listed,
1044
+ "delta.retired_context_items lists items that are still in context; remove them from context: "
1045
+ + ";".join(still_listed),
1046
+ )
1002
1047
  undeclared = sorted(dropped - set(transaction["delta"]["retired_context_items"]))
1003
1048
  require(
1004
1049
  not undeclared,
@@ -1188,6 +1233,126 @@ def check_project(project: Path) -> dict[str, Any]:
1188
1233
  return state
1189
1234
 
1190
1235
 
1236
+ # 事务草稿里各文本字段的字数上限(按全中文估:UTF-8 每字 3 字节),写进提示,免得模型反复试错。
1237
+ DRAFT_LIMITS = {
1238
+ "delta.result": 160, "character_changes[].change": 120, "foreshadow_changes[].summary": 120,
1239
+ "timeline_events[].objective_fact / reader_knowledge": 160, "context.position.story_time / scene": 80,
1240
+ "character_snapshots.*.goal / state": 100, "逐章记录总量": 1024,
1241
+ }
1242
+
1243
+
1244
+ def draft_transaction(project: Path, chapter: int) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
1245
+ """按当前 state 预填一份逐章事务:修订号、模式、章名和整份提交的上下文当前值都填好,
1246
+ 调用方只写本章变化。另返回在场核心角色的当前快照,供有变化时整份改写后放进事务。
1247
+ 新章(append)的故事时间与场景留空:它们必须是本章结束时的位置,沿用上一章的值能静默提交,
1248
+ 所以不预填,上一章的值另行返回作参考。"""
1249
+ state = load_state(project)
1250
+ last = state["last_committed_chapter"]
1251
+ require(1 <= chapter <= last + 1, f"draft chapter must be between 1 and {last + 1}")
1252
+ mode = "append" if chapter == last + 1 else "revision"
1253
+ title = ""
1254
+ for outline in sorted((project / "大纲").glob(f"细纲_第{chapter:03d}章*.md")):
1255
+ match = re.search(rf"^#{{1,4}}\s*第\s*0*{chapter}\s*章\s*[::]\s*(.+?)\s*$",
1256
+ outline.read_text(encoding="utf-8"), re.M)
1257
+ if match:
1258
+ title = match.group(1).strip()
1259
+ break
1260
+ context = json.loads(json.dumps(state["context"], ensure_ascii=False))
1261
+ previous_position = {key: context["position"][key] for key in ("story_time", "scene")}
1262
+ delta: dict[str, Any] = {
1263
+ "result": "", "character_changes": [], "foreshadow_changes": [], "timeline_events": [],
1264
+ "constraints": [], "next_chapter_commitments": [],
1265
+ }
1266
+ if mode == "append":
1267
+ delta.update({"retired_context_items": [], "retired_characters": []})
1268
+ context["position"].update({"story_time": "", "scene": ""})
1269
+ document = {
1270
+ "schema_version": INPUT_SCHEMA_VERSION,
1271
+ "mode": mode,
1272
+ "chapter": chapter,
1273
+ "chapter_title": title,
1274
+ "expected_state_revision": state["state_revision"],
1275
+ "delta": delta,
1276
+ "context": {key: context[key] for key in
1277
+ ("position", "long_term_constraints", "active_character_names", "continuity_risks")},
1278
+ "character_snapshots": {},
1279
+ }
1280
+ snapshots = {name: state["characters"][name] for name in context["active_character_names"]
1281
+ if name in state["characters"]}
1282
+ return document, snapshots, previous_position if mode == "append" else {}
1283
+
1284
+
1285
+ def rebuild_stale_draft(document: dict[str, Any], existing: dict[str, Any], append: bool,
1286
+ current_characters: Any = ()) -> str:
1287
+ """修订号变了:中间提交过别的事务。以当前状态为底,只自动合并确定安全的部分,其余列给调用方核对。
1288
+
1289
+ 新章草稿之间只可能插进修订事务,修订不能删约束与风险条目,所以当前约束与风险覆盖旧底稿:
1290
+ 旧草稿多出来的就是本章新增,delta 里声明退役的就是本章删除,可以精确合并。
1291
+ 修订草稿之间可能插进新章(新章能退役条目),在场角色名修订也能删,都无法判断谁新:
1292
+ 取当前状态,差异列出来;只有本章带了快照的新角色名自动加回。"""
1293
+ def items(value: object) -> list[str]:
1294
+ return [item for item in value if isinstance(item, str)] if isinstance(value, list) else []
1295
+
1296
+ def norm(item: str) -> str:
1297
+ return " ".join(item.replace("|", "|").split())
1298
+
1299
+ notes = ["修订号已变:context 按当前状态重建,本章已填的 delta" + ("、新增与退役的约束、结尾位置" if append else "")
1300
+ + "已合并"]
1301
+ for key in ("delta", "character_snapshots"):
1302
+ if isinstance(existing.get(key), dict):
1303
+ document[key] = existing[key]
1304
+ if existing.get("chapter_title"):
1305
+ document["chapter_title"] = existing["chapter_title"]
1306
+ old_context = existing.get("context") if isinstance(existing.get("context"), dict) else {}
1307
+ delta = document["delta"] if isinstance(document["delta"], dict) else {}
1308
+ context = document["context"]
1309
+ retired = {norm(item) for item in items(delta.get("retired_context_items"))}
1310
+ left_out: list[str] = []
1311
+ for key in ("long_term_constraints", "continuity_risks"):
1312
+ current = {norm(item) for item in context[key]}
1313
+ extra = []
1314
+ for item in items(old_context.get(key)):
1315
+ if norm(item) not in current:
1316
+ current.add(norm(item))
1317
+ extra.append(item)
1318
+ if append:
1319
+ context[key] = [item for item in context[key] if norm(item) not in retired] + extra
1320
+ else:
1321
+ left_out += extra
1322
+ snapshots = document["character_snapshots"] if isinstance(document["character_snapshots"], dict) else {}
1323
+ names = context["active_character_names"]
1324
+ for name in items(old_context.get("active_character_names")):
1325
+ if name in names:
1326
+ continue
1327
+ if append and name in snapshots:
1328
+ names.append(name)
1329
+ else:
1330
+ left_out.append(f"在场角色 {name}")
1331
+ names[:] = [name for name in names if name not in set(items(delta.get("retired_characters")))]
1332
+ known = set(current_characters)
1333
+ for name in list(snapshots):
1334
+ if name not in known and name not in names:
1335
+ snapshots.pop(name)
1336
+ left_out.append(f"角色快照 {name}")
1337
+ if left_out:
1338
+ notes.append("草稿里有、当前状态没有的条目没有自动带回(可能已被别的提交退役),本章确实需要就加回:"
1339
+ + ";".join(left_out))
1340
+ old_position = old_context.get("position") if isinstance(old_context.get("position"), dict) else {}
1341
+ position = context["position"]
1342
+ if append:
1343
+ for key in ("story_time", "scene"):
1344
+ if isinstance(old_position.get(key), str) and old_position[key].strip():
1345
+ position[key] = old_position[key]
1346
+ differs = [f"{key}:草稿写的是「{old_position[key]}」,当前状态是「{position[key]}」"
1347
+ for key in ("volume", "volume_start_chapter")
1348
+ if key in old_position and old_position[key] != position[key]]
1349
+ if differs:
1350
+ notes.append("卷信息与当前状态不同,已取当前状态,本章确实换卷就改回(" + ";".join(differs) + ")")
1351
+ if snapshots:
1352
+ notes.append("带过来的角色快照(" + "、".join(snapshots) + ")可能被中间的提交改过,逐个对照 current_snapshots 重核")
1353
+ return ";".join(notes)
1354
+
1355
+
1191
1356
  def build_parser() -> argparse.ArgumentParser:
1192
1357
  parser = argparse.ArgumentParser(description=__doc__)
1193
1358
  subparsers = parser.add_subparsers(dest="command", required=True)
@@ -1197,12 +1362,55 @@ def build_parser() -> argparse.ArgumentParser:
1197
1362
  subparser.add_argument("--input", type=Path, required=True, help="UTF-8 JSON input document")
1198
1363
  check_parser = subparsers.add_parser("check")
1199
1364
  check_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1365
+ draft_parser = subparsers.add_parser("draft", help="write a pre-filled chapter transaction to fill in")
1366
+ draft_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1367
+ draft_parser.add_argument("--chapter", type=int, required=True)
1368
+ draft_parser.add_argument("--out", type=Path, help="default: <project>/.story/work/第NNN章/tracking.json")
1369
+ draft_parser.add_argument("--force", action="store_true",
1370
+ help="discard an existing draft instead of only refreshing its revision")
1200
1371
  return parser
1201
1372
 
1202
1373
 
1203
1374
  def main() -> int:
1204
1375
  args = build_parser().parse_args()
1205
1376
  try:
1377
+ if args.command == "draft":
1378
+ document, snapshots, previous_position = draft_transaction(args.project, args.chapter)
1379
+ out = args.out or args.project / ".story" / "work" / f"第{args.chapter:03d}章" / "tracking.json"
1380
+ out.parent.mkdir(parents=True, exist_ok=True)
1381
+ refreshed = ""
1382
+ if out.exists() and not args.force:
1383
+ # 重跑 draft 不冲掉已经填好的变化。修订号变了说明中间提交过别的事务:context 按当前
1384
+ # 状态重建(沿用旧 context 会把那次变更整份覆盖回去),只带过本章自己填的部分。
1385
+ existing = read_json(out)
1386
+ require(isinstance(existing, dict) and existing.get("chapter") == document["chapter"]
1387
+ and existing.get("mode") == document["mode"],
1388
+ f"{out} already holds a draft for a different chapter or mode; pass --force to replace it")
1389
+ if existing.get("expected_state_revision") == document["expected_state_revision"]:
1390
+ document, refreshed = existing, "已有草稿且修订号未变,原样保留"
1391
+ else:
1392
+ refreshed = rebuild_stale_draft(document, existing, bool(previous_position),
1393
+ load_state(args.project)["characters"])
1394
+ out.write_text(json.dumps(document, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
1395
+ fill = ("只填 delta 里本章的变化;context 其余字段已是当前值,要撤下的长期约束或连贯性风险从 context 删掉并把原文放进 "
1396
+ "delta.retired_context_items(仅 append);本章有变化的核心角色把下面的当前快照整份改好放进 character_snapshots,"
1397
+ "并在 character_changes 写一句变化。不要从脚本源码或 state 文件里另找格式。")
1398
+ blank = not (document["context"]["position"].get("story_time") or document["context"]["position"].get("scene"))
1399
+ if previous_position and blank:
1400
+ fill = ("context.position 的 story_time 与 scene 留空,填本章结束时的故事时间与场景(上一章结束时见 "
1401
+ "previous_position;换卷时连同 volume 与 volume_start_chapter 一起改)。") + fill
1402
+ payload = {
1403
+ "draft": str(out),
1404
+ "mode": document["mode"],
1405
+ "expected_state_revision": document["expected_state_revision"],
1406
+ "fill": (f"{refreshed};要从头生成加 --force。" if refreshed else "") + fill,
1407
+ "limits_chars": DRAFT_LIMITS,
1408
+ "current_snapshots": snapshots,
1409
+ }
1410
+ if previous_position:
1411
+ payload["previous_position"] = previous_position
1412
+ emit(json.dumps(payload, ensure_ascii=False))
1413
+ return 0
1206
1414
  if args.command == "init":
1207
1415
  result = initialize(args.project, read_json(args.input))
1208
1416
  elif args.command == "commit":
@@ -172,6 +172,9 @@ def target_from_outline(value: str) -> int:
172
172
 
173
173
 
174
174
  def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
175
+ # 改稿流程把原文备份为 第N章_章名_原稿_{日期}.md,与 hooks 一样不把它当本章正文。
176
+ if not outline and "_原稿_" in name:
177
+ return None
175
178
  pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\-  ].*)?\.md$"
176
179
  match = re.match(pattern, name)
177
180
  return int(match.group(1)) if match else None