@oh-story/dsh 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +12 -12
  2. package/lib/client.js +204 -72
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +84 -74
  5. package/lib/drama/skills/short-drama/SKILL.md +2 -2
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
  7. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
  8. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
  9. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +27 -29
  11. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +2 -2
  12. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +24 -67
  13. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +27 -56
  14. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +37 -79
  15. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +39 -92
  16. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +12 -16
  17. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +126 -11
  18. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +64 -0
  19. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  20. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  21. package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
  22. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
  23. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
  24. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  25. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
  26. package/lib/drama/skills/short-drama-review/SKILL.md +2 -3
  27. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
  28. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +9 -35
  29. package/lib/drama/skills/short-drama-storyboard/SKILL.md +10 -6
  30. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  31. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +58 -21
  32. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +1 -1
  33. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +10 -9
  34. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
  35. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +12 -38
  36. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +4 -7
  37. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -22
  38. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -19
  39. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
  40. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
  41. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +2 -2
  42. package/lib/drama/skills/short-drama-write/SKILL.md +2 -4
  43. package/lib/index.js +289 -91
  44. package/lib/index.js.map +4 -4
  45. package/lib/novel-to-game/README.md +299 -59
  46. package/lib/novel-to-game/README_ZH.md +299 -56
  47. package/lib/novel-to-game/VERSION +1 -1
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  57. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  58. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  59. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  60. package/lib/novel-to-game/manifest.json +77 -87
  61. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
  62. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
  64. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  65. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  66. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  67. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  68. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  69. package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
  70. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  71. package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
  72. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
  73. package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
  74. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  75. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  76. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  77. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
  78. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  79. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
  80. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
  81. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
  82. package/lib/oh-story/manifest.json +230 -185
  83. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  84. package/lib/oh-story/roles/consistency-checker.md +4 -4
  85. package/lib/oh-story/roles/narrative-writer.md +24 -25
  86. package/lib/oh-story/roles/story-architect.md +5 -5
  87. package/lib/oh-story/roles/story-explorer.md +11 -11
  88. package/lib/oh-story/skills/story/SKILL.md +13 -7
  89. package/lib/oh-story/skills/story/VERSION +1 -1
  90. package/lib/oh-story/skills/story/references/author-memory.md +54 -24
  91. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  92. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  93. package/lib/oh-story/skills/story-deslop/SKILL.md +40 -53
  94. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +11 -10
  95. package/lib/oh-story/skills/story-deslop/references/author-memory.md +54 -24
  96. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +1 -1
  97. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  98. package/lib/oh-story/skills/story-import/SKILL.md +77 -81
  99. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  100. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  101. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
  102. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +8 -6
  103. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +34 -16
  104. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
  105. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
  106. package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
  107. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
  108. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
  109. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  110. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
  111. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
  112. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
  113. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  114. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
  115. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
  116. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  117. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  118. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  119. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
  120. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  121. package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
  122. package/lib/oh-story/skills/story-long-write/SKILL.md +38 -29
  123. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +11 -10
  124. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +25 -20
  125. package/lib/oh-story/skills/story-long-write/references/author-memory.md +54 -24
  126. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +3 -3
  127. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  128. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
  129. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  130. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +4 -13
  131. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  132. package/lib/oh-story/skills/story-long-write/references/project-files.md +9 -7
  133. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
  134. package/lib/oh-story/skills/story-long-write/references/reference-index.md +1 -2
  135. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +8 -6
  136. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +86 -115
  137. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -120
  138. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
  139. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +66 -68
  140. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +3 -3
  141. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +4 -12
  142. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  143. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +139 -18
  144. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +34 -16
  145. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +5 -3
  146. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
  147. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
  148. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
  149. package/lib/oh-story/skills/story-review/SKILL.md +68 -85
  150. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +11 -10
  151. package/lib/oh-story/skills/story-review/references/author-memory.md +54 -24
  152. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
  153. package/lib/oh-story/skills/story-review/references/review-quality.md +4 -10
  154. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +8 -6
  155. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  156. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
  157. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
  158. package/lib/oh-story/skills/story-setup/SKILL.md +50 -42
  159. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +11 -10
  160. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +1 -1
  161. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
  162. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  163. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
  164. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +4 -12
  165. package/lib/oh-story/skills/story-setup/references/diagnostics.md +1 -1
  166. package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
  167. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
  168. package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
  169. package/lib/oh-story/skills/story-short-write/SKILL.md +6 -147
  170. package/lib/oh-story/skills/story-short-write/references/author-memory.md +54 -24
  171. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
  172. package/lib/oh-story/skills/story-short-write/references/reference-index.md +2 -0
  173. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +3 -2
  174. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
  175. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
  176. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  177. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  178. package/lib/types/client/file-activity.d.ts +19 -1
  179. package/lib/types/client/file-activity.d.ts.map +1 -1
  180. package/lib/types/client/index.d.ts.map +1 -1
  181. package/lib/types/client/production-prompts.d.ts +7 -0
  182. package/lib/types/client/production-prompts.d.ts.map +1 -1
  183. package/lib/types/client/production-runtime.d.ts +16 -0
  184. package/lib/types/client/production-runtime.d.ts.map +1 -1
  185. package/lib/types/drama-adapters.d.ts +5 -3
  186. package/lib/types/drama-adapters.d.ts.map +1 -1
  187. package/lib/types/native-hooks.d.ts +52 -0
  188. package/lib/types/native-hooks.d.ts.map +1 -1
  189. package/lib/types/role-identity.d.ts +17 -0
  190. package/lib/types/role-identity.d.ts.map +1 -0
  191. package/lib/types/role-tool.d.ts.map +1 -1
  192. package/lib/types/skill-provider.d.ts.map +1 -1
  193. package/package.json +45 -45
  194. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
  195. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  196. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
  197. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
@@ -2,7 +2,12 @@
2
2
  """build_writer_prompt.py — 确定性组装 narrative-writer 的 spawn prompt 骨架。
3
3
 
4
4
  用法:
5
- python build_writer_prompt.py --project <书目录> --chapter N [--out <文件>]
5
+ python build_writer_prompt.py --project <书目录> --chapter N [--out [<文件>]]
6
+
7
+ `--out` 不带路径时留档到本章工作目录 `<书目录>/.story/work/第NNN章/writer_prompt.md`。
8
+ 分组 segment、prompt 留档和逐章事务 JSON 都只放这个书内工作目录——不写系统 /tmp
9
+ (多本书/多会话同章号会互相覆盖,Windows 也没有 /tmp),也不写进 正文/(会被当成章节)。
10
+ `storyctl.py chapter commit` 成功后自动删除该章工作目录。
6
11
 
7
12
  跑在「写前准备」第一步。stdout 分两区,`===` 分隔线以上是 prompt 正文
8
13
  (主会话照抄,空槽以外一字不改),以下是核对报告(不进 prompt)。
@@ -10,8 +15,9 @@
10
15
  职责边界:
11
16
  - 脚本做确定性部分:固定首行、定位、标题行字面量、细纲指针、文风全文路径与裁决、
12
17
  上一章结尾、降档判定与情绪/节奏槽、固定块指针。
18
+ - 脚本代查作者记忆(prose_style + story_design)并填好 author_preferences。
13
19
  - 主会话填八槽:执行安排 / 本章意图 / 参考技法 / 本节速记 / 涉及角色 / genre_prose_card /
14
- 必读设定 / author_preferences。降档不成立时情绪与节奏槽也归主会话。
20
+ 必读设定 / style_resolution。降档不成立时情绪与节奏槽也归主会话。
15
21
  材料槽对应原流程步骤 3「写前准备」的四项输出(本节速记 / 情绪目标 / 涉及角色 /
16
22
  参考技法)加上题材卡、设定补漏与作者偏好——都是判断,脚本做不了。
17
23
  - 续写状态卡校验后由主会话筛选,在「本节速记」槽内写入本章需要的状态。
@@ -23,7 +29,9 @@ Exit: 0 = 骨架已输出;2 = 输入缺失或无效。
23
29
 
24
30
  import argparse
25
31
  import io
32
+ import json
26
33
  import re
34
+ import subprocess
27
35
  import sys
28
36
  from pathlib import Path
29
37
  from outline_view import parse as parse_volume
@@ -31,6 +39,13 @@ from outline_view import parse as parse_volume
31
39
  TAIL_CHARS = 400 # 上一章结尾注入的目标字符数(按整行回退,不切半句)
32
40
  STATE_SECTIONS = ("当前位置", "长期约束", "核心角色状态", "活跃伏笔", "近三章速记", "下一章承诺", "连贯性风险")
33
41
  SLOT_MARK = "[主会话填]"
42
+ WORK_ROOT = (".story", "work")
43
+
44
+
45
+ def chapter_work_dir(project: Path, chapter: int) -> Path:
46
+ """本章临时文件的唯一落点;与 storyctl.py 提交后清理的目录同一口径。"""
47
+ width = max(3, len(str(chapter)))
48
+ return project.joinpath(*WORK_ROOT, f"第{chapter:0{width}d}章")
34
49
 
35
50
 
36
51
  def read_text(path: Path):
@@ -58,7 +73,7 @@ def find_chapter_file(directory: Path, chapter: int, prefix: str):
58
73
  return None
59
74
  pattern = re.compile(rf"^{prefix}第0*{chapter}章.*\.md$")
60
75
  for entry in sorted(directory.iterdir()):
61
- if entry.is_file() and pattern.match(entry.name):
76
+ if entry.is_file() and pattern.match(entry.name) and "_原稿_" not in entry.name:
62
77
  return entry
63
78
  return None
64
79
 
@@ -125,6 +140,83 @@ def previous_chapter_tail(project: Path, chapter: int):
125
140
  return prev, "\n".join(reversed(picked))
126
141
 
127
142
 
143
+ MEMORY_KINDS = ("prose_style", "story_design")
144
+ # 限定「流程」的作者记忆里,这些取值指的就是长篇写正文。
145
+ LONG_WRITE_WORKFLOWS = {"长篇", "长篇写作", "写长篇", "长篇连载", "长篇网文", "长篇正文", "story-long-write"}
146
+
147
+
148
+ def scoped_memory_values(workspace: Path):
149
+ """项目级 store 里限定题材/流程的 active 条目取值:{"genre": {...}, "workflow": {...}}。"""
150
+ values = {"genre": set(), "workflow": set()}
151
+ state = read_text(workspace / ".story" / "作者记忆" / "_author-memory-state.json")
152
+ try:
153
+ items = (json.loads(state) if state else {}).get("items") or {}
154
+ items = list(items.values())
155
+ except (ValueError, AttributeError, TypeError):
156
+ return values
157
+ for item in items:
158
+ if not isinstance(item, dict):
159
+ continue
160
+ scope = item.get("scope") if isinstance(item.get("scope"), dict) else {}
161
+ if (item.get("status") == "active" and item.get("kind") in MEMORY_KINDS
162
+ and scope.get("level") in values and scope.get("value")):
163
+ values[scope["level"]].add(scope["value"])
164
+ return values
165
+
166
+
167
+ def book_genres(project: Path):
168
+ """「题材类型」行按分隔符切成词;未填的模板占位({…})不算。"""
169
+ text = read_text(project / "设定" / "题材定位.md") or ""
170
+ match = re.search(r"^[ \t]*[-*+]?[ \t]*\**题材(?:类型)?\**[ \t]*[::](.*)$", text, re.M)
171
+ if not match or "{" in match.group(1):
172
+ return set()
173
+ return {word.casefold() for word in re.split(r"[\W_丨×]+", match.group(1)) if word}
174
+
175
+
176
+ def query_author_memory(project: Path):
177
+ """代主会话做写正文那一次作者记忆查询(prose_style + story_design,每次 ≤2KB)。
178
+
179
+ 工作区取书目录及其祖先里第一个带 `.story-deployed` 的目录;找不到就把书目录当工作区。
180
+ 限定题材的条目只在取值出现在本书「题材类型」里时代查,限定流程的只认长篇写作的取值;
181
+ 其余限定条目不猜,报给主会话自己判断。
182
+ 返回 (条目列表, 超编 ID 列表, 未代查的限定取值, 错误);错误时由主会话按 author-memory.md 手动查询。
183
+ """
184
+ workspace = next((d for d in (project, *project.parents) if (d / ".story-deployed").is_file()), project)
185
+ script = Path(__file__).with_name("author_memory_commit.py")
186
+ if not script.is_file():
187
+ return None, [], {}, "author_memory_commit.py 缺失"
188
+ scoped = scoped_memory_values(workspace)
189
+ book = book_genres(project)
190
+ genres = sorted(v for v in scoped["genre"] if v.casefold() in book) or [None]
191
+ workflows = sorted(v for v in scoped["workflow"] if v.casefold() in {w.casefold() for w in LONG_WRITE_WORKFLOWS})
192
+ skipped = {"genre": sorted(v for v in scoped["genre"] if v not in genres),
193
+ "workflow": sorted(v for v in scoped["workflow"] if v not in workflows)}
194
+ items, omitted, seen = [], [], set()
195
+ for genre in genres:
196
+ for workflow in workflows or [None]:
197
+ command = [sys.executable, str(script), "query", "--workspace", str(workspace), "--book-root", str(project)]
198
+ for kind in MEMORY_KINDS:
199
+ command += ["--kind", kind]
200
+ if genre:
201
+ command += ["--genre", genre]
202
+ if workflow:
203
+ command += ["--workflow", workflow]
204
+ completed = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", check=False)
205
+ try:
206
+ result = json.loads(completed.stdout or "{}")
207
+ except ValueError:
208
+ result = {}
209
+ if not result.get("ok"):
210
+ return None, [], {}, result.get("error") or completed.stderr.strip() or "query 失败"
211
+ for item in result.get("items") or []:
212
+ if item.get("id") not in seen:
213
+ seen.add(item.get("id"))
214
+ items.append(item)
215
+ omitted += [i for i in result.get("omitted_ids") or [] if i not in omitted and i not in seen]
216
+ omitted = [i for i in omitted if i not in seen]
217
+ return items, omitted, {k: v for k, v in skipped.items() if v}, None
218
+
219
+
128
220
  def learn_heading_form(project: Path, chapter: int, title: str):
129
221
  """从已有正文学标题行形态(层级/章号填充位数/分隔符),不按通用规则推导。
130
222
 
@@ -233,7 +325,7 @@ def build(project: Path, chapter: int, report: list):
233
325
  parts.append(
234
326
  "——— 文风 ———\n"
235
327
  "(本书无可用的 设定/文风.md,未进入自定义文风模式;"
236
- "按 workflow-chapter 3(d) 走对标文风召回,由主会话补路径与召回指令)")
328
+ "按 benchmark-recall.md 走对标文风召回,由主会话补路径与召回指令)")
237
329
  report.append("文风:custom_style=false,文风召回归主会话(未跳过,留标题)")
238
330
 
239
331
  # ---- 上一章结尾(不给路径,避免写手回头读整章)----
@@ -296,15 +388,18 @@ def build(project: Path, chapter: int, report: list):
296
388
  else:
297
389
  slot_recall = ("——— 情绪与节奏召回 ———\n"
298
390
  f"{SLOT_MARK} 降档不成立(" + "、".join(why) +
299
- "),按 workflow-chapter 3(a)(b)(e)(f) 走全量召回后填此槽")
391
+ "),按 benchmark-recall.md 走全量召回后填此槽")
300
392
  report.append("召回降档:不成立(" + "、".join(why) + ")—— 全量召回归主会话")
301
393
 
302
394
  # ---- 需要主会话判断的槽位 ----
395
+ work_dir = chapter_work_dir(project, chapter)
303
396
  parts.append(
304
397
  "——— 执行安排 ———\n"
305
398
  f"{SLOT_MARK} 全章细纲用于整体编排。默认按自然转场或因果停顿分前后两组,"
306
- "填写当前组的情节点/片段及临时输出路径;先只写前组,父流程测一次 checkpoint 后"
307
- "再给后组和机器剩余区间。只有用户明确要求一次成文时才填「全章,直接写最终路径」。")
399
+ "填写当前组的情节点/片段;先只写前组,父流程测一次 checkpoint 后"
400
+ "再给后组和机器剩余区间。只有用户明确要求一次成文时才填「全章,直接写最终路径」。\n"
401
+ f"分组临时文件:前组 {work_dir / '前组.md'},后组 {work_dir / '后组.md'}"
402
+ "(只写这里,不写 /tmp 或 正文/)。")
308
403
  parts.append(f"——— 本章意图(一句话)———\n{SLOT_MARK}")
309
404
  parts.append(slot_recall)
310
405
  # 伏笔与卷级禁忌走「主会话筛选后写进速记」这条原设计路线(步骤 3 状态筛选),
@@ -312,7 +407,7 @@ def build(project: Path, chapter: int, report: list):
312
407
  # 代价是它依赖主会话逐章想起来,所以这里把提示语写成写死的三问清单。
313
408
  parts.append(
314
409
  "——— 参考技法 ———\n"
315
- f"{SLOT_MARK} 步骤 3 三问的第 ②③ 问:借鉴哪个参考文件的哪个技法、用在哪些段落。"
410
+ f"{SLOT_MARK} 借鉴哪个参考文件的哪个技法、用在哪些段落;没有就写「无」。"
316
411
  "按 reference 表的任务条件读取;本书文风只覆盖冲突表达条款,不停读整份文件。")
317
412
  parts.append(
318
413
  "——— 本节速记 ———\n"
@@ -328,15 +423,33 @@ def build(project: Path, chapter: int, report: list):
328
423
  parts.append("——— 题材正文提示卡(genre_prose_card,只含本章相关条目)———\n"
329
424
  f"{SLOT_MARK} 主题材抽 3-5 条、辅题材 1-2 条;只作内部校准,不进正文")
330
425
  parts.append(slot_setting)
331
- parts.append("——— style_resolution / author_preferences ———\n"
332
- f"{SLOT_MARK} author_memory query 命中本章的 prose_style/story_design 项;"
333
- "query 显式传本书/题材/流程;偏好是低优先级倾向,无则写「无」。附 style_resolution:生效要求及来源、被覆盖的默认条款和事实边界;同一裁决传去味与审稿,不逐条追求命中。")
334
-
335
- parts.append("检查分工:写手负责编排、内容覆盖和格式自检;父流程质量阶段负责语义去味及最终文件扫描。写手不提前重复整轮去味或相同检查链,保留时空表、新增申报与原定交付。")
426
+ items, omitted, skipped, memory_error = query_author_memory(project)
427
+ if memory_error:
428
+ memory_block = (f"{SLOT_MARK} 组装脚本查询作者记忆失败({memory_error}),"
429
+ "按 author-memory.md 手动 query prose_style + story_design 后填入;无则写「无」")
430
+ report.append(f"作者记忆:脚本查询失败——{memory_error},归主会话手动查询")
431
+ elif items:
432
+ memory_block = "\n".join(f"- {item.get('assertion', '').strip()}({item.get('id', '')})" for item in items)
433
+ report.append(f"作者记忆:已注入 {len(items)} 条" + (
434
+ f";超编未装下 {len(omitted)} 条({'、'.join(omitted)}),转告作者建议「整理作者记忆」" if omitted else ""))
435
+ else:
436
+ memory_block = "无"
437
+ report.append("作者记忆:无相关 active 条目")
438
+ if skipped and not memory_error:
439
+ named = ";".join(f"{'题材' if k == 'genre' else '流程'}:{'、'.join(v)}" for k, v in skipped.items())
440
+ memory_block += (f"\n{SLOT_MARK} 另有限定范围的作者记忆未代查({named}):本书适用的,"
441
+ "按 author-memory.md 带 --genre/--workflow 查询后补进本块")
442
+ report.append(f"作者记忆:限定范围未代查({named}),归主会话判断是否适用")
443
+ parts.append("——— author_preferences(作者记忆,低优先级倾向,不逐条追求命中)———\n" + memory_block)
444
+ parts.append("——— style_resolution ———\n"
445
+ f"{SLOT_MARK} 本轮请求里作者对表达的明确要求及其覆盖的默认条款(没有写「无」);"
446
+ "本书文风与作者记忆的裁决见上方各块,同一裁决传去味与审稿。")
447
+
448
+ parts.append("检查分工:写手负责编排、内容覆盖和格式自检;父流程质量阶段负责语义去味及最终文件扫描。写手不提前重复整轮去味或相同检查链,保留情节点落点、新增申报与原定交付。")
336
449
 
337
450
  # ---- 固定块:压成指针,不重述 agent 定义 ----
338
451
  parts.append(
339
- "本次照你的铁律 1-8 与被调用协议执行(细纲优先边界、正文形状、新增物三档、"
452
+ "本次照你的铁律 1-8 与被调用协议执行(细纲优先边界、正文形状、新增物三级、"
340
453
  "阅读体验字段、交付三附件均以你的定义为准,此处不重述)。")
341
454
  parts.append(
342
455
  "字数目标按细纲执行,字数口径 visible_chars_v1;按执行安排交付,"
@@ -360,7 +473,8 @@ def main(argv=None):
360
473
  parser = argparse.ArgumentParser()
361
474
  parser.add_argument("--project", required=True)
362
475
  parser.add_argument("--chapter", required=True, type=int)
363
- parser.add_argument("--out", default=None)
476
+ parser.add_argument("--out", nargs="?", const="", default=None,
477
+ help="prompt 留档路径;不带值时留档到本章工作目录 writer_prompt.md")
364
478
  args = parser.parse_args(argv)
365
479
 
366
480
  project = Path(args.project).resolve()
@@ -391,9 +505,16 @@ def main(argv=None):
391
505
  f"标题预检:《{title}》" + ("与既有章重名 → " + "、".join(clashes)
392
506
  if clashes else "无重名"))
393
507
 
394
- if args.out:
395
- io.open(args.out, "w", encoding="utf-8", newline="\n").write(prompt)
396
- report.append(f"留档:{args.out}")
508
+ if args.out is not None:
509
+ out = Path(args.out) if args.out else chapter_work_dir(project, args.chapter) / "writer_prompt.md"
510
+ try:
511
+ out.parent.mkdir(parents=True, exist_ok=True)
512
+ with io.open(out, "w", encoding="utf-8", newline="\n") as handle:
513
+ handle.write(prompt)
514
+ except OSError as exc:
515
+ sys.stderr.write(f"留档写入失败:{out}({exc})\n")
516
+ return 2
517
+ report.append(f"留档:{out}")
397
518
 
398
519
  sys.stdout.write(prompt)
399
520
  sys.stdout.write("\n" + "=" * 60 + "\n")
@@ -18,11 +18,13 @@
18
18
  const fs = require('fs')
19
19
  const path = require('path')
20
20
 
21
- // 权威模板:references/workflow-setup.md「细纲(全书每章)」
21
+ // 权威模板:references/workflow-setup.md「排纲自查与细纲」里的细纲模板
22
+ // 必填只留有证据或事故撑腰的核心字段;阶段位置、章节定位、结构公式、本章标价、闭环状态、
23
+ // 写手自由区、契约风险、镜头准入、信息差触发点与「分辨率」列是可选的阅读体验字段——
24
+ // 写了照样消费,没写不拦(#383 隔离实验:只补目标情绪与主角目标就复现了补满全部字段的收益)。
22
25
  const FIELDS = [
23
- '核心事件', '字数目标', '字数口径', '阶段位置', '单元ID/位置', '目标情绪',
24
- '主角目标/关键选择', '章节定位', '本章结构公式', '章首钩子', '爽点',
25
- '本章标价', '闭环状态', '本章禁止提前释放', '写手自由区', '契约风险',
26
+ '核心事件', '字数目标', '字数口径', '单元ID/位置', '目标情绪',
27
+ '主角目标/关键选择', '章首钩子', '爽点', '本章禁止提前释放',
26
28
  ]
27
29
  const SUBSECTIONS = ['内容概括', '情节安排', '人物关系和出场顺序', '情节细化']
28
30
  const FIVE_ACT = ['起因', '发展', '转折', '高潮', '结尾']
@@ -101,7 +103,7 @@ function checkSettingRefs(text, name, file, projectRoot) {
101
103
  name,
102
104
  missing.length ? `引用的路径不是可用文件:${missing.join('、')}` : `${refs.size} 处项目内引用全部存在`,
103
105
  '细纲引用的每个项目内 .md 文件都真实存在——不许把槽位指向查无实据的权威文件',
104
- '修正引用路径;或按「创作自主权分级」先把缺的设定落档(B 级直接补、C 级进《供给单》提案),再在细纲引用。'
106
+ '修正引用路径;或按新增物三级先把缺的设定落档(「写了要报」直接补并登记、「先问作者」进排纲底稿的供给自查提案),再在细纲引用。'
105
107
  )
106
108
  }
107
109
 
@@ -173,8 +175,8 @@ function verify(file, projectRoot = null) {
173
175
  let header = null
174
176
  for (const line of lines) {
175
177
  const cells = parseTableRow(line)
176
- // 四列是旧表(# / 情节点 / 功能标签 / 执行边界),五列是现行模板(中间多一列「分辨率」)。
177
- // 两种都收:功能标签固定在第 3 列,执行边界一律取最后一列,旧细纲不因加列而失效。
178
+ // 现行模板默认四列(# / 情节点 / 功能标签 / 执行边界(禁+放)),需要时在功能标签后加「分辨率」列成五列。
179
+ // 两种都收:功能标签固定在第 3 列,执行边界一律取最后一列。
178
180
  if (cells && (cells.length === 4 || cells.length === 5) && PLOT_HEADER_FIRST.test(cells[0])) {
179
181
  header = cells
180
182
  break
@@ -189,21 +191,21 @@ function verify(file, projectRoot = null) {
189
191
  headerOk,
190
192
  name,
191
193
  header ? `表头:${header.join(' | ')}` : '未找到 | # | 情节点 | 功能标签 | [分辨率] | 执行边界 | 表头',
192
- '情节细化使用五列表格:# / 情节点(谁做了什么) / 功能标签 / 分辨率 / 执行边界(四列旧表仍收,但新建细纲按五列模板走)',
193
- '只把情节点序列改成五列表格,逐点补功能标签、分辨率与执行边界;不增删情节点本身。'
194
+ '情节细化使用表格:# / 情节点(谁做了什么) / 功能标签 / 执行边界(禁+放);需要时可在功能标签后加「分辨率」列',
195
+ '只把情节点序列改成这张表,逐点补功能标签与执行边界;不增删情节点本身。'
194
196
  ))
195
197
 
196
- // 「放」半边:五列表的执行边界必须至少一个点解除限制(模板既有要求),
197
- // 全是禁令的情节点表实测会把整章压成同一个温度(015 章十一个点全「不许」)。
198
- if (headerOk && header.length === 5) {
198
+ // 「放」半边:五列表或表头写明「禁+放」的四列表,执行边界必须至少一个点解除限制,
199
+ // 全是禁令的情节点表实测会把整章压成同一个温度(015 章十一个点全「不许」)。表头没写「禁+放」的四列表不追溯。
200
+ if (headerOk && (header.length === 5 || /放/.test(header[header.length - 1]))) {
199
201
  const rows = []
200
202
  for (const line of lines) {
201
203
  const cells = parseTableRow(line)
202
- if (cells && cells.length === 5 && /^\d+$/.test(cells[0])) rows.push(cells)
204
+ if (cells && cells.length === header.length && /^\d+$/.test(cells[0])) rows.push(cells)
203
205
  }
204
206
  if (rows.length) {
205
207
  const released = rows.filter((cells) => {
206
- const match = cells[4].match(/(?:^|[。;;\s])放\s*[::]([^]*?)(?=(?:禁|放)\s*[::]|$)/)
208
+ const match = cells[cells.length - 1].match(/(?:^|[。;;\s])放\s*[::]([^]*?)(?=(?:禁|放)\s*[::]|$)/)
207
209
  if (!match) return false
208
210
  const value = match[1].replace(/[\s。;;,,、\[\]【】]/g, '')
209
211
  return value.length > 0 && !/^(?:无|暂无|待补充|待定|无缺口)$/.test(value)
@@ -303,8 +305,24 @@ function resolveChapter(project, chapter) {
303
305
  return { file: path.join(dir, hit) }
304
306
  }
305
307
 
306
- // --supply:批末验证《供给单》已落卷纲——定位单元卡块,确认其中有「供给自查」小节。
308
+ // --supply:批末验证供给自查已落盘。新书写在卷纲旁的 排纲底稿_{单元ID}.md;老卷纲写在单元卡块内。
309
+ function hasSupplyHeading(text) {
310
+ return text.split(/\r?\n/).some((line) => {
311
+ const heading = line.replace(/\*\*/g, '').match(/^#{1,6}\s+(.+)$/)
312
+ return Boolean(heading) && /^供给自查(?:\s|[((]|$)/.test(heading[1])
313
+ })
314
+ }
315
+
307
316
  function verifySupply(volumeFile, unitId) {
317
+ const draftFile = path.join(path.dirname(volumeFile), `排纲底稿_${unitId}.md`)
318
+ const draft = readUtf8(draftFile)
319
+ if (draft.ok) {
320
+ const ok = hasSupplyHeading(draft.text)
321
+ return {
322
+ schema_version: 1, verifier: 'story-long-write.outline-supply', file: path.resolve(draftFile), unit: unitId, ok,
323
+ evidence: ok ? '排纲底稿含「供给自查」小节' : `${path.basename(draftFile)} 里没有「供给自查」小节——每批出细纲前须做供给自查(含「无缺口」情形),见 workflow-setup.md「排纲自查与细纲」步骤 3`,
324
+ }
325
+ }
308
326
  const read = readUtf8(volumeFile)
309
327
  if (!read.ok) {
310
328
  return { schema_version: 1, verifier: 'story-long-write.outline-supply', file: path.resolve(volumeFile), unit: unitId, ok: false, evidence: read.error || '卷纲文件为空' }
@@ -345,7 +363,7 @@ function verifySupply(volumeFile, unitId) {
345
363
  file: path.resolve(volumeFile),
346
364
  unit: unitId,
347
365
  ok,
348
- evidence: ok ? '单元卡含「供给自查」小节' : `剧情单元 ${unitId} 的卡内没有「供给自查」小节——每批出细纲前须产出《供给单》(含「无缺口」情形),见 workflow-setup.md「按剧情批出细纲」步骤 3`,
366
+ evidence: ok ? '单元卡含「供给自查」小节' : `没有 排纲底稿_${unitId}.md,剧情单元 ${unitId} 的卡内也没有「供给自查」小节——每批出细纲前须做供给自查(含「无缺口」情形),见 workflow-setup.md「排纲自查与细纲」步骤 3`,
349
367
  }
350
368
  }
351
369
 
@@ -217,9 +217,11 @@ def main():
217
217
  parser.add_argument("--check", action="store_true")
218
218
  parser.add_argument("--history", action="store_true")
219
219
  parser.add_argument("--strict", action="store_true", help="新建卷纲校验:缺作用域也视为错误")
220
- parser.add_argument("--stage", choices=["outline", "write"], default="outline",
221
- help="outline=排纲档(带在用批次底稿);"
222
- "write=写作档(只要卷级常任+单元级,底稿一概不给)")
220
+ # 排纲底稿现在放在卷纲外的 大纲/排纲底稿_{单元ID}.md,默认输出就是写作要的闭包。
221
+ # --stage outline 只为老卷纲保留:它们的供给自查等底稿还写在卷纲里(作用域:批次底稿)。
222
+ parser.add_argument("--stage", choices=["outline", "write"], default="write",
223
+ help="write(默认)=卷级常任+单元级;"
224
+ "outline=另带老卷纲里仍在用的批次底稿段")
223
225
  args = parser.parse_args()
224
226
 
225
227
  if hasattr(sys.stdout, "reconfigure"):
@@ -30,6 +30,9 @@ for _name in dir(core):
30
30
 
31
31
  CHAPTER_CHECK_SCHEMA = "story-chapter-check/v1"
32
32
  CHAPTER_ERROR_SCHEMA = "story-chapter-error/v1"
33
+ # 书内工作目录:分组 segment、writer prompt 留档、逐章事务 JSON 的唯一落点。
34
+ # 与 build_writer_prompt.py 同一口径;提交成功后整目录删除,失败时原样保留供重跑。
35
+ WORK_ROOT = (".story", "work")
33
36
 
34
37
 
35
38
  class CliArgumentError(ValueError):
@@ -72,6 +75,32 @@ def _read_json_object(path: Path, label: str) -> dict[str, Any]:
72
75
  return value
73
76
 
74
77
 
78
+ def chapter_work_dir(project: Path, chapter: int) -> Path:
79
+ width = max(3, len(str(chapter)))
80
+ return project.resolve().joinpath(*WORK_ROOT, f"第{chapter:0{width}d}章")
81
+
82
+
83
+ def _remove_chapter_work_dir(project: Path, chapter: int) -> str | None:
84
+ """Delete this chapter's scratch directory after a successful commit.
85
+
86
+ Only the per-chapter directory is removed; `.story/work` and `.story` are
87
+ pruned only when empty, so book-level author memory under `.story/` stays.
88
+ """
89
+ path = chapter_work_dir(project, chapter)
90
+ if path.is_symlink():
91
+ path.unlink()
92
+ elif path.is_dir():
93
+ shutil.rmtree(path)
94
+ else:
95
+ return None
96
+ for parent in (path.parent, path.parent.parent):
97
+ try:
98
+ parent.rmdir()
99
+ except OSError:
100
+ break
101
+ return "/".join((*WORK_ROOT, path.name))
102
+
103
+
75
104
  def _project_files(project: Path, chapter: int) -> tuple[Path, Path, int]:
76
105
  root = project.resolve()
77
106
  outline = find_chapter_file(root / "大纲", chapter, outline=True)
@@ -200,6 +229,23 @@ def chapter_check(project: Path, chapter: int) -> dict[str, Any]:
200
229
  }
201
230
 
202
231
 
232
+ def fix_punctuation(project: Path, chapter: int) -> bool:
233
+ """Run the deterministic punctuation normalizer on the chapter body in place.
234
+
235
+ Folding it into `chapter check --fix-punctuation` lets the parent flow close a
236
+ chapter with one call instead of running each cleanup script separately.
237
+ """
238
+ _, body, _ = _project_files(project, chapter)
239
+ node = shutil.which("node")
240
+ script = Path(__file__).with_name("normalize-punctuation.js")
241
+ if node is None or not script.is_file():
242
+ return False # chapter check reports TOOL_UNAVAILABLE as a blocking finding
243
+ before = body.read_bytes()
244
+ subprocess.run([node, str(script), str(body)], text=True, encoding="utf-8",
245
+ capture_output=True, check=False)
246
+ return body.read_bytes() != before
247
+
248
+
203
249
  def chapter_commit(project: Path, chapter: int, input_path: Path, *, accept_current_length: bool) -> dict[str, Any]:
204
250
  checked = chapter_check(project, chapter)
205
251
  require(checked["quality"]["status"] == "pass", "blocking quality findings must be fixed before commit")
@@ -220,6 +266,12 @@ def chapter_commit(project: Path, chapter: int, input_path: Path, *, accept_curr
220
266
  checked["tracking_committed"] = state["last_committed_chapter"] >= chapter
221
267
  checked["next_chapter_started"] = state["last_committed_chapter"] > chapter
222
268
  checked["wordcount"] = state["wordcount_records"].get(str(chapter))
269
+ # The commit is already durable; a cleanup failure is reported, never turned into a failed commit.
270
+ try:
271
+ checked["work_dir_removed"] = _remove_chapter_work_dir(project, chapter)
272
+ except OSError as exc:
273
+ checked["work_dir_removed"] = None
274
+ checked["work_dir_cleanup_error"] = str(exc)
223
275
  return checked
224
276
 
225
277
 
@@ -243,6 +295,9 @@ def _build_parser() -> StructuredArgumentParser:
243
295
  subparser.add_argument("--chapter", type=int, required=True)
244
296
  if command != "check":
245
297
  subparser.add_argument("--input", type=Path, required=True)
298
+ else:
299
+ subparser.add_argument("--fix-punctuation", action="store_true",
300
+ help="先就地整理正文标点,再做检查")
246
301
  return parser
247
302
 
248
303
 
@@ -286,7 +341,10 @@ def main(argv: Sequence[str] | None = None) -> int:
286
341
  return _wordcount_command(args)
287
342
  try:
288
343
  if args.chapter_command == "check":
344
+ fixed = fix_punctuation(args.project, args.chapter) if args.fix_punctuation else None
289
345
  result = chapter_check(args.project, args.chapter)
346
+ if fixed is not None:
347
+ result["punctuation_fixed"] = fixed
290
348
  else:
291
349
  result = chapter_commit(
292
350
  args.project, args.chapter, args.input,