@oh-story/dsh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/client.js +1201 -0
  5. package/lib/client.js.map +7 -0
  6. package/lib/drama/LICENSE.upstream +21 -0
  7. package/lib/drama/manifest.json +1012 -0
  8. package/lib/drama/skills/short-drama/SKILL.md +153 -0
  9. package/lib/drama/skills/short-drama/agents/openai.yaml +4 -0
  10. package/lib/drama/skills/short-drama/assets/creator-decision.example.jsonl +4 -0
  11. package/lib/drama/skills/short-drama/assets/production-observation.example.jsonl +2 -0
  12. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +36 -0
  13. package/lib/drama/skills/short-drama/assets/reference-observation.example.jsonl +2 -0
  14. package/lib/drama/skills/short-drama/references/audience-reveal.md +74 -0
  15. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +94 -0
  16. package/lib/drama/skills/short-drama/references/creator-workflow.md +82 -0
  17. package/lib/drama/skills/short-drama/references/form-cards/Q/347/211/210/350/241/250/350/276/276.md +73 -0
  18. package/lib/drama/skills/short-drama/references/form-cards//344/272/214/347/273/264/345/212/250/346/200/201/346/274/253.md +74 -0
  19. package/lib/drama/skills/short-drama/references/form-cards//345/233/275/346/274/253/344/272/214/346/254/241/345/205/203.md +79 -0
  20. package/lib/drama/skills/short-drama/references/form-cards//345/256/236/346/213/215.md +75 -0
  21. package/lib/drama/skills/short-drama/references/form-cards//346/260/264/345/242/250/347/254/224/350/247/246.md +85 -0
  22. package/lib/drama/skills/short-drama/references/form-cards//351/243/216/346/240/274/345/214/226/344/270/211/347/273/264.md +72 -0
  23. package/lib/drama/skills/short-drama/references/knowhow-index.md +80 -0
  24. package/lib/drama/skills/short-drama/references/lifecycle-commands.md +130 -0
  25. package/lib/drama/skills/short-drama/references/look-development.md +52 -0
  26. package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +58 -0
  27. package/lib/drama/skills/short-drama/references/production-form-profiles.md +126 -0
  28. package/lib/drama/skills/short-drama/references/reference-roles.md +142 -0
  29. package/lib/drama/skills/short-drama/references/runtime-preflight.md +27 -0
  30. package/lib/drama/skills/short-drama/scripts/project_tool.py +1963 -0
  31. package/lib/drama/skills/short-drama/scripts/selftest.py +287 -0
  32. package/lib/drama/skills/short-drama-assets/SKILL.md +187 -0
  33. package/lib/drama/skills/short-drama-assets/agents/openai.yaml +4 -0
  34. package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +3 -0
  35. package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +4 -0
  36. package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +5 -0
  37. package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +3 -0
  38. package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +5 -0
  39. package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +3 -0
  40. package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +2 -0
  41. package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +2 -0
  42. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +89 -0
  43. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +164 -0
  44. package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +123 -0
  45. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +146 -0
  46. package/lib/drama/skills/short-drama-assets/references/location-and-view.md +84 -0
  47. package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +111 -0
  48. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +107 -0
  49. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +136 -0
  50. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +171 -0
  51. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +267 -0
  52. package/lib/drama/skills/short-drama-assets/scripts/selftest.py +89 -0
  53. package/lib/drama/skills/short-drama-develop/SKILL.md +157 -0
  54. package/lib/drama/skills/short-drama-develop/agents/openai.yaml +4 -0
  55. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -0
  56. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +120 -0
  57. package/lib/drama/skills/short-drama-develop/assets/episode-map.jsonl +1 -0
  58. package/lib/drama/skills/short-drama-develop/assets/story-engine.md +181 -0
  59. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +219 -0
  60. package/lib/drama/skills/short-drama-develop/references/creative-reference-intake.md +57 -0
  61. package/lib/drama/skills/short-drama-develop/references/director-brief-craft.md +113 -0
  62. package/lib/drama/skills/short-drama-develop/references/episode-design.md +509 -0
  63. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +240 -0
  64. package/lib/drama/skills/short-drama-develop/references/genre-cards//344/272/262/345/255/220/351/232/220/347/247/230.md +112 -0
  65. package/lib/drama/skills/short-drama-develop/references/genre-cards//344/273/231/344/276/240/344/277/256/347/234/237.md +51 -0
  66. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/212/250/344/275/234/344/273/273/345/212/241.md +51 -0
  67. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/217/244/350/243/205/346/235/203/350/260/213.md +51 -0
  68. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +51 -0
  69. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/256/266/345/272/255/345/205/263/347/263/273.md +51 -0
  70. package/lib/drama/skills/short-drama-develop/references/genre-cards//346/202/254/347/226/221/350/247/204/345/210/231.md +51 -0
  71. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/224/237/346/264/273/346/265/201.md +51 -0
  72. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/240/264/351/225/234/351/207/215/345/234/206.md +109 -0
  73. package/lib/drama/skills/short-drama-develop/references/genre-cards//350/201/214/345/234/272/345/226/234/345/211/247.md +51 -0
  74. package/lib/drama/skills/short-drama-develop/references/genre-cards//350/261/252/351/227/250/345/251/232/346/201/213.md +51 -0
  75. package/lib/drama/skills/short-drama-develop/references/genre-cards//350/272/253/344/273/275/351/224/231/344/275/215.md +51 -0
  76. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +92 -0
  77. package/lib/drama/skills/short-drama-develop/references/mechanism-loop.md +282 -0
  78. package/lib/drama/skills/short-drama-develop/references/multi-episode-intake.md +128 -0
  79. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +211 -0
  80. package/lib/drama/skills/short-drama-develop/references/reveal-reversal-payoff.md +99 -0
  81. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +293 -0
  82. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +88 -0
  83. package/lib/drama/skills/short-drama-develop/references/story-craft.md +359 -0
  84. package/lib/drama/skills/short-drama-develop/scripts/episode_intake.py +699 -0
  85. package/lib/drama/skills/short-drama-develop/scripts/selftest.py +47 -0
  86. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +181 -0
  87. package/lib/drama/skills/short-drama-image-prompts/agents/openai.yaml +4 -0
  88. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompt-spec.jsonl.md +187 -0
  89. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +37 -0
  90. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-frame-spec.jsonl.md +128 -0
  91. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +31 -0
  92. package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +2 -0
  93. package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +154 -0
  94. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +231 -0
  95. package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +122 -0
  96. package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +152 -0
  97. package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +96 -0
  98. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +38 -0
  99. package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +177 -0
  100. package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +140 -0
  101. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +92 -0
  102. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +89 -0
  103. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +443 -0
  104. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +93 -0
  105. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +245 -0
  106. package/lib/drama/skills/short-drama-novel-analyze/agents/openai.yaml +4 -0
  107. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +3 -0
  108. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +116 -0
  109. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +124 -0
  110. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +155 -0
  111. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +141 -0
  112. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +67 -0
  113. package/lib/drama/skills/short-drama-novel-analyze/scripts/novel_index.py +651 -0
  114. package/lib/drama/skills/short-drama-novel-analyze/scripts/selftest.py +51 -0
  115. package/lib/drama/skills/short-drama-produce/SKILL.md +103 -0
  116. package/lib/drama/skills/short-drama-produce/agents/openai.yaml +4 -0
  117. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +140 -0
  118. package/lib/drama/skills/short-drama-produce/references/providers/gpt-image-2.md +24 -0
  119. package/lib/drama/skills/short-drama-produce/references/providers/minimax-music.md +25 -0
  120. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +40 -0
  121. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +60 -0
  122. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +1635 -0
  123. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +964 -0
  124. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +134 -0
  125. package/lib/drama/skills/short-drama-review/SKILL.md +178 -0
  126. package/lib/drama/skills/short-drama-review/agents/openai.yaml +4 -0
  127. package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +2 -0
  128. package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +42 -0
  129. package/lib/drama/skills/short-drama-review/assets/verdict-template.json +32 -0
  130. package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +2 -0
  131. package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +34 -0
  132. package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +114 -0
  133. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +270 -0
  134. package/lib/drama/skills/short-drama-review/references/project-calibration.md +85 -0
  135. package/lib/drama/skills/short-drama-review/references/review-method.md +82 -0
  136. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +115 -0
  137. package/lib/drama/skills/short-drama-review/references/rubric-source-analysis.md +108 -0
  138. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +164 -0
  139. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +166 -0
  140. package/lib/drama/skills/short-drama-review/references/stage-contract.md +87 -0
  141. package/lib/drama/skills/short-drama-review/scripts/review_check.py +325 -0
  142. package/lib/drama/skills/short-drama-review/scripts/selftest.py +87 -0
  143. package/lib/drama/skills/short-drama-storyboard/SKILL.md +264 -0
  144. package/lib/drama/skills/short-drama-storyboard/agents/openai.yaml +4 -0
  145. package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +2 -0
  146. package/lib/drama/skills/short-drama-storyboard/assets/coverage-template.json +49 -0
  147. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +32 -0
  148. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-template.jsonl +2 -0
  149. package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +12 -0
  150. package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +2 -0
  151. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +2 -0
  152. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +241 -0
  153. package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +39 -0
  154. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +169 -0
  155. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +264 -0
  156. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +70 -0
  157. package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +39 -0
  158. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +157 -0
  159. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +192 -0
  160. package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +29 -0
  161. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +110 -0
  162. package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +191 -0
  163. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +824 -0
  164. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +245 -0
  165. package/lib/drama/skills/short-drama-video-prompts/agents/openai.yaml +4 -0
  166. package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +21 -0
  167. package/lib/drama/skills/short-drama-video-prompts/assets/delivery-container.jsonl.md +101 -0
  168. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +212 -0
  169. package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +2 -0
  170. package/lib/drama/skills/short-drama-video-prompts/assets/music-spec.jsonl.md +31 -0
  171. package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +38 -0
  172. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +46 -0
  173. package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +2 -0
  174. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +240 -0
  175. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +255 -0
  176. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +93 -0
  177. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +304 -0
  178. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +191 -0
  179. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +312 -0
  180. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +168 -0
  181. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +110 -0
  182. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +329 -0
  183. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +450 -0
  184. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +327 -0
  185. package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +82 -0
  186. package/lib/drama/skills/short-drama-write/SKILL.md +284 -0
  187. package/lib/drama/skills/short-drama-write/agents/openai.yaml +4 -0
  188. package/lib/drama/skills/short-drama-write/assets/beats.jsonl +2 -0
  189. package/lib/drama/skills/short-drama-write/assets/episode-card-standalone.json +120 -0
  190. package/lib/drama/skills/short-drama-write/assets/episode-card.json +55 -0
  191. package/lib/drama/skills/short-drama-write/assets/screenplay.md +36 -0
  192. package/lib/drama/skills/short-drama-write/assets/voice-record-sheet.jsonl.md +89 -0
  193. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +519 -0
  194. package/lib/drama/skills/short-drama-write/references/production-format-dialect.md +273 -0
  195. package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +51 -0
  196. package/lib/drama/skills/short-drama-write/references/scene-sound-dramaturgy.md +43 -0
  197. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +260 -0
  198. package/lib/drama/skills/short-drama-write/references/script-craft.md +558 -0
  199. package/lib/drama/skills/short-drama-write/references/stage-contract.md +94 -0
  200. package/lib/drama/skills/short-drama-write/references/substitutable-realization.md +107 -0
  201. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +309 -0
  202. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +1091 -0
  203. package/lib/drama/skills/short-drama-write/scripts/selftest.py +229 -0
  204. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +416 -0
  205. package/lib/index.js +756 -0
  206. package/lib/index.js.map +7 -0
  207. package/lib/oh-story/LICENSE.upstream +21 -0
  208. package/lib/oh-story/manifest.json +1351 -0
  209. package/lib/oh-story/roles/chapter-extractor.md +314 -0
  210. package/lib/oh-story/roles/character-designer.md +169 -0
  211. package/lib/oh-story/roles/consistency-checker.md +190 -0
  212. package/lib/oh-story/roles/narrative-writer.md +294 -0
  213. package/lib/oh-story/roles/story-architect.md +195 -0
  214. package/lib/oh-story/roles/story-explorer.md +364 -0
  215. package/lib/oh-story/roles/story-researcher.md +304 -0
  216. package/lib/oh-story/skills/browser-cdp/SKILL.md +175 -0
  217. package/lib/oh-story/skills/story/SKILL.md +116 -0
  218. package/lib/oh-story/skills/story/VERSION +1 -0
  219. package/lib/oh-story/skills/story-cover/SKILL.md +327 -0
  220. package/lib/oh-story/skills/story-cover/references/cover-styles.md +210 -0
  221. package/lib/oh-story/skills/story-deslop/SKILL.md +445 -0
  222. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +582 -0
  223. package/lib/oh-story/skills/story-deslop/references/banned-words.md +147 -0
  224. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +1371 -0
  225. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +330 -0
  226. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +241 -0
  227. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +410 -0
  228. package/lib/oh-story/skills/story-import/SKILL.md +634 -0
  229. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +82 -0
  230. package/lib/oh-story/skills/story-import/references/format-and-structure.md +169 -0
  231. package/lib/oh-story/skills/story-import/references/length-routing.md +134 -0
  232. package/lib/oh-story/skills/story-import/references/state-tracking.md +108 -0
  233. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +442 -0
  234. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +175 -0
  235. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +161 -0
  236. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +1139 -0
  237. package/lib/oh-story/skills/story-long-analyze/SKILL.md +329 -0
  238. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +270 -0
  239. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +530 -0
  240. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +559 -0
  241. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +80 -0
  242. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +149 -0
  243. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +133 -0
  244. package/lib/oh-story/skills/story-long-scan/SKILL.md +342 -0
  245. package/lib/oh-story/skills/story-long-scan/references/genre-trends.md +197 -0
  246. package/lib/oh-story/skills/story-long-scan/references/publishing-guide.md +114 -0
  247. package/lib/oh-story/skills/story-long-scan/references/reader-profiling.md +105 -0
  248. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +207 -0
  249. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +78 -0
  250. package/lib/oh-story/skills/story-long-write/SKILL.md +327 -0
  251. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +582 -0
  252. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +302 -0
  253. package/lib/oh-story/skills/story-long-write/references/banned-words.md +147 -0
  254. package/lib/oh-story/skills/story-long-write/references/character-basics.md +445 -0
  255. package/lib/oh-story/skills/story-long-write/references/character-design-methods.md +428 -0
  256. package/lib/oh-story/skills/story-long-write/references/character-relations.md +385 -0
  257. package/lib/oh-story/skills/story-long-write/references/commercial-core-methods.md +502 -0
  258. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +62 -0
  259. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +282 -0
  260. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +426 -0
  261. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +209 -0
  262. package/lib/oh-story/skills/story-long-write/references/female-audience-writing.md +188 -0
  263. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +169 -0
  264. package/lib/oh-story/skills/story-long-write/references/genre-catalog.md +480 -0
  265. package/lib/oh-story/skills/story-long-write/references/genre-core-mechanics.md +415 -0
  266. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  267. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  268. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  269. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  270. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  271. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  272. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  273. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  274. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  275. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  276. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  277. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  278. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  279. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  280. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  281. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  282. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  283. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  284. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  285. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  286. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  287. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  288. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  289. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  290. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  291. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  292. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  293. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  294. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  295. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  296. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  297. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  298. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +91 -0
  299. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +235 -0
  300. package/lib/oh-story/skills/story-long-write/references/genre-writing-formulas.md +444 -0
  301. package/lib/oh-story/skills/story-long-write/references/hooks-chapter.md +225 -0
  302. package/lib/oh-story/skills/story-long-write/references/hooks-paragraph.md +146 -0
  303. package/lib/oh-story/skills/story-long-write/references/hooks-suspense.md +352 -0
  304. package/lib/oh-story/skills/story-long-write/references/opening-design.md +349 -0
  305. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +457 -0
  306. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +287 -0
  307. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +389 -0
  308. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +326 -0
  309. package/lib/oh-story/skills/story-long-write/references/plot-core-methods.md +522 -0
  310. package/lib/oh-story/skills/story-long-write/references/plot-emotion-system.md +354 -0
  311. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +711 -0
  312. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +622 -0
  313. package/lib/oh-story/skills/story-long-write/references/quality-checklist.md +184 -0
  314. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +109 -0
  315. package/lib/oh-story/skills/story-long-write/references/reversal-toolkit.md +349 -0
  316. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +108 -0
  317. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +409 -0
  318. package/lib/oh-story/skills/story-long-write/references/style-craft.md +347 -0
  319. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +510 -0
  320. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +161 -0
  321. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +140 -0
  322. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +171 -0
  323. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +99 -0
  324. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +284 -0
  325. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +421 -0
  326. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +1371 -0
  327. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +330 -0
  328. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +241 -0
  329. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +410 -0
  330. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +1139 -0
  331. package/lib/oh-story/skills/story-review/SKILL.md +483 -0
  332. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +582 -0
  333. package/lib/oh-story/skills/story-review/references/banned-words.md +147 -0
  334. package/lib/oh-story/skills/story-review/references/character-relations.md +385 -0
  335. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +282 -0
  336. package/lib/oh-story/skills/story-review/references/plot-core-methods.md +522 -0
  337. package/lib/oh-story/skills/story-review/references/quality-checklist.md +250 -0
  338. package/lib/oh-story/skills/story-review/references/quality-rubric.md +55 -0
  339. package/lib/oh-story/skills/story-review/references/rubrics/fanqie.md +18 -0
  340. package/lib/oh-story/skills/story-review/references/rubrics/qidian.md +19 -0
  341. package/lib/oh-story/skills/story-review/references/rubrics/zhihu.md +20 -0
  342. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +161 -0
  343. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +1371 -0
  344. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +330 -0
  345. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +410 -0
  346. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +1139 -0
  347. package/lib/oh-story/skills/story-setup/SKILL.md +495 -0
  348. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +582 -0
  349. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +147 -0
  350. package/lib/oh-story/skills/story-setup/references/agent-references/character-basics.md +445 -0
  351. package/lib/oh-story/skills/story-setup/references/agent-references/character-design-methods.md +428 -0
  352. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +385 -0
  353. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +282 -0
  354. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +426 -0
  355. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-methods.md +179 -0
  356. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +169 -0
  357. package/lib/oh-story/skills/story-setup/references/agent-references/genre-catalog.md +480 -0
  358. package/lib/oh-story/skills/story-setup/references/agent-references/genre-core-mechanics.md +415 -0
  359. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  360. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  361. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  362. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  363. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  364. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  365. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  366. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  367. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  368. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  369. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  370. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  371. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  372. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  373. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  374. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  375. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  376. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  377. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  378. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  379. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  380. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  381. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  382. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  383. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  384. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  385. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  386. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  387. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  388. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  389. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  390. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  391. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +91 -0
  392. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +235 -0
  393. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +444 -0
  394. package/lib/oh-story/skills/story-setup/references/agent-references/hooks-chapter.md +225 -0
  395. package/lib/oh-story/skills/story-setup/references/agent-references/hooks-suspense.md +352 -0
  396. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +349 -0
  397. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +457 -0
  398. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +287 -0
  399. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +389 -0
  400. package/lib/oh-story/skills/story-setup/references/agent-references/plot-core-methods.md +522 -0
  401. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +247 -0
  402. package/lib/oh-story/skills/story-setup/references/agent-references/reversal-toolkit.md +349 -0
  403. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +409 -0
  404. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +510 -0
  405. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +421 -0
  406. package/lib/oh-story/skills/story-short-analyze/SKILL.md +278 -0
  407. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +582 -0
  408. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +147 -0
  409. package/lib/oh-story/skills/story-short-analyze/references/character-basics.md +453 -0
  410. package/lib/oh-story/skills/story-short-analyze/references/character-design-methods.md +438 -0
  411. package/lib/oh-story/skills/story-short-analyze/references/character-relations.md +395 -0
  412. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +79 -0
  413. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +490 -0
  414. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +425 -0
  415. package/lib/oh-story/skills/story-short-analyze/references/genre-readers.md +245 -0
  416. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +453 -0
  417. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-techniques.md +475 -0
  418. package/lib/oh-story/skills/story-short-analyze/references/hooks-chapter.md +225 -0
  419. package/lib/oh-story/skills/story-short-analyze/references/hooks-paragraph.md +146 -0
  420. package/lib/oh-story/skills/story-short-analyze/references/hooks-suspense.md +352 -0
  421. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +685 -0
  422. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +165 -0
  423. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +561 -0
  424. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +242 -0
  425. package/lib/oh-story/skills/story-short-analyze/references/real-market-data.md +90 -0
  426. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +54 -0
  427. package/lib/oh-story/skills/story-short-scan/SKILL.md +212 -0
  428. package/lib/oh-story/skills/story-short-scan/references/real-market-data.md +90 -0
  429. package/lib/oh-story/skills/story-short-write/SKILL.md +433 -0
  430. package/lib/oh-story/skills/story-short-write/references/banned-words.md +147 -0
  431. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +62 -0
  432. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +282 -0
  433. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +179 -0
  434. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +159 -0
  435. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +156 -0
  436. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +163 -0
  437. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +167 -0
  438. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +178 -0
  439. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +166 -0
  440. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +159 -0
  441. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +151 -0
  442. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +153 -0
  443. 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 +210 -0
  444. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +444 -0
  445. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +488 -0
  446. package/lib/oh-story/skills/story-short-write/references/hooks-chapter.md +225 -0
  447. package/lib/oh-story/skills/story-short-write/references/hooks-paragraph.md +146 -0
  448. package/lib/oh-story/skills/story-short-write/references/hooks-suspense.md +352 -0
  449. package/lib/oh-story/skills/story-short-write/references/output-contract.md +165 -0
  450. package/lib/oh-story/skills/story-short-write/references/quality-checklist.md +222 -0
  451. package/lib/oh-story/skills/story-short-write/references/reversal-toolkit.md +349 -0
  452. package/lib/oh-story/skills/story-short-write/references/short-craft.md +185 -0
  453. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +133 -0
  454. package/lib/oh-story/skills/story-short-write/references/short-format.md +167 -0
  455. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +35 -0
  456. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +158 -0
  457. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +153 -0
  458. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +1371 -0
  459. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +330 -0
  460. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +241 -0
  461. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +410 -0
  462. package/lib/types/client/file-activity.d.ts +30 -0
  463. package/lib/types/client/file-activity.d.ts.map +1 -0
  464. package/lib/types/client/file-tree.d.ts +20 -0
  465. package/lib/types/client/file-tree.d.ts.map +1 -0
  466. package/lib/types/client/index.d.ts +12 -0
  467. package/lib/types/client/index.d.ts.map +1 -0
  468. package/lib/types/client/jsonl-preview.d.ts +15 -0
  469. package/lib/types/client/jsonl-preview.d.ts.map +1 -0
  470. package/lib/types/client/markdown-preview.d.ts +38 -0
  471. package/lib/types/client/markdown-preview.d.ts.map +1 -0
  472. package/lib/types/index.d.ts +25 -0
  473. package/lib/types/index.d.ts.map +1 -0
  474. package/lib/types/native-hooks.d.ts +16 -0
  475. package/lib/types/native-hooks.d.ts.map +1 -0
  476. package/lib/types/role-provider.d.ts +6 -0
  477. package/lib/types/role-provider.d.ts.map +1 -0
  478. package/lib/types/role-tool.d.ts +10 -0
  479. package/lib/types/role-tool.d.ts.map +1 -0
  480. package/lib/types/skill-provider.d.ts +15 -0
  481. package/lib/types/skill-provider.d.ts.map +1 -0
  482. package/lib/types/workspace-request-trust.d.ts +13 -0
  483. package/lib/types/workspace-request-trust.d.ts.map +1 -0
  484. package/lib/types/workspace-route.d.ts +10 -0
  485. package/lib/types/workspace-route.d.ts.map +1 -0
  486. package/lib/types/workspace-security.d.ts +9 -0
  487. package/lib/types/workspace-security.d.ts.map +1 -0
  488. package/package.json +107 -0
@@ -0,0 +1,1139 @@
1
+ #!/usr/bin/env python3
2
+ """Maintain one structured story state and its deterministic Markdown views.
3
+
4
+ The language model supplies compact semantic JSON. This tool validates and
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.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import copy
14
+ import json
15
+ import os
16
+ import re
17
+ import stat
18
+ import sys
19
+ import tempfile
20
+ import unicodedata
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+
25
+ INPUT_SCHEMA_VERSION = 1
26
+ TRACKING_SCHEMA_VERSION = 4
27
+ DELTA_TARGET_BYTES = 1536
28
+ DELTA_MAX_BYTES = 3072
29
+ CONTEXT_TARGET_BYTES = 8192
30
+ CONTEXT_MAX_BYTES = 12288
31
+ SNAPSHOT_TARGET_BYTES = 4096
32
+ SNAPSHOT_MAX_BYTES = 8192
33
+
34
+ CONTEXT_HEADINGS = (
35
+ "## 当前位置",
36
+ "## 长期约束",
37
+ "## 核心角色状态",
38
+ "## 活跃伏笔",
39
+ "## 近三章速记",
40
+ "## 下一章承诺",
41
+ "## 连贯性风险",
42
+ )
43
+ FORESHADOW_STATUSES = ("已埋", "已回收", "已过期", "放弃")
44
+ FORESHADOW_IMPORTANCE = ("高", "中", "低")
45
+ REVEAL_STATUSES = ("未揭示", "部分揭示", "已揭示")
46
+ INVALID_FILE_CHARS = re.compile(r"[<>:\"/\\|?*\x00-\x1f]")
47
+ FORESHADOW_ID = re.compile(r"^F\d{3,}$")
48
+ EVENT_ID = re.compile(r"^E\d{3,}$")
49
+ WINDOWS_RESERVED_NAMES = {
50
+ "CON",
51
+ "PRN",
52
+ "AUX",
53
+ "NUL",
54
+ *(f"COM{index}" for index in range(1, 10)),
55
+ *(f"LPT{index}" for index in range(1, 10)),
56
+ }
57
+ RETIRED_TRACKING_PATHS = (
58
+ "_tracking-meta.json",
59
+ "阶段摘要.md",
60
+ "角色状态.md",
61
+ "时间线.md",
62
+ "摘要",
63
+ "时间线/事件库.json",
64
+ )
65
+ RETIRED_ARCHIVE_DIR = "_旧追踪存档"
66
+
67
+
68
+ class TrackingError(ValueError):
69
+ """Expected validation or tracking-state error."""
70
+
71
+
72
+ def require(condition: bool, message: str) -> None:
73
+ if not condition:
74
+ raise TrackingError(message)
75
+
76
+
77
+ def as_mapping(value: object, label: str) -> dict[str, Any]:
78
+ require(isinstance(value, dict), f"{label} must be a JSON object")
79
+ return value
80
+
81
+
82
+ def as_list(value: object, label: str) -> list[Any]:
83
+ require(isinstance(value, list), f"{label} must be a JSON array")
84
+ return value
85
+
86
+
87
+ def as_int(value: object, label: str, *, minimum: int = 0) -> int:
88
+ require(isinstance(value, int) and not isinstance(value, bool), f"{label} must be an integer")
89
+ require(value >= minimum, f"{label} must be >= {minimum}")
90
+ return value
91
+
92
+
93
+ def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -> None:
94
+ unknown = set(mapping) - allowed
95
+ require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}")
96
+
97
+
98
+ def clean_text(value: object, label: str, *, allow_empty: bool = False, max_bytes: int = 768) -> str:
99
+ require(isinstance(value, str), f"{label} must be a string")
100
+ cleaned = " ".join(value.replace("|", "|").split())
101
+ require(allow_empty or bool(cleaned), f"{label} must not be empty")
102
+ require(len(cleaned.encode("utf-8")) <= max_bytes, f"{label} exceeds {max_bytes} bytes")
103
+ return cleaned
104
+
105
+
106
+ def clean_string_list(
107
+ value: object,
108
+ label: str,
109
+ *,
110
+ maximum: int | None = None,
111
+ item_max_bytes: int = 384,
112
+ ) -> list[str]:
113
+ values = as_list(value, label)
114
+ if maximum is not None:
115
+ require(len(values) <= maximum, f"{label} may contain at most {maximum} items")
116
+ return [clean_text(item, f"{label}[{index}]", max_bytes=item_max_bytes) for index, item in enumerate(values)]
117
+
118
+
119
+ def safe_file_component(value: object, label: str) -> str:
120
+ name = unicodedata.normalize("NFC", clean_text(value, label, max_bytes=180))
121
+ require(not INVALID_FILE_CHARS.search(name), f"{label} contains an invalid filename character")
122
+ require(name not in {".", ".."} and not name.endswith((".", " ")), f"{label} is not a safe filename")
123
+ require(name.split(".", 1)[0].upper() not in WINDOWS_RESERVED_NAMES, f"{label} is reserved on Windows")
124
+ return name
125
+
126
+
127
+ def portable_name_key(name: str) -> str:
128
+ return unicodedata.normalize("NFC", name).casefold()
129
+
130
+
131
+ def byte_size(text: str) -> int:
132
+ return len(text.encode("utf-8"))
133
+
134
+
135
+ def emit(text: str, *, error: bool = False) -> None:
136
+ """Write UTF-8 bytes directly.
137
+
138
+ Windows 的文本 stdout 是 cp1252(含中文即 UnicodeEncodeError),stderr 默认
139
+ backslashreplace(中文被转义成反斜杠码位,作者看不懂)。两条路都要绕开。
140
+ """
141
+ stream = sys.stderr if error else sys.stdout
142
+ stream.flush()
143
+ stream.buffer.write((text + "\n").encode("utf-8"))
144
+ stream.buffer.flush()
145
+
146
+
147
+ def read_json(path: Path) -> object:
148
+ try:
149
+ return json.loads(path.read_text(encoding="utf-8"))
150
+ except (OSError, json.JSONDecodeError) as exc:
151
+ raise TrackingError(f"unable to read JSON {path}: {exc}") from exc
152
+
153
+
154
+ def json_payload(document: object) -> str:
155
+ return json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
156
+
157
+
158
+ def atomic_write_text(path: Path, payload: str) -> None:
159
+ path.parent.mkdir(parents=True, exist_ok=True)
160
+ mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o644
161
+ fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
162
+ temporary = Path(temporary_name)
163
+ try:
164
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
165
+ handle.write(payload)
166
+ handle.flush()
167
+ os.fsync(handle.fileno())
168
+ os.chmod(temporary, mode)
169
+ os.replace(temporary, path)
170
+ finally:
171
+ temporary.unlink(missing_ok=True)
172
+
173
+
174
+ def write_if_changed(path: Path, payload: str) -> None:
175
+ try:
176
+ if path.read_text(encoding="utf-8") == payload:
177
+ return
178
+ except FileNotFoundError:
179
+ pass
180
+ atomic_write_text(path, payload)
181
+
182
+
183
+ def tracking_root(project: Path) -> Path:
184
+ return project.resolve() / "追踪"
185
+
186
+
187
+ def state_path(project: Path) -> Path:
188
+ return tracking_root(project) / "_tracking-state.json"
189
+
190
+
191
+ def delta_path(tracking: Path, chapter: int) -> Path:
192
+ width = max(3, len(str(chapter)))
193
+ return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
194
+
195
+
196
+ def find_retired_tracking_paths(tracking: Path) -> list[str]:
197
+ found = [relative for relative in RETIRED_TRACKING_PATHS if (tracking / relative).exists()]
198
+ found.extend(sorted(path.name for path in tracking.glob("基线_截至第*章.md")))
199
+ return found
200
+
201
+
202
+ def require_no_retired_tracking_paths(tracking: Path) -> None:
203
+ found = find_retired_tracking_paths(tracking)
204
+ require(not found, f"retired tracking files are not supported: {', '.join(found)}")
205
+
206
+
207
+ def archive_retired_tracking_paths(tracking: Path) -> list[str]:
208
+ """Move a pre-transaction 追踪/ aside so init can build the current protocol in place.
209
+
210
+ Nothing is parsed or converted: the old files are kept verbatim for the author to
211
+ consult, and the new state is reconstructed from the init document alone.
212
+ """
213
+ retired = find_retired_tracking_paths(tracking)
214
+ if not retired:
215
+ return []
216
+ archive = tracking / RETIRED_ARCHIVE_DIR
217
+ for relative in retired:
218
+ require(
219
+ not (archive / relative).exists(),
220
+ f"追踪/{RETIRED_ARCHIVE_DIR}/{relative} already exists; move it away before initializing",
221
+ )
222
+ # 先全量校验再搬运;中断后重跑时已搬走的条目不再出现在待搬列表里,可直接续做。
223
+ for relative in retired:
224
+ target = archive / relative
225
+ target.parent.mkdir(parents=True, exist_ok=True)
226
+ os.replace(tracking / relative, target)
227
+ return retired
228
+
229
+
230
+ def validate_position(value: object, label: str = "context.position") -> dict[str, Any]:
231
+ position = as_mapping(value, label)
232
+ require_known_keys(position, {"volume", "volume_start_chapter", "story_time", "scene"}, label)
233
+ return {
234
+ "volume": safe_file_component(position.get("volume"), f"{label}.volume"),
235
+ "volume_start_chapter": as_int(
236
+ position.get("volume_start_chapter"), f"{label}.volume_start_chapter", minimum=1
237
+ ),
238
+ "story_time": clean_text(position.get("story_time"), f"{label}.story_time", max_bytes=240),
239
+ "scene": clean_text(position.get("scene"), f"{label}.scene", max_bytes=240),
240
+ }
241
+
242
+
243
+ def normalize_snapshot(value: object, label: str) -> dict[str, Any]:
244
+ snapshot = as_mapping(value, label)
245
+ require_known_keys(
246
+ snapshot,
247
+ {"identity", "location", "goal", "state", "abilities_resources", "relationships", "knowledge", "open_threads"},
248
+ label,
249
+ )
250
+ return {
251
+ "identity": clean_text(snapshot.get("identity"), f"{label}.identity", max_bytes=240),
252
+ "location": clean_text(snapshot.get("location"), f"{label}.location", max_bytes=240),
253
+ "goal": clean_text(snapshot.get("goal"), f"{label}.goal", max_bytes=300),
254
+ "state": clean_text(snapshot.get("state"), f"{label}.state", max_bytes=300),
255
+ "abilities_resources": clean_string_list(
256
+ snapshot.get("abilities_resources", []), f"{label}.abilities_resources"
257
+ ),
258
+ "relationships": clean_string_list(snapshot.get("relationships", []), f"{label}.relationships"),
259
+ "knowledge": clean_string_list(snapshot.get("knowledge", []), f"{label}.knowledge"),
260
+ "open_threads": clean_string_list(snapshot.get("open_threads", []), f"{label}.open_threads"),
261
+ }
262
+
263
+
264
+ def normalize_snapshots(value: object, label: str = "character_snapshots") -> dict[str, dict[str, Any]]:
265
+ snapshots = as_mapping(value, label)
266
+ normalized: dict[str, dict[str, Any]] = {}
267
+ portable_names: set[str] = set()
268
+ for raw_name, raw_snapshot in snapshots.items():
269
+ name = safe_file_component(raw_name, f"{label} character name")
270
+ key = portable_name_key(name)
271
+ require(key not in portable_names, f"{label} contains a cross-platform duplicate character {name}")
272
+ portable_names.add(key)
273
+ normalized[name] = normalize_snapshot(raw_snapshot, f"{label}.{name}")
274
+ return normalized
275
+
276
+
277
+ def render_snapshot(name: str, snapshot: dict[str, Any], through_chapter: int, revision: int) -> str:
278
+ def section(title: str, values: list[str]) -> list[str]:
279
+ return [f"## {title}", *(f"- {item}" for item in values or ["无"]), ""]
280
+
281
+ lines = [
282
+ f"# {name}|当前状态",
283
+ "",
284
+ f"- 状态修订:{revision}",
285
+ f"- 截至章节:第{through_chapter}章",
286
+ f"- 身份:{snapshot['identity']}",
287
+ f"- 位置:{snapshot['location']}",
288
+ f"- 当前目标:{snapshot['goal']}",
289
+ f"- 身心状态:{snapshot['state']}",
290
+ "",
291
+ ]
292
+ lines.extend(section("能力与资源", snapshot["abilities_resources"]))
293
+ lines.extend(section("关键关系", snapshot["relationships"]))
294
+ lines.extend(section("已知信息", snapshot["knowledge"]))
295
+ lines.extend(section("未结事项", snapshot["open_threads"]))
296
+ payload = "\n".join(lines).rstrip() + "\n"
297
+ require(
298
+ byte_size(payload) <= SNAPSHOT_MAX_BYTES,
299
+ f"character snapshot {name} exceeds hard cap of {SNAPSHOT_MAX_BYTES} bytes",
300
+ )
301
+ return payload
302
+
303
+
304
+ def normalize_foreshadow_change(
305
+ value: object,
306
+ label: str,
307
+ *,
308
+ allow_delete: bool,
309
+ through_chapter: int,
310
+ ) -> dict[str, Any]:
311
+ row = as_mapping(value, label)
312
+ require_known_keys(
313
+ row,
314
+ {"action", "id", "summary", "planted_chapter", "planned_resolution_chapter", "status", "importance"},
315
+ label,
316
+ )
317
+ action = clean_text(row.get("action", "upsert"), f"{label}.action", max_bytes=24)
318
+ require(action in ({"upsert", "delete"} if allow_delete else {"upsert"}), f"{label}.action is invalid")
319
+ identifier = clean_text(row.get("id"), f"{label}.id", max_bytes=24)
320
+ require(FORESHADOW_ID.fullmatch(identifier) is not None, f"{label}.id must look like F001")
321
+ if action == "delete":
322
+ return {"action": action, "id": identifier}
323
+ planted_chapter = as_int(row.get("planted_chapter"), f"{label}.planted_chapter", minimum=1)
324
+ require(planted_chapter <= through_chapter, f"{label}.planted_chapter cannot be in the future")
325
+ planned_raw = row.get("planned_resolution_chapter")
326
+ planned_chapter = (
327
+ None if planned_raw is None else as_int(planned_raw, f"{label}.planned_resolution_chapter", minimum=1)
328
+ )
329
+ require(
330
+ planned_chapter is None or planned_chapter >= planted_chapter,
331
+ f"{label}.planned_resolution_chapter cannot precede planted_chapter",
332
+ )
333
+ status = clean_text(row.get("status"), f"{label}.status", max_bytes=24)
334
+ importance = clean_text(row.get("importance"), f"{label}.importance", max_bytes=12)
335
+ require(status in FORESHADOW_STATUSES, f"{label}.status must be one of {FORESHADOW_STATUSES}")
336
+ require(importance in FORESHADOW_IMPORTANCE, f"{label}.importance must be one of {FORESHADOW_IMPORTANCE}")
337
+ return {
338
+ "action": action,
339
+ "id": identifier,
340
+ "summary": clean_text(row.get("summary"), f"{label}.summary", max_bytes=360),
341
+ "planted_chapter": planted_chapter,
342
+ "planned_resolution_chapter": planned_chapter,
343
+ "status": status,
344
+ "importance": importance,
345
+ }
346
+
347
+
348
+ def normalize_foreshadow_state(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
349
+ rows = as_mapping(value, "tracking state.foreshadow")
350
+ normalized: dict[str, dict[str, Any]] = {}
351
+ for raw_identifier, raw_row in rows.items():
352
+ identifier = clean_text(raw_identifier, "tracking state.foreshadow ID", max_bytes=24)
353
+ row = as_mapping(raw_row, f"tracking state.foreshadow.{identifier}")
354
+ require_known_keys(
355
+ row,
356
+ {"id", "summary", "planted_chapter", "planned_resolution_chapter", "status", "importance", "updated_chapter"},
357
+ f"tracking state.foreshadow.{identifier}",
358
+ )
359
+ require(row.get("id") == identifier, f"tracking state.foreshadow.{identifier}.id does not match its key")
360
+ change = normalize_foreshadow_change(
361
+ {
362
+ "action": "upsert",
363
+ **{key: value for key, value in row.items() if key != "updated_chapter"},
364
+ },
365
+ f"tracking state.foreshadow.{identifier}",
366
+ allow_delete=False,
367
+ through_chapter=last_chapter,
368
+ )
369
+ change.pop("action")
370
+ updated = as_int(row.get("updated_chapter"), f"tracking state.foreshadow.{identifier}.updated_chapter", minimum=1)
371
+ require(updated <= last_chapter, f"foreshadow {identifier} updates after current chapter")
372
+ change["updated_chapter"] = updated
373
+ normalized[identifier] = change
374
+ return normalized
375
+
376
+
377
+ def render_foreshadow(rows: dict[str, dict[str, Any]], revision: int) -> str:
378
+ lines = [
379
+ "# 伏笔当前状态",
380
+ "",
381
+ f"> 状态修订:{revision}。每个 ID 只保留一行当前状态;历史变化见 `逐章记录/`。",
382
+ "",
383
+ "| ID | 内容 | 埋设章 | 计划回收章 | 状态 | 重要度 | 最近变更章 |",
384
+ "|---|---|---:|---:|---|---|---:|",
385
+ ]
386
+ for identifier in sorted(rows):
387
+ row = rows[identifier]
388
+ planned = f"第{row['planned_resolution_chapter']}章" if row["planned_resolution_chapter"] else "—"
389
+ lines.append(
390
+ f"| {identifier} | {row['summary']} | 第{row['planted_chapter']}章 | {planned} | "
391
+ f"{row['status']} | {row['importance']} | 第{row['updated_chapter']}章 |"
392
+ )
393
+ return "\n".join(lines) + "\n"
394
+
395
+
396
+ def normalize_timeline_change(
397
+ value: object,
398
+ label: str,
399
+ *,
400
+ allow_delete: bool,
401
+ through_chapter: int,
402
+ ) -> dict[str, Any]:
403
+ event = as_mapping(value, label)
404
+ require_known_keys(
405
+ event,
406
+ {"action", "id", "story_time", "objective_fact", "reader_knowledge", "reveal_status", "reveal_chapter", "characters"},
407
+ label,
408
+ )
409
+ action = clean_text(event.get("action", "upsert"), f"{label}.action", max_bytes=24)
410
+ require(action in ({"upsert", "delete"} if allow_delete else {"upsert"}), f"{label}.action is invalid")
411
+ identifier = clean_text(event.get("id"), f"{label}.id", max_bytes=24)
412
+ require(EVENT_ID.fullmatch(identifier) is not None, f"{label}.id must look like E001")
413
+ if action == "delete":
414
+ return {"action": action, "id": identifier}
415
+ reveal_status = clean_text(event.get("reveal_status"), f"{label}.reveal_status", max_bytes=24)
416
+ require(reveal_status in REVEAL_STATUSES, f"{label}.reveal_status must be one of {REVEAL_STATUSES}")
417
+ reveal_raw = event.get("reveal_chapter")
418
+ reveal_chapter = None if reveal_raw is None else as_int(reveal_raw, f"{label}.reveal_chapter", minimum=1)
419
+ if reveal_status == "未揭示":
420
+ require(reveal_chapter is None, f"{label} must not put a future reveal chapter in established timeline facts")
421
+ else:
422
+ require(reveal_chapter is not None, f"{label}.reveal_chapter is required once revealed")
423
+ require(reveal_chapter <= through_chapter, f"{label}.reveal_chapter cannot be in the future")
424
+ return {
425
+ "action": action,
426
+ "id": identifier,
427
+ "story_time": clean_text(event.get("story_time"), f"{label}.story_time", max_bytes=240),
428
+ "objective_fact": clean_text(event.get("objective_fact"), f"{label}.objective_fact", max_bytes=480),
429
+ "reader_knowledge": clean_text(event.get("reader_knowledge"), f"{label}.reader_knowledge", max_bytes=480),
430
+ "reveal_status": reveal_status,
431
+ "reveal_chapter": reveal_chapter,
432
+ "characters": clean_string_list(event.get("characters", []), f"{label}.characters", maximum=12, item_max_bytes=120),
433
+ }
434
+
435
+
436
+ def normalize_timeline_state(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
437
+ events = as_mapping(value, "tracking state.timeline")
438
+ normalized: dict[str, dict[str, Any]] = {}
439
+ for raw_identifier, raw_event in events.items():
440
+ identifier = clean_text(raw_identifier, "tracking state.timeline ID", max_bytes=24)
441
+ event = as_mapping(raw_event, f"tracking state.timeline.{identifier}")
442
+ require_known_keys(
443
+ event,
444
+ {
445
+ "id", "story_time", "objective_fact", "reader_knowledge", "reveal_status", "reveal_chapter",
446
+ "characters", "first_recorded_chapter", "updated_chapter",
447
+ },
448
+ f"tracking state.timeline.{identifier}",
449
+ )
450
+ require(event.get("id") == identifier, f"tracking state.timeline.{identifier}.id does not match its key")
451
+ change = normalize_timeline_change(
452
+ {
453
+ "action": "upsert",
454
+ **{
455
+ key: value
456
+ for key, value in event.items()
457
+ if key not in {"first_recorded_chapter", "updated_chapter"}
458
+ },
459
+ },
460
+ f"tracking state.timeline.{identifier}",
461
+ allow_delete=False,
462
+ through_chapter=last_chapter,
463
+ )
464
+ change.pop("action")
465
+ first = as_int(event.get("first_recorded_chapter"), f"tracking state.timeline.{identifier}.first_recorded_chapter", minimum=1)
466
+ updated = as_int(event.get("updated_chapter"), f"tracking state.timeline.{identifier}.updated_chapter", minimum=1)
467
+ require(first <= last_chapter, f"timeline event {identifier} starts after current chapter")
468
+ require(updated <= last_chapter, f"timeline event {identifier} updates after current chapter")
469
+ change["first_recorded_chapter"] = first
470
+ change["updated_chapter"] = updated
471
+ normalized[identifier] = change
472
+ return normalized
473
+
474
+
475
+ def render_timeline_views(events: dict[str, dict[str, Any]], revision: int) -> tuple[str, str]:
476
+ author_lines = [
477
+ "# 作者真相时间线",
478
+ "",
479
+ f"> 状态修订:{revision}。客观事实与读者认知的权威对照;未来揭示计划仍留在大纲。",
480
+ "",
481
+ "| ID | 首次登记章 | 故事时间 | 客观事实 | 读者当前认知 | 揭示状态 | 实际揭示章 |",
482
+ "|---|---:|---|---|---|---|---:|",
483
+ ]
484
+ reader_lines = [
485
+ "# 读者已知时间线",
486
+ "",
487
+ f"> 状态修订:{revision}。只呈现读者截至当前章节已经知道或相信的内容,不泄露作者侧客观真相。",
488
+ "",
489
+ "| ID | 读者当前认知 | 认知截至章 |",
490
+ "|---|---|---:|",
491
+ ]
492
+ for identifier in sorted(events):
493
+ event = events[identifier]
494
+ reveal = f"第{event['reveal_chapter']}章" if event.get("reveal_chapter") else "—"
495
+ characters = "、".join(event.get("characters", []))
496
+ objective = event["objective_fact"] + (f"(涉及:{characters})" if characters else "")
497
+ author_lines.append(
498
+ f"| {identifier} | 第{event['first_recorded_chapter']}章 | {event['story_time']} | {objective} | "
499
+ f"{event['reader_knowledge']} | {event['reveal_status']} | {reveal} |"
500
+ )
501
+ reader_lines.append(f"| {identifier} | {event['reader_knowledge']} | 第{event['updated_chapter']}章 |")
502
+ return "\n".join(author_lines) + "\n", "\n".join(reader_lines) + "\n"
503
+
504
+
505
+ def validate_context_input(value: object, *, include_initial_fields: bool) -> dict[str, Any]:
506
+ context = as_mapping(value, "context")
507
+ allowed = {"position", "long_term_constraints", "active_character_names", "continuity_risks"}
508
+ if include_initial_fields:
509
+ allowed.update({"recent_chapters", "next_chapter_commitments"})
510
+ require_known_keys(context, allowed, "context")
511
+ normalized: dict[str, Any] = {
512
+ "position": validate_position(context.get("position")),
513
+ "long_term_constraints": clean_string_list(
514
+ context.get("long_term_constraints", []), "context.long_term_constraints", maximum=6
515
+ ),
516
+ "active_character_names": [
517
+ safe_file_component(name, f"context.active_character_names[{index}]")
518
+ for index, name in enumerate(as_list(context.get("active_character_names", []), "context.active_character_names"))
519
+ ],
520
+ "continuity_risks": clean_string_list(
521
+ context.get("continuity_risks", []), "context.continuity_risks", maximum=5
522
+ ),
523
+ }
524
+ require(len(normalized["active_character_names"]) <= 6, "context.active_character_names may contain at most 6 names")
525
+ require(
526
+ len({portable_name_key(name) for name in normalized["active_character_names"]})
527
+ == len(normalized["active_character_names"]),
528
+ "context.active_character_names contains cross-platform duplicates",
529
+ )
530
+ if include_initial_fields:
531
+ recent: list[dict[str, Any]] = []
532
+ for index, raw_item in enumerate(as_list(context.get("recent_chapters", []), "context.recent_chapters")):
533
+ item = as_mapping(raw_item, f"context.recent_chapters[{index}]")
534
+ require_known_keys(item, {"chapter", "summary"}, f"context.recent_chapters[{index}]")
535
+ recent.append(
536
+ {
537
+ "chapter": as_int(item.get("chapter"), f"context.recent_chapters[{index}].chapter", minimum=1),
538
+ "summary": clean_text(item.get("summary"), f"context.recent_chapters[{index}].summary", max_bytes=360),
539
+ }
540
+ )
541
+ require(len(recent) <= 3, "context.recent_chapters may contain at most 3 items")
542
+ normalized["recent_chapters"] = recent
543
+ normalized["next_chapter_commitments"] = clean_string_list(
544
+ context.get("next_chapter_commitments", []), "context.next_chapter_commitments", maximum=5
545
+ )
546
+ return normalized
547
+
548
+
549
+ def active_foreshadow_lines(rows: dict[str, dict[str, Any]]) -> list[str]:
550
+ importance = {value: index for index, value in enumerate(FORESHADOW_IMPORTANCE)}
551
+ candidates = [row for row in rows.values() if row["status"] == "已埋"]
552
+ candidates.sort(
553
+ key=lambda row: (importance[row["importance"]], row["planned_resolution_chapter"] or 10**12, row["id"])
554
+ )
555
+ result = []
556
+ for row in candidates[:8]:
557
+ planned = f"第{row['planned_resolution_chapter']}章" if row["planned_resolution_chapter"] else "回收章未定"
558
+ result.append(f"{row['id']}|{row['summary']}|埋第{row['planted_chapter']}章|{planned}|{row['importance']}")
559
+ return result
560
+
561
+
562
+ def render_context(state: dict[str, Any]) -> str:
563
+ context = state["context"]
564
+ position = context["position"]
565
+ current_chapter = (
566
+ "尚未开篇" if state["last_committed_chapter"] == 0 else f"第{state['last_committed_chapter']}章"
567
+ )
568
+ character_lines = [
569
+ f"{name}|{state['characters'][name]['identity']}|{state['characters'][name]['state']}|"
570
+ f"目标:{state['characters'][name]['goal']}"
571
+ for name in context["active_character_names"]
572
+ ]
573
+ sections: list[tuple[str, list[str]]] = [
574
+ (
575
+ "## 当前位置",
576
+ [
577
+ f"当前章:{current_chapter}",
578
+ f"卷:{position['volume']}(始于第{position['volume_start_chapter']}章)",
579
+ f"故事时间:{position['story_time']}",
580
+ f"场景:{position['scene']}",
581
+ ],
582
+ ),
583
+ ("## 长期约束", context["long_term_constraints"]),
584
+ ("## 核心角色状态", character_lines),
585
+ ("## 活跃伏笔", active_foreshadow_lines(state["foreshadow"])),
586
+ ("## 近三章速记", [f"第{item['chapter']}章|{item['summary']}" for item in context["recent_chapters"]]),
587
+ ("## 下一章承诺", context["next_chapter_commitments"]),
588
+ ("## 连贯性风险", context["continuity_risks"]),
589
+ ]
590
+ lines = [
591
+ f"# 写作连续性上下文 — {state['book_title']}",
592
+ "",
593
+ f"> 状态修订:{state['state_revision']}。截至当前章的续写状态卡,只放下一章真正需要的连续性状态。",
594
+ "",
595
+ ]
596
+ for heading, values in sections:
597
+ lines.append(heading)
598
+ lines.extend(f"- {value}" for value in values or ["无"])
599
+ lines.append("")
600
+ payload = "\n".join(lines).rstrip() + "\n"
601
+ headings = tuple(line for line in payload.splitlines() if line.startswith("## "))
602
+ require(headings == CONTEXT_HEADINGS, "generated context headings do not match the seven-section schema")
603
+ require(byte_size(payload) <= CONTEXT_MAX_BYTES, f"hot context exceeds {CONTEXT_MAX_BYTES} bytes")
604
+ return payload
605
+
606
+
607
+ def normalize_delta(
608
+ value: object,
609
+ *,
610
+ through_chapter: int,
611
+ snapshots: dict[str, dict[str, Any]],
612
+ existing_core_names: dict[str, str],
613
+ ) -> dict[str, Any]:
614
+ delta = as_mapping(value, "delta")
615
+ require_known_keys(
616
+ delta,
617
+ {
618
+ "result", "character_changes", "foreshadow_changes", "timeline_events", "constraints",
619
+ "next_chapter_commitments", "retired_context_items", "retired_characters",
620
+ },
621
+ "delta",
622
+ )
623
+ retired_characters = [
624
+ safe_file_component(name, f"delta.retired_characters[{index}]")
625
+ for index, name in enumerate(as_list(delta.get("retired_characters", []), "delta.retired_characters"))
626
+ ]
627
+ retired_keys = [portable_name_key(name) for name in retired_characters]
628
+ require(len(retired_keys) == len(set(retired_keys)), "delta.retired_characters contains duplicate characters")
629
+ retiring = set(retired_keys)
630
+ character_changes: list[dict[str, Any]] = []
631
+ for index, raw_change in enumerate(as_list(delta.get("character_changes", []), "delta.character_changes")):
632
+ change = as_mapping(raw_change, f"delta.character_changes[{index}]")
633
+ require_known_keys(change, {"name", "change"}, f"delta.character_changes[{index}]")
634
+ name = safe_file_component(change.get("name"), f"delta.character_changes[{index}].name")
635
+ existing = existing_core_names.get(portable_name_key(name))
636
+ is_core = name in snapshots or existing is not None
637
+ # 本章退役的角色记录最后一次变化即可,不必再交一份马上要删的快照。
638
+ require(
639
+ not is_core or name in snapshots or portable_name_key(name) in retiring,
640
+ f"core character {name} changed but has no current snapshot",
641
+ )
642
+ character_changes.append(
643
+ {"name": name, "change": clean_text(change.get("change"), f"delta.character_changes[{index}].change", max_bytes=360)}
644
+ )
645
+ character_keys = [portable_name_key(item["name"]) for item in character_changes]
646
+ require(len(character_keys) == len(set(character_keys)), "delta.character_changes contains duplicate characters")
647
+ foreshadow_changes = [
648
+ normalize_foreshadow_change(
649
+ raw, f"delta.foreshadow_changes[{index}]", allow_delete=True, through_chapter=through_chapter
650
+ )
651
+ for index, raw in enumerate(as_list(delta.get("foreshadow_changes", []), "delta.foreshadow_changes"))
652
+ ]
653
+ timeline_events = [
654
+ normalize_timeline_change(
655
+ raw, f"delta.timeline_events[{index}]", allow_delete=True, through_chapter=through_chapter
656
+ )
657
+ for index, raw in enumerate(as_list(delta.get("timeline_events", []), "delta.timeline_events"))
658
+ ]
659
+ require(
660
+ len({item["id"] for item in foreshadow_changes}) == len(foreshadow_changes),
661
+ "delta.foreshadow_changes contains duplicate IDs",
662
+ )
663
+ require(
664
+ len({item["id"] for item in timeline_events}) == len(timeline_events),
665
+ "delta.timeline_events contains duplicate IDs",
666
+ )
667
+ require(
668
+ set(snapshots).issubset({item["name"] for item in character_changes}),
669
+ "character_snapshots must contain exactly the core characters changed by this transaction",
670
+ )
671
+ return {
672
+ "result": clean_text(delta.get("result"), "delta.result", max_bytes=480),
673
+ "character_changes": character_changes,
674
+ "foreshadow_changes": foreshadow_changes,
675
+ "timeline_events": timeline_events,
676
+ "constraints": clean_string_list(delta.get("constraints", []), "delta.constraints", maximum=6),
677
+ "next_chapter_commitments": clean_string_list(
678
+ delta.get("next_chapter_commitments", []), "delta.next_chapter_commitments", maximum=5
679
+ ),
680
+ "retired_context_items": clean_string_list(
681
+ delta.get("retired_context_items", []), "delta.retired_context_items", maximum=11
682
+ ),
683
+ "retired_characters": retired_characters,
684
+ }
685
+
686
+
687
+ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: set[str]) -> str:
688
+ lines = [
689
+ f"# 第{chapter:03d}章 · {title}",
690
+ f"- 结果:{delta['result']}",
691
+ "- 下一章承诺:" + (";".join(delta["next_chapter_commitments"]) or "无"),
692
+ "",
693
+ "## 角色变化",
694
+ ]
695
+ lines.extend(
696
+ f"- {item['name']}|{'核心' if item['name'] in core_names else '临时'}|{item['change']}"
697
+ for item in delta["character_changes"]
698
+ )
699
+ if not delta["character_changes"]:
700
+ lines.append("- 无")
701
+ lines.extend(["", "## 伏笔变化"])
702
+ for item in delta["foreshadow_changes"]:
703
+ if item["action"] == "delete":
704
+ lines.append(f"- {item['id']}|删除当前登记")
705
+ else:
706
+ planned = f"第{item['planned_resolution_chapter']}章" if item["planned_resolution_chapter"] else "未定"
707
+ lines.append(f"- {item['id']}|{item['status']}|{item['summary']}|回收{planned}")
708
+ if not delta["foreshadow_changes"]:
709
+ lines.append("- 无")
710
+ lines.extend(["", "## 时间与揭示"])
711
+ for item in delta["timeline_events"]:
712
+ if item["action"] == "delete":
713
+ lines.append(f"- {item['id']}|删除当前登记")
714
+ else:
715
+ lines.append(
716
+ f"- {item['id']}|{item['story_time']}|事实:{item['objective_fact']}|"
717
+ f"读者:{item['reader_knowledge']}|{item['reveal_status']}"
718
+ )
719
+ if not delta["timeline_events"]:
720
+ lines.append("- 无")
721
+ lines.extend(["", "## 连贯性约束"])
722
+ lines.extend(f"- {item}" for item in delta["constraints"])
723
+ if not delta["constraints"]:
724
+ lines.append("- 无")
725
+ retired = delta.get("retired_context_items", []) + [
726
+ f"角色状态:{name}" for name in delta.get("retired_characters", [])
727
+ ]
728
+ if retired:
729
+ # 退役条目在此留档,续写状态卡收缩后仍可回查当初撤下了什么。
730
+ lines.extend(["", "## 本章退役登记"])
731
+ lines.extend(f"- {item}" for item in retired)
732
+ payload = "\n".join(lines) + "\n"
733
+ size = byte_size(payload)
734
+ require(size <= DELTA_MAX_BYTES, f"chapter delta is {size} bytes; hard cap is {DELTA_MAX_BYTES}")
735
+ return payload
736
+
737
+
738
+ def normalize_state(document: object) -> dict[str, Any]:
739
+ root = as_mapping(document, "tracking state")
740
+ require_known_keys(
741
+ root,
742
+ {
743
+ "schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
744
+ "state_revision", "context", "characters", "foreshadow", "timeline",
745
+ },
746
+ "tracking state",
747
+ )
748
+ require(root.get("schema_version") == TRACKING_SCHEMA_VERSION, "tracking state schema is unsupported")
749
+ last_chapter = as_int(root.get("last_committed_chapter"), "tracking state.last_committed_chapter")
750
+ imported_through = as_int(root.get("imported_through_chapter"), "tracking state.imported_through_chapter")
751
+ require(imported_through <= last_chapter, "imported chapter cutoff exceeds current chapter")
752
+ context = validate_context_input(root.get("context"), include_initial_fields=True)
753
+ require(
754
+ context["position"]["volume_start_chapter"] <= max(1, last_chapter),
755
+ "context.position.volume_start_chapter is after the current writing position",
756
+ )
757
+ recent_numbers = [item["chapter"] for item in context["recent_chapters"]]
758
+ require(recent_numbers == sorted(recent_numbers), "context.recent_chapters must be ordered")
759
+ require(len(recent_numbers) == len(set(recent_numbers)), "context.recent_chapters contains duplicates")
760
+ require(all(chapter <= last_chapter for chapter in recent_numbers), "context.recent_chapters cannot include future chapters")
761
+ characters = normalize_snapshots(root.get("characters", {}), "tracking state.characters")
762
+ for name in context["active_character_names"]:
763
+ require(name in characters, f"active core character {name} has no current snapshot")
764
+ foreshadow = normalize_foreshadow_state(root.get("foreshadow", {}), last_chapter)
765
+ timeline = normalize_timeline_state(root.get("timeline", {}), last_chapter)
766
+ if last_chapter == 0:
767
+ require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
768
+ require(not timeline, "a chapter-0 project cannot have established timeline facts")
769
+ return {
770
+ "schema_version": TRACKING_SCHEMA_VERSION,
771
+ "book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
772
+ "last_committed_chapter": last_chapter,
773
+ "imported_through_chapter": imported_through,
774
+ "state_revision": as_int(root.get("state_revision"), "tracking state.state_revision"),
775
+ "context": context,
776
+ "characters": characters,
777
+ "foreshadow": foreshadow,
778
+ "timeline": timeline,
779
+ }
780
+
781
+
782
+ def load_state(project: Path) -> dict[str, Any]:
783
+ path = state_path(project)
784
+ require(path.exists(), "tracking state is missing; run init first")
785
+ return normalize_state(read_json(path))
786
+
787
+
788
+ def normalize_initial_document(document: object) -> dict[str, Any]:
789
+ root = as_mapping(document, "init input")
790
+ require_known_keys(
791
+ root,
792
+ {"schema_version", "book_title", "last_chapter", "context", "character_snapshots", "foreshadow", "timeline_events"},
793
+ "init input",
794
+ )
795
+ require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
796
+ last_chapter = as_int(root.get("last_chapter"), "last_chapter")
797
+ context = validate_context_input(root.get("context"), include_initial_fields=True)
798
+ snapshots = normalize_snapshots(root.get("character_snapshots", {}))
799
+ foreshadow: dict[str, dict[str, Any]] = {}
800
+ for index, raw_row in enumerate(as_list(root.get("foreshadow", []), "foreshadow")):
801
+ row = normalize_foreshadow_change(
802
+ raw_row, f"foreshadow[{index}]", allow_delete=False, through_chapter=last_chapter
803
+ )
804
+ require(row["id"] not in foreshadow, f"duplicate foreshadow ID {row['id']}")
805
+ row.pop("action")
806
+ row["updated_chapter"] = max(1, last_chapter)
807
+ foreshadow[row["id"]] = row
808
+ timeline: dict[str, dict[str, Any]] = {}
809
+ for index, raw_event in enumerate(as_list(root.get("timeline_events", []), "timeline_events")):
810
+ event = normalize_timeline_change(
811
+ raw_event, f"timeline_events[{index}]", allow_delete=False, through_chapter=last_chapter
812
+ )
813
+ require(event["id"] not in timeline, f"duplicate timeline event ID {event['id']}")
814
+ event.pop("action")
815
+ event["first_recorded_chapter"] = max(1, last_chapter)
816
+ event["updated_chapter"] = max(1, last_chapter)
817
+ timeline[event["id"]] = event
818
+ return normalize_state(
819
+ {
820
+ "schema_version": TRACKING_SCHEMA_VERSION,
821
+ "book_title": clean_text(root.get("book_title"), "book_title", max_bytes=240),
822
+ "last_committed_chapter": last_chapter,
823
+ "imported_through_chapter": last_chapter,
824
+ "state_revision": 0,
825
+ "context": context,
826
+ "characters": snapshots,
827
+ "foreshadow": foreshadow,
828
+ "timeline": timeline,
829
+ }
830
+ )
831
+
832
+
833
+ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str, Any]:
834
+ root = as_mapping(document, "transaction")
835
+ require_known_keys(
836
+ root,
837
+ {
838
+ "schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
839
+ "delta", "context", "character_snapshots",
840
+ },
841
+ "transaction",
842
+ )
843
+ require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "transaction schema_version is unsupported")
844
+ mode = clean_text(root.get("mode"), "mode", max_bytes=24)
845
+ require(mode in {"append", "revision"}, "mode must be append or revision")
846
+ chapter = as_int(root.get("chapter"), "chapter", minimum=1)
847
+ expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
848
+ require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
849
+ last = state["last_committed_chapter"]
850
+ if mode == "append":
851
+ require(chapter == last + 1, f"append chapter must be {last + 1}, got {chapter}")
852
+ else:
853
+ require(chapter <= last, f"cannot revise unwritten chapter {chapter}; last committed chapter is {last}")
854
+ context = validate_context_input(root.get("context"), include_initial_fields=False)
855
+ snapshots = normalize_snapshots(root.get("character_snapshots", {}))
856
+ existing_names = {portable_name_key(name): name for name in state["characters"]}
857
+ for name in snapshots:
858
+ existing = existing_names.get(portable_name_key(name))
859
+ require(existing is None or existing == name, f"character {name} conflicts with existing character {existing}")
860
+ through_chapter = chapter if mode == "append" else last
861
+ delta = normalize_delta(
862
+ root.get("delta"),
863
+ through_chapter=through_chapter,
864
+ snapshots=snapshots,
865
+ existing_core_names=existing_names,
866
+ )
867
+ return {
868
+ "mode": mode,
869
+ "chapter": chapter,
870
+ "title": clean_text(root.get("chapter_title"), "chapter_title", max_bytes=240),
871
+ "delta": delta,
872
+ "context": context,
873
+ "snapshots": snapshots,
874
+ }
875
+
876
+
877
+ def checkpoint_record(
878
+ change: dict[str, Any], chapter: int, previous: dict[str, Any] | None, *, keep_first_chapter: bool = False
879
+ ) -> dict[str, Any]:
880
+ current = {key: value for key, value in change.items() if key != "action"}
881
+ current["updated_chapter"] = max(previous["updated_chapter"] if previous else chapter, chapter)
882
+ if keep_first_chapter:
883
+ current["first_recorded_chapter"] = previous["first_recorded_chapter"] if previous else chapter
884
+ return current
885
+
886
+
887
+ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dict[str, Any]:
888
+ next_state = copy.deepcopy(state)
889
+ chapter = transaction["chapter"]
890
+ if transaction["mode"] == "append":
891
+ next_state["last_committed_chapter"] = chapter
892
+ next_state["state_revision"] += 1
893
+ next_state["characters"].update(transaction["snapshots"])
894
+
895
+ next_context = transaction["context"]
896
+ # 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
897
+ # 修订记录属于被改写的旧章,落在那里会谎报退役发生的章节。
898
+ is_revision = transaction["mode"] == "revision"
899
+ require(
900
+ not (is_revision and transaction["delta"]["retired_characters"]),
901
+ "retired_characters must be committed in an append transaction, not a revision",
902
+ )
903
+ for name in transaction["delta"]["retired_characters"]:
904
+ require(name in next_state["characters"], f"retired character {name} has no current snapshot")
905
+ require(
906
+ name not in transaction["snapshots"],
907
+ f"character {name} cannot be retired and updated in the same transaction",
908
+ )
909
+ require(
910
+ name not in next_context["active_character_names"],
911
+ f"retired character {name} is still listed in context.active_character_names",
912
+ )
913
+ next_state["characters"].pop(name)
914
+
915
+ # 上下文条目是整份提交的;漏写会静默丢历史裁定,因此掉落必须显式声明。
916
+ previous_items = set(state["context"]["long_term_constraints"]) | set(state["context"]["continuity_risks"])
917
+ dropped = previous_items - (set(next_context["long_term_constraints"]) | set(next_context["continuity_risks"]))
918
+ require(
919
+ not (is_revision and dropped),
920
+ "a revision must resubmit every current context item; retire them in an append transaction instead: "
921
+ + ";".join(sorted(dropped)),
922
+ )
923
+ undeclared = sorted(dropped - set(transaction["delta"]["retired_context_items"]))
924
+ require(
925
+ not undeclared,
926
+ "context items were dropped without being declared in delta.retired_context_items: "
927
+ + ";".join(undeclared),
928
+ )
929
+ transaction["delta"]["retired_context_items"] = sorted(dropped)
930
+
931
+ for change in transaction["delta"]["foreshadow_changes"]:
932
+ if change["action"] == "delete":
933
+ next_state["foreshadow"].pop(change["id"], None)
934
+ else:
935
+ next_state["foreshadow"][change["id"]] = checkpoint_record(
936
+ change, chapter, next_state["foreshadow"].get(change["id"])
937
+ )
938
+ for change in transaction["delta"]["timeline_events"]:
939
+ if change["action"] == "delete":
940
+ next_state["timeline"].pop(change["id"], None)
941
+ else:
942
+ next_state["timeline"][change["id"]] = checkpoint_record(
943
+ change, chapter, next_state["timeline"].get(change["id"]), keep_first_chapter=True
944
+ )
945
+
946
+ recent_by_chapter = {item["chapter"]: item for item in state["context"]["recent_chapters"]}
947
+ if chapter in recent_by_chapter or transaction["mode"] == "append":
948
+ recent_by_chapter[chapter] = {"chapter": chapter, "summary": transaction["delta"]["result"]}
949
+ recent = sorted(recent_by_chapter.values(), key=lambda item: item["chapter"])[-3:]
950
+ current_last = next_state["last_committed_chapter"]
951
+ next_commitments = (
952
+ transaction["delta"]["next_chapter_commitments"]
953
+ if transaction["mode"] == "append" or chapter == current_last
954
+ else state["context"]["next_chapter_commitments"]
955
+ )
956
+ next_state["context"] = {
957
+ **next_context,
958
+ "recent_chapters": recent,
959
+ "next_chapter_commitments": next_commitments,
960
+ }
961
+ return normalize_state(next_state)
962
+
963
+
964
+ def render_views(state: dict[str, Any]) -> dict[str, str]:
965
+ revision = state["state_revision"]
966
+ views = {
967
+ "上下文.md": render_context(state),
968
+ "伏笔.md": render_foreshadow(state["foreshadow"], revision),
969
+ }
970
+ author, reader = render_timeline_views(state["timeline"], revision)
971
+ views["时间线/作者真相.md"] = author
972
+ views["时间线/读者已知.md"] = reader
973
+ for name, snapshot in state["characters"].items():
974
+ views[f"角色状态/{name}.md"] = render_snapshot(
975
+ name, snapshot, state["last_committed_chapter"], revision
976
+ )
977
+ return views
978
+
979
+
980
+ def write_views(tracking: Path, views: dict[str, str]) -> None:
981
+ # 上下文携带 next revision,先写它;任何后续失败都会让 hook/check 发现
982
+ # 上下文 revision 与最后提交的 _tracking-state.json 不一致。
983
+ write_if_changed(tracking / "上下文.md", views["上下文.md"])
984
+ for relative in sorted(path for path in views if path != "上下文.md"):
985
+ write_if_changed(tracking / relative, views[relative])
986
+ expected_character_files = {
987
+ Path(relative).name for relative in views if relative.startswith("角色状态/")
988
+ }
989
+ character_dir = tracking / "角色状态"
990
+ character_dir.mkdir(parents=True, exist_ok=True)
991
+ for path in character_dir.glob("*.md"):
992
+ if path.name not in expected_character_files:
993
+ path.unlink()
994
+
995
+
996
+ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
997
+ if delta_payload is not None and byte_size(delta_payload) > DELTA_TARGET_BYTES:
998
+ emit(
999
+ f"WARNING: chapter delta is {byte_size(delta_payload)} bytes; target is <= {DELTA_TARGET_BYTES}",
1000
+ error=True,
1001
+ )
1002
+ context_size = byte_size(views["上下文.md"])
1003
+ if context_size > CONTEXT_TARGET_BYTES:
1004
+ emit(f"WARNING: hot context is {context_size} bytes; target is <= {CONTEXT_TARGET_BYTES}", error=True)
1005
+ for relative, payload in views.items():
1006
+ if not relative.startswith("角色状态/"):
1007
+ continue
1008
+ size = byte_size(payload)
1009
+ if size > SNAPSHOT_TARGET_BYTES:
1010
+ emit(
1011
+ f"WARNING: character snapshot {Path(relative).stem} is {size} bytes; target is <= {SNAPSHOT_TARGET_BYTES}",
1012
+ error=True,
1013
+ )
1014
+
1015
+
1016
+ def initialize(project: Path, document: object) -> dict[str, Any]:
1017
+ tracking = tracking_root(project)
1018
+ require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
1019
+ state = normalize_initial_document(document)
1020
+ views = render_views(state)
1021
+ state_payload = json_payload(state)
1022
+
1023
+ # 输入全部校验通过后才动用户文件,失败的 init 不会挪走任何东西。
1024
+ archived = archive_retired_tracking_paths(tracking)
1025
+ for directory in (tracking / "逐章记录", tracking / "角色状态", tracking / "时间线"):
1026
+ directory.mkdir(parents=True, exist_ok=True)
1027
+ write_views(tracking, views)
1028
+ atomic_write_text(state_path(project), state_payload)
1029
+ warn_sizes(views)
1030
+ if archived:
1031
+ emit(
1032
+ f"NOTE: 旧追踪结构已原样移入 追踪/{RETIRED_ARCHIVE_DIR}/:{', '.join(archived)};"
1033
+ "当前状态以本次 init 输入为准,旧文件不参与解析。",
1034
+ error=True,
1035
+ )
1036
+ return state
1037
+
1038
+
1039
+ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1040
+ tracking = tracking_root(project)
1041
+ require_no_retired_tracking_paths(tracking)
1042
+ state = load_state(project)
1043
+ transaction = normalize_transaction(state, document)
1044
+ next_state = merge_transaction(state, transaction)
1045
+
1046
+ delta_payload = render_delta(
1047
+ transaction["chapter"],
1048
+ transaction["title"],
1049
+ transaction["delta"],
1050
+ # 本章退役的角色在 next_state 里已被删除,但本章记录里仍应标为核心。
1051
+ set(next_state["characters"]) | set(transaction["delta"]["retired_characters"]),
1052
+ )
1053
+ views = render_views(next_state)
1054
+ next_state_payload = json_payload(next_state)
1055
+ path = delta_path(tracking, transaction["chapter"])
1056
+ if transaction["mode"] == "append" and path.exists():
1057
+ require(
1058
+ path.read_text(encoding="utf-8") == delta_payload,
1059
+ f"chapter delta {transaction['chapter']} already exists with different content",
1060
+ )
1061
+
1062
+ write_if_changed(path, delta_payload)
1063
+ write_views(tracking, views)
1064
+ # 唯一权威文件最后落盘;在此之前失败可用同一事务直接重跑。
1065
+ atomic_write_text(state_path(project), next_state_payload)
1066
+ warn_sizes(views, delta_payload)
1067
+ return next_state
1068
+
1069
+
1070
+ def check_project(project: Path) -> dict[str, Any]:
1071
+ tracking = tracking_root(project)
1072
+ require_no_retired_tracking_paths(tracking)
1073
+ state = load_state(project)
1074
+ last_chapter = state["last_committed_chapter"]
1075
+ required_delta_start = state["imported_through_chapter"] + 1
1076
+ for chapter in range(required_delta_start, last_chapter + 1):
1077
+ require(delta_path(tracking, chapter).exists(), f"chapter delta {chapter} is missing")
1078
+ for path in (tracking / "逐章记录").glob("第*章.md"):
1079
+ match = re.fullmatch(r"第(\d+)章\.md", path.name)
1080
+ require(match is not None, f"chapter delta has an invalid filename: {path.name}")
1081
+ chapter = as_int(int(match.group(1)), f"chapter delta {path.name}", minimum=1)
1082
+ require(path == delta_path(tracking, chapter), f"chapter delta {chapter} filename is not canonical")
1083
+ require(chapter <= last_chapter, f"chapter delta {chapter} exceeds last_committed_chapter")
1084
+ require(path.stat().st_size <= DELTA_MAX_BYTES, f"chapter delta {chapter} exceeds {DELTA_MAX_BYTES} bytes")
1085
+
1086
+ expected_views = render_views(state)
1087
+ for relative, expected in expected_views.items():
1088
+ path = tracking / relative
1089
+ require(path.exists(), f"derived view is missing: {relative}")
1090
+ require(
1091
+ path.read_text(encoding="utf-8") == expected,
1092
+ f"derived view differs from _tracking-state.json: {relative}",
1093
+ )
1094
+ expected_character_files = {
1095
+ Path(relative).name for relative in expected_views if relative.startswith("角色状态/")
1096
+ }
1097
+ actual_character_files = {path.name for path in (tracking / "角色状态").glob("*.md")}
1098
+ require(actual_character_files == expected_character_files, "character snapshot files differ from tracking state")
1099
+ return state
1100
+
1101
+
1102
+ def build_parser() -> argparse.ArgumentParser:
1103
+ parser = argparse.ArgumentParser(description=__doc__)
1104
+ subparsers = parser.add_subparsers(dest="command", required=True)
1105
+ for command in ("init", "commit"):
1106
+ subparser = subparsers.add_parser(command)
1107
+ subparser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1108
+ subparser.add_argument("--input", type=Path, required=True, help="UTF-8 JSON input document")
1109
+ check_parser = subparsers.add_parser("check")
1110
+ check_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1111
+ return parser
1112
+
1113
+
1114
+ def main() -> int:
1115
+ args = build_parser().parse_args()
1116
+ try:
1117
+ if args.command == "init":
1118
+ result = initialize(args.project, read_json(args.input))
1119
+ elif args.command == "commit":
1120
+ result = apply_transaction(args.project, read_json(args.input))
1121
+ else:
1122
+ result = check_project(args.project)
1123
+ except (TrackingError, OSError, UnicodeError) as exc:
1124
+ emit(f"ERROR: {exc}", error=True)
1125
+ return 2
1126
+ emit(
1127
+ json.dumps(
1128
+ {
1129
+ "last_committed_chapter": result["last_committed_chapter"],
1130
+ "state_revision": result["state_revision"],
1131
+ },
1132
+ ensure_ascii=False,
1133
+ )
1134
+ )
1135
+ return 0
1136
+
1137
+
1138
+ if __name__ == "__main__":
1139
+ raise SystemExit(main())