@lemoncat7/dsh-partner 2.8.1 → 2.8.3

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 (833) hide show
  1. package/docs/THIRD_PARTY_LICENSES.md +10 -0
  2. package/docs/blocked-delivery-recovery.md +7 -0
  3. package/docs/concern-attention.md +19 -0
  4. package/docs/observation-contract.md +21 -0
  5. package/docs/pendant-loading.md +30 -0
  6. package/docs/releases/2.8.2.md +7 -0
  7. package/docs/releases/2.8.3.md +20 -0
  8. package/lib/agent-runtime.d.ts +7 -0
  9. package/lib/agent-runtime.d.ts.map +1 -1
  10. package/lib/agent-runtime.js +119 -35
  11. package/lib/agent-runtime.js.map +1 -1
  12. package/lib/api/features/pendant-api.d.ts.map +1 -1
  13. package/lib/api/features/pendant-api.js +11 -2
  14. package/lib/api/features/pendant-api.js.map +1 -1
  15. package/lib/api.d.ts +1 -0
  16. package/lib/api.d.ts.map +1 -1
  17. package/lib/api.js +41 -6
  18. package/lib/api.js.map +1 -1
  19. package/lib/autonomy.d.ts.map +1 -1
  20. package/lib/autonomy.js +9 -2
  21. package/lib/autonomy.js.map +1 -1
  22. package/lib/channel-message.d.ts +2 -0
  23. package/lib/channel-message.d.ts.map +1 -1
  24. package/lib/channel-message.js.map +1 -1
  25. package/lib/channels/direct/message-format.js +127 -127
  26. package/lib/channels/direct/message-format.js.map +1 -1
  27. package/lib/channels/manager.d.ts.map +1 -1
  28. package/lib/channels/manager.js +4 -3
  29. package/lib/channels/manager.js.map +1 -1
  30. package/lib/channels/notification-delivery.d.ts.map +1 -1
  31. package/lib/channels/notification-delivery.js +10 -0
  32. package/lib/channels/notification-delivery.js.map +1 -1
  33. package/lib/channels/weixin/connect.d.ts.map +1 -1
  34. package/lib/client-api.d.ts +17 -2
  35. package/lib/client-api.d.ts.map +1 -1
  36. package/lib/client-api.js +8 -1
  37. package/lib/client-api.js.map +1 -1
  38. package/lib/client.d.ts.map +1 -1
  39. package/lib/client.js +2398 -1970
  40. package/lib/client.js.map +4 -4
  41. package/lib/client.pendant.js +4157 -0
  42. package/lib/collaboration/composition.js +1 -1
  43. package/lib/collaboration/composition.js.map +1 -1
  44. package/lib/concern-attention.d.ts +19 -0
  45. package/lib/concern-attention.d.ts.map +1 -0
  46. package/lib/concern-attention.js +36 -0
  47. package/lib/concern-attention.js.map +1 -0
  48. package/lib/concern-domain.d.ts +10 -1
  49. package/lib/concern-domain.d.ts.map +1 -1
  50. package/lib/concern-domain.js +4 -0
  51. package/lib/concern-domain.js.map +1 -1
  52. package/lib/concern-execution-notes.d.ts +18 -0
  53. package/lib/concern-execution-notes.d.ts.map +1 -0
  54. package/lib/concern-execution-notes.js +41 -0
  55. package/lib/concern-execution-notes.js.map +1 -0
  56. package/lib/concern-execution-receipt.d.ts +4 -0
  57. package/lib/concern-execution-receipt.d.ts.map +1 -0
  58. package/lib/concern-execution-receipt.js +28 -0
  59. package/lib/concern-execution-receipt.js.map +1 -0
  60. package/lib/concern-execution.d.ts +32 -0
  61. package/lib/concern-execution.d.ts.map +1 -0
  62. package/lib/concern-execution.js +112 -0
  63. package/lib/concern-execution.js.map +1 -0
  64. package/lib/concern-management-tool.d.ts.map +1 -1
  65. package/lib/concern-management-tool.js +21 -19
  66. package/lib/concern-management-tool.js.map +1 -1
  67. package/lib/concern-note-source.d.ts +7 -0
  68. package/lib/concern-note-source.d.ts.map +1 -0
  69. package/lib/concern-note-source.js +25 -0
  70. package/lib/concern-note-source.js.map +1 -0
  71. package/lib/concern-resources.d.ts +27 -0
  72. package/lib/concern-resources.d.ts.map +1 -0
  73. package/lib/concern-resources.js +36 -0
  74. package/lib/concern-resources.js.map +1 -0
  75. package/lib/concern-sources.d.ts +1 -1
  76. package/lib/concern-sources.d.ts.map +1 -1
  77. package/lib/concern-store.d.ts +10 -1
  78. package/lib/concern-store.d.ts.map +1 -1
  79. package/lib/concern-store.js +135 -36
  80. package/lib/concern-store.js.map +1 -1
  81. package/lib/concern-tool.d.ts +1 -0
  82. package/lib/concern-tool.d.ts.map +1 -1
  83. package/lib/concern-tool.js +8 -4
  84. package/lib/concern-tool.js.map +1 -1
  85. package/lib/decision/conversation.d.ts +11 -0
  86. package/lib/decision/conversation.d.ts.map +1 -0
  87. package/lib/decision/conversation.js +52 -0
  88. package/lib/decision/conversation.js.map +1 -0
  89. package/lib/decision/events.d.ts +16 -0
  90. package/lib/decision/events.d.ts.map +1 -0
  91. package/lib/decision/events.js +9 -0
  92. package/lib/decision/events.js.map +1 -0
  93. package/lib/domain.d.ts +2 -0
  94. package/lib/domain.d.ts.map +1 -1
  95. package/lib/domain.js.map +1 -1
  96. package/lib/heartbeat.d.ts +12 -10
  97. package/lib/heartbeat.d.ts.map +1 -1
  98. package/lib/heartbeat.js +55 -26
  99. package/lib/heartbeat.js.map +1 -1
  100. package/lib/index.d.ts.map +1 -1
  101. package/lib/index.js +4 -1
  102. package/lib/index.js.map +1 -1
  103. package/lib/memory-prompt-context.js +1 -0
  104. package/lib/memory-prompt-context.js.map +1 -1
  105. package/lib/memory-reflection.js +1 -1
  106. package/lib/observation-contract.d.ts +9 -0
  107. package/lib/observation-contract.d.ts.map +1 -0
  108. package/lib/observation-contract.js +19 -0
  109. package/lib/observation-contract.js.map +1 -0
  110. package/lib/observation-executor.d.ts +5 -0
  111. package/lib/observation-executor.d.ts.map +1 -1
  112. package/lib/observation-executor.js +23 -1
  113. package/lib/observation-executor.js.map +1 -1
  114. package/lib/observation-loop.d.ts +7 -0
  115. package/lib/observation-loop.d.ts.map +1 -1
  116. package/lib/observation-loop.js +65 -4
  117. package/lib/observation-loop.js.map +1 -1
  118. package/lib/observation-progress.d.ts +18 -0
  119. package/lib/observation-progress.d.ts.map +1 -0
  120. package/lib/observation-progress.js +57 -0
  121. package/lib/observation-progress.js.map +1 -0
  122. package/lib/observation-sources.d.ts +14 -0
  123. package/lib/observation-sources.d.ts.map +1 -0
  124. package/lib/observation-sources.js +12 -0
  125. package/lib/observation-sources.js.map +1 -0
  126. package/lib/pendant/badge-art.d.ts +9 -0
  127. package/lib/pendant/badge-art.d.ts.map +1 -0
  128. package/lib/pendant/badge-art.js +68 -0
  129. package/lib/pendant/badge-art.js.map +1 -0
  130. package/lib/pendant/card-image.d.ts.map +1 -1
  131. package/lib/pendant/card-image.js +11 -9
  132. package/lib/pendant/card-image.js.map +1 -1
  133. package/lib/pendant/renderer-loader.d.ts +9 -0
  134. package/lib/pendant/renderer-loader.d.ts.map +1 -0
  135. package/lib/pendant/renderer-loader.js +12 -0
  136. package/lib/pendant/renderer-loader.js.map +1 -0
  137. package/lib/pendant/renderer.d.ts +2 -6
  138. package/lib/pendant/renderer.d.ts.map +1 -1
  139. package/lib/pendant/renderer.js +1 -67
  140. package/lib/pendant/renderer.js.map +1 -1
  141. package/lib/pendant/static-card.d.ts +20 -0
  142. package/lib/pendant/static-card.d.ts.map +1 -0
  143. package/lib/pendant/static-card.js +55 -0
  144. package/lib/pendant/static-card.js.map +1 -0
  145. package/lib/pendant/widget.d.ts.map +1 -1
  146. package/lib/pendant/widget.js +9 -5
  147. package/lib/pendant/widget.js.map +1 -1
  148. package/lib/pendant-renderer.js +14 -14
  149. package/lib/requirements/service.d.ts.map +1 -1
  150. package/lib/requirements/service.js +2 -0
  151. package/lib/requirements/service.js.map +1 -1
  152. package/lib/requirements/worker.d.ts.map +1 -1
  153. package/lib/requirements/worker.js +1 -0
  154. package/lib/requirements/worker.js.map +1 -1
  155. package/lib/scheduler/continuations.d.ts.map +1 -1
  156. package/lib/scheduler/continuations.js +17 -3
  157. package/lib/scheduler/continuations.js.map +1 -1
  158. package/lib/skills/builtin.d.ts.map +1 -1
  159. package/lib/skills/builtin.js +2 -0
  160. package/lib/skills/builtin.js.map +1 -1
  161. package/lib/skills/drama-builtin.d.ts.map +1 -1
  162. package/lib/skills/drama-builtin.js +13 -1
  163. package/lib/skills/drama-builtin.js.map +1 -1
  164. package/lib/skills/story-builtins.d.ts +3 -0
  165. package/lib/skills/story-builtins.d.ts.map +1 -0
  166. package/lib/skills/story-builtins.js +72 -0
  167. package/lib/skills/story-builtins.js.map +1 -0
  168. package/lib/skills/viora-canvas-builtin.d.ts.map +1 -1
  169. package/lib/skills/viora-canvas-builtin.js +12 -1
  170. package/lib/skills/viora-canvas-builtin.js.map +1 -1
  171. package/lib/snapshot-cache.d.ts +7 -0
  172. package/lib/snapshot-cache.d.ts.map +1 -0
  173. package/lib/snapshot-cache.js +26 -0
  174. package/lib/snapshot-cache.js.map +1 -0
  175. package/lib/tasks/result.js +1 -1
  176. package/lib/tasks/result.js.map +1 -1
  177. package/lib/ui/memory/concern-board.d.ts +1 -1
  178. package/lib/ui/memory/concern-board.d.ts.map +1 -1
  179. package/lib/ui/memory/concern-board.js +14 -6
  180. package/lib/ui/memory/concern-board.js.map +1 -1
  181. package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
  182. package/lib/ui/memory/concern-deletion.js +2 -1
  183. package/lib/ui/memory/concern-deletion.js.map +1 -1
  184. package/lib/ui/memory/concern-editor.d.ts.map +1 -1
  185. package/lib/ui/memory/concern-editor.js +5 -3
  186. package/lib/ui/memory/concern-editor.js.map +1 -1
  187. package/lib/ui/memory/concern-execution-fields.d.ts +7 -0
  188. package/lib/ui/memory/concern-execution-fields.d.ts.map +1 -0
  189. package/lib/ui/memory/concern-execution-fields.js +8 -0
  190. package/lib/ui/memory/concern-execution-fields.js.map +1 -0
  191. package/lib/ui/memory/concern-execution-status.d.ts +8 -0
  192. package/lib/ui/memory/concern-execution-status.d.ts.map +1 -0
  193. package/lib/ui/memory/concern-execution-status.js +31 -0
  194. package/lib/ui/memory/concern-execution-status.js.map +1 -0
  195. package/lib/ui/memory/concern-feedback.d.ts +15 -0
  196. package/lib/ui/memory/concern-feedback.d.ts.map +1 -0
  197. package/lib/ui/memory/concern-feedback.js +13 -0
  198. package/lib/ui/memory/concern-feedback.js.map +1 -0
  199. package/lib/ui/memory/concern-panel.d.ts.map +1 -1
  200. package/lib/ui/memory/concern-panel.js +11 -3
  201. package/lib/ui/memory/concern-panel.js.map +1 -1
  202. package/package.json +7 -1
  203. package/resources/skills/novel-game-creation/manifest.json +393 -0
  204. package/resources/skills/novel-game-creation/upstream/LICENSE +21 -0
  205. package/resources/skills/novel-game-creation/upstream/README.md +420 -0
  206. package/resources/skills/novel-game-creation/upstream/README_ZH.md +421 -0
  207. package/resources/skills/novel-game-creation/upstream/VERSION +1 -0
  208. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/BUILD_BRIEF.md +219 -0
  209. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  210. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/RUN.md +138 -0
  211. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  212. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  213. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  214. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  215. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  216. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  217. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  218. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  219. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  220. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  221. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  222. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  223. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  224. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  225. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  226. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  227. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  228. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  229. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  230. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  231. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  232. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  233. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  234. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  235. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  236. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  237. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  238. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  239. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  240. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  241. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  242. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  243. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  244. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  245. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  246. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  247. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  248. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  249. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  250. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  251. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  252. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/new-guofeng.css +1494 -0
  253. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/style.css +2508 -0
  254. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/index.html +18 -0
  255. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  256. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  257. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/data.js +7470 -0
  258. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/engine.js +13066 -0
  259. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/main.js +3051 -0
  260. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/text.js +32 -0
  261. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/package.json +11 -0
  262. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/vercel.json +23 -0
  263. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  264. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/example.json +12 -0
  265. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/README.md +26 -0
  266. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/playable-model.json +99 -0
  267. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verification.json +41 -0
  268. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verify_playable_model.mjs +152 -0
  269. package/resources/skills/novel-game-creation/upstream/manifest.json +498 -0
  270. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/SKILL.md +47 -0
  271. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/agents/openai.yaml +4 -0
  272. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/references/art-direction-method.md +40 -0
  273. package/resources/skills/novel-game-creation/upstream/skills/game-build/SKILL.md +66 -0
  274. package/resources/skills/novel-game-creation/upstream/skills/game-build/agents/openai.yaml +4 -0
  275. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/blender-asset-workflow.md +24 -0
  276. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/build-brief-contract.md +72 -0
  277. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/generative-media-pipeline.md +25 -0
  278. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/playable-model-contract.md +46 -0
  279. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/tts-production-contract.md +23 -0
  280. package/resources/skills/novel-game-creation/upstream/skills/game-concept/SKILL.md +54 -0
  281. package/resources/skills/novel-game-creation/upstream/skills/game-concept/agents/openai.yaml +4 -0
  282. package/resources/skills/novel-game-creation/upstream/skills/game-concept/references/concept-method.md +47 -0
  283. package/resources/skills/novel-game-creation/upstream/skills/game-qa/SKILL.md +39 -0
  284. package/resources/skills/novel-game-creation/upstream/skills/game-qa/agents/openai.yaml +4 -0
  285. package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/qa-contract.md +66 -0
  286. package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/test-design-method.md +49 -0
  287. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/SKILL.md +50 -0
  288. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/agents/openai.yaml +4 -0
  289. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/narrative-design-method.md +35 -0
  290. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/numeric-design-method.md +22 -0
  291. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/world-design-method.md +57 -0
  292. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/SKILL.md +68 -0
  293. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/agents/openai.yaml +4 -0
  294. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  295. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/SKILL.md +54 -0
  296. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/agents/openai.yaml +4 -0
  297. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/intake-method.md +50 -0
  298. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/pipeline-contract.md +60 -0
  299. package/resources/skills/story-creation/manifest.json +1477 -0
  300. package/resources/skills/story-creation/upstream/LICENSE.upstream +21 -0
  301. package/resources/skills/story-creation/upstream/manifest.json +1861 -0
  302. package/resources/skills/story-creation/upstream/roles/chapter-extractor.md +254 -0
  303. package/resources/skills/story-creation/upstream/roles/character-designer.md +168 -0
  304. package/resources/skills/story-creation/upstream/roles/consistency-checker.md +192 -0
  305. package/resources/skills/story-creation/upstream/roles/narrative-writer.md +105 -0
  306. package/resources/skills/story-creation/upstream/roles/story-architect.md +139 -0
  307. package/resources/skills/story-creation/upstream/roles/story-explorer.md +262 -0
  308. package/resources/skills/story-creation/upstream/roles/story-researcher.md +303 -0
  309. package/resources/skills/story-creation/upstream/skills/browser-cdp/SKILL.md +175 -0
  310. package/resources/skills/story-creation/upstream/skills/story/SKILL.md +129 -0
  311. package/resources/skills/story-creation/upstream/skills/story/VERSION +1 -0
  312. package/resources/skills/story-creation/upstream/skills/story/references/author-memory-maintenance.md +75 -0
  313. package/resources/skills/story-creation/upstream/skills/story/references/author-memory.md +117 -0
  314. package/resources/skills/story-creation/upstream/skills/story/scripts/author_memory_commit.py +1635 -0
  315. package/resources/skills/story-creation/upstream/skills/story-cover/SKILL.md +336 -0
  316. package/resources/skills/story-creation/upstream/skills/story-cover/references/cover-styles.md +210 -0
  317. package/resources/skills/story-creation/upstream/skills/story-deslop/SKILL.md +274 -0
  318. package/resources/skills/story-creation/upstream/skills/story-deslop/references/agent-calls.md +5 -0
  319. package/resources/skills/story-creation/upstream/skills/story-deslop/references/anti-ai-writing.md +556 -0
  320. package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  321. package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory.md +117 -0
  322. package/resources/skills/story-creation/upstream/skills/story-deslop/references/banned-words.md +151 -0
  323. package/resources/skills/story-creation/upstream/skills/story-deslop/references/deslop-gates.md +171 -0
  324. package/resources/skills/story-creation/upstream/skills/story-deslop/references/style-resolution.md +27 -0
  325. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/author_memory_commit.py +1635 -0
  326. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-ai-patterns.js +1586 -0
  327. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-degeneration.js +349 -0
  328. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/normalize-punctuation.js +415 -0
  329. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/style-whitelist.js +39 -0
  330. package/resources/skills/story-creation/upstream/skills/story-import/SKILL.md +127 -0
  331. package/resources/skills/story-creation/upstream/skills/story-import/references/character-state-reverse.md +82 -0
  332. package/resources/skills/story-creation/upstream/skills/story-import/references/deep-analysis.md +47 -0
  333. package/resources/skills/story-creation/upstream/skills/story-import/references/format-and-structure.md +168 -0
  334. package/resources/skills/story-creation/upstream/skills/story-import/references/import-report.md +64 -0
  335. package/resources/skills/story-creation/upstream/skills/story-import/references/import-special-cases.md +34 -0
  336. package/resources/skills/story-creation/upstream/skills/story-import/references/import-tracking.md +95 -0
  337. package/resources/skills/story-creation/upstream/skills/story-import/references/length-routing.md +134 -0
  338. package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse-rules.md +79 -0
  339. package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse.md +24 -0
  340. package/resources/skills/story-creation/upstream/skills/story-import/references/state-tracking.md +43 -0
  341. package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-long.md +448 -0
  342. package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-short.md +187 -0
  343. package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-initialization.md +31 -0
  344. package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-transaction.md +146 -0
  345. package/resources/skills/story-creation/upstream/skills/story-import/scripts/build_outline_brief.py +332 -0
  346. package/resources/skills/story-creation/upstream/skills/story-import/scripts/check-outline-contract.js +520 -0
  347. package/resources/skills/story-creation/upstream/skills/story-import/scripts/tracking_commit.py +1691 -0
  348. package/resources/skills/story-creation/upstream/skills/story-import/scripts/wordcount_core.py +405 -0
  349. package/resources/skills/story-creation/upstream/skills/story-long-analyze/SKILL.md +140 -0
  350. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/author-facing.md +252 -0
  351. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/deconstruction-notes.md +270 -0
  352. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/final-checks.md +19 -0
  353. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/index-rebuild.md +11 -0
  354. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/inspiration-library.md +105 -0
  355. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/pipeline-ops.md +154 -0
  356. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  357. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  358. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  359. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  360. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  361. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage5-report.md +33 -0
  362. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-generator.md +76 -0
  363. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-protocol.md +133 -0
  364. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  365. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  366. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  367. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  368. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
  369. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  370. package/resources/skills/story-creation/upstream/skills/story-long-scan/SKILL.md +193 -0
  371. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/genre-trends.md +197 -0
  372. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  373. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-fanqie.md +45 -0
  374. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  375. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qidian.md +35 -0
  376. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qimao.md +27 -0
  377. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/publishing-guide.md +114 -0
  378. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/reader-profiling.md +105 -0
  379. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/topic-decision.md +82 -0
  380. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/aggregate-rank.js +682 -0
  381. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/cdp-utils.js +242 -0
  382. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/ciweimao-rank-scraper.js +240 -0
  383. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/fanqie-rank-scraper.js +658 -0
  384. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/jjwxc-rank-scraper.js +400 -0
  385. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qidian-rank-scraper.js +592 -0
  386. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qimao-rank-scraper.js +435 -0
  387. package/resources/skills/story-creation/upstream/skills/story-long-write/SKILL.md +136 -0
  388. package/resources/skills/story-creation/upstream/skills/story-long-write/references/agent-calls.md +47 -0
  389. package/resources/skills/story-creation/upstream/skills/story-long-write/references/anti-ai-writing.md +556 -0
  390. package/resources/skills/story-creation/upstream/skills/story-long-write/references/artifact-protocols.md +183 -0
  391. package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  392. package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory.md +117 -0
  393. package/resources/skills/story-creation/upstream/skills/story-long-write/references/banned-words.md +151 -0
  394. package/resources/skills/story-creation/upstream/skills/story-long-write/references/benchmark-recall.md +16 -0
  395. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-basics.md +445 -0
  396. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-design-methods.md +428 -0
  397. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-relations.md +433 -0
  398. package/resources/skills/story-creation/upstream/skills/story-long-write/references/commercial-core-methods.md +502 -0
  399. package/resources/skills/story-creation/upstream/skills/story-long-write/references/cross-book-recall.md +88 -0
  400. package/resources/skills/story-creation/upstream/skills/story-long-write/references/deslop-gates.md +171 -0
  401. package/resources/skills/story-creation/upstream/skills/story-long-write/references/dialogue-mastery.md +288 -0
  402. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotion-on-page.md +116 -0
  403. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-arc-design.md +313 -0
  404. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-methods.md +209 -0
  405. package/resources/skills/story-creation/upstream/skills/story-long-write/references/female-audience-writing.md +188 -0
  406. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  407. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  408. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  409. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  410. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  411. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  412. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  413. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  414. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  415. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  416. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  417. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  418. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  419. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  420. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  421. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  422. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  423. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  424. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  425. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  426. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  427. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  428. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  429. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  430. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  431. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  432. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  433. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  434. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  435. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  436. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  437. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  438. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards.md +91 -0
  439. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-readers.md +235 -0
  440. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-hooks.md +225 -0
  441. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-quality.md +177 -0
  442. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-format.md +36 -0
  443. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-catalog.md +394 -0
  444. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-mechanics.md +415 -0
  445. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-reversal.md +335 -0
  446. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-suspense.md +352 -0
  447. package/resources/skills/story-creation/upstream/skills/story-long-write/references/opening-design.md +349 -0
  448. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-conflict.md +457 -0
  449. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-methods.md +287 -0
  450. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-rhythm.md +389 -0
  451. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-structure-theory.md +273 -0
  452. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-core-methods.md +522 -0
  453. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-emotion-system.md +354 -0
  454. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-frameworks.md +711 -0
  455. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-special-topics.md +622 -0
  456. package/resources/skills/story-creation/upstream/skills/story-long-write/references/project-files.md +114 -0
  457. package/resources/skills/story-creation/upstream/skills/story-long-write/references/reader-contract-and-progression.md +117 -0
  458. package/resources/skills/story-creation/upstream/skills/story-long-write/references/reference-index.md +86 -0
  459. package/resources/skills/story-creation/upstream/skills/story-long-write/references/stop-and-fix.md +9 -0
  460. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-combat-face.md +352 -0
  461. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-craft.md +327 -0
  462. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-genre-modules.md +510 -0
  463. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-resolution.md +27 -0
  464. package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-initialization.md +31 -0
  465. package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-transaction.md +146 -0
  466. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-chapter.md +104 -0
  467. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-daily.md +75 -0
  468. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-outline.md +157 -0
  469. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-revision.md +89 -0
  470. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-setup.md +170 -0
  471. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-volume.md +142 -0
  472. package/resources/skills/story-creation/upstream/skills/story-long-write/references/writer-prompt-fallback.md +24 -0
  473. package/resources/skills/story-creation/upstream/skills/story-long-write/references/writing-craft.md +442 -0
  474. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/author_memory_commit.py +1635 -0
  475. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  476. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_writer_prompt.py +611 -0
  477. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-ai-patterns.js +1586 -0
  478. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-degeneration.js +349 -0
  479. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-contract.js +520 -0
  480. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-copy.js +313 -0
  481. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/normalize-punctuation.js +415 -0
  482. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/outline_view.py +286 -0
  483. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/storyctl.py +561 -0
  484. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/style-whitelist.js +39 -0
  485. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/tracking_commit.py +1691 -0
  486. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/wordcount_core.py +405 -0
  487. package/resources/skills/story-creation/upstream/skills/story-review/SKILL.md +231 -0
  488. package/resources/skills/story-creation/upstream/skills/story-review/references/agent-prompts.md +184 -0
  489. package/resources/skills/story-creation/upstream/skills/story-review/references/anti-ai-writing.md +556 -0
  490. package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory-maintenance.md +75 -0
  491. package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory.md +117 -0
  492. package/resources/skills/story-creation/upstream/skills/story-review/references/banned-words.md +151 -0
  493. package/resources/skills/story-creation/upstream/skills/story-review/references/batch-review.md +19 -0
  494. package/resources/skills/story-creation/upstream/skills/story-review/references/character-relations.md +433 -0
  495. package/resources/skills/story-creation/upstream/skills/story-review/references/dialogue-mastery.md +288 -0
  496. package/resources/skills/story-creation/upstream/skills/story-review/references/plot-core-methods.md +522 -0
  497. package/resources/skills/story-creation/upstream/skills/story-review/references/quality-rubric.md +55 -0
  498. package/resources/skills/story-creation/upstream/skills/story-review/references/review-quality.md +246 -0
  499. package/resources/skills/story-creation/upstream/skills/story-review/references/review-tracking.md +10 -0
  500. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/fanqie.md +18 -0
  501. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/qidian.md +19 -0
  502. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/zhihu.md +20 -0
  503. package/resources/skills/story-creation/upstream/skills/story-review/references/solo.md +52 -0
  504. package/resources/skills/story-creation/upstream/skills/story-review/references/style-resolution.md +27 -0
  505. package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-initialization.md +31 -0
  506. package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-transaction.md +146 -0
  507. package/resources/skills/story-creation/upstream/skills/story-review/scripts/author_memory_commit.py +1635 -0
  508. package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-ai-patterns.js +1586 -0
  509. package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-degeneration.js +349 -0
  510. package/resources/skills/story-creation/upstream/skills/story-review/scripts/normalize-punctuation.js +415 -0
  511. package/resources/skills/story-creation/upstream/skills/story-review/scripts/style-whitelist.js +39 -0
  512. package/resources/skills/story-creation/upstream/skills/story-review/scripts/tracking_commit.py +1691 -0
  513. package/resources/skills/story-creation/upstream/skills/story-review/scripts/wordcount_core.py +405 -0
  514. package/resources/skills/story-creation/upstream/skills/story-setup/SKILL.md +165 -0
  515. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  516. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  517. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/anti-ai-writing.md +556 -0
  518. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/banned-words.md +151 -0
  519. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  520. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-basics.md +445 -0
  521. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-design-methods.md +428 -0
  522. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-relations.md +433 -0
  523. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/deslop-gates.md +171 -0
  524. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/dialogue-mastery.md +288 -0
  525. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  526. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotional-arc-design.md +313 -0
  527. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/format-and-structure.md +168 -0
  528. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  529. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  530. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  531. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  532. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  533. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  534. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  535. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  536. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  537. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  538. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  539. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  540. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  541. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  542. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  543. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  544. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  545. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  546. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  547. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  548. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  549. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  550. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  551. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  552. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  553. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  554. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  555. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  556. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  557. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  558. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  559. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  560. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards.md +91 -0
  561. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-readers.md +235 -0
  562. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-chapter-hooks.md +225 -0
  563. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  564. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-catalog.md +394 -0
  565. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-mechanics.md +415 -0
  566. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-quality.md +42 -0
  567. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-reversal.md +335 -0
  568. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-suspense.md +352 -0
  569. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/opening-design.md +349 -0
  570. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-conflict.md +457 -0
  571. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-methods.md +287 -0
  572. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-rhythm.md +389 -0
  573. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/plot-core-methods.md +522 -0
  574. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-chapter-hooks.md +225 -0
  575. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-emotional-methods.md +179 -0
  576. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-genre-formulas.md +417 -0
  577. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-paragraph-hooks.md +146 -0
  578. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-quality.md +39 -0
  579. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-reversal.md +355 -0
  580. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-suspense.md +352 -0
  581. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-combat-face.md +352 -0
  582. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-genre-modules.md +510 -0
  583. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  584. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/writing-craft.md +442 -0
  585. package/resources/skills/story-creation/upstream/skills/story-setup/references/diagnostics.md +38 -0
  586. package/resources/skills/story-creation/upstream/skills/story-short-analyze/SKILL.md +224 -0
  587. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-basics.md +453 -0
  588. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-design.md +438 -0
  589. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-relations.md +395 -0
  590. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-method.md +42 -0
  591. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-paragraph-hooks.md +146 -0
  592. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-reader-profile.md +245 -0
  593. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  594. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  595. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  596. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  597. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  598. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  599. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-writing-techniques.md +477 -0
  600. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/deconstruction-examples.md +79 -0
  601. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/optional-modules.md +97 -0
  602. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/output-contract.md +127 -0
  603. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/quality-checklist.md +80 -0
  604. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/real-market-data.md +90 -0
  605. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/source-story-quality.md +221 -0
  606. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  607. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
  608. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/zhihu-style.md +54 -0
  609. package/resources/skills/story-creation/upstream/skills/story-short-scan/SKILL.md +182 -0
  610. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/manual-rank-input.md +36 -0
  611. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  612. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-heiyan.md +21 -0
  613. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/real-market-data.md +90 -0
  614. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/topic-match.md +38 -0
  615. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/aggregate-rank.js +682 -0
  616. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/cdp-utils.js +242 -0
  617. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/dz-browse-scraper.js +232 -0
  618. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/heiyan-booklist-scraper.js +345 -0
  619. package/resources/skills/story-creation/upstream/skills/story-short-write/SKILL.md +136 -0
  620. package/resources/skills/story-creation/upstream/skills/story-short-write/references/agent-calls.md +13 -0
  621. package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  622. package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory.md +117 -0
  623. package/resources/skills/story-creation/upstream/skills/story-short-write/references/banned-words.md +151 -0
  624. package/resources/skills/story-creation/upstream/skills/story-short-write/references/benchmark-recall.md +40 -0
  625. package/resources/skills/story-creation/upstream/skills/story-short-write/references/cross-book-recall.md +62 -0
  626. package/resources/skills/story-creation/upstream/skills/story-short-write/references/dialogue-mastery.md +288 -0
  627. package/resources/skills/story-creation/upstream/skills/story-short-write/references/emotional-methods.md +179 -0
  628. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +159 -0
  629. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +156 -0
  630. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +163 -0
  631. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +167 -0
  632. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +178 -0
  633. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +166 -0
  634. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +159 -0
  635. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +151 -0
  636. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +153 -0
  637. package/resources/skills/story-creation/upstream/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
  638. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-formulas.md +417 -0
  639. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-techniques.md +490 -0
  640. package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-chapter.md +225 -0
  641. package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-paragraph.md +146 -0
  642. package/resources/skills/story-creation/upstream/skills/story-short-write/references/output-contract.md +127 -0
  643. package/resources/skills/story-creation/upstream/skills/story-short-write/references/reference-index.md +23 -0
  644. package/resources/skills/story-creation/upstream/skills/story-short-write/references/scene-craft.md +40 -0
  645. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-craft.md +147 -0
  646. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-deslop.md +133 -0
  647. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-format.md +199 -0
  648. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-prose-quality.md +224 -0
  649. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-reversal.md +355 -0
  650. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-suspense.md +352 -0
  651. package/resources/skills/story-creation/upstream/skills/story-short-write/references/style-resolution.md +27 -0
  652. package/resources/skills/story-creation/upstream/skills/story-short-write/references/submission-craft.md +35 -0
  653. package/resources/skills/story-creation/upstream/skills/story-short-write/references/villain-and-reveal.md +153 -0
  654. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-design.md +85 -0
  655. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-draft.md +116 -0
  656. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-revision.md +99 -0
  657. package/resources/skills/story-creation/upstream/skills/story-short-write/references/writing-workflow.md +50 -0
  658. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/author_memory_commit.py +1635 -0
  659. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-ai-patterns.js +1586 -0
  660. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-degeneration.js +349 -0
  661. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-delivery-contract.js +241 -0
  662. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-outline-copy.js +313 -0
  663. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  664. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/normalize-punctuation.js +415 -0
  665. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/style-whitelist.js +39 -0
  666. package/resources/skills/video-recap-production/manifest.json +673 -0
  667. package/resources/skills/video-recap-production/upstream/CHANGELOG.md +371 -0
  668. package/resources/skills/video-recap-production/upstream/LICENSE +21 -0
  669. package/resources/skills/video-recap-production/upstream/README.en.md +468 -0
  670. package/resources/skills/video-recap-production/upstream/README.md +459 -0
  671. package/resources/skills/video-recap-production/upstream/manifest.json +839 -0
  672. package/resources/skills/video-recap-production/upstream/skills/video-assemble/SKILL.md +112 -0
  673. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/audio-modes.md +63 -0
  674. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  675. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/foreground-compose.md +118 -0
  676. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  677. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  678. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  679. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  680. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  681. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  682. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  683. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  684. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  685. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  686. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  687. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  688. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  689. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/narration-adoption.md +103 -0
  690. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/packaging.md +45 -0
  691. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/pair-media.md +74 -0
  692. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/source-score.md +147 -0
  693. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/subtitle-track.md +202 -0
  694. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  695. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  696. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  697. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  698. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  699. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  700. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/artifacts.py +47 -0
  701. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble.py +591 -0
  702. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble_constants.py +51 -0
  703. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_contract.py +318 -0
  704. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_settings.py +138 -0
  705. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_automation.py +195 -0
  706. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_mix.py +465 -0
  707. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/compose_foreground.py +293 -0
  708. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/export_jianying.py +254 -0
  709. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  710. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/builders.py +718 -0
  711. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/model.py +105 -0
  712. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/optional.py +23 -0
  713. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/schema.py +53 -0
  714. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/templates.py +37 -0
  715. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/timeline_contract.py +223 -0
  716. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/tracks.py +72 -0
  717. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/writer.py +383 -0
  718. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/lib.py +218 -0
  719. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/media.py +190 -0
  720. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/narration_audio.py +413 -0
  721. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/packaging.py +122 -0
  722. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/pair_media.py +216 -0
  723. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/render_preflight.py +33 -0
  724. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_score.py +469 -0
  725. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_subtitles.py +423 -0
  726. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  727. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/core.py +330 -0
  728. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/render.py +82 -0
  729. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track.py +324 -0
  730. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  731. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline.py +217 -0
  732. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline_emit.py +167 -0
  733. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/visual_render.py +448 -0
  734. package/resources/skills/video-recap-production/upstream/skills/video-cut/SKILL.md +108 -0
  735. package/resources/skills/video-recap-production/upstream/skills/video-cut/references/shot-review.md +50 -0
  736. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut.py +41 -0
  737. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_cli.py +308 -0
  738. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_contract.py +424 -0
  739. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_render.py +304 -0
  740. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/lib.py +114 -0
  741. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/media_geometry.py +238 -0
  742. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narration_mapping.py +71 -0
  743. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narrative_selection.py +244 -0
  744. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/sentence_boundaries.py +585 -0
  745. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/shot_review.py +357 -0
  746. package/resources/skills/video-recap-production/upstream/skills/video-recap/SKILL.md +260 -0
  747. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/audio-routing.md +126 -0
  748. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/config-playbook.md +65 -0
  749. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/data-schema.md +545 -0
  750. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/resource-library.md +151 -0
  751. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  752. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/doctor.py +556 -0
  753. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/final_qc.py +527 -0
  754. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/lib.py +220 -0
  755. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/library.py +509 -0
  756. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/materials.py +406 -0
  757. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/mimo_qc.py +33 -0
  758. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/project_binding.py +286 -0
  759. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/__init__.py +1 -0
  760. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_client.py +13 -0
  761. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_contract.py +9 -0
  762. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_evidence.py +287 -0
  763. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_observations.py +175 -0
  764. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_payload.py +206 -0
  765. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_report.py +318 -0
  766. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_runner.py +130 -0
  767. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc_contract.py +320 -0
  768. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap.py +9 -0
  769. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_cli.py +209 -0
  770. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_inspect.py +452 -0
  771. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_review.py +70 -0
  772. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runner.py +696 -0
  773. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runtime.py +260 -0
  774. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_source.py +278 -0
  775. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_stage_qc.py +160 -0
  776. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_timeline.py +621 -0
  777. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/resource_lock.py +162 -0
  778. package/resources/skills/video-recap-production/upstream/skills/video-script/SKILL.md +251 -0
  779. package/resources/skills/video-recap-production/upstream/skills/video-script/references/creative-editing-playbook.md +229 -0
  780. package/resources/skills/video-recap-production/upstream/skills/video-script/references/promotional-copy.md +68 -0
  781. package/resources/skills/video-recap-production/upstream/skills/video-script/references/research-guide.md +75 -0
  782. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/agent_text.py +301 -0
  783. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/deslop_qc.py +211 -0
  784. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/evidence_bundle.py +440 -0
  785. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/lib.py +254 -0
  786. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/narration_lint.py +707 -0
  787. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review.py +40 -0
  788. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_grounding.py +238 -0
  789. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_response.py +669 -0
  790. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_runner.py +186 -0
  791. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/speech_ownership.py +143 -0
  792. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/timeline_fusion.py +108 -0
  793. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/validate.py +175 -0
  794. package/resources/skills/video-recap-production/upstream/skills/video-understanding/SKILL.md +77 -0
  795. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/data-schema.md +193 -0
  796. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/prompt-templates.md +37 -0
  797. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/research-guide.md +74 -0
  798. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/agent_text.py +301 -0
  799. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr.py +279 -0
  800. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  801. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/brief.py +9 -0
  802. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  803. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/builder.py +402 -0
  804. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/context.py +317 -0
  805. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/inputs.py +268 -0
  806. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/timeline.py +326 -0
  807. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/consolidate.py +596 -0
  808. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/deslop_qc.py +211 -0
  809. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/detect.py +504 -0
  810. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/extract.py +62 -0
  811. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/lib.py +419 -0
  812. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/narration_lint.py +707 -0
  813. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/speech_ownership.py +143 -0
  814. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/storyboard.py +445 -0
  815. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/timeline_fusion.py +108 -0
  816. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understand.py +9 -0
  817. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_brief.py +190 -0
  818. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_cache.py +345 -0
  819. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_runner.py +387 -0
  820. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_storyboard.py +167 -0
  821. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/vlm.py +674 -0
  822. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/SKILL.md +91 -0
  823. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/references/index-tts.md +31 -0
  824. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  825. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/dub.py +623 -0
  826. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/lib.py +352 -0
  827. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  828. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/fish_audio.py +91 -0
  829. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  830. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/tts_audio.py +102 -0
  831. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/voiceover.py +771 -0
  832. package/resources/skills/viora-canvas-production/manifest.json +5 -1
  833. package/resources/skills/viora-canvas-production/references/story-handoff.md +49 -0
@@ -0,0 +1,1691 @@
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. Per-project locking
7
+ serializes concurrent writers before revision and wordcount checks.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import copy
14
+ import importlib.util
15
+ import json
16
+ import os
17
+ import re
18
+ import stat
19
+ import sys
20
+ import tempfile
21
+ import time
22
+ import unicodedata
23
+ from contextlib import contextmanager
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+
28
+ _WORDCOUNT_CORE_PATH = Path(__file__).with_name("wordcount_core.py")
29
+ if not _WORDCOUNT_CORE_PATH.is_file(): # pragma: no cover - broken deployment
30
+ raise RuntimeError("TOOL_UNAVAILABLE: wordcount_core.py")
31
+ _WORDCOUNT_CORE_SPEC = importlib.util.spec_from_file_location(
32
+ "story_wordcount_core", _WORDCOUNT_CORE_PATH
33
+ )
34
+ if _WORDCOUNT_CORE_SPEC is None or _WORDCOUNT_CORE_SPEC.loader is None: # pragma: no cover
35
+ raise RuntimeError("unable to load wordcount core")
36
+ wordcount_core = importlib.util.module_from_spec(_WORDCOUNT_CORE_SPEC)
37
+ _WORDCOUNT_CORE_SPEC.loader.exec_module(wordcount_core)
38
+
39
+
40
+ INPUT_SCHEMA_VERSION = 1
41
+ TRACKING_SCHEMA_VERSION = 4
42
+ DELTA_TARGET_BYTES = 1536
43
+ DELTA_MAX_BYTES = 3072
44
+ CONTEXT_TARGET_BYTES = 8192
45
+ CONTEXT_MAX_BYTES = 12288
46
+ SNAPSHOT_TARGET_BYTES = 4096
47
+ SNAPSHOT_MAX_BYTES = 8192
48
+
49
+ CONTEXT_HEADINGS = (
50
+ "## 当前位置",
51
+ "## 长期约束",
52
+ "## 核心角色状态",
53
+ "## 活跃伏笔",
54
+ "## 近三章速记",
55
+ "## 下一章承诺",
56
+ "## 连贯性风险",
57
+ )
58
+ FORESHADOW_STATUSES = ("已埋", "已回收", "已过期", "放弃")
59
+ FORESHADOW_IMPORTANCE = ("高", "中", "低")
60
+ REVEAL_STATUSES = ("未揭示", "部分揭示", "已揭示")
61
+ # 伏笔与时间线条目里模型常用的同义写法:含义明确的直接改成正式名,不让一次提交为措辞失败。
62
+ ACTION_ALIASES = {
63
+ "add": "upsert", "new": "upsert", "create": "upsert", "update": "upsert", "plant": "upsert",
64
+ "advance": "upsert", "resolve": "upsert", "insert": "upsert",
65
+ "新增": "upsert", "埋设": "upsert", "更新": "upsert", "推进": "upsert", "回收": "upsert",
66
+ "remove": "delete", "删除": "delete",
67
+ }
68
+ # 带状态含义的伏笔动作词:它们也映射成 upsert,但动作词本身说了伏笔走到哪一步。
69
+ # status 缺省时按动作词补上(意图只有这一个来源,不会猜错);status 与动作词矛盾时退回,
70
+ # 因为无法判断是动作词用错还是状态写错——静默改任何一边都可能把没回收的伏笔记成已回收。
71
+ FORESHADOW_ACTION_STATUS = {
72
+ "resolve": "已回收", "回收": "已回收",
73
+ "advance": "已埋", "推进": "已埋",
74
+ }
75
+ FORESHADOW_KEY_ALIASES = {
76
+ "planned_chapter": "planned_resolution_chapter", "planned_payoff_chapter": "planned_resolution_chapter",
77
+ "payoff_chapter": "planned_resolution_chapter", "resolution_chapter": "planned_resolution_chapter",
78
+ "resolve_chapter": "planned_resolution_chapter", "plant_chapter": "planted_chapter",
79
+ "planted": "planted_chapter", "description": "summary", "content": "summary",
80
+ }
81
+ TIMELINE_KEY_ALIASES = {"time": "story_time", "fact": "objective_fact", "reveal": "reveal_status"}
82
+ INVALID_FILE_CHARS = re.compile(r"[<>:\"/\\|?*\x00-\x1f]")
83
+ FORESHADOW_ID = re.compile(r"^F\d{3,}$")
84
+ SNAPSHOT_TEXT_FIELDS = ("identity", "location", "goal", "state")
85
+ SNAPSHOT_LIST_FIELDS = ("abilities_resources", "relationships", "knowledge", "open_threads")
86
+ EVENT_ID = re.compile(r"^E\d{3,}$")
87
+ WINDOWS_RESERVED_NAMES = {
88
+ "CON",
89
+ "PRN",
90
+ "AUX",
91
+ "NUL",
92
+ *(f"COM{index}" for index in range(1, 10)),
93
+ *(f"LPT{index}" for index in range(1, 10)),
94
+ }
95
+ RETIRED_TRACKING_PATHS = (
96
+ "_tracking-meta.json",
97
+ "阶段摘要.md",
98
+ "角色状态.md",
99
+ "时间线.md",
100
+ "摘要",
101
+ "时间线/事件库.json",
102
+ )
103
+ RETIRED_ARCHIVE_DIR = "_旧追踪存档"
104
+
105
+
106
+ class TrackingError(ValueError):
107
+ """Expected validation or tracking-state error."""
108
+
109
+
110
+ def require(condition: bool, message: str) -> None:
111
+ if not condition:
112
+ raise TrackingError(message)
113
+
114
+
115
+ def wordcount_value(function: Any, *args: Any, **kwargs: Any) -> Any:
116
+ try:
117
+ return function(*args, **kwargs)
118
+ except wordcount_core.WordcountError as exc:
119
+ raise TrackingError(str(exc)) from exc
120
+
121
+
122
+ def as_mapping(value: object, label: str) -> dict[str, Any]:
123
+ require(isinstance(value, dict), f"{label} must be a JSON object")
124
+ return value
125
+
126
+
127
+ def as_list(value: object, label: str) -> list[Any]:
128
+ require(isinstance(value, list), f"{label} must be a JSON array")
129
+ return value
130
+
131
+
132
+ def as_int(value: object, label: str, *, minimum: int = 0) -> int:
133
+ require(isinstance(value, int) and not isinstance(value, bool), f"{label} must be an integer")
134
+ require(value >= minimum, f"{label} must be >= {minimum}")
135
+ return value
136
+
137
+
138
+ def as_chapter(value: object, label: str, *, minimum: int = 1) -> int:
139
+ """章号:模型常写成 "22"。纯数字字符串按整数收,语义不变;其余照旧报错。"""
140
+ if isinstance(value, str) and value.strip().isdecimal():
141
+ value = int(value.strip())
142
+ return as_int(value, label, minimum=minimum)
143
+
144
+
145
+ def with_aliases(row: dict[str, Any], aliases: dict[str, str]) -> dict[str, Any]:
146
+ """把含义明确的同义字段名改成正式字段名;正式字段已在时不动,留给未知字段报错。"""
147
+ row = dict(row)
148
+ for alias, canonical in aliases.items():
149
+ if alias in row and canonical not in row:
150
+ row[canonical] = row.pop(alias)
151
+ return row
152
+
153
+
154
+ def normalize_action(value: object, label: str, *, allow_delete: bool) -> str:
155
+ action = clean_text(value, label, max_bytes=24)
156
+ action = ACTION_ALIASES.get(action.lower(), action.lower())
157
+ allowed = ("upsert", "delete") if allow_delete else ("upsert",)
158
+ require(action in allowed, f"{label} is invalid: use {' or '.join(allowed)}")
159
+ return action
160
+
161
+
162
+ def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -> None:
163
+ unknown = set(mapping) - allowed
164
+ require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}"
165
+ f"(只收 {', '.join(sorted(allowed))})")
166
+
167
+
168
+ # 事务校验期间收集全部超长字段,一次报完;None 表示逐条立即报错(初始化、状态读取等路径)。
169
+ _LENGTH_ISSUES: list[str] | None = None
170
+
171
+
172
+ def length_hint(label: str, text: str, max_bytes: int) -> str:
173
+ """把字节上限换算成字数:模型写的是中文,只看得懂「要删几个字」。"""
174
+ size = len(text.encode("utf-8"))
175
+ allowed = max(1, max_bytes * len(text) // size)
176
+ return (f"{label} exceeds {max_bytes} bytes(现 {len(text)} 字,上限约 {allowed} 字,"
177
+ f"至少删 {len(text) - allowed} 字)")
178
+
179
+
180
+ def clean_text(value: object, label: str, *, allow_empty: bool = False, max_bytes: int = 768) -> str:
181
+ require(isinstance(value, str), f"{label} must be a string")
182
+ cleaned = " ".join(value.replace("|", "|").split())
183
+ require(allow_empty or bool(cleaned), f"{label} must not be empty")
184
+ if len(cleaned.encode("utf-8")) > max_bytes:
185
+ if _LENGTH_ISSUES is None:
186
+ raise TrackingError(length_hint(label, cleaned, max_bytes))
187
+ _LENGTH_ISSUES.append(length_hint(label, cleaned, max_bytes))
188
+ return cleaned
189
+
190
+
191
+ def clean_string_list(
192
+ value: object,
193
+ label: str,
194
+ *,
195
+ maximum: int | None = None,
196
+ item_max_bytes: int = 384,
197
+ ) -> list[str]:
198
+ values = as_list(value, label)
199
+ if maximum is not None:
200
+ require(len(values) <= maximum, f"{label} may contain at most {maximum} items")
201
+ return [clean_text(item, f"{label}[{index}]", max_bytes=item_max_bytes) for index, item in enumerate(values)]
202
+
203
+
204
+ def safe_file_component(value: object, label: str) -> str:
205
+ name = unicodedata.normalize("NFC", clean_text(value, label, max_bytes=180))
206
+ require(not INVALID_FILE_CHARS.search(name), f"{label} contains an invalid filename character")
207
+ require(name not in {".", ".."} and not name.endswith((".", " ")), f"{label} is not a safe filename")
208
+ require(name.split(".", 1)[0].upper() not in WINDOWS_RESERVED_NAMES, f"{label} is reserved on Windows")
209
+ return name
210
+
211
+
212
+ def portable_name_key(name: str) -> str:
213
+ return unicodedata.normalize("NFC", name).casefold()
214
+
215
+
216
+ def byte_size(text: str) -> int:
217
+ return len(text.encode("utf-8"))
218
+
219
+
220
+ def emit(text: str, *, error: bool = False) -> None:
221
+ """Write UTF-8 bytes directly.
222
+
223
+ Windows 的文本 stdout 是 cp1252(含中文即 UnicodeEncodeError),stderr 默认
224
+ backslashreplace(中文被转义成反斜杠码位,作者看不懂)。两条路都要绕开。
225
+ """
226
+ stream = sys.stderr if error else sys.stdout
227
+ stream.flush()
228
+ stream.buffer.write((text + "\n").encode("utf-8"))
229
+ stream.buffer.flush()
230
+
231
+
232
+ def read_json(path: Path) -> object:
233
+ try:
234
+ return json.loads(path.read_text(encoding="utf-8"))
235
+ except (OSError, json.JSONDecodeError) as exc:
236
+ raise TrackingError(f"unable to read JSON {path}: {exc}") from exc
237
+
238
+
239
+ def json_payload(document: object) -> str:
240
+ return json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
241
+
242
+
243
+ def atomic_write_text(path: Path, payload: str) -> None:
244
+ path.parent.mkdir(parents=True, exist_ok=True)
245
+ mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o644
246
+ fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
247
+ temporary = Path(temporary_name)
248
+ try:
249
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
250
+ handle.write(payload)
251
+ handle.flush()
252
+ os.fsync(handle.fileno())
253
+ os.chmod(temporary, mode)
254
+ os.replace(temporary, path)
255
+ finally:
256
+ temporary.unlink(missing_ok=True)
257
+
258
+
259
+ def write_if_changed(path: Path, payload: str) -> None:
260
+ try:
261
+ if path.read_text(encoding="utf-8") == payload:
262
+ return
263
+ except FileNotFoundError:
264
+ pass
265
+ atomic_write_text(path, payload)
266
+
267
+
268
+ def tracking_root(project: Path) -> Path:
269
+ return project.resolve() / "追踪"
270
+
271
+
272
+ def state_path(project: Path) -> Path:
273
+ return tracking_root(project) / "_tracking-state.json"
274
+
275
+
276
+ @contextmanager
277
+ def project_write_lock(project: Path, *, timeout_seconds: float = 10.0):
278
+ tracking = tracking_root(project)
279
+ tracking.mkdir(parents=True, exist_ok=True)
280
+ path = tracking / ".tracking-commit.lock"
281
+ deadline = time.monotonic() + timeout_seconds
282
+ descriptor: int | None = None
283
+ while descriptor is None:
284
+ try:
285
+ descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
286
+ except FileExistsError:
287
+ if time.monotonic() >= deadline:
288
+ raise TrackingError(
289
+ "tracking commit lock is busy or stale; retry, or remove 追踪/.tracking-commit.lock after confirming no commit is running"
290
+ )
291
+ time.sleep(0.05)
292
+ try:
293
+ os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
294
+ os.fsync(descriptor)
295
+ yield
296
+ finally:
297
+ os.close(descriptor)
298
+ try:
299
+ path.unlink()
300
+ except FileNotFoundError:
301
+ pass
302
+
303
+
304
+ def delta_path(tracking: Path, chapter: int) -> Path:
305
+ width = max(3, len(str(chapter)))
306
+ return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
307
+
308
+
309
+ def find_retired_tracking_paths(tracking: Path) -> list[str]:
310
+ found = [relative for relative in RETIRED_TRACKING_PATHS if (tracking / relative).exists()]
311
+ found.extend(sorted(path.name for path in tracking.glob("基线_截至第*章.md")))
312
+ return found
313
+
314
+
315
+ def require_no_retired_tracking_paths(tracking: Path) -> None:
316
+ found = find_retired_tracking_paths(tracking)
317
+ require(not found, f"retired tracking files are not supported: {', '.join(found)}")
318
+
319
+
320
+ def archive_retired_tracking_paths(tracking: Path) -> list[str]:
321
+ """Move a pre-transaction 追踪/ aside so init can build the current protocol in place.
322
+
323
+ Nothing is parsed or converted: the old files are kept verbatim for the author to
324
+ consult, and the new state is reconstructed from the init document alone.
325
+ """
326
+ retired = find_retired_tracking_paths(tracking)
327
+ if not retired:
328
+ return []
329
+ archive = tracking / RETIRED_ARCHIVE_DIR
330
+ for relative in retired:
331
+ require(
332
+ not (archive / relative).exists(),
333
+ f"追踪/{RETIRED_ARCHIVE_DIR}/{relative} already exists; move it away before initializing",
334
+ )
335
+ # 先全量校验再搬运;中断后重跑时已搬走的条目不再出现在待搬列表里,可直接续做。
336
+ for relative in retired:
337
+ target = archive / relative
338
+ target.parent.mkdir(parents=True, exist_ok=True)
339
+ os.replace(tracking / relative, target)
340
+ return retired
341
+
342
+
343
+ def _both_blank(position: dict[str, Any]) -> bool:
344
+ return all(isinstance(position.get(key), str) and not position[key].strip() for key in ("story_time", "scene"))
345
+
346
+
347
+ def validate_position(value: object, label: str = "context.position") -> dict[str, Any]:
348
+ position = as_mapping(value, label)
349
+ require_known_keys(position, {"volume", "volume_start_chapter", "story_time", "scene"}, label)
350
+ require(not _both_blank(position),
351
+ f"{label}.story_time and {label}.scene must not be empty: fill where this chapter ends")
352
+ return {
353
+ "volume": safe_file_component(position.get("volume"), f"{label}.volume"),
354
+ "volume_start_chapter": as_int(
355
+ position.get("volume_start_chapter"), f"{label}.volume_start_chapter", minimum=1
356
+ ),
357
+ "story_time": clean_text(position.get("story_time"), f"{label}.story_time", max_bytes=240),
358
+ "scene": clean_text(position.get("scene"), f"{label}.scene", max_bytes=240),
359
+ }
360
+
361
+
362
+ def joined_text(value: object) -> object:
363
+ """单句字段写成了字符串列表时按「;」连成一句,内容不丢。"""
364
+ if isinstance(value, list) and value and all(isinstance(item, str) for item in value):
365
+ return ";".join(value)
366
+ return value
367
+
368
+
369
+ def as_text_list(value: object) -> object:
370
+ """列表字段只写了一句时当作一项。"""
371
+ return [value] if isinstance(value, str) and value.strip() else value
372
+
373
+
374
+ def normalize_snapshot(value: object, label: str) -> dict[str, Any]:
375
+ snapshot = as_mapping(value, label)
376
+ snapshot = {key: joined_text(item) if key in SNAPSHOT_TEXT_FIELDS else as_text_list(item)
377
+ if key in SNAPSHOT_LIST_FIELDS else item for key, item in snapshot.items()}
378
+ require_known_keys(
379
+ snapshot,
380
+ {"identity", "location", "goal", "state", "abilities_resources", "relationships", "knowledge", "open_threads"},
381
+ label,
382
+ )
383
+ return {
384
+ "identity": clean_text(snapshot.get("identity"), f"{label}.identity", max_bytes=240),
385
+ "location": clean_text(snapshot.get("location"), f"{label}.location", max_bytes=240),
386
+ "goal": clean_text(snapshot.get("goal"), f"{label}.goal", max_bytes=300),
387
+ "state": clean_text(snapshot.get("state"), f"{label}.state", max_bytes=300),
388
+ "abilities_resources": clean_string_list(
389
+ snapshot.get("abilities_resources", []), f"{label}.abilities_resources"
390
+ ),
391
+ "relationships": clean_string_list(snapshot.get("relationships", []), f"{label}.relationships"),
392
+ "knowledge": clean_string_list(snapshot.get("knowledge", []), f"{label}.knowledge"),
393
+ "open_threads": clean_string_list(snapshot.get("open_threads", []), f"{label}.open_threads"),
394
+ }
395
+
396
+
397
+ def normalize_snapshots(value: object, label: str = "character_snapshots") -> dict[str, dict[str, Any]]:
398
+ snapshots = as_mapping(value, label)
399
+ normalized: dict[str, dict[str, Any]] = {}
400
+ portable_names: set[str] = set()
401
+ for raw_name, raw_snapshot in snapshots.items():
402
+ name = safe_file_component(raw_name, f"{label} character name")
403
+ key = portable_name_key(name)
404
+ require(key not in portable_names, f"{label} contains a cross-platform duplicate character {name}")
405
+ portable_names.add(key)
406
+ normalized[name] = normalize_snapshot(raw_snapshot, f"{label}.{name}")
407
+ return normalized
408
+
409
+
410
+ def render_snapshot(name: str, snapshot: dict[str, Any], through_chapter: int, revision: int) -> str:
411
+ def section(title: str, values: list[str]) -> list[str]:
412
+ return [f"## {title}", *(f"- {item}" for item in values or ["无"]), ""]
413
+
414
+ lines = [
415
+ f"# {name}|当前状态",
416
+ "",
417
+ f"- 状态修订:{revision}",
418
+ f"- 截至章节:第{through_chapter}章",
419
+ f"- 身份:{snapshot['identity']}",
420
+ f"- 位置:{snapshot['location']}",
421
+ f"- 当前目标:{snapshot['goal']}",
422
+ f"- 身心状态:{snapshot['state']}",
423
+ "",
424
+ ]
425
+ lines.extend(section("能力与资源", snapshot["abilities_resources"]))
426
+ lines.extend(section("关键关系", snapshot["relationships"]))
427
+ lines.extend(section("已知信息", snapshot["knowledge"]))
428
+ lines.extend(section("未结事项", snapshot["open_threads"]))
429
+ payload = "\n".join(lines).rstrip() + "\n"
430
+ require(
431
+ byte_size(payload) <= SNAPSHOT_MAX_BYTES,
432
+ f"character snapshot {name} exceeds hard cap of {SNAPSHOT_MAX_BYTES} bytes",
433
+ )
434
+ return payload
435
+
436
+
437
+ def normalize_foreshadow_change(
438
+ value: object,
439
+ label: str,
440
+ *,
441
+ allow_delete: bool,
442
+ through_chapter: int,
443
+ ) -> dict[str, Any]:
444
+ row = with_aliases(as_mapping(value, label), FORESHADOW_KEY_ALIASES)
445
+ require_known_keys(
446
+ row,
447
+ {"action", "id", "summary", "planted_chapter", "planned_resolution_chapter", "status", "importance"},
448
+ label,
449
+ )
450
+ raw_action = row.get("action", "upsert")
451
+ action = normalize_action(raw_action, f"{label}.action", allow_delete=allow_delete)
452
+ identifier = clean_text(row.get("id"), f"{label}.id", max_bytes=24)
453
+ require(FORESHADOW_ID.fullmatch(identifier) is not None, f"{label}.id must look like F001")
454
+ if action == "delete":
455
+ return {"action": action, "id": identifier}
456
+ implied = FORESHADOW_ACTION_STATUS.get(" ".join(str(raw_action).split()).lower())
457
+ if implied is not None:
458
+ if row.get("status") in (None, ""):
459
+ row["status"] = implied
460
+ right = "回收了就写 status=已回收" if implied == "已回收" else "只是推进、还没回收就写 status=已埋"
461
+ require(
462
+ row["status"] == implied,
463
+ f"{label}.action={raw_action} 与 status={row['status']} 矛盾:{right};"
464
+ "不是这个意思就把 action 改成 upsert 并写实际状态",
465
+ )
466
+ planted_chapter = as_chapter(row.get("planted_chapter"), f"{label}.planted_chapter")
467
+ require(planted_chapter <= through_chapter, f"{label}.planted_chapter cannot be in the future")
468
+ planned_raw = row.get("planned_resolution_chapter")
469
+ planned_chapter = (
470
+ None if planned_raw in (None, "") else as_chapter(planned_raw, f"{label}.planned_resolution_chapter")
471
+ )
472
+ require(
473
+ planned_chapter is None or planned_chapter >= planted_chapter,
474
+ f"{label}.planned_resolution_chapter cannot precede planted_chapter",
475
+ )
476
+ status = clean_text(row.get("status"), f"{label}.status", max_bytes=24)
477
+ importance = clean_text(row.get("importance"), f"{label}.importance", max_bytes=12)
478
+ require(status in FORESHADOW_STATUSES, f"{label}.status must be one of {FORESHADOW_STATUSES}")
479
+ require(importance in FORESHADOW_IMPORTANCE, f"{label}.importance must be one of {FORESHADOW_IMPORTANCE}")
480
+ return {
481
+ "action": action,
482
+ "id": identifier,
483
+ "summary": clean_text(row.get("summary"), f"{label}.summary", max_bytes=360),
484
+ "planted_chapter": planted_chapter,
485
+ "planned_resolution_chapter": planned_chapter,
486
+ "status": status,
487
+ "importance": importance,
488
+ }
489
+
490
+
491
+ def normalize_foreshadow_state(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
492
+ rows = as_mapping(value, "tracking state.foreshadow")
493
+ normalized: dict[str, dict[str, Any]] = {}
494
+ for raw_identifier, raw_row in rows.items():
495
+ identifier = clean_text(raw_identifier, "tracking state.foreshadow ID", max_bytes=24)
496
+ row = as_mapping(raw_row, f"tracking state.foreshadow.{identifier}")
497
+ require_known_keys(
498
+ row,
499
+ {"id", "summary", "planted_chapter", "planned_resolution_chapter", "status", "importance", "updated_chapter"},
500
+ f"tracking state.foreshadow.{identifier}",
501
+ )
502
+ require(row.get("id") == identifier, f"tracking state.foreshadow.{identifier}.id does not match its key")
503
+ change = normalize_foreshadow_change(
504
+ {
505
+ "action": "upsert",
506
+ **{key: value for key, value in row.items() if key != "updated_chapter"},
507
+ },
508
+ f"tracking state.foreshadow.{identifier}",
509
+ allow_delete=False,
510
+ through_chapter=last_chapter,
511
+ )
512
+ change.pop("action")
513
+ updated = as_int(row.get("updated_chapter"), f"tracking state.foreshadow.{identifier}.updated_chapter", minimum=1)
514
+ require(updated <= last_chapter, f"foreshadow {identifier} updates after current chapter")
515
+ change["updated_chapter"] = updated
516
+ normalized[identifier] = change
517
+ return normalized
518
+
519
+
520
+ def render_foreshadow(rows: dict[str, dict[str, Any]], revision: int) -> str:
521
+ lines = [
522
+ "# 伏笔当前状态",
523
+ "",
524
+ f"> 状态修订:{revision}。每个 ID 只保留一行当前状态;历史变化见 `逐章记录/`。",
525
+ "",
526
+ "| ID | 内容 | 埋设章 | 计划回收章 | 状态 | 重要度 | 最近变更章 |",
527
+ "|---|---|---:|---:|---|---|---:|",
528
+ ]
529
+ for identifier in sorted(rows):
530
+ row = rows[identifier]
531
+ planned = f"第{row['planned_resolution_chapter']}章" if row["planned_resolution_chapter"] else "—"
532
+ lines.append(
533
+ f"| {identifier} | {row['summary']} | 第{row['planted_chapter']}章 | {planned} | "
534
+ f"{row['status']} | {row['importance']} | 第{row['updated_chapter']}章 |"
535
+ )
536
+ return "\n".join(lines) + "\n"
537
+
538
+
539
+ def normalize_timeline_change(
540
+ value: object,
541
+ label: str,
542
+ *,
543
+ allow_delete: bool,
544
+ through_chapter: int,
545
+ ) -> dict[str, Any]:
546
+ event = with_aliases(as_mapping(value, label), TIMELINE_KEY_ALIASES)
547
+ require_known_keys(
548
+ event,
549
+ {"action", "id", "story_time", "objective_fact", "reader_knowledge", "reveal_status", "reveal_chapter", "characters"},
550
+ label,
551
+ )
552
+ action = normalize_action(event.get("action", "upsert"), f"{label}.action", allow_delete=allow_delete)
553
+ identifier = clean_text(event.get("id"), f"{label}.id", max_bytes=24)
554
+ require(EVENT_ID.fullmatch(identifier) is not None, f"{label}.id must look like E001")
555
+ if action == "delete":
556
+ return {"action": action, "id": identifier}
557
+ reveal_status = clean_text(event.get("reveal_status"), f"{label}.reveal_status", max_bytes=24)
558
+ require(reveal_status in REVEAL_STATUSES, f"{label}.reveal_status must be one of {REVEAL_STATUSES}")
559
+ reveal_raw = event.get("reveal_chapter")
560
+ reveal_chapter = None if reveal_raw in (None, "") else as_chapter(reveal_raw, f"{label}.reveal_chapter")
561
+ if reveal_status == "未揭示":
562
+ require(reveal_chapter is None, f"{label} must not put a future reveal chapter in established timeline facts")
563
+ else:
564
+ require(reveal_chapter is not None, f"{label}.reveal_chapter is required once revealed")
565
+ require(reveal_chapter <= through_chapter, f"{label}.reveal_chapter cannot be in the future")
566
+ return {
567
+ "action": action,
568
+ "id": identifier,
569
+ "story_time": clean_text(event.get("story_time"), f"{label}.story_time", max_bytes=240),
570
+ "objective_fact": clean_text(event.get("objective_fact"), f"{label}.objective_fact", max_bytes=480),
571
+ "reader_knowledge": clean_text(event.get("reader_knowledge"), f"{label}.reader_knowledge", max_bytes=480),
572
+ "reveal_status": reveal_status,
573
+ "reveal_chapter": reveal_chapter,
574
+ "characters": clean_string_list(event.get("characters", []), f"{label}.characters", maximum=12, item_max_bytes=120),
575
+ }
576
+
577
+
578
+ def normalize_timeline_state(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
579
+ events = as_mapping(value, "tracking state.timeline")
580
+ normalized: dict[str, dict[str, Any]] = {}
581
+ for raw_identifier, raw_event in events.items():
582
+ identifier = clean_text(raw_identifier, "tracking state.timeline ID", max_bytes=24)
583
+ event = as_mapping(raw_event, f"tracking state.timeline.{identifier}")
584
+ require_known_keys(
585
+ event,
586
+ {
587
+ "id", "story_time", "objective_fact", "reader_knowledge", "reveal_status", "reveal_chapter",
588
+ "characters", "first_recorded_chapter", "updated_chapter",
589
+ },
590
+ f"tracking state.timeline.{identifier}",
591
+ )
592
+ require(event.get("id") == identifier, f"tracking state.timeline.{identifier}.id does not match its key")
593
+ change = normalize_timeline_change(
594
+ {
595
+ "action": "upsert",
596
+ **{
597
+ key: value
598
+ for key, value in event.items()
599
+ if key not in {"first_recorded_chapter", "updated_chapter"}
600
+ },
601
+ },
602
+ f"tracking state.timeline.{identifier}",
603
+ allow_delete=False,
604
+ through_chapter=last_chapter,
605
+ )
606
+ change.pop("action")
607
+ first = as_int(event.get("first_recorded_chapter"), f"tracking state.timeline.{identifier}.first_recorded_chapter", minimum=1)
608
+ updated = as_int(event.get("updated_chapter"), f"tracking state.timeline.{identifier}.updated_chapter", minimum=1)
609
+ require(first <= last_chapter, f"timeline event {identifier} starts after current chapter")
610
+ require(updated <= last_chapter, f"timeline event {identifier} updates after current chapter")
611
+ change["first_recorded_chapter"] = first
612
+ change["updated_chapter"] = updated
613
+ normalized[identifier] = change
614
+ return normalized
615
+
616
+
617
+ def render_timeline_views(events: dict[str, dict[str, Any]], revision: int) -> tuple[str, str]:
618
+ author_lines = [
619
+ "# 作者真相时间线",
620
+ "",
621
+ f"> 状态修订:{revision}。客观事实与读者认知的权威对照;未来揭示计划仍留在大纲。",
622
+ "",
623
+ "| ID | 首次登记章 | 故事时间 | 客观事实 | 读者当前认知 | 揭示状态 | 实际揭示章 |",
624
+ "|---|---:|---|---|---|---|---:|",
625
+ ]
626
+ reader_lines = [
627
+ "# 读者已知时间线",
628
+ "",
629
+ f"> 状态修订:{revision}。只呈现读者截至当前章节已经知道或相信的内容,不泄露作者侧客观真相。",
630
+ "",
631
+ "| ID | 读者当前认知 | 认知截至章 |",
632
+ "|---|---|---:|",
633
+ ]
634
+ for identifier in sorted(events):
635
+ event = events[identifier]
636
+ reveal = f"第{event['reveal_chapter']}章" if event.get("reveal_chapter") else "—"
637
+ characters = "、".join(event.get("characters", []))
638
+ objective = event["objective_fact"] + (f"(涉及:{characters})" if characters else "")
639
+ author_lines.append(
640
+ f"| {identifier} | 第{event['first_recorded_chapter']}章 | {event['story_time']} | {objective} | "
641
+ f"{event['reader_knowledge']} | {event['reveal_status']} | {reveal} |"
642
+ )
643
+ reader_lines.append(f"| {identifier} | {event['reader_knowledge']} | 第{event['updated_chapter']}章 |")
644
+ return "\n".join(author_lines) + "\n", "\n".join(reader_lines) + "\n"
645
+
646
+
647
+ def validate_context_input(value: object, *, include_initial_fields: bool) -> dict[str, Any]:
648
+ context = as_mapping(value, "context")
649
+ allowed = {"position", "long_term_constraints", "active_character_names", "continuity_risks"}
650
+ if include_initial_fields:
651
+ allowed.update({"recent_chapters", "next_chapter_commitments"})
652
+ require_known_keys(context, allowed, "context")
653
+ normalized: dict[str, Any] = {
654
+ "position": validate_position(context.get("position")),
655
+ "long_term_constraints": clean_string_list(
656
+ context.get("long_term_constraints", []), "context.long_term_constraints", maximum=6
657
+ ),
658
+ "active_character_names": [
659
+ safe_file_component(name, f"context.active_character_names[{index}]")
660
+ for index, name in enumerate(as_list(context.get("active_character_names", []), "context.active_character_names"))
661
+ ],
662
+ "continuity_risks": clean_string_list(
663
+ context.get("continuity_risks", []), "context.continuity_risks", maximum=5
664
+ ),
665
+ }
666
+ require(len(normalized["active_character_names"]) <= 6, "context.active_character_names may contain at most 6 names")
667
+ require(
668
+ len({portable_name_key(name) for name in normalized["active_character_names"]})
669
+ == len(normalized["active_character_names"]),
670
+ "context.active_character_names contains cross-platform duplicates",
671
+ )
672
+ if include_initial_fields:
673
+ recent: list[dict[str, Any]] = []
674
+ for index, raw_item in enumerate(as_list(context.get("recent_chapters", []), "context.recent_chapters")):
675
+ item = as_mapping(raw_item, f"context.recent_chapters[{index}]")
676
+ require_known_keys(item, {"chapter", "summary"}, f"context.recent_chapters[{index}]")
677
+ recent.append(
678
+ {
679
+ "chapter": as_int(item.get("chapter"), f"context.recent_chapters[{index}].chapter", minimum=1),
680
+ "summary": clean_text(item.get("summary"), f"context.recent_chapters[{index}].summary", max_bytes=360),
681
+ }
682
+ )
683
+ require(len(recent) <= 3, "context.recent_chapters may contain at most 3 items")
684
+ normalized["recent_chapters"] = recent
685
+ normalized["next_chapter_commitments"] = clean_string_list(
686
+ context.get("next_chapter_commitments", []), "context.next_chapter_commitments", maximum=5
687
+ )
688
+ return normalized
689
+
690
+
691
+ def active_foreshadow_lines(rows: dict[str, dict[str, Any]]) -> list[str]:
692
+ importance = {value: index for index, value in enumerate(FORESHADOW_IMPORTANCE)}
693
+ candidates = [row for row in rows.values() if row["status"] == "已埋"]
694
+ candidates.sort(
695
+ key=lambda row: (importance[row["importance"]], row["planned_resolution_chapter"] or 10**12, row["id"])
696
+ )
697
+ result = []
698
+ for row in candidates[:8]:
699
+ planned = f"第{row['planned_resolution_chapter']}章" if row["planned_resolution_chapter"] else "回收章未定"
700
+ result.append(f"{row['id']}|{row['summary']}|埋第{row['planted_chapter']}章|{planned}|{row['importance']}")
701
+ return result
702
+
703
+
704
+ def render_context(state: dict[str, Any]) -> str:
705
+ context = state["context"]
706
+ position = context["position"]
707
+ current_chapter = (
708
+ "尚未开篇" if state["last_committed_chapter"] == 0 else f"第{state['last_committed_chapter']}章"
709
+ )
710
+ character_lines = [
711
+ f"{name}|{state['characters'][name]['identity']}|{state['characters'][name]['state']}|"
712
+ f"目标:{state['characters'][name]['goal']}"
713
+ for name in context["active_character_names"]
714
+ ]
715
+ sections: list[tuple[str, list[str]]] = [
716
+ (
717
+ "## 当前位置",
718
+ [
719
+ f"当前章:{current_chapter}",
720
+ f"卷:{position['volume']}(始于第{position['volume_start_chapter']}章)",
721
+ f"故事时间:{position['story_time']}",
722
+ f"场景:{position['scene']}",
723
+ ],
724
+ ),
725
+ ("## 长期约束", context["long_term_constraints"]),
726
+ ("## 核心角色状态", character_lines),
727
+ ("## 活跃伏笔", active_foreshadow_lines(state["foreshadow"])),
728
+ ("## 近三章速记", [f"第{item['chapter']}章|{item['summary']}" for item in context["recent_chapters"]]),
729
+ ("## 下一章承诺", context["next_chapter_commitments"]),
730
+ ("## 连贯性风险", context["continuity_risks"]),
731
+ ]
732
+ lines = [
733
+ f"# 写作连续性上下文 — {state['book_title']}",
734
+ "",
735
+ f"> 状态修订:{state['state_revision']}。截至当前章的续写状态卡,只放下一章真正需要的连续性状态。",
736
+ "",
737
+ ]
738
+ for heading, values in sections:
739
+ lines.append(heading)
740
+ lines.extend(f"- {value}" for value in values or ["无"])
741
+ lines.append("")
742
+ payload = "\n".join(lines).rstrip() + "\n"
743
+ headings = tuple(line for line in payload.splitlines() if line.startswith("## "))
744
+ require(headings == CONTEXT_HEADINGS, "generated context headings do not match the seven-section schema")
745
+ require(byte_size(payload) <= CONTEXT_MAX_BYTES, f"hot context exceeds {CONTEXT_MAX_BYTES} bytes")
746
+ return payload
747
+
748
+
749
+ def normalize_delta(
750
+ value: object,
751
+ *,
752
+ through_chapter: int,
753
+ snapshots: dict[str, dict[str, Any]],
754
+ existing_core_names: dict[str, str],
755
+ ) -> dict[str, Any]:
756
+ delta = as_mapping(value, "delta")
757
+ require_known_keys(
758
+ delta,
759
+ {
760
+ "result", "character_changes", "foreshadow_changes", "timeline_events", "constraints",
761
+ "next_chapter_commitments", "retired_context_items", "retired_characters",
762
+ },
763
+ "delta",
764
+ )
765
+ retired_characters = [
766
+ safe_file_component(name, f"delta.retired_characters[{index}]")
767
+ for index, name in enumerate(as_list(delta.get("retired_characters", []), "delta.retired_characters"))
768
+ ]
769
+ retired_keys = [portable_name_key(name) for name in retired_characters]
770
+ require(len(retired_keys) == len(set(retired_keys)), "delta.retired_characters contains duplicate characters")
771
+ retiring = set(retired_keys)
772
+ character_changes: list[dict[str, Any]] = []
773
+ for index, raw_change in enumerate(as_list(delta.get("character_changes", []), "delta.character_changes")):
774
+ change = as_mapping(raw_change, f"delta.character_changes[{index}]")
775
+ require_known_keys(change, {"name", "change"}, f"delta.character_changes[{index}]")
776
+ name = safe_file_component(change.get("name"), f"delta.character_changes[{index}].name")
777
+ existing = existing_core_names.get(portable_name_key(name))
778
+ is_core = name in snapshots or existing is not None
779
+ # 本章退役的角色记录最后一次变化即可,不必再交一份马上要删的快照。
780
+ require(
781
+ not is_core or name in snapshots or portable_name_key(name) in retiring,
782
+ f"core character {name} changed but has no current snapshot: 把 draft 输出 current_snapshots 里"
783
+ f"「{name}」的快照整份改成本章结束时的状态,放进 character_snapshots",
784
+ )
785
+ character_changes.append(
786
+ {"name": name, "change": clean_text(change.get("change"), f"delta.character_changes[{index}].change", max_bytes=360)}
787
+ )
788
+ character_keys = [portable_name_key(item["name"]) for item in character_changes]
789
+ require(len(character_keys) == len(set(character_keys)), "delta.character_changes contains duplicate characters")
790
+ foreshadow_changes = [
791
+ normalize_foreshadow_change(
792
+ raw, f"delta.foreshadow_changes[{index}]", allow_delete=True, through_chapter=through_chapter
793
+ )
794
+ for index, raw in enumerate(as_list(delta.get("foreshadow_changes", []), "delta.foreshadow_changes"))
795
+ ]
796
+ timeline_events = [
797
+ normalize_timeline_change(
798
+ raw, f"delta.timeline_events[{index}]", allow_delete=True, through_chapter=through_chapter
799
+ )
800
+ for index, raw in enumerate(as_list(delta.get("timeline_events", []), "delta.timeline_events"))
801
+ ]
802
+ require(
803
+ len({item["id"] for item in foreshadow_changes}) == len(foreshadow_changes),
804
+ "delta.foreshadow_changes contains duplicate IDs",
805
+ )
806
+ require(
807
+ len({item["id"] for item in timeline_events}) == len(timeline_events),
808
+ "delta.timeline_events contains duplicate IDs",
809
+ )
810
+ require(
811
+ set(snapshots).issubset({item["name"] for item in character_changes}),
812
+ "character_snapshots must contain exactly the core characters changed by this transaction",
813
+ )
814
+ return {
815
+ "result": clean_text(delta.get("result"), "delta.result", max_bytes=480),
816
+ "character_changes": character_changes,
817
+ "foreshadow_changes": foreshadow_changes,
818
+ "timeline_events": timeline_events,
819
+ "constraints": clean_string_list(delta.get("constraints", []), "delta.constraints", maximum=6),
820
+ "next_chapter_commitments": clean_string_list(
821
+ delta.get("next_chapter_commitments", []), "delta.next_chapter_commitments", maximum=5
822
+ ),
823
+ "retired_context_items": clean_string_list(
824
+ delta.get("retired_context_items", []), "delta.retired_context_items", maximum=11
825
+ ),
826
+ "retired_characters": retired_characters,
827
+ }
828
+
829
+
830
+ def render_commitments(commitments: list[str]) -> list[str]:
831
+ # 常见情形一行用「;」连写;有承诺原文自带「;」时连写就拆不回来,改成逐条缩进子项。
832
+ if any(";" in item for item in commitments):
833
+ return ["- 下一章承诺:"] + [f" - {item}" for item in commitments]
834
+ return ["- 下一章承诺:" + (";".join(commitments) or "无")]
835
+
836
+
837
+ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: set[str]) -> str:
838
+ lines = [
839
+ f"# 第{chapter:03d}章 · {title}",
840
+ f"- 结果:{delta['result']}",
841
+ *render_commitments(delta["next_chapter_commitments"]),
842
+ "",
843
+ "## 角色变化",
844
+ ]
845
+ lines.extend(
846
+ f"- {item['name']}|{'核心' if item['name'] in core_names else '临时'}|{item['change']}"
847
+ for item in delta["character_changes"]
848
+ )
849
+ if not delta["character_changes"]:
850
+ lines.append("- 无")
851
+ lines.extend(["", "## 伏笔变化"])
852
+ for item in delta["foreshadow_changes"]:
853
+ if item["action"] == "delete":
854
+ lines.append(f"- {item['id']}|删除当前登记")
855
+ else:
856
+ planned = f"第{item['planned_resolution_chapter']}章" if item["planned_resolution_chapter"] else "未定"
857
+ lines.append(f"- {item['id']}|{item['status']}|{item['summary']}|回收{planned}")
858
+ if not delta["foreshadow_changes"]:
859
+ lines.append("- 无")
860
+ lines.extend(["", "## 时间与揭示"])
861
+ for item in delta["timeline_events"]:
862
+ if item["action"] == "delete":
863
+ lines.append(f"- {item['id']}|删除当前登记")
864
+ else:
865
+ lines.append(
866
+ f"- {item['id']}|{item['story_time']}|事实:{item['objective_fact']}|"
867
+ f"读者:{item['reader_knowledge']}|{item['reveal_status']}"
868
+ )
869
+ if not delta["timeline_events"]:
870
+ lines.append("- 无")
871
+ lines.extend(["", "## 连贯性约束"])
872
+ lines.extend(f"- {item}" for item in delta["constraints"])
873
+ if not delta["constraints"]:
874
+ lines.append("- 无")
875
+ retired = delta.get("retired_context_items", []) + [
876
+ f"角色状态:{name}" for name in delta.get("retired_characters", [])
877
+ ]
878
+ if retired:
879
+ # 退役条目在此留档,续写状态卡收缩后仍可回查当初撤下了什么。
880
+ lines.extend(["", "## 本章退役登记"])
881
+ lines.extend(f"- {item}" for item in retired)
882
+ payload = "\n".join(lines) + "\n"
883
+ size = byte_size(payload)
884
+ if size > DELTA_MAX_BYTES:
885
+ longest = sorted((line for line in lines if line.startswith("- ")), key=len, reverse=True)[:3]
886
+ allowed = DELTA_MAX_BYTES * len(payload) // size
887
+ raise TrackingError(
888
+ f"chapter delta is {size} bytes; hard cap is {DELTA_MAX_BYTES}(逐章记录现 {len(payload)} 字,"
889
+ f"上限约 {allowed} 字,至少压缩 {len(payload) - allowed} 字;最长几项:"
890
+ + ";".join(f"{line[2:18]}…({len(line) - 2} 字)" for line in longest) + ")")
891
+ return payload
892
+
893
+
894
+ def normalize_wordcount_records(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
895
+ records = as_mapping(value, "tracking state.wordcount_records")
896
+ normalized: dict[str, dict[str, Any]] = {}
897
+ for raw_chapter, raw_record in records.items():
898
+ require(isinstance(raw_chapter, str) and re.fullmatch(r"[1-9]\d*", raw_chapter) is not None,
899
+ "wordcount record chapter key is invalid")
900
+ chapter = int(raw_chapter)
901
+ require(chapter <= last_chapter, "wordcount record exceeds last committed chapter")
902
+ normalized[raw_chapter] = wordcount_value(wordcount_core.normalize_wordcount_record, raw_record)
903
+ return normalized
904
+
905
+
906
+ def normalize_state(document: object) -> dict[str, Any]:
907
+ root = as_mapping(document, "tracking state")
908
+ require_known_keys(
909
+ root,
910
+ {
911
+ "schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
912
+ "state_revision", "context", "characters", "foreshadow", "timeline",
913
+ "wordcount_records",
914
+ },
915
+ "tracking state",
916
+ )
917
+ require(root.get("schema_version") == TRACKING_SCHEMA_VERSION, "tracking state schema is unsupported")
918
+ last_chapter = as_int(root.get("last_committed_chapter"), "tracking state.last_committed_chapter")
919
+ imported_through = as_int(root.get("imported_through_chapter"), "tracking state.imported_through_chapter")
920
+ require(imported_through <= last_chapter, "imported chapter cutoff exceeds current chapter")
921
+ context = validate_context_input(root.get("context"), include_initial_fields=True)
922
+ require(
923
+ context["position"]["volume_start_chapter"] <= max(1, last_chapter),
924
+ "context.position.volume_start_chapter is after the current writing position",
925
+ )
926
+ recent_numbers = [item["chapter"] for item in context["recent_chapters"]]
927
+ require(recent_numbers == sorted(recent_numbers), "context.recent_chapters must be ordered")
928
+ require(len(recent_numbers) == len(set(recent_numbers)), "context.recent_chapters contains duplicates")
929
+ require(all(chapter <= last_chapter for chapter in recent_numbers), "context.recent_chapters cannot include future chapters")
930
+ characters = normalize_snapshots(root.get("characters", {}), "tracking state.characters")
931
+ for name in context["active_character_names"]:
932
+ require(name in characters, f"active core character {name} has no current snapshot")
933
+ foreshadow = normalize_foreshadow_state(root.get("foreshadow", {}), last_chapter)
934
+ timeline = normalize_timeline_state(root.get("timeline", {}), last_chapter)
935
+ if last_chapter == 0:
936
+ require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
937
+ require(not timeline, "a chapter-0 project cannot have established timeline facts")
938
+ state_revision = as_int(root.get("state_revision"), "tracking state.state_revision")
939
+ wordcount_records = normalize_wordcount_records(root.get("wordcount_records", {}), last_chapter)
940
+ return {
941
+ "schema_version": TRACKING_SCHEMA_VERSION,
942
+ "book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
943
+ "last_committed_chapter": last_chapter,
944
+ "imported_through_chapter": imported_through,
945
+ "state_revision": state_revision,
946
+ "context": context,
947
+ "characters": characters,
948
+ "foreshadow": foreshadow,
949
+ "timeline": timeline,
950
+ "wordcount_records": wordcount_records,
951
+ }
952
+
953
+
954
+ def load_state(project: Path) -> dict[str, Any]:
955
+ path = state_path(project)
956
+ require(path.exists(), "tracking state is missing; run init first")
957
+ return normalize_state(read_json(path))
958
+
959
+
960
+ def normalize_initial_document(document: object) -> dict[str, Any]:
961
+ root = as_mapping(document, "init input")
962
+ require_known_keys(
963
+ root,
964
+ {
965
+ "schema_version", "book_title", "last_chapter", "context", "character_snapshots",
966
+ "foreshadow", "timeline_events",
967
+ },
968
+ "init input",
969
+ )
970
+ require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
971
+ last_chapter = as_int(root.get("last_chapter"), "last_chapter")
972
+ context = validate_context_input(root.get("context"), include_initial_fields=True)
973
+ snapshots = normalize_snapshots(root.get("character_snapshots", {}))
974
+ foreshadow: dict[str, dict[str, Any]] = {}
975
+ for index, raw_row in enumerate(as_list(root.get("foreshadow", []), "foreshadow")):
976
+ row = normalize_foreshadow_change(
977
+ raw_row, f"foreshadow[{index}]", allow_delete=False, through_chapter=last_chapter
978
+ )
979
+ require(row["id"] not in foreshadow, f"duplicate foreshadow ID {row['id']}")
980
+ row.pop("action")
981
+ row["updated_chapter"] = max(1, last_chapter)
982
+ foreshadow[row["id"]] = row
983
+ timeline: dict[str, dict[str, Any]] = {}
984
+ for index, raw_event in enumerate(as_list(root.get("timeline_events", []), "timeline_events")):
985
+ event = normalize_timeline_change(
986
+ raw_event, f"timeline_events[{index}]", allow_delete=False, through_chapter=last_chapter
987
+ )
988
+ require(event["id"] not in timeline, f"duplicate timeline event ID {event['id']}")
989
+ event.pop("action")
990
+ event["first_recorded_chapter"] = max(1, last_chapter)
991
+ event["updated_chapter"] = max(1, last_chapter)
992
+ timeline[event["id"]] = event
993
+ return normalize_state(
994
+ {
995
+ "schema_version": TRACKING_SCHEMA_VERSION,
996
+ "book_title": clean_text(root.get("book_title"), "book_title", max_bytes=240),
997
+ "last_committed_chapter": last_chapter,
998
+ "imported_through_chapter": last_chapter,
999
+ "state_revision": 0,
1000
+ "context": context,
1001
+ "characters": snapshots,
1002
+ "foreshadow": foreshadow,
1003
+ "timeline": timeline,
1004
+ "wordcount_records": {},
1005
+ }
1006
+ )
1007
+
1008
+
1009
+ def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
1010
+ global _LENGTH_ISSUES
1011
+ _LENGTH_ISSUES = []
1012
+ try:
1013
+ transaction = _normalize_transaction(project, state, document)
1014
+ finally:
1015
+ issues, _LENGTH_ISSUES = _LENGTH_ISSUES, None
1016
+ require(not issues, f"{len(issues)} 个字段超长,一次改完再提交:" + ";".join(issues))
1017
+ return transaction
1018
+
1019
+
1020
+ # 本文件在 story-long-write / story-review / story-import 三份逐字相同;只有 story-long-write 带 storyctl.py。
1021
+ RECOUNT_HINT = ("修订请用 story-long-write 的 storyctl.py chapter commit(带外用 chapter accept-current-length)"
1022
+ "提交同一份事务,它会重新计数并清理本章工作目录")
1023
+
1024
+
1025
+ def _normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
1026
+ root = as_mapping(document, "transaction")
1027
+ require_known_keys(
1028
+ root,
1029
+ {
1030
+ "schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
1031
+ "delta", "context", "character_snapshots", "wordcount",
1032
+ },
1033
+ "transaction",
1034
+ )
1035
+ require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "transaction schema_version is unsupported")
1036
+ mode = clean_text(root.get("mode"), "mode", max_bytes=24)
1037
+ require(mode in {"append", "revision"}, "mode must be append or revision")
1038
+ chapter = as_int(root.get("chapter"), "chapter", minimum=1)
1039
+ expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
1040
+ wordcount_input = root.get("wordcount")
1041
+ require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
1042
+ last = state["last_committed_chapter"]
1043
+ if mode == "append":
1044
+ require(chapter == last + 1, f"append chapter must be {last + 1}, got {chapter}")
1045
+ else:
1046
+ require(chapter <= last, f"cannot revise unwritten chapter {chapter}; last committed chapter is {last}")
1047
+ context = validate_context_input(root.get("context"), include_initial_fields=False)
1048
+ snapshots = normalize_snapshots(root.get("character_snapshots", {}))
1049
+ existing_names = {portable_name_key(name): name for name in state["characters"]}
1050
+ for name in snapshots:
1051
+ existing = existing_names.get(portable_name_key(name))
1052
+ require(existing is None or existing == name, f"character {name} conflicts with existing character {existing}")
1053
+ through_chapter = chapter if mode == "append" else last
1054
+ delta = normalize_delta(
1055
+ root.get("delta"),
1056
+ through_chapter=through_chapter,
1057
+ snapshots=snapshots,
1058
+ existing_core_names=existing_names,
1059
+ )
1060
+ wordcount = None
1061
+ if wordcount_input is not None:
1062
+ wordcount = wordcount_value(
1063
+ wordcount_core.validate_current_wordcount_record, project, chapter, wordcount_input
1064
+ )
1065
+ elif mode == "revision" and str(chapter) in state["wordcount_records"]:
1066
+ # 已提交字数记录的章节:正文或目标改过却绕开 storyctl 提交修订,会留下过期的 actual/body_sha256。
1067
+ # 只核对记录钉住的正文与目标;细纲事后补的「字数范围」不算过期。
1068
+ try:
1069
+ drift = wordcount_core.wordcount_record_drift(project, chapter, state["wordcount_records"][str(chapter)])
1070
+ except wordcount_core.WordcountError as exc:
1071
+ raise TrackingError(f"第{chapter}章的字数记录无法核对({exc});{RECOUNT_HINT}") from exc
1072
+ if drift:
1073
+ raise TrackingError(f"第{chapter}章{'和'.join(drift)}已改;{RECOUNT_HINT}")
1074
+ return {
1075
+ "mode": mode,
1076
+ "chapter": chapter,
1077
+ "title": clean_text(root.get("chapter_title"), "chapter_title", max_bytes=240),
1078
+ "delta": delta,
1079
+ "context": context,
1080
+ "snapshots": snapshots,
1081
+ "wordcount": wordcount,
1082
+ }
1083
+
1084
+
1085
+ def checkpoint_record(
1086
+ change: dict[str, Any], chapter: int, previous: dict[str, Any] | None, *, keep_first_chapter: bool = False
1087
+ ) -> dict[str, Any]:
1088
+ current = {key: value for key, value in change.items() if key != "action"}
1089
+ current["updated_chapter"] = max(previous["updated_chapter"] if previous else chapter, chapter)
1090
+ if keep_first_chapter:
1091
+ current["first_recorded_chapter"] = previous["first_recorded_chapter"] if previous else chapter
1092
+ return current
1093
+
1094
+
1095
+ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dict[str, Any]:
1096
+ next_state = copy.deepcopy(state)
1097
+ chapter = transaction["chapter"]
1098
+ if transaction["mode"] == "append":
1099
+ next_state["last_committed_chapter"] = chapter
1100
+ next_state["state_revision"] += 1
1101
+ next_state["characters"].update(transaction["snapshots"])
1102
+ if transaction["wordcount"] is not None:
1103
+ next_state["wordcount_records"][str(chapter)] = transaction["wordcount"]
1104
+
1105
+ next_context = transaction["context"]
1106
+ # 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
1107
+ # 修订记录属于被改写的旧章,落在那里会谎报退役发生的章节。
1108
+ is_revision = transaction["mode"] == "revision"
1109
+ require(
1110
+ not (is_revision and transaction["delta"]["retired_characters"]),
1111
+ "retired_characters must be committed in an append transaction, not a revision",
1112
+ )
1113
+ for name in transaction["delta"]["retired_characters"]:
1114
+ require(name in next_state["characters"], f"retired character {name} has no current snapshot")
1115
+ require(
1116
+ name not in transaction["snapshots"],
1117
+ f"character {name} cannot be retired and updated in the same transaction",
1118
+ )
1119
+ require(
1120
+ name not in next_context["active_character_names"],
1121
+ f"retired character {name} is still listed in context.active_character_names",
1122
+ )
1123
+ next_state["characters"].pop(name)
1124
+
1125
+ # 上下文条目是整份提交的;漏写会静默丢历史裁定,因此掉落必须显式声明。
1126
+ previous_items = set(state["context"]["long_term_constraints"]) | set(state["context"]["continuity_risks"])
1127
+ dropped = previous_items - (set(next_context["long_term_constraints"]) | set(next_context["continuity_risks"]))
1128
+ require(
1129
+ not (is_revision and dropped),
1130
+ "a revision must resubmit every current context item; retire them in an append transaction instead: "
1131
+ + ";".join(sorted(dropped)),
1132
+ )
1133
+ kept = set(next_context["long_term_constraints"]) | set(next_context["continuity_risks"])
1134
+ still_listed = sorted(set(transaction["delta"]["retired_context_items"]) & kept)
1135
+ require(
1136
+ not still_listed,
1137
+ "delta.retired_context_items lists items that are still in context; remove them from context: "
1138
+ + ";".join(still_listed),
1139
+ )
1140
+ undeclared = sorted(dropped - set(transaction["delta"]["retired_context_items"]))
1141
+ require(
1142
+ not undeclared,
1143
+ "context items were dropped without being declared in delta.retired_context_items: "
1144
+ + ";".join(undeclared),
1145
+ )
1146
+ transaction["delta"]["retired_context_items"] = sorted(dropped)
1147
+
1148
+ for change in transaction["delta"]["foreshadow_changes"]:
1149
+ if change["action"] == "delete":
1150
+ next_state["foreshadow"].pop(change["id"], None)
1151
+ else:
1152
+ next_state["foreshadow"][change["id"]] = checkpoint_record(
1153
+ change, chapter, next_state["foreshadow"].get(change["id"])
1154
+ )
1155
+ for change in transaction["delta"]["timeline_events"]:
1156
+ if change["action"] == "delete":
1157
+ next_state["timeline"].pop(change["id"], None)
1158
+ else:
1159
+ next_state["timeline"][change["id"]] = checkpoint_record(
1160
+ change, chapter, next_state["timeline"].get(change["id"]), keep_first_chapter=True
1161
+ )
1162
+
1163
+ recent_by_chapter = {item["chapter"]: item for item in state["context"]["recent_chapters"]}
1164
+ if chapter in recent_by_chapter or transaction["mode"] == "append":
1165
+ recent_by_chapter[chapter] = {"chapter": chapter, "summary": transaction["delta"]["result"]}
1166
+ recent = sorted(recent_by_chapter.values(), key=lambda item: item["chapter"])[-3:]
1167
+ current_last = next_state["last_committed_chapter"]
1168
+ next_commitments = (
1169
+ transaction["delta"]["next_chapter_commitments"]
1170
+ if transaction["mode"] == "append" or chapter == current_last
1171
+ else state["context"]["next_chapter_commitments"]
1172
+ )
1173
+ next_state["context"] = {
1174
+ **next_context,
1175
+ "recent_chapters": recent,
1176
+ "next_chapter_commitments": next_commitments,
1177
+ }
1178
+ return normalize_state(next_state)
1179
+
1180
+
1181
+ def render_views(state: dict[str, Any]) -> dict[str, str]:
1182
+ revision = state["state_revision"]
1183
+ views = {
1184
+ "上下文.md": render_context(state),
1185
+ "伏笔.md": render_foreshadow(state["foreshadow"], revision),
1186
+ }
1187
+ author, reader = render_timeline_views(state["timeline"], revision)
1188
+ views["时间线/作者真相.md"] = author
1189
+ views["时间线/读者已知.md"] = reader
1190
+ for name, snapshot in state["characters"].items():
1191
+ views[f"角色状态/{name}.md"] = render_snapshot(
1192
+ name, snapshot, state["last_committed_chapter"], revision
1193
+ )
1194
+ return views
1195
+
1196
+
1197
+ def write_views(tracking: Path, views: dict[str, str]) -> None:
1198
+ # 上下文携带 next revision,先写它;任何后续失败都会让 hook/check 发现
1199
+ # 上下文 revision 与最后提交的 _tracking-state.json 不一致。
1200
+ write_if_changed(tracking / "上下文.md", views["上下文.md"])
1201
+ for relative in sorted(path for path in views if path != "上下文.md"):
1202
+ write_if_changed(tracking / relative, views[relative])
1203
+ expected_character_files = {
1204
+ Path(relative).name for relative in views if relative.startswith("角色状态/")
1205
+ }
1206
+ character_dir = tracking / "角色状态"
1207
+ character_dir.mkdir(parents=True, exist_ok=True)
1208
+ for path in character_dir.glob("*.md"):
1209
+ if path.name not in expected_character_files:
1210
+ path.unlink()
1211
+
1212
+
1213
+ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
1214
+ if delta_payload is not None and byte_size(delta_payload) > DELTA_TARGET_BYTES:
1215
+ emit(
1216
+ f"WARNING: chapter delta is {byte_size(delta_payload)} bytes; target is <= {DELTA_TARGET_BYTES}",
1217
+ error=True,
1218
+ )
1219
+ context_size = byte_size(views["上下文.md"])
1220
+ if context_size > CONTEXT_TARGET_BYTES:
1221
+ emit(f"WARNING: hot context is {context_size} bytes; target is <= {CONTEXT_TARGET_BYTES}", error=True)
1222
+ for relative, payload in views.items():
1223
+ if not relative.startswith("角色状态/"):
1224
+ continue
1225
+ size = byte_size(payload)
1226
+ if size > SNAPSHOT_TARGET_BYTES:
1227
+ emit(
1228
+ f"WARNING: character snapshot {Path(relative).stem} is {size} bytes; target is <= {SNAPSHOT_TARGET_BYTES}",
1229
+ error=True,
1230
+ )
1231
+
1232
+
1233
+ def _initialize_locked(project: Path, document: object) -> dict[str, Any]:
1234
+ tracking = tracking_root(project)
1235
+ require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
1236
+ state = normalize_initial_document(document)
1237
+ views = render_views(state)
1238
+ state_payload = json_payload(state)
1239
+
1240
+ # 输入全部校验通过后才动用户文件,失败的 init 不会挪走任何东西。
1241
+ archived = archive_retired_tracking_paths(tracking)
1242
+ for directory in (tracking / "逐章记录", tracking / "角色状态", tracking / "时间线"):
1243
+ directory.mkdir(parents=True, exist_ok=True)
1244
+ write_views(tracking, views)
1245
+ atomic_write_text(state_path(project), state_payload)
1246
+ warn_sizes(views)
1247
+ if archived:
1248
+ emit(
1249
+ f"NOTE: 旧追踪结构已原样移入 追踪/{RETIRED_ARCHIVE_DIR}/:{', '.join(archived)};"
1250
+ "当前状态以本次 init 输入为准,旧文件不参与解析。",
1251
+ error=True,
1252
+ )
1253
+ return state
1254
+
1255
+
1256
+ def initialize(project: Path, document: object) -> dict[str, Any]:
1257
+ with project_write_lock(project):
1258
+ return _initialize_locked(project, document)
1259
+
1260
+
1261
+ def _apply_transaction_locked(project: Path, document: object) -> dict[str, Any]:
1262
+ tracking = tracking_root(project)
1263
+ require_no_retired_tracking_paths(tracking)
1264
+ state = load_state(project)
1265
+ transaction = normalize_transaction(project, state, document)
1266
+ next_state = merge_transaction(state, transaction)
1267
+ path = delta_path(tracking, transaction["chapter"])
1268
+ previous_record = parse_chapter_record(path) if transaction["mode"] == "revision" else None
1269
+ if previous_record is not None:
1270
+ # 退役只在 append 发生;修订重写旧章记录时把当初的退役登记原样带过来,不让它随修订消失。
1271
+ transaction["delta"]["retired_context_items"] = previous_record["retired"]
1272
+
1273
+ delta_payload = render_delta(
1274
+ transaction["chapter"],
1275
+ transaction["title"],
1276
+ transaction["delta"],
1277
+ # 本章退役的角色在 next_state 里已被删除,但本章记录里仍应标为核心。
1278
+ set(next_state["characters"]) | set(transaction["delta"]["retired_characters"]),
1279
+ )
1280
+ views = render_views(next_state)
1281
+ next_state_payload = json_payload(next_state)
1282
+ if transaction["mode"] == "append" and path.exists():
1283
+ require(
1284
+ path.read_text(encoding="utf-8") == delta_payload,
1285
+ f"chapter delta {transaction['chapter']} already exists with different content",
1286
+ )
1287
+
1288
+ write_if_changed(path, delta_payload)
1289
+ write_views(tracking, views)
1290
+ # 唯一权威文件最后落盘;在此之前失败可用同一事务直接重跑。
1291
+ atomic_write_text(state_path(project), next_state_payload)
1292
+ warn_sizes(views, delta_payload)
1293
+ if previous_record is not None:
1294
+ warn_emptied_record(transaction["chapter"], previous_record, transaction["delta"])
1295
+ return next_state
1296
+
1297
+
1298
+ def warn_emptied_record(chapter: int, previous: dict[str, Any], delta: dict[str, Any]) -> None:
1299
+ labels = {"character_changes": "角色变化", "foreshadow_changes": "伏笔变化", "timeline_events": "时间与揭示",
1300
+ "constraints": "连贯性约束", "next_chapter_commitments": "下一章承诺"}
1301
+ emptied = [f"{label}(原有 {len(previous[key])} 项)" for key, label in labels.items()
1302
+ if previous[key] and not delta[key]]
1303
+ if emptied:
1304
+ emit(f"WARNING: 第{chapter}章修订把逐章记录的 " + "、".join(emptied)
1305
+ + " 清空了;修订的 delta 是本章完整记录,不是只写改动。不是有意删除就用 draft 重新预填后再提交。", error=True)
1306
+
1307
+
1308
+ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
1309
+ with project_write_lock(project):
1310
+ return _apply_transaction_locked(project, document)
1311
+
1312
+
1313
+ def check_project(project: Path) -> dict[str, Any]:
1314
+ tracking = tracking_root(project)
1315
+ require_no_retired_tracking_paths(tracking)
1316
+ state = load_state(project)
1317
+ last_chapter = state["last_committed_chapter"]
1318
+ required_delta_start = state["imported_through_chapter"] + 1
1319
+ for chapter in range(required_delta_start, last_chapter + 1):
1320
+ require(delta_path(tracking, chapter).exists(), f"chapter delta {chapter} is missing")
1321
+ for path in (tracking / "逐章记录").glob("第*章.md"):
1322
+ match = re.fullmatch(r"第(\d+)章\.md", path.name)
1323
+ require(match is not None, f"chapter delta has an invalid filename: {path.name}")
1324
+ chapter = as_int(int(match.group(1)), f"chapter delta {path.name}", minimum=1)
1325
+ require(path == delta_path(tracking, chapter), f"chapter delta {chapter} filename is not canonical")
1326
+ require(chapter <= last_chapter, f"chapter delta {chapter} exceeds last_committed_chapter")
1327
+ require(path.stat().st_size <= DELTA_MAX_BYTES, f"chapter delta {chapter} exceeds {DELTA_MAX_BYTES} bytes")
1328
+
1329
+ expected_views = render_views(state)
1330
+ for relative, expected in expected_views.items():
1331
+ path = tracking / relative
1332
+ require(path.exists(), f"derived view is missing: {relative}")
1333
+ require(
1334
+ path.read_text(encoding="utf-8") == expected,
1335
+ f"derived view differs from _tracking-state.json: {relative}",
1336
+ )
1337
+ expected_character_files = {
1338
+ Path(relative).name for relative in expected_views if relative.startswith("角色状态/")
1339
+ }
1340
+ actual_character_files = {path.name for path in (tracking / "角色状态").glob("*.md")}
1341
+ require(actual_character_files == expected_character_files, "character snapshot files differ from tracking state")
1342
+ return state
1343
+
1344
+
1345
+ # 事务草稿里各文本字段的字数上限(按全中文估:UTF-8 每字 3 字节),写进提示,免得模型反复试错。
1346
+ DRAFT_LIMITS = {
1347
+ "delta.result": 160, "character_changes[].change": 120, "foreshadow_changes[].summary": 120,
1348
+ "timeline_events[].objective_fact / reader_knowledge": 160, "context.position.story_time / scene": 80,
1349
+ "character_snapshots.*.goal / state": 100, "逐章记录总量": 1024,
1350
+ }
1351
+
1352
+
1353
+ RECORD_SECTIONS = {"角色变化": "character_changes", "伏笔变化": "foreshadow_changes",
1354
+ "时间与揭示": "timeline_events", "连贯性约束": "constraints", "本章退役登记": "retired"}
1355
+
1356
+
1357
+ def parse_chapter_record(path: Path) -> dict[str, Any] | None:
1358
+ """读回本工具自己渲染的逐章记录(render_delta 的固定格式),供修订草稿预填与修订提交核对。
1359
+ 这是对自家确定性输出的逆运算,不是解析手写 Markdown;读不到返回 None。"""
1360
+ try:
1361
+ text = path.read_text(encoding="utf-8")
1362
+ except (OSError, UnicodeError):
1363
+ return None
1364
+ record: dict[str, Any] = {"title": "", "result": "", "next_chapter_commitments": [],
1365
+ **{key: [] for key in RECORD_SECTIONS.values()}}
1366
+ section = None
1367
+ in_commitments = False
1368
+ for line in text.splitlines():
1369
+ heading = re.match(r"^# 第\d+章 · (.+)$", line)
1370
+ if heading and not record["title"]:
1371
+ record["title"] = heading.group(1).strip()
1372
+ continue
1373
+ if line.startswith("## "):
1374
+ section = RECORD_SECTIONS.get(line[3:].strip())
1375
+ in_commitments = False
1376
+ continue
1377
+ if in_commitments and line.startswith(" - "):
1378
+ record["next_chapter_commitments"].append(line[4:])
1379
+ continue
1380
+ in_commitments = False
1381
+ if not line.startswith("- "):
1382
+ continue
1383
+ item = line[2:]
1384
+ if section is None:
1385
+ if item.startswith("结果:"):
1386
+ record["result"] = item[len("结果:"):]
1387
+ elif item.startswith("下一章承诺:"):
1388
+ value = item[len("下一章承诺:"):]
1389
+ # 空值 = 逐条子项格式(render_commitments),后面的缩进行逐条读回。
1390
+ in_commitments = value == ""
1391
+ record["next_chapter_commitments"] = [] if value in ("无", "") else value.split(";")
1392
+ continue
1393
+ if item == "无" and section != "retired":
1394
+ continue
1395
+ record[section].append(item)
1396
+ return record
1397
+
1398
+
1399
+ def record_prefill(state: dict[str, Any], record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any], list[str]]:
1400
+ """把已提交的逐章记录还原成修订 delta:结果、承诺、约束、角色变化按记录原文;伏笔与时间线
1401
+ 取受影响 ID 的当前值(修订提交的就是截至最新章的当前值)。另返回记录里核心角色的当前快照。"""
1402
+ notes: list[str] = []
1403
+ changes = []
1404
+ for line in record["character_changes"]:
1405
+ parts = line.split("|", 2)
1406
+ if len(parts) == 3:
1407
+ changes.append({"name": parts[0], "change": parts[2]})
1408
+ snapshots = {item["name"]: copy.deepcopy(state["characters"][item["name"]])
1409
+ for item in changes if item["name"] in state["characters"]}
1410
+
1411
+ def rows(lines: list[str], current: dict[str, Any], label: str) -> list[dict[str, Any]]:
1412
+ prefilled = []
1413
+ for line in lines:
1414
+ parts = line.split("|")
1415
+ identifier = parts[0]
1416
+ if len(parts) > 1 and parts[1] == "删除当前登记":
1417
+ if identifier in current:
1418
+ notes.append(f"{label} {identifier} 本章删过、后来又登记了,没有预填删除,确需删除自行加回")
1419
+ else:
1420
+ prefilled.append({"action": "delete", "id": identifier})
1421
+ elif identifier in current:
1422
+ prefilled.append({"action": "upsert", **{key: copy.deepcopy(value) for key, value in current[identifier].items()
1423
+ if key not in ("updated_chapter", "first_recorded_chapter")}})
1424
+ else:
1425
+ notes.append(f"{label} {identifier} 已在后续章节删除,没有预填")
1426
+ return prefilled
1427
+
1428
+ delta = {
1429
+ "result": record["result"],
1430
+ "character_changes": changes,
1431
+ "foreshadow_changes": rows(record["foreshadow_changes"], state["foreshadow"], "伏笔"),
1432
+ "timeline_events": rows(record["timeline_events"], state["timeline"], "时间线事件"),
1433
+ "constraints": list(record["constraints"]),
1434
+ "next_chapter_commitments": list(record["next_chapter_commitments"]),
1435
+ }
1436
+ return delta, snapshots, notes
1437
+
1438
+
1439
+ def entry_shapes(chapter: int) -> dict[str, Any]:
1440
+ """draft 当场给出每类条目的形状:模型填 delta 时手里只有空数组,字段名、整数、枚举都靠猜,
1441
+ 首次提交几乎都因此被退回。取值从校验用的同一组常量生成,不另立一套。"""
1442
+ return {
1443
+ "delta.character_changes[]": {"name": "角色名", "change": "一句话:本章起了什么变化"},
1444
+ "delta.foreshadow_changes[]": {
1445
+ "action": "upsert(撤掉整条写 delete,只带 id)", "id": "F007", "summary": "伏笔一句话",
1446
+ "planted_chapter": chapter, "planned_resolution_chapter": "计划回收章的整数,未定写 null",
1447
+ "status": "|".join(FORESHADOW_STATUSES), "importance": "|".join(FORESHADOW_IMPORTANCE),
1448
+ },
1449
+ "delta.timeline_events[]": {
1450
+ "action": "upsert(撤掉整条写 delete,只带 id)", "id": "E012", "story_time": "故事内时间",
1451
+ "objective_fact": "客观发生了什么", "reader_knowledge": "读者此刻知道/以为什么",
1452
+ "reveal_status": "|".join(REVEAL_STATUSES),
1453
+ "reveal_chapter": f"已揭示或部分揭示时写揭示章的整数(不晚于 {chapter}),未揭示写 null",
1454
+ "characters": ["角色名"],
1455
+ },
1456
+ "character_snapshots.{角色名}": {
1457
+ **{key: "一句话" for key in SNAPSHOT_TEXT_FIELDS},
1458
+ **{key: ["一条一句"] for key in SNAPSHOT_LIST_FIELDS},
1459
+ },
1460
+ }
1461
+
1462
+
1463
+ def draft_transaction(project: Path, chapter: int) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
1464
+ """按当前 state 预填一份逐章事务:修订号、模式、章名和整份提交的上下文当前值都填好。
1465
+ 新章(append)调用方只写本章变化;另返回在场核心角色的当前快照,供有变化时整份改写后放进事务。
1466
+ 新章的故事时间与场景留空:它们必须是本章结束时的位置,沿用上一章的值能静默提交,
1467
+ 所以不预填,上一章的值另行返回作参考。
1468
+ 修订(revision)的 delta 是修订后本章的完整记录,所以按已提交的逐章记录预填,调用方在上面改;
1469
+ 第三项返回 {"notes": [...], "prefilled": bool}:没能预填的条目,以及逐章记录是否读到。"""
1470
+ state = load_state(project)
1471
+ last = state["last_committed_chapter"]
1472
+ require(1 <= chapter <= last + 1, f"draft chapter must be between 1 and {last + 1}")
1473
+ mode = "append" if chapter == last + 1 else "revision"
1474
+ title = ""
1475
+ for outline in sorted((project / "大纲").glob(f"细纲_第{chapter:03d}章*.md")):
1476
+ match = re.search(rf"^#{{1,4}}\s*第\s*0*{chapter}\s*章\s*[::]\s*(.+?)\s*$",
1477
+ outline.read_text(encoding="utf-8"), re.M)
1478
+ if match:
1479
+ title = match.group(1).strip()
1480
+ break
1481
+ context = json.loads(json.dumps(state["context"], ensure_ascii=False))
1482
+ previous_position = {key: context["position"][key] for key in ("story_time", "scene")}
1483
+ delta: dict[str, Any] = {
1484
+ "result": "", "character_changes": [], "foreshadow_changes": [], "timeline_events": [],
1485
+ "constraints": [], "next_chapter_commitments": [],
1486
+ }
1487
+ snapshots_prefill: dict[str, Any] = {}
1488
+ notes: list[str] = []
1489
+ prefilled = False
1490
+ if mode == "append":
1491
+ delta.update({"retired_context_items": [], "retired_characters": []})
1492
+ context["position"].update({"story_time": "", "scene": ""})
1493
+ else:
1494
+ record = parse_chapter_record(delta_path(tracking_root(project), chapter))
1495
+ if record is not None:
1496
+ prefilled = True
1497
+ delta, snapshots_prefill, notes = record_prefill(state, record)
1498
+ title = title or record["title"]
1499
+ if chapter == last:
1500
+ # 最新章的承诺原样在 state 里,比从记录行按「;」拆回更准。
1501
+ delta["next_chapter_commitments"] = list(state["context"]["next_chapter_commitments"])
1502
+ document = {
1503
+ "schema_version": INPUT_SCHEMA_VERSION,
1504
+ "mode": mode,
1505
+ "chapter": chapter,
1506
+ "chapter_title": title,
1507
+ "expected_state_revision": state["state_revision"],
1508
+ "delta": delta,
1509
+ "context": {key: context[key] for key in
1510
+ ("position", "long_term_constraints", "active_character_names", "continuity_risks")},
1511
+ "character_snapshots": snapshots_prefill,
1512
+ }
1513
+ snapshots = {name: state["characters"][name] for name in context["active_character_names"]
1514
+ if name in state["characters"]}
1515
+ if mode == "revision":
1516
+ return document, snapshots, {"notes": notes, "prefilled": prefilled}
1517
+ return document, snapshots, previous_position
1518
+
1519
+
1520
+ def rebuild_stale_draft(document: dict[str, Any], existing: dict[str, Any], append: bool,
1521
+ current_characters: Any = ()) -> str:
1522
+ """修订号变了:中间提交过别的事务。以当前状态为底,只自动合并确定安全的部分,其余列给调用方核对。
1523
+
1524
+ 新章草稿之间只可能插进修订事务,修订不能删约束与风险条目,所以当前约束与风险覆盖旧底稿:
1525
+ 旧草稿多出来的就是本章新增,delta 里声明退役的就是本章删除,可以精确合并。
1526
+ 修订草稿之间可能插进新章(新章能退役条目),在场角色名修订也能删,都无法判断谁新:
1527
+ 取当前状态,差异列出来;只有本章带了快照的新角色名自动加回。"""
1528
+ def items(value: object) -> list[str]:
1529
+ return [item for item in value if isinstance(item, str)] if isinstance(value, list) else []
1530
+
1531
+ def norm(item: str) -> str:
1532
+ return " ".join(item.replace("|", "|").split())
1533
+
1534
+ notes = ["修订号已变:context 按当前状态重建,本章已填的 delta" + ("、新增与退役的约束、结尾位置" if append else "")
1535
+ + "已合并"]
1536
+ for key in ("delta", "character_snapshots"):
1537
+ if isinstance(existing.get(key), dict):
1538
+ document[key] = existing[key]
1539
+ if existing.get("chapter_title"):
1540
+ document["chapter_title"] = existing["chapter_title"]
1541
+ old_context = existing.get("context") if isinstance(existing.get("context"), dict) else {}
1542
+ delta = document["delta"] if isinstance(document["delta"], dict) else {}
1543
+ context = document["context"]
1544
+ retired = {norm(item) for item in items(delta.get("retired_context_items"))}
1545
+ left_out: list[str] = []
1546
+ for key in ("long_term_constraints", "continuity_risks"):
1547
+ current = {norm(item) for item in context[key]}
1548
+ extra = []
1549
+ for item in items(old_context.get(key)):
1550
+ if norm(item) not in current:
1551
+ current.add(norm(item))
1552
+ extra.append(item)
1553
+ if append:
1554
+ context[key] = [item for item in context[key] if norm(item) not in retired] + extra
1555
+ else:
1556
+ left_out += extra
1557
+ snapshots = document["character_snapshots"] if isinstance(document["character_snapshots"], dict) else {}
1558
+ names = context["active_character_names"]
1559
+ for name in items(old_context.get("active_character_names")):
1560
+ if name in names:
1561
+ continue
1562
+ if append and name in snapshots:
1563
+ names.append(name)
1564
+ else:
1565
+ left_out.append(f"在场角色 {name}")
1566
+ names[:] = [name for name in names if name not in set(items(delta.get("retired_characters")))]
1567
+ known = set(current_characters)
1568
+ for name in list(snapshots):
1569
+ if name not in known and name not in names:
1570
+ snapshots.pop(name)
1571
+ left_out.append(f"角色快照 {name}")
1572
+ if left_out:
1573
+ notes.append("草稿里有、当前状态没有的条目没有自动带回(可能已被别的提交退役),本章确实需要就加回:"
1574
+ + ";".join(left_out))
1575
+ old_position = old_context.get("position") if isinstance(old_context.get("position"), dict) else {}
1576
+ position = context["position"]
1577
+ if append:
1578
+ for key in ("story_time", "scene"):
1579
+ if isinstance(old_position.get(key), str) and old_position[key].strip():
1580
+ position[key] = old_position[key]
1581
+ differs = [f"{key}:草稿写的是「{old_position[key]}」,当前状态是「{position[key]}」"
1582
+ for key in ("volume", "volume_start_chapter")
1583
+ if key in old_position and old_position[key] != position[key]]
1584
+ if differs:
1585
+ notes.append("卷信息与当前状态不同,已取当前状态,本章确实换卷就改回(" + ";".join(differs) + ")")
1586
+ if snapshots:
1587
+ notes.append("带过来的角色快照(" + "、".join(snapshots) + ")可能被中间的提交改过,逐个对照 current_snapshots 重核")
1588
+ return ";".join(notes)
1589
+
1590
+
1591
+ def build_parser() -> argparse.ArgumentParser:
1592
+ parser = argparse.ArgumentParser(description=__doc__)
1593
+ subparsers = parser.add_subparsers(dest="command", required=True)
1594
+ for command in ("init", "commit"):
1595
+ subparser = subparsers.add_parser(command)
1596
+ subparser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1597
+ subparser.add_argument("--input", type=Path, required=True, help="UTF-8 JSON input document")
1598
+ check_parser = subparsers.add_parser("check")
1599
+ check_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1600
+ draft_parser = subparsers.add_parser("draft", help="write a pre-filled chapter transaction to fill in")
1601
+ draft_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
1602
+ draft_parser.add_argument("--chapter", type=int, required=True)
1603
+ draft_parser.add_argument("--out", type=Path, help="default: <project>/.story/work/第NNN章/tracking.json")
1604
+ draft_parser.add_argument("--force", action="store_true",
1605
+ help="discard an existing draft instead of only refreshing its revision")
1606
+ return parser
1607
+
1608
+
1609
+ def main() -> int:
1610
+ args = build_parser().parse_args()
1611
+ try:
1612
+ if args.command == "draft":
1613
+ document, snapshots, extra = draft_transaction(args.project, args.chapter)
1614
+ previous_position = extra if document["mode"] == "append" else {}
1615
+ prefill_notes = extra.get("notes", []) if document["mode"] == "revision" else []
1616
+ record_prefilled = bool(extra.get("prefilled")) if document["mode"] == "revision" else False
1617
+ out = args.out or args.project / ".story" / "work" / f"第{args.chapter:03d}章" / "tracking.json"
1618
+ out.parent.mkdir(parents=True, exist_ok=True)
1619
+ refreshed = ""
1620
+ if out.exists() and not args.force:
1621
+ # 重跑 draft 不冲掉已经填好的变化。修订号变了说明中间提交过别的事务:context 按当前
1622
+ # 状态重建(沿用旧 context 会把那次变更整份覆盖回去),只带过本章自己填的部分。
1623
+ existing = read_json(out)
1624
+ require(isinstance(existing, dict) and existing.get("chapter") == document["chapter"]
1625
+ and existing.get("mode") == document["mode"],
1626
+ f"{out} already holds a draft for a different chapter or mode; pass --force to replace it")
1627
+ if existing.get("expected_state_revision") == document["expected_state_revision"]:
1628
+ document, refreshed = existing, "已有草稿且修订号未变,原样保留"
1629
+ else:
1630
+ refreshed = rebuild_stale_draft(document, existing, bool(previous_position),
1631
+ load_state(args.project)["characters"])
1632
+ out.write_text(json.dumps(document, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
1633
+ if document["mode"] == "revision" and not record_prefilled:
1634
+ fill = (f"这是修订,但本章逐章记录缺失或读不了({delta_path(Path('追踪'), args.chapter).as_posix()}),delta 没有预填:"
1635
+ "按修订后正文把本章完整记录整份写上(结果、承诺、角色变化、伏笔、时间线、约束),没写的项不会进记录。"
1636
+ "角色快照、伏笔、时间线写截至最新已写章的当前值;context 原样保留全部当前条目(修订不能退役)。"
1637
+ "正文改过就用 story-long-write 的 storyctl.py chapter commit 提交(重新计数并清理本章工作目录)。"
1638
+ "不要从脚本源码或 state 文件里另找格式。")
1639
+ elif document["mode"] == "revision":
1640
+ fill = ("这是修订:delta 是修订后本章的完整记录,已按本章现有逐章记录预填(伏笔与时间线取当前值,"
1641
+ "记录里的核心角色快照已放进 character_snapshots)。对照修订后正文逐项改:仍成立的原样保留,"
1642
+ "被推翻的删掉或改写,新增的补上;不要清空后只写改动,没写的项会从本章记录里消失。"
1643
+ "角色快照、伏笔、时间线改成截至最新已写章的当前值;context 原样保留全部当前条目(修订不能退役)。"
1644
+ "正文改过就用 story-long-write 的 storyctl.py chapter commit 提交(重新计数并清理本章工作目录)。"
1645
+ "不要从脚本源码或 state 文件里另找格式。")
1646
+ if prefill_notes:
1647
+ fill += "没能预填:" + ";".join(prefill_notes) + "。"
1648
+ else:
1649
+ fill = ("只填 delta 里本章的变化;context 其余字段已是当前值,要撤下的长期约束或连贯性风险从 context 删掉并把原文放进 "
1650
+ "delta.retired_context_items(仅 append);本章有变化的核心角色把下面的当前快照整份改好放进 character_snapshots,"
1651
+ "并在 character_changes 写一句变化。每类条目照 shapes 的形状写,不要从脚本源码或 state 文件里另找格式。")
1652
+ blank = not (document["context"]["position"].get("story_time") or document["context"]["position"].get("scene"))
1653
+ if previous_position and blank:
1654
+ fill = ("context.position 的 story_time 与 scene 留空,填本章结束时的故事时间与场景(上一章结束时见 "
1655
+ "previous_position;换卷时连同 volume 与 volume_start_chapter 一起改)。") + fill
1656
+ payload = {
1657
+ "draft": str(out),
1658
+ "mode": document["mode"],
1659
+ "expected_state_revision": document["expected_state_revision"],
1660
+ "fill": (f"{refreshed};要从头生成加 --force。" if refreshed else "") + fill,
1661
+ "limits_chars": DRAFT_LIMITS,
1662
+ "shapes": entry_shapes(args.chapter),
1663
+ "current_snapshots": snapshots,
1664
+ }
1665
+ if previous_position:
1666
+ payload["previous_position"] = previous_position
1667
+ emit(json.dumps(payload, ensure_ascii=False))
1668
+ return 0
1669
+ if args.command == "init":
1670
+ result = initialize(args.project, read_json(args.input))
1671
+ elif args.command == "commit":
1672
+ result = apply_transaction(args.project, read_json(args.input))
1673
+ else:
1674
+ result = check_project(args.project)
1675
+ except (TrackingError, OSError, UnicodeError) as exc:
1676
+ emit(f"ERROR: {exc}", error=True)
1677
+ return 2
1678
+ emit(
1679
+ json.dumps(
1680
+ {
1681
+ "last_committed_chapter": result["last_committed_chapter"],
1682
+ "state_revision": result["state_revision"],
1683
+ },
1684
+ ensure_ascii=False,
1685
+ )
1686
+ )
1687
+ return 0
1688
+
1689
+
1690
+ if __name__ == "__main__":
1691
+ raise SystemExit(main())