@oh-story/dsh 0.1.3 → 0.1.5

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 (524) hide show
  1. package/README.md +58 -12
  2. package/lib/client.js +2970 -537
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +218 -178
  5. package/lib/drama/skills/short-drama/SKILL.md +110 -117
  6. package/lib/drama/skills/short-drama/references/audience-reveal.md +14 -67
  7. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +26 -77
  8. package/lib/drama/skills/short-drama/references/creator-documents.md +204 -0
  9. package/lib/drama/skills/short-drama/references/creator-workflow.md +73 -63
  10. package/lib/drama/skills/short-drama/references/knowhow-index.md +5 -0
  11. package/lib/drama/skills/short-drama/references/look-development.md +2 -3
  12. package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +14 -48
  13. package/lib/drama/skills/short-drama/references/production-form-profiles.md +1 -1
  14. package/lib/drama/skills/short-drama/references/reference-roles.md +32 -135
  15. package/lib/drama/skills/short-drama/references/runtime-preflight.md +16 -19
  16. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  17. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  18. package/lib/drama/skills/short-drama-assets/SKILL.md +59 -170
  19. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +18 -25
  20. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +13 -15
  21. package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +11 -14
  22. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  23. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +6 -12
  24. package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +8 -11
  25. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +21 -2
  26. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +12 -105
  27. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +12 -32
  28. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  29. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +45 -155
  30. package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +12 -13
  31. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +12 -13
  32. package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +39 -51
  33. package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +3 -4
  34. package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +15 -14
  35. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +11 -15
  36. package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +1 -1
  37. package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +4 -7
  38. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +23 -23
  39. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +8 -65
  40. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  41. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  42. package/lib/drama/skills/short-drama-produce/SKILL.md +67 -15
  43. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  44. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  45. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  46. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  47. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  48. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  49. package/lib/drama/skills/short-drama-review/SKILL.md +61 -123
  50. package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +10 -15
  51. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +15 -16
  52. package/lib/drama/skills/short-drama-review/references/project-calibration.md +7 -9
  53. package/lib/drama/skills/short-drama-review/references/review-method.md +8 -9
  54. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +9 -13
  55. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +104 -4
  56. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +27 -19
  57. package/lib/drama/skills/short-drama-review/references/stage-contract.md +4 -63
  58. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  59. package/lib/drama/skills/short-drama-storyboard/SKILL.md +116 -238
  60. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  61. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +9 -34
  62. package/lib/drama/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
  63. package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +5 -10
  64. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +12 -7
  65. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  66. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +27 -62
  67. package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +16 -25
  68. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +21 -23
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +8 -10
  70. package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +9 -13
  71. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +16 -71
  72. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  73. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +123 -230
  74. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  75. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +10 -8
  77. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +24 -6
  78. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +2 -2
  79. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  80. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +35 -27
  81. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -11
  82. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +13 -10
  83. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +34 -63
  84. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  85. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  86. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +17 -73
  87. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  88. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  89. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  90. package/lib/drama/skills/short-drama-write/SKILL.md +74 -268
  91. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +7 -0
  92. package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +17 -43
  93. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +15 -15
  94. package/lib/drama/skills/short-drama-write/references/script-craft.md +208 -21
  95. package/lib/drama/skills/short-drama-write/references/stage-contract.md +11 -70
  96. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  97. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  98. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  99. package/lib/index.js +971 -46
  100. package/lib/index.js.map +4 -4
  101. package/lib/novel-to-game/LICENSE +21 -0
  102. package/lib/novel-to-game/README.md +180 -0
  103. package/lib/novel-to-game/README_ZH.md +178 -0
  104. package/lib/novel-to-game/VERSION +1 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  128. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  129. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  130. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  131. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  132. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  133. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  134. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  135. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  136. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  137. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  138. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  139. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  140. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  141. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  142. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  143. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  144. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  145. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  146. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  147. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  148. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  149. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  150. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  151. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  152. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  153. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  154. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  155. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  156. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  157. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  158. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  159. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  160. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  161. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  162. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  163. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  164. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  165. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  166. package/lib/novel-to-game/manifest.json +508 -0
  167. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  168. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  169. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  170. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  171. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  172. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  173. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  174. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  175. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  176. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  177. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  178. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  179. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  180. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  181. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  182. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  183. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  184. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  185. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  186. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  187. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  188. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  189. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  190. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  191. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  192. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  193. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  194. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  195. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  196. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  197. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  198. package/lib/oh-story/manifest.json +419 -309
  199. package/lib/oh-story/roles/character-designer.md +4 -4
  200. package/lib/oh-story/roles/consistency-checker.md +1 -1
  201. package/lib/oh-story/roles/narrative-writer.md +41 -42
  202. package/lib/oh-story/roles/story-architect.md +19 -23
  203. package/lib/oh-story/roles/story-explorer.md +8 -5
  204. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  205. package/lib/oh-story/skills/story/SKILL.md +16 -9
  206. package/lib/oh-story/skills/story/VERSION +1 -1
  207. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  208. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  209. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  210. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  211. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  212. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  213. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  214. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  215. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  216. package/lib/oh-story/skills/story-import/SKILL.md +21 -8
  217. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  218. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  219. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  220. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  221. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  222. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  223. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  224. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  225. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  226. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  227. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  228. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  229. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  230. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  232. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  236. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  237. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  238. package/lib/oh-story/skills/story-long-write/references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
  239. package/lib/oh-story/skills/story-long-write/references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  243. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  244. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  245. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  246. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +5 -4
  247. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  248. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +17 -11
  249. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  250. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  251. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  252. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  253. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  254. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  255. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  256. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  257. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  258. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  259. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  260. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  261. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  262. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  263. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  264. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  265. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  266. package/lib/oh-story/skills/story-setup/SKILL.md +85 -41
  267. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  268. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  269. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  270. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  271. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  272. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  273. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  274. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  275. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  276. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  277. package/lib/oh-story/skills/story-setup/references/agent-references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
  278. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  279. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-setup/references/agent-references/long-reversal.md} +18 -32
  280. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  281. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  282. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  283. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  284. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  285. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → short-reversal.md} +29 -23
  286. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  287. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  288. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  289. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  290. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  291. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  292. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  293. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  294. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  295. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  296. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  297. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  298. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  299. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  300. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  301. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  302. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  303. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  304. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  305. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  306. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  307. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  308. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  309. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  310. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  311. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  312. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  313. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  314. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
  315. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  316. 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
  317. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
  318. 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
  319. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  320. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
  321. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
  322. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  323. 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
  324. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  325. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  326. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  327. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  328. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  329. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  330. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  331. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  332. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  333. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  334. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  335. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  336. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  337. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  338. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  339. package/lib/types/client/drama-production-view.d.ts +34 -0
  340. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  341. package/lib/types/client/drama-production.d.ts +92 -0
  342. package/lib/types/client/drama-production.d.ts.map +1 -0
  343. package/lib/types/client/file-activity.d.ts +9 -2
  344. package/lib/types/client/file-activity.d.ts.map +1 -1
  345. package/lib/types/client/index.d.ts +1 -1
  346. package/lib/types/client/index.d.ts.map +1 -1
  347. package/lib/types/client/production-intents.d.ts +9 -0
  348. package/lib/types/client/production-intents.d.ts.map +1 -0
  349. package/lib/types/client/production-prompts.d.ts +9 -0
  350. package/lib/types/client/production-prompts.d.ts.map +1 -0
  351. package/lib/types/client/production-runtime.d.ts +58 -0
  352. package/lib/types/client/production-runtime.d.ts.map +1 -0
  353. package/lib/types/client/video-studio.d.ts +41 -0
  354. package/lib/types/client/video-studio.d.ts.map +1 -0
  355. package/lib/types/client/workbench-ui.d.ts +5 -0
  356. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  357. package/lib/types/game-verification.d.ts +16 -0
  358. package/lib/types/game-verification.d.ts.map +1 -0
  359. package/lib/types/index.d.ts +5 -2
  360. package/lib/types/index.d.ts.map +1 -1
  361. package/lib/types/production-intent.d.ts +21 -0
  362. package/lib/types/production-intent.d.ts.map +1 -0
  363. package/lib/types/production-tool.d.ts +5 -0
  364. package/lib/types/production-tool.d.ts.map +1 -0
  365. package/lib/types/reference-tool.d.ts +6 -0
  366. package/lib/types/reference-tool.d.ts.map +1 -0
  367. package/lib/types/role-provider.d.ts.map +1 -1
  368. package/lib/types/role-tool.d.ts +3 -1
  369. package/lib/types/role-tool.d.ts.map +1 -1
  370. package/lib/types/skill-provider.d.ts +7 -0
  371. package/lib/types/skill-provider.d.ts.map +1 -1
  372. package/lib/types/video-project.d.ts +42 -0
  373. package/lib/types/video-project.d.ts.map +1 -0
  374. package/lib/types/workspace-request-trust.d.ts +6 -0
  375. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  376. package/lib/types/workspace-route.d.ts +20 -0
  377. package/lib/types/workspace-route.d.ts.map +1 -1
  378. package/lib/video-recap/CHANGELOG.md +282 -0
  379. package/lib/video-recap/LICENSE +21 -0
  380. package/lib/video-recap/README.en.md +257 -0
  381. package/lib/video-recap/README.md +255 -0
  382. package/lib/video-recap/manifest.json +684 -0
  383. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  384. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  385. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  386. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  387. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  388. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  389. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  390. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  391. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  392. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  393. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  394. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  395. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  396. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  397. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  398. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  399. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  400. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  401. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  402. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  403. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  404. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  405. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  406. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  407. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  408. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  409. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  410. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  411. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  412. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  413. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  414. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  415. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  416. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  417. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  418. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  419. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  420. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  421. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  422. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  423. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  424. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  425. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  426. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  427. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  428. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  429. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  430. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  431. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  432. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  433. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  435. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  436. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  437. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  438. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  439. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  440. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  441. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  442. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  443. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  444. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  445. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  446. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  447. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  448. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  449. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  450. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  451. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  452. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  453. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  454. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  455. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  456. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  457. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  458. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  459. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  460. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  461. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  462. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  463. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  464. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  465. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  466. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  467. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  468. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  469. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  470. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  471. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  472. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  473. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  474. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  475. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  476. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  477. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  478. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  479. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  480. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  481. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  482. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  483. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  484. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  485. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  486. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  487. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  488. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  489. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  490. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  491. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  492. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  493. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  494. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  495. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  496. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  497. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  498. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  499. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  500. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  501. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  502. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  503. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  504. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  505. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  506. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  507. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  508. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  509. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  510. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  511. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  512. package/package.json +57 -41
  513. package/lib/drama/skills/short-drama/references/lifecycle-commands.md +0 -130
  514. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  515. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  516. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  517. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  518. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  519. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  520. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  521. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  522. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  523. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  524. /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
@@ -13,6 +13,7 @@ Detect high-risk AI-flavor prose patterns that need human rewrite:
13
13
  - repeated negative setup followed by positive flip
14
14
  - em-dash (按功能改写), 碎句号 (连续短叙述句), 长段落 (按镜头断段)
15
15
  - 微动作复读 (「了下/了一下」式轻量补语高密度,电报体指纹)
16
+ - 套式反应细节 (指尖/指节/目光等无功能微动作与「平静得像在念」式语气比喻成片)
16
17
  - 抽象总结复读 (命运/棋局/这一刻终于明白/才刚刚开始,AI 结尾腔)
17
18
  - 套词密度过高 (仿佛/一丝/深吸一口气/平静无波等禁用词聚集)
18
19
  - 比喻密度过高 (像/好像/仿佛/如同等比喻标记成片复现)
@@ -30,7 +31,7 @@ Detect high-risk AI-flavor prose patterns that need human rewrite:
30
31
  - 引号强调滥用 (叙述里 1-4 字短词加引号强调,密度型)
31
32
 
32
33
  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.
33
- 或 advisory (period-stutter / long-paragraph / micro-action-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 的长推理/氛围段可保留)。
34
+ 或 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 的长推理/氛围段可保留)。
34
35
  --fail-on=blocking 只在出现 blocking finding 时退出 1;默认 --fail-on=all 有任何 finding 即退出 1。
35
36
 
36
37
  The script reports findings only. It never rewrites text, because the safe fix is
@@ -57,6 +58,26 @@ const MICRO_TIC_PATTERN = /了(?:[一两三几半])?[下阵圈道声眼口气会
57
58
  const MICRO_TIC_MIN_HITS = 5;
58
59
  const MICRO_TIC_PER_KILO = 6;
59
60
 
61
+ // 套式反应细节:不是禁写身体,而是提示成片出现的“部位 + 轻微动作/状态”、
62
+ // “胸口像被撞了一下”、喉结/眼圈/声音放轻等通用情绪尾巴,以及“平静语气 +
63
+ // 像在念/宣判”模板。此类句子词面变化大,不能逐词 blocking;按章聚集到 4 处才
64
+ // advisory,要求逐处做删除测试。正常受伤、打斗、生理反应若承担物理后果可保留。
65
+ const STOCK_REACTION_PATTERNS = [
66
+ /(?:指尖|手指|指节|手背|掌心|拳头|袖口|衣角|裙角|下唇|嘴唇|唇角|嘴角|眉头|眼底|眸光|目光|视线|肩膀|呼吸)[^。!?!?\n]{0,16}(?:轻轻|微微|缓缓|悄然|不自觉|无意识|下意识|攥紧|握紧|收紧|绞紧|泛白|发白|叩|敲|摩挲|抿紧|抿成|移开|垂下|躲开|一颤|颤了?一下|停了?一下|顿了?一下)/g,
67
+ /(?:语气|声音)[^。!?!?\n]{0,12}(?:平静|冷静|平淡|冷淡|淡漠|平直)[^。!?!?\n]{0,12}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:念|读|报|说|陈述|宣判|背诵)/g,
68
+ /(?:胸口|心口)[^。!?!?\n]{0,16}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:撞|锤|压|攥|堵)[^。!?!?\n]{0,8}(?:一下|一记|一拳)?/g,
69
+ /(?:声音|嗓音|语气)[^。!?!?\n]{0,12}(?:放轻|压低|发紧|发颤|很轻|轻了些)/g,
70
+ /(?:喉结|喉头|喉咙)[^。!?!?\n]{0,10}(?:滚|动|紧|堵|发涩|发干)/g,
71
+ /(?:眼眶|眼圈|鼻子)[^。!?!?\n]{0,8}(?:发红|红了|发热|发酸|一酸)/g,
72
+ /(?:抿了?下唇|抿了?抿唇|抿了?下嘴|抿着笑)/g,
73
+ ];
74
+ const STOCK_REACTION_MIN_HITS = 4;
75
+ // 校准(真人语料,<br> 已还原为换行):qimao 长篇 5584 章 + heiyan 短篇整篇 3983 篇。
76
+ // 长篇章尺度(中位约 2100 字)per-kilo 1.0→1.5 误报 0.43%→0.39%,几乎不动;
77
+ // 短篇整篇 8000-20000 字下 MIN_HITS 形同虚设、只剩密度门,1.0 时误报 5.57%,
78
+ // 1.5 降到 1.46%。故取 1.5,把两个总体拉到同一量级(四份副本共用一组阈值)。
79
+ const STOCK_REACTION_PER_KILO = 1.5;
80
+
60
81
  // 监控摄像头式动作清单:同一段连续堆叠通用动作动词(伸手/拿起/取过/挑开/放下/转身等),
61
82
  // 且用逗号/顿号串联成步骤表时,读感像无视角温度的监控记录。只做 advisory;
62
83
  // 打斗/追逐等功能性动作编排可保留或人工复核。
@@ -406,6 +427,7 @@ function scanProsePatterns(proseLines) {
406
427
  findings.push(...findQuoteEmphasisTic(proseLines));
407
428
  findings.push(...findPeriodStutter(proseLines));
408
429
  findings.push(...findMicroActionTic(proseLines));
430
+ findings.push(...findStockReactionTic(proseLines));
409
431
  findings.push(...findActionListTic(proseLines));
410
432
  findings.push(...findAbstractSummaryTic(proseLines));
411
433
  findings.push(...findClicheDensityTic(proseLines));
@@ -700,6 +722,47 @@ function findMicroActionTic(proseLines) {
700
722
  }];
701
723
  }
702
724
 
725
+ // 套式反应细节:统计引号外叙述中通用的部位/声线反应与固定语气比喻。
726
+ // 这是删除测试的候选集,不是身体描写黑名单;全篇只报一条,保留有动作后果、
727
+ // 伤势、人物习惯或情节功能的细节。
728
+ function findStockReactionTic(proseLines) {
729
+ let hits = 0;
730
+ let narrativeChars = 0;
731
+ let firstLine = null;
732
+ const samples = [];
733
+
734
+ for (const { text, lineNo } of proseLines) {
735
+ const trimmed = text.trim();
736
+ if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
737
+ const narrative = stripQuoted(trimmed);
738
+ narrativeChars += visibleLength(narrative);
739
+
740
+ for (const pattern of STOCK_REACTION_PATTERNS) {
741
+ pattern.lastIndex = 0;
742
+ let match;
743
+ while ((match = pattern.exec(narrative)) !== null) {
744
+ hits += 1;
745
+ if (firstLine === null) firstLine = lineNo;
746
+ const sample = sentenceAround(narrative, match.index);
747
+ if (samples.length < 6 && sample && !samples.includes(sample)) samples.push(sample);
748
+ }
749
+ }
750
+ }
751
+
752
+ if (narrativeChars === 0 || hits < STOCK_REACTION_MIN_HITS) return [];
753
+ const perKilo = (hits / narrativeChars) * 1000;
754
+ if (perKilo < STOCK_REACTION_PER_KILO) return [];
755
+
756
+ return [{
757
+ line: firstLine,
758
+ column: 1,
759
+ type: 'stock-reaction-tic',
760
+ severity: 'advisory',
761
+ message: `套式反应细节:指尖/指节/喉结/眼圈/声音放轻等通用反应或“平静得像在念”式语气比喻 ${hits} 处(${perKilo.toFixed(1)}/千字);逐处做删除测试,只标注情绪、不改变选择、关系、物件或动作结果的删掉,不要换部位或同义动作。`,
762
+ excerpt: compact(samples.join(' | ')),
763
+ }];
764
+ }
765
+
703
766
  function findActionListTic(proseLines) {
704
767
  const findings = [];
705
768
 
@@ -3,25 +3,40 @@
3
3
 
4
4
  The language model supplies compact semantic JSON. This tool validates and
5
5
  merges that input in memory, renders every derived view, then atomically writes
6
- ``_tracking-state.json`` last as the single commit point. One book project has
7
- one serial writer; concurrent commits are intentionally unsupported.
6
+ ``_tracking-state.json`` last as the single commit point. Per-project locking
7
+ serializes concurrent writers before revision and wordcount checks.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import copy
14
+ import importlib.util
14
15
  import json
15
16
  import os
16
17
  import re
17
18
  import stat
18
19
  import sys
19
20
  import tempfile
21
+ import time
20
22
  import unicodedata
23
+ from contextlib import contextmanager
21
24
  from pathlib import Path
22
25
  from typing import Any
23
26
 
24
27
 
28
+ _WORDCOUNT_CORE_PATH = Path(__file__).with_name("wordcount_core.py")
29
+ if not _WORDCOUNT_CORE_PATH.is_file(): # pragma: no cover - broken deployment
30
+ raise RuntimeError("TOOL_UNAVAILABLE: wordcount_core.py")
31
+ _WORDCOUNT_CORE_SPEC = importlib.util.spec_from_file_location(
32
+ "story_wordcount_core", _WORDCOUNT_CORE_PATH
33
+ )
34
+ if _WORDCOUNT_CORE_SPEC is None or _WORDCOUNT_CORE_SPEC.loader is None: # pragma: no cover
35
+ raise RuntimeError("unable to load wordcount core")
36
+ wordcount_core = importlib.util.module_from_spec(_WORDCOUNT_CORE_SPEC)
37
+ _WORDCOUNT_CORE_SPEC.loader.exec_module(wordcount_core)
38
+
39
+
25
40
  INPUT_SCHEMA_VERSION = 1
26
41
  TRACKING_SCHEMA_VERSION = 4
27
42
  DELTA_TARGET_BYTES = 1536
@@ -74,6 +89,13 @@ def require(condition: bool, message: str) -> None:
74
89
  raise TrackingError(message)
75
90
 
76
91
 
92
+ def wordcount_value(function: Any, *args: Any, **kwargs: Any) -> Any:
93
+ try:
94
+ return function(*args, **kwargs)
95
+ except wordcount_core.WordcountError as exc:
96
+ raise TrackingError(str(exc)) from exc
97
+
98
+
77
99
  def as_mapping(value: object, label: str) -> dict[str, Any]:
78
100
  require(isinstance(value, dict), f"{label} must be a JSON object")
79
101
  return value
@@ -188,6 +210,34 @@ def state_path(project: Path) -> Path:
188
210
  return tracking_root(project) / "_tracking-state.json"
189
211
 
190
212
 
213
+ @contextmanager
214
+ def project_write_lock(project: Path, *, timeout_seconds: float = 10.0):
215
+ tracking = tracking_root(project)
216
+ tracking.mkdir(parents=True, exist_ok=True)
217
+ path = tracking / ".tracking-commit.lock"
218
+ deadline = time.monotonic() + timeout_seconds
219
+ descriptor: int | None = None
220
+ while descriptor is None:
221
+ try:
222
+ descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
223
+ except FileExistsError:
224
+ if time.monotonic() >= deadline:
225
+ raise TrackingError(
226
+ "tracking commit lock is busy or stale; retry, or remove 追踪/.tracking-commit.lock after confirming no commit is running"
227
+ )
228
+ time.sleep(0.05)
229
+ try:
230
+ os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
231
+ os.fsync(descriptor)
232
+ yield
233
+ finally:
234
+ os.close(descriptor)
235
+ try:
236
+ path.unlink()
237
+ except FileNotFoundError:
238
+ pass
239
+
240
+
191
241
  def delta_path(tracking: Path, chapter: int) -> Path:
192
242
  width = max(3, len(str(chapter)))
193
243
  return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
@@ -735,6 +785,18 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
735
785
  return payload
736
786
 
737
787
 
788
+ def normalize_wordcount_records(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
789
+ records = as_mapping(value, "tracking state.wordcount_records")
790
+ normalized: dict[str, dict[str, Any]] = {}
791
+ for raw_chapter, raw_record in records.items():
792
+ require(isinstance(raw_chapter, str) and re.fullmatch(r"[1-9]\d*", raw_chapter) is not None,
793
+ "wordcount record chapter key is invalid")
794
+ chapter = int(raw_chapter)
795
+ require(chapter <= last_chapter, "wordcount record exceeds last committed chapter")
796
+ normalized[raw_chapter] = wordcount_value(wordcount_core.normalize_wordcount_record, raw_record)
797
+ return normalized
798
+
799
+
738
800
  def normalize_state(document: object) -> dict[str, Any]:
739
801
  root = as_mapping(document, "tracking state")
740
802
  require_known_keys(
@@ -742,6 +804,7 @@ def normalize_state(document: object) -> dict[str, Any]:
742
804
  {
743
805
  "schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
744
806
  "state_revision", "context", "characters", "foreshadow", "timeline",
807
+ "wordcount_records",
745
808
  },
746
809
  "tracking state",
747
810
  )
@@ -766,16 +829,19 @@ def normalize_state(document: object) -> dict[str, Any]:
766
829
  if last_chapter == 0:
767
830
  require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
768
831
  require(not timeline, "a chapter-0 project cannot have established timeline facts")
832
+ state_revision = as_int(root.get("state_revision"), "tracking state.state_revision")
833
+ wordcount_records = normalize_wordcount_records(root.get("wordcount_records", {}), last_chapter)
769
834
  return {
770
835
  "schema_version": TRACKING_SCHEMA_VERSION,
771
836
  "book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
772
837
  "last_committed_chapter": last_chapter,
773
838
  "imported_through_chapter": imported_through,
774
- "state_revision": as_int(root.get("state_revision"), "tracking state.state_revision"),
839
+ "state_revision": state_revision,
775
840
  "context": context,
776
841
  "characters": characters,
777
842
  "foreshadow": foreshadow,
778
843
  "timeline": timeline,
844
+ "wordcount_records": wordcount_records,
779
845
  }
780
846
 
781
847
 
@@ -789,7 +855,10 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
789
855
  root = as_mapping(document, "init input")
790
856
  require_known_keys(
791
857
  root,
792
- {"schema_version", "book_title", "last_chapter", "context", "character_snapshots", "foreshadow", "timeline_events"},
858
+ {
859
+ "schema_version", "book_title", "last_chapter", "context", "character_snapshots",
860
+ "foreshadow", "timeline_events",
861
+ },
793
862
  "init input",
794
863
  )
795
864
  require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
@@ -826,17 +895,18 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
826
895
  "characters": snapshots,
827
896
  "foreshadow": foreshadow,
828
897
  "timeline": timeline,
898
+ "wordcount_records": {},
829
899
  }
830
900
  )
831
901
 
832
902
 
833
- def normalize_transaction(state: dict[str, Any], document: object) -> dict[str, Any]:
903
+ def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
834
904
  root = as_mapping(document, "transaction")
835
905
  require_known_keys(
836
906
  root,
837
907
  {
838
908
  "schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
839
- "delta", "context", "character_snapshots",
909
+ "delta", "context", "character_snapshots", "wordcount",
840
910
  },
841
911
  "transaction",
842
912
  )
@@ -845,6 +915,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
845
915
  require(mode in {"append", "revision"}, "mode must be append or revision")
846
916
  chapter = as_int(root.get("chapter"), "chapter", minimum=1)
847
917
  expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
918
+ wordcount_input = root.get("wordcount")
848
919
  require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
849
920
  last = state["last_committed_chapter"]
850
921
  if mode == "append":
@@ -864,6 +935,11 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
864
935
  snapshots=snapshots,
865
936
  existing_core_names=existing_names,
866
937
  )
938
+ wordcount = None
939
+ if wordcount_input is not None:
940
+ wordcount = wordcount_value(
941
+ wordcount_core.validate_current_wordcount_record, project, chapter, wordcount_input
942
+ )
867
943
  return {
868
944
  "mode": mode,
869
945
  "chapter": chapter,
@@ -871,6 +947,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
871
947
  "delta": delta,
872
948
  "context": context,
873
949
  "snapshots": snapshots,
950
+ "wordcount": wordcount,
874
951
  }
875
952
 
876
953
 
@@ -891,6 +968,8 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
891
968
  next_state["last_committed_chapter"] = chapter
892
969
  next_state["state_revision"] += 1
893
970
  next_state["characters"].update(transaction["snapshots"])
971
+ if transaction["wordcount"] is not None:
972
+ next_state["wordcount_records"][str(chapter)] = transaction["wordcount"]
894
973
 
895
974
  next_context = transaction["context"]
896
975
  # 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
@@ -1013,7 +1092,7 @@ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
1013
1092
  )
1014
1093
 
1015
1094
 
1016
- def initialize(project: Path, document: object) -> dict[str, Any]:
1095
+ def _initialize_locked(project: Path, document: object) -> dict[str, Any]:
1017
1096
  tracking = tracking_root(project)
1018
1097
  require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
1019
1098
  state = normalize_initial_document(document)
@@ -1036,11 +1115,16 @@ def initialize(project: Path, document: object) -> dict[str, Any]:
1036
1115
  return state
1037
1116
 
1038
1117
 
1039
- def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1118
+ def initialize(project: Path, document: object) -> dict[str, Any]:
1119
+ with project_write_lock(project):
1120
+ return _initialize_locked(project, document)
1121
+
1122
+
1123
+ def _apply_transaction_locked(project: Path, document: object) -> dict[str, Any]:
1040
1124
  tracking = tracking_root(project)
1041
1125
  require_no_retired_tracking_paths(tracking)
1042
1126
  state = load_state(project)
1043
- transaction = normalize_transaction(state, document)
1127
+ transaction = normalize_transaction(project, state, document)
1044
1128
  next_state = merge_transaction(state, transaction)
1045
1129
 
1046
1130
  delta_payload = render_delta(
@@ -1067,6 +1151,11 @@ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1067
1151
  return next_state
1068
1152
 
1069
1153
 
1154
+ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1155
+ with project_write_lock(project):
1156
+ return _apply_transaction_locked(project, document)
1157
+
1158
+
1070
1159
  def check_project(project: Path) -> dict[str, Any]:
1071
1160
  tracking = tracking_root(project)
1072
1161
  require_no_retired_tracking_paths(tracking)
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """Small deterministic wordcount core shared by storyctl and tracking."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ WORDCOUNT_SCHEMA = "story-wordcount-result/v1"
13
+ MEASUREMENT_SCHEMA = "story-wordcount-measurement/v1"
14
+ CHECKPOINT_SCHEMA = "story-wordcount-checkpoint/v1"
15
+ METRIC = "visible_chars_v1"
16
+ RESOLUTIONS = frozenset({"within_user_band", "accepted_current_length"})
17
+
18
+ _WHITE_SPACE_CODEPOINTS = frozenset(
19
+ [*range(0x0009, 0x000E)]
20
+ + [
21
+ 0x0020, 0x0085, 0x00A0, 0x1680, *range(0x2000, 0x200B),
22
+ 0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
23
+ ]
24
+ )
25
+ _FRONTMATTER_KEY_RE = re.compile(r"^[A-Za-z_\u3400-\u9FFF][^:\n]{0,80}:[ \t]*(?:.*)$")
26
+ _LEADING_BLANK_RE = re.compile(r"^[\u0009\u0020\u3000]*$")
27
+ _ATX_HEADING_RE = re.compile(r"^[\u0009\u0020]{0,3}#{1,6}[\u0009\u0020]+\S")
28
+ _POSITIVE_INTEGER_RE = re.compile(r"^[1-9]\d*$")
29
+ _TARGET_LINE_RE = re.compile(r"^[ \t>*-]*字数目标[ \t]*[::][ \t]*([1-9]\d*)[ \t]*(?:字)?[ \t]*$", re.MULTILINE)
30
+ _METRIC_LINE_RE = re.compile(r"^[ \t>*-]*字数口径[ \t]*[::][ \t]*([A-Za-z0-9_-]+)[ \t]*$", re.MULTILINE)
31
+
32
+
33
+ class WordcountError(ValueError):
34
+ """Expected deterministic wordcount contract failure."""
35
+
36
+
37
+ def require(condition: bool, message: str) -> None:
38
+ if not condition:
39
+ raise WordcountError(message)
40
+
41
+
42
+ def normalize_newlines(value: str) -> str:
43
+ return value.replace("\r\n", "\n").replace("\r", "\n")
44
+
45
+
46
+ def strip_recognizable_frontmatter(value: str) -> str:
47
+ lines = value.split("\n")
48
+ if not lines or lines[0] != "---":
49
+ return value
50
+ closing = next((index for index in range(1, min(len(lines), 201)) if lines[index] in {"---", "..."}), -1)
51
+ if closing < 2 or not any(_FRONTMATTER_KEY_RE.match(line) for line in lines[1:closing]):
52
+ return value
53
+ return "\n".join(lines[closing + 1 :])
54
+
55
+
56
+ def visible_body(value: str) -> str:
57
+ if not isinstance(value, str):
58
+ raise TypeError("body must be a string")
59
+ text = normalize_newlines(value)
60
+ if text.startswith("\ufeff"):
61
+ text = text[1:]
62
+ lines = strip_recognizable_frontmatter(text).split("\n")
63
+ while lines and _LEADING_BLANK_RE.match(lines[0]):
64
+ lines.pop(0)
65
+ if lines and _ATX_HEADING_RE.match(lines[0]):
66
+ lines.pop(0)
67
+ return "\n".join(lines)
68
+
69
+
70
+ def count_visible_chars(value: str) -> int:
71
+ return sum(ord(character) not in _WHITE_SPACE_CODEPOINTS for character in visible_body(value))
72
+
73
+
74
+ def parse_target(value: Any) -> int:
75
+ raw = str(value if value is not None else "")
76
+ if not _POSITIVE_INTEGER_RE.fullmatch(raw):
77
+ raise WordcountError("target must be a positive integer")
78
+ target = int(raw)
79
+ require(target <= 9_007_199_254_740_991, "target exceeds Number.MAX_SAFE_INTEGER")
80
+ return target
81
+
82
+
83
+ def compute_wordcount_bands(value: Any) -> dict[str, dict[str, int]]:
84
+ target = parse_target(value)
85
+ return {
86
+ "internal": {"min": (target * 88 + 99) // 100, "max": target * 112 // 100},
87
+ "user": {"min": (target * 85 + 99) // 100, "max": target * 115 // 100},
88
+ }
89
+
90
+
91
+ def invalid_wordcount_result(
92
+ reason: str, *, chapter: Any = None, case_id: Any = None,
93
+ target: int | None = None, actual: int | None = None,
94
+ ) -> dict[str, Any]:
95
+ return {
96
+ "schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
97
+ "target": target, "actual": actual, "internal_band": None, "user_band": None,
98
+ "signed_error_pct": None, "absolute_error_pct": None,
99
+ "status": "invalid", "invalid_reason": reason,
100
+ }
101
+
102
+
103
+ def measure_wordcount(value: str, *, chapter: Any = None, case_id: Any = None) -> dict[str, Any]:
104
+ try:
105
+ actual = count_visible_chars(value)
106
+ except (TypeError, ValueError):
107
+ return {
108
+ "schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
109
+ "case_id": case_id, "actual": None, "status": "invalid", "invalid_reason": "INVALID_BODY",
110
+ }
111
+ return {
112
+ "schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
113
+ "case_id": case_id, "actual": actual, "status": "measured", "invalid_reason": None,
114
+ }
115
+
116
+
117
+ def evaluate_wordcount(
118
+ value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
119
+ ) -> dict[str, Any]:
120
+ try:
121
+ target = parse_target(target_value)
122
+ except (TypeError, ValueError):
123
+ return invalid_wordcount_result("INVALID_TARGET", chapter=chapter, case_id=case_id)
124
+ try:
125
+ actual = count_visible_chars(value)
126
+ except (TypeError, ValueError):
127
+ return invalid_wordcount_result("INVALID_BODY", chapter=chapter, case_id=case_id, target=target)
128
+ if actual == 0:
129
+ return invalid_wordcount_result("EMPTY_BODY", chapter=chapter, case_id=case_id, target=target, actual=actual)
130
+ bands = compute_wordcount_bands(target)
131
+ internal_pass = bands["internal"]["min"] <= actual <= bands["internal"]["max"]
132
+ user_pass = bands["user"]["min"] <= actual <= bands["user"]["max"]
133
+ status = "internal_pass" if internal_pass else (
134
+ "borderline" if user_pass else ("under" if actual < bands["user"]["min"] else "over")
135
+ )
136
+ signed_error = (actual - target) / target
137
+ return {
138
+ "schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
139
+ "target": target, "actual": actual,
140
+ "internal_band": {**bands["internal"], "status": "pass" if internal_pass else "fail"},
141
+ "user_band": {**bands["user"], "status": "pass" if user_pass else "fail"},
142
+ "signed_error_pct": signed_error, "absolute_error_pct": abs(signed_error),
143
+ "status": status, "invalid_reason": None,
144
+ }
145
+
146
+
147
+ def checkpoint_wordcount(
148
+ value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
149
+ ) -> dict[str, Any]:
150
+ target = parse_target(target_value)
151
+ actual = count_visible_chars(value)
152
+ user = compute_wordcount_bands(target)["user"]
153
+ return {
154
+ "schema": CHECKPOINT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
155
+ "target": target, "actual": actual, "user_band": user,
156
+ "remaining_user_range": {
157
+ "min": max(0, user["min"] - actual),
158
+ "max": max(0, user["max"] - actual),
159
+ },
160
+ }
161
+
162
+
163
+ def target_from_outline(value: str) -> int:
164
+ text = normalize_newlines(value)
165
+ if text.startswith("\ufeff"):
166
+ text = text[1:]
167
+ targets = list(dict.fromkeys(_TARGET_LINE_RE.findall(text)))
168
+ metrics = list(dict.fromkeys(_METRIC_LINE_RE.findall(text)))
169
+ require(len(targets) == 1, "字数目标 must appear exactly once with one value")
170
+ require(metrics == [METRIC], f"字数口径 must appear exactly once as {METRIC}")
171
+ return parse_target(targets[0])
172
+
173
+
174
+ def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
175
+ pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\-  ].*)?\.md$"
176
+ match = re.match(pattern, name)
177
+ return int(match.group(1)) if match else None
178
+
179
+
180
+ def find_chapter_file(directory: Path, chapter: int, *, outline: bool) -> Path:
181
+ require(isinstance(chapter, int) and not isinstance(chapter, bool) and chapter >= 1, "chapter must be >= 1")
182
+ require(directory.is_dir(), f"chapter directory is missing: {directory}")
183
+ matches = sorted(
184
+ path for path in directory.iterdir()
185
+ if path.is_file() and _chapter_number_from_name(path.name, outline=outline) == chapter
186
+ )
187
+ label = "outline" if outline else "body"
188
+ require(len(matches) == 1, f"chapter {chapter} must have exactly one {label} file")
189
+ return matches[0]
190
+
191
+
192
+ def build_project_wordcount_record(project: Path, chapter: int, *, resolution: str) -> dict[str, Any]:
193
+ require(resolution in RESOLUTIONS, f"unsupported wordcount resolution: {resolution}")
194
+ root = project.resolve()
195
+ outline_path = find_chapter_file(root / "大纲", chapter, outline=True)
196
+ body_path = find_chapter_file(root / "正文", chapter, outline=False)
197
+ try:
198
+ target = target_from_outline(outline_path.read_text(encoding="utf-8"))
199
+ body_bytes = body_path.read_bytes()
200
+ body = body_bytes.decode("utf-8")
201
+ except (OSError, UnicodeError) as exc:
202
+ raise WordcountError(f"unable to read chapter files: {exc}") from exc
203
+ result = evaluate_wordcount(body, target, chapter=chapter)
204
+ require(result["status"] != "invalid", f"invalid chapter wordcount: {result['invalid_reason']}")
205
+ in_user_band = result["status"] in {"internal_pass", "borderline"}
206
+ require(
207
+ (resolution == "within_user_band" and in_user_band)
208
+ or (resolution == "accepted_current_length" and not in_user_band),
209
+ "wordcount resolution does not match the current length",
210
+ )
211
+ return {
212
+ "metric": METRIC,
213
+ "target": result["target"],
214
+ "actual": result["actual"],
215
+ "status": result["status"],
216
+ "resolution": resolution,
217
+ "body_sha256": hashlib.sha256(body_bytes).hexdigest(),
218
+ }
219
+
220
+
221
+ def normalize_wordcount_record(value: object) -> dict[str, Any]:
222
+ require(isinstance(value, dict), "wordcount record must be an object")
223
+ require(
224
+ set(value) == {"metric", "target", "actual", "status", "resolution", "body_sha256"},
225
+ "wordcount record fields are invalid",
226
+ )
227
+ require(value.get("metric") == METRIC, "wordcount metric is unsupported")
228
+ target = parse_target(value.get("target"))
229
+ actual = value.get("actual")
230
+ require(isinstance(actual, int) and not isinstance(actual, bool) and actual >= 1, "wordcount actual is invalid")
231
+ expected = evaluate_wordcount("字" * actual, target)
232
+ require(value.get("status") == expected["status"], "wordcount status does not match target and actual")
233
+ resolution = value.get("resolution")
234
+ require(resolution in RESOLUTIONS, "wordcount resolution is unsupported")
235
+ in_user_band = expected["status"] in {"internal_pass", "borderline"}
236
+ require((resolution == "within_user_band") == in_user_band, "wordcount resolution does not match status")
237
+ digest = value.get("body_sha256")
238
+ require(isinstance(digest, str) and re.fullmatch(r"[0-9a-f]{64}", digest) is not None, "body_sha256 is invalid")
239
+ return dict(value)
240
+
241
+
242
+ def validate_current_wordcount_record(project: Path, chapter: int, value: object) -> dict[str, Any]:
243
+ record = normalize_wordcount_record(value)
244
+ current = build_project_wordcount_record(project, chapter, resolution=record["resolution"])
245
+ require(record == current, "wordcount record is stale for the current body or target")
246
+ return record