@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.
- package/README.md +12 -12
- package/lib/client.js +204 -72
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +84 -74
- package/lib/drama/skills/short-drama/SKILL.md +2 -2
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-edit/SKILL.md +27 -29
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +2 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +24 -67
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +27 -56
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +37 -79
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +39 -92
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +12 -16
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +126 -11
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +64 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -3
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +9 -35
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +10 -6
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +58 -21
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +10 -9
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +12 -38
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +4 -7
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -22
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -19
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +2 -2
- package/lib/drama/skills/short-drama-write/SKILL.md +2 -4
- package/lib/index.js +289 -91
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
- package/lib/oh-story/manifest.json +230 -185
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/consistency-checker.md +4 -4
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +5 -5
- package/lib/oh-story/roles/story-explorer.md +11 -11
- package/lib/oh-story/skills/story/SKILL.md +13 -7
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +40 -53
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-import/SKILL.md +77 -81
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +38 -29
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +25 -20
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +4 -13
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/project-files.md +9 -7
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +1 -2
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +86 -115
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -120
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +66 -68
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +139 -18
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +5 -3
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-review/SKILL.md +68 -85
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-review/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-review/references/review-quality.md +4 -10
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +50 -42
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
- package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +6 -147
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +2 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +3 -2
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +16 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +52 -0
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
|
@@ -117,11 +117,26 @@ def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -
|
|
|
117
117
|
require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}")
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
# 事务校验期间收集全部超长字段,一次报完;None 表示逐条立即报错(初始化、状态读取等路径)。
|
|
121
|
+
_LENGTH_ISSUES: list[str] | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def length_hint(label: str, text: str, max_bytes: int) -> str:
|
|
125
|
+
"""把字节上限换算成字数:模型写的是中文,只看得懂「要删几个字」。"""
|
|
126
|
+
size = len(text.encode("utf-8"))
|
|
127
|
+
allowed = max(1, max_bytes * len(text) // size)
|
|
128
|
+
return (f"{label} exceeds {max_bytes} bytes(现 {len(text)} 字,上限约 {allowed} 字,"
|
|
129
|
+
f"至少删 {len(text) - allowed} 字)")
|
|
130
|
+
|
|
131
|
+
|
|
120
132
|
def clean_text(value: object, label: str, *, allow_empty: bool = False, max_bytes: int = 768) -> str:
|
|
121
133
|
require(isinstance(value, str), f"{label} must be a string")
|
|
122
134
|
cleaned = " ".join(value.replace("|", "|").split())
|
|
123
135
|
require(allow_empty or bool(cleaned), f"{label} must not be empty")
|
|
124
|
-
|
|
136
|
+
if len(cleaned.encode("utf-8")) > max_bytes:
|
|
137
|
+
if _LENGTH_ISSUES is None:
|
|
138
|
+
raise TrackingError(length_hint(label, cleaned, max_bytes))
|
|
139
|
+
_LENGTH_ISSUES.append(length_hint(label, cleaned, max_bytes))
|
|
125
140
|
return cleaned
|
|
126
141
|
|
|
127
142
|
|
|
@@ -277,9 +292,15 @@ def archive_retired_tracking_paths(tracking: Path) -> list[str]:
|
|
|
277
292
|
return retired
|
|
278
293
|
|
|
279
294
|
|
|
295
|
+
def _both_blank(position: dict[str, Any]) -> bool:
|
|
296
|
+
return all(isinstance(position.get(key), str) and not position[key].strip() for key in ("story_time", "scene"))
|
|
297
|
+
|
|
298
|
+
|
|
280
299
|
def validate_position(value: object, label: str = "context.position") -> dict[str, Any]:
|
|
281
300
|
position = as_mapping(value, label)
|
|
282
301
|
require_known_keys(position, {"volume", "volume_start_chapter", "story_time", "scene"}, label)
|
|
302
|
+
require(not _both_blank(position),
|
|
303
|
+
f"{label}.story_time and {label}.scene must not be empty: fill where this chapter ends")
|
|
283
304
|
return {
|
|
284
305
|
"volume": safe_file_component(position.get("volume"), f"{label}.volume"),
|
|
285
306
|
"volume_start_chapter": as_int(
|
|
@@ -781,7 +802,13 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
|
|
|
781
802
|
lines.extend(f"- {item}" for item in retired)
|
|
782
803
|
payload = "\n".join(lines) + "\n"
|
|
783
804
|
size = byte_size(payload)
|
|
784
|
-
|
|
805
|
+
if size > DELTA_MAX_BYTES:
|
|
806
|
+
longest = sorted((line for line in lines if line.startswith("- ")), key=len, reverse=True)[:3]
|
|
807
|
+
allowed = DELTA_MAX_BYTES * len(payload) // size
|
|
808
|
+
raise TrackingError(
|
|
809
|
+
f"chapter delta is {size} bytes; hard cap is {DELTA_MAX_BYTES}(逐章记录现 {len(payload)} 字,"
|
|
810
|
+
f"上限约 {allowed} 字,至少压缩 {len(payload) - allowed} 字;最长几项:"
|
|
811
|
+
+ ";".join(f"{line[2:18]}…({len(line) - 2} 字)" for line in longest) + ")")
|
|
785
812
|
return payload
|
|
786
813
|
|
|
787
814
|
|
|
@@ -901,6 +928,17 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
|
|
|
901
928
|
|
|
902
929
|
|
|
903
930
|
def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
931
|
+
global _LENGTH_ISSUES
|
|
932
|
+
_LENGTH_ISSUES = []
|
|
933
|
+
try:
|
|
934
|
+
transaction = _normalize_transaction(project, state, document)
|
|
935
|
+
finally:
|
|
936
|
+
issues, _LENGTH_ISSUES = _LENGTH_ISSUES, None
|
|
937
|
+
require(not issues, f"{len(issues)} 个字段超长,一次改完再提交:" + ";".join(issues))
|
|
938
|
+
return transaction
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
904
942
|
root = as_mapping(document, "transaction")
|
|
905
943
|
require_known_keys(
|
|
906
944
|
root,
|
|
@@ -999,6 +1037,13 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
|
|
|
999
1037
|
"a revision must resubmit every current context item; retire them in an append transaction instead: "
|
|
1000
1038
|
+ ";".join(sorted(dropped)),
|
|
1001
1039
|
)
|
|
1040
|
+
kept = set(next_context["long_term_constraints"]) | set(next_context["continuity_risks"])
|
|
1041
|
+
still_listed = sorted(set(transaction["delta"]["retired_context_items"]) & kept)
|
|
1042
|
+
require(
|
|
1043
|
+
not still_listed,
|
|
1044
|
+
"delta.retired_context_items lists items that are still in context; remove them from context: "
|
|
1045
|
+
+ ";".join(still_listed),
|
|
1046
|
+
)
|
|
1002
1047
|
undeclared = sorted(dropped - set(transaction["delta"]["retired_context_items"]))
|
|
1003
1048
|
require(
|
|
1004
1049
|
not undeclared,
|
|
@@ -1188,6 +1233,126 @@ def check_project(project: Path) -> dict[str, Any]:
|
|
|
1188
1233
|
return state
|
|
1189
1234
|
|
|
1190
1235
|
|
|
1236
|
+
# 事务草稿里各文本字段的字数上限(按全中文估:UTF-8 每字 3 字节),写进提示,免得模型反复试错。
|
|
1237
|
+
DRAFT_LIMITS = {
|
|
1238
|
+
"delta.result": 160, "character_changes[].change": 120, "foreshadow_changes[].summary": 120,
|
|
1239
|
+
"timeline_events[].objective_fact / reader_knowledge": 160, "context.position.story_time / scene": 80,
|
|
1240
|
+
"character_snapshots.*.goal / state": 100, "逐章记录总量": 1024,
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def draft_transaction(project: Path, chapter: int) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
1245
|
+
"""按当前 state 预填一份逐章事务:修订号、模式、章名和整份提交的上下文当前值都填好,
|
|
1246
|
+
调用方只写本章变化。另返回在场核心角色的当前快照,供有变化时整份改写后放进事务。
|
|
1247
|
+
新章(append)的故事时间与场景留空:它们必须是本章结束时的位置,沿用上一章的值能静默提交,
|
|
1248
|
+
所以不预填,上一章的值另行返回作参考。"""
|
|
1249
|
+
state = load_state(project)
|
|
1250
|
+
last = state["last_committed_chapter"]
|
|
1251
|
+
require(1 <= chapter <= last + 1, f"draft chapter must be between 1 and {last + 1}")
|
|
1252
|
+
mode = "append" if chapter == last + 1 else "revision"
|
|
1253
|
+
title = ""
|
|
1254
|
+
for outline in sorted((project / "大纲").glob(f"细纲_第{chapter:03d}章*.md")):
|
|
1255
|
+
match = re.search(rf"^#{{1,4}}\s*第\s*0*{chapter}\s*章\s*[::]\s*(.+?)\s*$",
|
|
1256
|
+
outline.read_text(encoding="utf-8"), re.M)
|
|
1257
|
+
if match:
|
|
1258
|
+
title = match.group(1).strip()
|
|
1259
|
+
break
|
|
1260
|
+
context = json.loads(json.dumps(state["context"], ensure_ascii=False))
|
|
1261
|
+
previous_position = {key: context["position"][key] for key in ("story_time", "scene")}
|
|
1262
|
+
delta: dict[str, Any] = {
|
|
1263
|
+
"result": "", "character_changes": [], "foreshadow_changes": [], "timeline_events": [],
|
|
1264
|
+
"constraints": [], "next_chapter_commitments": [],
|
|
1265
|
+
}
|
|
1266
|
+
if mode == "append":
|
|
1267
|
+
delta.update({"retired_context_items": [], "retired_characters": []})
|
|
1268
|
+
context["position"].update({"story_time": "", "scene": ""})
|
|
1269
|
+
document = {
|
|
1270
|
+
"schema_version": INPUT_SCHEMA_VERSION,
|
|
1271
|
+
"mode": mode,
|
|
1272
|
+
"chapter": chapter,
|
|
1273
|
+
"chapter_title": title,
|
|
1274
|
+
"expected_state_revision": state["state_revision"],
|
|
1275
|
+
"delta": delta,
|
|
1276
|
+
"context": {key: context[key] for key in
|
|
1277
|
+
("position", "long_term_constraints", "active_character_names", "continuity_risks")},
|
|
1278
|
+
"character_snapshots": {},
|
|
1279
|
+
}
|
|
1280
|
+
snapshots = {name: state["characters"][name] for name in context["active_character_names"]
|
|
1281
|
+
if name in state["characters"]}
|
|
1282
|
+
return document, snapshots, previous_position if mode == "append" else {}
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def rebuild_stale_draft(document: dict[str, Any], existing: dict[str, Any], append: bool,
|
|
1286
|
+
current_characters: Any = ()) -> str:
|
|
1287
|
+
"""修订号变了:中间提交过别的事务。以当前状态为底,只自动合并确定安全的部分,其余列给调用方核对。
|
|
1288
|
+
|
|
1289
|
+
新章草稿之间只可能插进修订事务,修订不能删约束与风险条目,所以当前约束与风险覆盖旧底稿:
|
|
1290
|
+
旧草稿多出来的就是本章新增,delta 里声明退役的就是本章删除,可以精确合并。
|
|
1291
|
+
修订草稿之间可能插进新章(新章能退役条目),在场角色名修订也能删,都无法判断谁新:
|
|
1292
|
+
取当前状态,差异列出来;只有本章带了快照的新角色名自动加回。"""
|
|
1293
|
+
def items(value: object) -> list[str]:
|
|
1294
|
+
return [item for item in value if isinstance(item, str)] if isinstance(value, list) else []
|
|
1295
|
+
|
|
1296
|
+
def norm(item: str) -> str:
|
|
1297
|
+
return " ".join(item.replace("|", "|").split())
|
|
1298
|
+
|
|
1299
|
+
notes = ["修订号已变:context 按当前状态重建,本章已填的 delta" + ("、新增与退役的约束、结尾位置" if append else "")
|
|
1300
|
+
+ "已合并"]
|
|
1301
|
+
for key in ("delta", "character_snapshots"):
|
|
1302
|
+
if isinstance(existing.get(key), dict):
|
|
1303
|
+
document[key] = existing[key]
|
|
1304
|
+
if existing.get("chapter_title"):
|
|
1305
|
+
document["chapter_title"] = existing["chapter_title"]
|
|
1306
|
+
old_context = existing.get("context") if isinstance(existing.get("context"), dict) else {}
|
|
1307
|
+
delta = document["delta"] if isinstance(document["delta"], dict) else {}
|
|
1308
|
+
context = document["context"]
|
|
1309
|
+
retired = {norm(item) for item in items(delta.get("retired_context_items"))}
|
|
1310
|
+
left_out: list[str] = []
|
|
1311
|
+
for key in ("long_term_constraints", "continuity_risks"):
|
|
1312
|
+
current = {norm(item) for item in context[key]}
|
|
1313
|
+
extra = []
|
|
1314
|
+
for item in items(old_context.get(key)):
|
|
1315
|
+
if norm(item) not in current:
|
|
1316
|
+
current.add(norm(item))
|
|
1317
|
+
extra.append(item)
|
|
1318
|
+
if append:
|
|
1319
|
+
context[key] = [item for item in context[key] if norm(item) not in retired] + extra
|
|
1320
|
+
else:
|
|
1321
|
+
left_out += extra
|
|
1322
|
+
snapshots = document["character_snapshots"] if isinstance(document["character_snapshots"], dict) else {}
|
|
1323
|
+
names = context["active_character_names"]
|
|
1324
|
+
for name in items(old_context.get("active_character_names")):
|
|
1325
|
+
if name in names:
|
|
1326
|
+
continue
|
|
1327
|
+
if append and name in snapshots:
|
|
1328
|
+
names.append(name)
|
|
1329
|
+
else:
|
|
1330
|
+
left_out.append(f"在场角色 {name}")
|
|
1331
|
+
names[:] = [name for name in names if name not in set(items(delta.get("retired_characters")))]
|
|
1332
|
+
known = set(current_characters)
|
|
1333
|
+
for name in list(snapshots):
|
|
1334
|
+
if name not in known and name not in names:
|
|
1335
|
+
snapshots.pop(name)
|
|
1336
|
+
left_out.append(f"角色快照 {name}")
|
|
1337
|
+
if left_out:
|
|
1338
|
+
notes.append("草稿里有、当前状态没有的条目没有自动带回(可能已被别的提交退役),本章确实需要就加回:"
|
|
1339
|
+
+ ";".join(left_out))
|
|
1340
|
+
old_position = old_context.get("position") if isinstance(old_context.get("position"), dict) else {}
|
|
1341
|
+
position = context["position"]
|
|
1342
|
+
if append:
|
|
1343
|
+
for key in ("story_time", "scene"):
|
|
1344
|
+
if isinstance(old_position.get(key), str) and old_position[key].strip():
|
|
1345
|
+
position[key] = old_position[key]
|
|
1346
|
+
differs = [f"{key}:草稿写的是「{old_position[key]}」,当前状态是「{position[key]}」"
|
|
1347
|
+
for key in ("volume", "volume_start_chapter")
|
|
1348
|
+
if key in old_position and old_position[key] != position[key]]
|
|
1349
|
+
if differs:
|
|
1350
|
+
notes.append("卷信息与当前状态不同,已取当前状态,本章确实换卷就改回(" + ";".join(differs) + ")")
|
|
1351
|
+
if snapshots:
|
|
1352
|
+
notes.append("带过来的角色快照(" + "、".join(snapshots) + ")可能被中间的提交改过,逐个对照 current_snapshots 重核")
|
|
1353
|
+
return ";".join(notes)
|
|
1354
|
+
|
|
1355
|
+
|
|
1191
1356
|
def build_parser() -> argparse.ArgumentParser:
|
|
1192
1357
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1193
1358
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
@@ -1197,12 +1362,55 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1197
1362
|
subparser.add_argument("--input", type=Path, required=True, help="UTF-8 JSON input document")
|
|
1198
1363
|
check_parser = subparsers.add_parser("check")
|
|
1199
1364
|
check_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
|
|
1365
|
+
draft_parser = subparsers.add_parser("draft", help="write a pre-filled chapter transaction to fill in")
|
|
1366
|
+
draft_parser.add_argument("--project", type=Path, required=True, help="book project root containing 追踪/")
|
|
1367
|
+
draft_parser.add_argument("--chapter", type=int, required=True)
|
|
1368
|
+
draft_parser.add_argument("--out", type=Path, help="default: <project>/.story/work/第NNN章/tracking.json")
|
|
1369
|
+
draft_parser.add_argument("--force", action="store_true",
|
|
1370
|
+
help="discard an existing draft instead of only refreshing its revision")
|
|
1200
1371
|
return parser
|
|
1201
1372
|
|
|
1202
1373
|
|
|
1203
1374
|
def main() -> int:
|
|
1204
1375
|
args = build_parser().parse_args()
|
|
1205
1376
|
try:
|
|
1377
|
+
if args.command == "draft":
|
|
1378
|
+
document, snapshots, previous_position = draft_transaction(args.project, args.chapter)
|
|
1379
|
+
out = args.out or args.project / ".story" / "work" / f"第{args.chapter:03d}章" / "tracking.json"
|
|
1380
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
1381
|
+
refreshed = ""
|
|
1382
|
+
if out.exists() and not args.force:
|
|
1383
|
+
# 重跑 draft 不冲掉已经填好的变化。修订号变了说明中间提交过别的事务:context 按当前
|
|
1384
|
+
# 状态重建(沿用旧 context 会把那次变更整份覆盖回去),只带过本章自己填的部分。
|
|
1385
|
+
existing = read_json(out)
|
|
1386
|
+
require(isinstance(existing, dict) and existing.get("chapter") == document["chapter"]
|
|
1387
|
+
and existing.get("mode") == document["mode"],
|
|
1388
|
+
f"{out} already holds a draft for a different chapter or mode; pass --force to replace it")
|
|
1389
|
+
if existing.get("expected_state_revision") == document["expected_state_revision"]:
|
|
1390
|
+
document, refreshed = existing, "已有草稿且修订号未变,原样保留"
|
|
1391
|
+
else:
|
|
1392
|
+
refreshed = rebuild_stale_draft(document, existing, bool(previous_position),
|
|
1393
|
+
load_state(args.project)["characters"])
|
|
1394
|
+
out.write_text(json.dumps(document, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
|
1395
|
+
fill = ("只填 delta 里本章的变化;context 其余字段已是当前值,要撤下的长期约束或连贯性风险从 context 删掉并把原文放进 "
|
|
1396
|
+
"delta.retired_context_items(仅 append);本章有变化的核心角色把下面的当前快照整份改好放进 character_snapshots,"
|
|
1397
|
+
"并在 character_changes 写一句变化。不要从脚本源码或 state 文件里另找格式。")
|
|
1398
|
+
blank = not (document["context"]["position"].get("story_time") or document["context"]["position"].get("scene"))
|
|
1399
|
+
if previous_position and blank:
|
|
1400
|
+
fill = ("context.position 的 story_time 与 scene 留空,填本章结束时的故事时间与场景(上一章结束时见 "
|
|
1401
|
+
"previous_position;换卷时连同 volume 与 volume_start_chapter 一起改)。") + fill
|
|
1402
|
+
payload = {
|
|
1403
|
+
"draft": str(out),
|
|
1404
|
+
"mode": document["mode"],
|
|
1405
|
+
"expected_state_revision": document["expected_state_revision"],
|
|
1406
|
+
"fill": (f"{refreshed};要从头生成加 --force。" if refreshed else "") + fill,
|
|
1407
|
+
"limits_chars": DRAFT_LIMITS,
|
|
1408
|
+
"current_snapshots": snapshots,
|
|
1409
|
+
}
|
|
1410
|
+
if previous_position:
|
|
1411
|
+
payload["previous_position"] = previous_position
|
|
1412
|
+
emit(json.dumps(payload, ensure_ascii=False))
|
|
1413
|
+
return 0
|
|
1206
1414
|
if args.command == "init":
|
|
1207
1415
|
result = initialize(args.project, read_json(args.input))
|
|
1208
1416
|
elif args.command == "commit":
|
|
@@ -172,6 +172,9 @@ def target_from_outline(value: str) -> int:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
|
|
175
|
+
# 改稿流程把原文备份为 第N章_章名_原稿_{日期}.md,与 hooks 一样不把它当本章正文。
|
|
176
|
+
if not outline and "_原稿_" in name:
|
|
177
|
+
return None
|
|
175
178
|
pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\- ].*)?\.md$"
|
|
176
179
|
match = re.match(pattern, name)
|
|
177
180
|
return int(match.group(1)) if match else None
|