@oh-story/dsh 0.1.4 → 0.1.6
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 +56 -12
- package/lib/client.js +2707 -268
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +136 -101
- package/lib/drama/skills/short-drama/SKILL.md +61 -1
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
- package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
- package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
- package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
- package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
- package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
- package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
- package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
- package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
- package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
- package/lib/index.js +865 -33
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/LICENSE +21 -0
- package/lib/novel-to-game/README.md +180 -0
- package/lib/novel-to-game/README_ZH.md +178 -0
- package/lib/novel-to-game/VERSION +1 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
- package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
- package/lib/novel-to-game/manifest.json +508 -0
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
- package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
- package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
- package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
- package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
- package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
- package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
- package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
- package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
- package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
- package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
- package/lib/oh-story/manifest.json +415 -305
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +32 -33
- package/lib/oh-story/roles/story-architect.md +16 -26
- package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +16 -9
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-import/SKILL.md +19 -10
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
- package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
- package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
- package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-review/SKILL.md +26 -17
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
- package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
- package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
- package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
- package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
- package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
- package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
- package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
- package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
- package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
- package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
- package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
- package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
- package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
- package/lib/types/client/drama-production-view.d.ts +34 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -0
- package/lib/types/client/drama-production.d.ts +92 -0
- package/lib/types/client/drama-production.d.ts.map +1 -0
- package/lib/types/client/file-activity.d.ts +4 -2
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-intents.d.ts +9 -0
- package/lib/types/client/production-intents.d.ts.map +1 -0
- package/lib/types/client/production-prompts.d.ts +9 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -0
- package/lib/types/client/production-runtime.d.ts +58 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -0
- package/lib/types/client/video-studio.d.ts +41 -0
- package/lib/types/client/video-studio.d.ts.map +1 -0
- package/lib/types/client/workbench-ui.d.ts +5 -0
- package/lib/types/client/workbench-ui.d.ts.map +1 -0
- package/lib/types/game-verification.d.ts +16 -0
- package/lib/types/game-verification.d.ts.map +1 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/production-intent.d.ts +21 -0
- package/lib/types/production-intent.d.ts.map +1 -0
- package/lib/types/production-tool.d.ts +5 -0
- package/lib/types/production-tool.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts +7 -0
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts +42 -0
- package/lib/types/video-project.d.ts.map +1 -0
- package/lib/types/workspace-request-trust.d.ts +6 -0
- package/lib/types/workspace-request-trust.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts +20 -0
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +282 -0
- package/lib/video-recap/LICENSE +21 -0
- package/lib/video-recap/README.en.md +257 -0
- package/lib/video-recap/README.md +255 -0
- package/lib/video-recap/manifest.json +684 -0
- package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
- package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
- package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
- package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
- package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
- package/lib/video-recap/skills/video-script/SKILL.md +248 -0
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
- package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
- package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
- package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
- package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
- package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
- package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
- package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
- package/package.json +55 -40
- package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
- package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
- package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
- package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
- package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
- package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
- package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
- /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
- /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
|
@@ -3,25 +3,40 @@
|
|
|
3
3
|
|
|
4
4
|
The language model supplies compact semantic JSON. This tool validates and
|
|
5
5
|
merges that input in memory, renders every derived view, then atomically writes
|
|
6
|
-
``_tracking-state.json`` last as the single commit point.
|
|
7
|
-
|
|
6
|
+
``_tracking-state.json`` last as the single commit point. Per-project locking
|
|
7
|
+
serializes concurrent writers before revision and wordcount checks.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import copy
|
|
14
|
+
import importlib.util
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
17
18
|
import stat
|
|
18
19
|
import sys
|
|
19
20
|
import tempfile
|
|
21
|
+
import time
|
|
20
22
|
import unicodedata
|
|
23
|
+
from contextlib import contextmanager
|
|
21
24
|
from pathlib import Path
|
|
22
25
|
from typing import Any
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
_WORDCOUNT_CORE_PATH = Path(__file__).with_name("wordcount_core.py")
|
|
29
|
+
if not _WORDCOUNT_CORE_PATH.is_file(): # pragma: no cover - broken deployment
|
|
30
|
+
raise RuntimeError("TOOL_UNAVAILABLE: wordcount_core.py")
|
|
31
|
+
_WORDCOUNT_CORE_SPEC = importlib.util.spec_from_file_location(
|
|
32
|
+
"story_wordcount_core", _WORDCOUNT_CORE_PATH
|
|
33
|
+
)
|
|
34
|
+
if _WORDCOUNT_CORE_SPEC is None or _WORDCOUNT_CORE_SPEC.loader is None: # pragma: no cover
|
|
35
|
+
raise RuntimeError("unable to load wordcount core")
|
|
36
|
+
wordcount_core = importlib.util.module_from_spec(_WORDCOUNT_CORE_SPEC)
|
|
37
|
+
_WORDCOUNT_CORE_SPEC.loader.exec_module(wordcount_core)
|
|
38
|
+
|
|
39
|
+
|
|
25
40
|
INPUT_SCHEMA_VERSION = 1
|
|
26
41
|
TRACKING_SCHEMA_VERSION = 4
|
|
27
42
|
DELTA_TARGET_BYTES = 1536
|
|
@@ -74,6 +89,13 @@ def require(condition: bool, message: str) -> None:
|
|
|
74
89
|
raise TrackingError(message)
|
|
75
90
|
|
|
76
91
|
|
|
92
|
+
def wordcount_value(function: Any, *args: Any, **kwargs: Any) -> Any:
|
|
93
|
+
try:
|
|
94
|
+
return function(*args, **kwargs)
|
|
95
|
+
except wordcount_core.WordcountError as exc:
|
|
96
|
+
raise TrackingError(str(exc)) from exc
|
|
97
|
+
|
|
98
|
+
|
|
77
99
|
def as_mapping(value: object, label: str) -> dict[str, Any]:
|
|
78
100
|
require(isinstance(value, dict), f"{label} must be a JSON object")
|
|
79
101
|
return value
|
|
@@ -188,6 +210,34 @@ def state_path(project: Path) -> Path:
|
|
|
188
210
|
return tracking_root(project) / "_tracking-state.json"
|
|
189
211
|
|
|
190
212
|
|
|
213
|
+
@contextmanager
|
|
214
|
+
def project_write_lock(project: Path, *, timeout_seconds: float = 10.0):
|
|
215
|
+
tracking = tracking_root(project)
|
|
216
|
+
tracking.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
path = tracking / ".tracking-commit.lock"
|
|
218
|
+
deadline = time.monotonic() + timeout_seconds
|
|
219
|
+
descriptor: int | None = None
|
|
220
|
+
while descriptor is None:
|
|
221
|
+
try:
|
|
222
|
+
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
|
223
|
+
except FileExistsError:
|
|
224
|
+
if time.monotonic() >= deadline:
|
|
225
|
+
raise TrackingError(
|
|
226
|
+
"tracking commit lock is busy or stale; retry, or remove 追踪/.tracking-commit.lock after confirming no commit is running"
|
|
227
|
+
)
|
|
228
|
+
time.sleep(0.05)
|
|
229
|
+
try:
|
|
230
|
+
os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
|
|
231
|
+
os.fsync(descriptor)
|
|
232
|
+
yield
|
|
233
|
+
finally:
|
|
234
|
+
os.close(descriptor)
|
|
235
|
+
try:
|
|
236
|
+
path.unlink()
|
|
237
|
+
except FileNotFoundError:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
191
241
|
def delta_path(tracking: Path, chapter: int) -> Path:
|
|
192
242
|
width = max(3, len(str(chapter)))
|
|
193
243
|
return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
|
|
@@ -735,6 +785,18 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
|
|
|
735
785
|
return payload
|
|
736
786
|
|
|
737
787
|
|
|
788
|
+
def normalize_wordcount_records(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
|
|
789
|
+
records = as_mapping(value, "tracking state.wordcount_records")
|
|
790
|
+
normalized: dict[str, dict[str, Any]] = {}
|
|
791
|
+
for raw_chapter, raw_record in records.items():
|
|
792
|
+
require(isinstance(raw_chapter, str) and re.fullmatch(r"[1-9]\d*", raw_chapter) is not None,
|
|
793
|
+
"wordcount record chapter key is invalid")
|
|
794
|
+
chapter = int(raw_chapter)
|
|
795
|
+
require(chapter <= last_chapter, "wordcount record exceeds last committed chapter")
|
|
796
|
+
normalized[raw_chapter] = wordcount_value(wordcount_core.normalize_wordcount_record, raw_record)
|
|
797
|
+
return normalized
|
|
798
|
+
|
|
799
|
+
|
|
738
800
|
def normalize_state(document: object) -> dict[str, Any]:
|
|
739
801
|
root = as_mapping(document, "tracking state")
|
|
740
802
|
require_known_keys(
|
|
@@ -742,6 +804,7 @@ def normalize_state(document: object) -> dict[str, Any]:
|
|
|
742
804
|
{
|
|
743
805
|
"schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
|
|
744
806
|
"state_revision", "context", "characters", "foreshadow", "timeline",
|
|
807
|
+
"wordcount_records",
|
|
745
808
|
},
|
|
746
809
|
"tracking state",
|
|
747
810
|
)
|
|
@@ -766,16 +829,19 @@ def normalize_state(document: object) -> dict[str, Any]:
|
|
|
766
829
|
if last_chapter == 0:
|
|
767
830
|
require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
|
|
768
831
|
require(not timeline, "a chapter-0 project cannot have established timeline facts")
|
|
832
|
+
state_revision = as_int(root.get("state_revision"), "tracking state.state_revision")
|
|
833
|
+
wordcount_records = normalize_wordcount_records(root.get("wordcount_records", {}), last_chapter)
|
|
769
834
|
return {
|
|
770
835
|
"schema_version": TRACKING_SCHEMA_VERSION,
|
|
771
836
|
"book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
|
|
772
837
|
"last_committed_chapter": last_chapter,
|
|
773
838
|
"imported_through_chapter": imported_through,
|
|
774
|
-
"state_revision":
|
|
839
|
+
"state_revision": state_revision,
|
|
775
840
|
"context": context,
|
|
776
841
|
"characters": characters,
|
|
777
842
|
"foreshadow": foreshadow,
|
|
778
843
|
"timeline": timeline,
|
|
844
|
+
"wordcount_records": wordcount_records,
|
|
779
845
|
}
|
|
780
846
|
|
|
781
847
|
|
|
@@ -789,7 +855,10 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
|
|
|
789
855
|
root = as_mapping(document, "init input")
|
|
790
856
|
require_known_keys(
|
|
791
857
|
root,
|
|
792
|
-
{
|
|
858
|
+
{
|
|
859
|
+
"schema_version", "book_title", "last_chapter", "context", "character_snapshots",
|
|
860
|
+
"foreshadow", "timeline_events",
|
|
861
|
+
},
|
|
793
862
|
"init input",
|
|
794
863
|
)
|
|
795
864
|
require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
|
|
@@ -826,17 +895,18 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
|
|
|
826
895
|
"characters": snapshots,
|
|
827
896
|
"foreshadow": foreshadow,
|
|
828
897
|
"timeline": timeline,
|
|
898
|
+
"wordcount_records": {},
|
|
829
899
|
}
|
|
830
900
|
)
|
|
831
901
|
|
|
832
902
|
|
|
833
|
-
def normalize_transaction(state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
903
|
+
def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
834
904
|
root = as_mapping(document, "transaction")
|
|
835
905
|
require_known_keys(
|
|
836
906
|
root,
|
|
837
907
|
{
|
|
838
908
|
"schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
|
|
839
|
-
"delta", "context", "character_snapshots",
|
|
909
|
+
"delta", "context", "character_snapshots", "wordcount",
|
|
840
910
|
},
|
|
841
911
|
"transaction",
|
|
842
912
|
)
|
|
@@ -845,6 +915,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
845
915
|
require(mode in {"append", "revision"}, "mode must be append or revision")
|
|
846
916
|
chapter = as_int(root.get("chapter"), "chapter", minimum=1)
|
|
847
917
|
expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
|
|
918
|
+
wordcount_input = root.get("wordcount")
|
|
848
919
|
require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
|
|
849
920
|
last = state["last_committed_chapter"]
|
|
850
921
|
if mode == "append":
|
|
@@ -864,6 +935,11 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
864
935
|
snapshots=snapshots,
|
|
865
936
|
existing_core_names=existing_names,
|
|
866
937
|
)
|
|
938
|
+
wordcount = None
|
|
939
|
+
if wordcount_input is not None:
|
|
940
|
+
wordcount = wordcount_value(
|
|
941
|
+
wordcount_core.validate_current_wordcount_record, project, chapter, wordcount_input
|
|
942
|
+
)
|
|
867
943
|
return {
|
|
868
944
|
"mode": mode,
|
|
869
945
|
"chapter": chapter,
|
|
@@ -871,6 +947,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
871
947
|
"delta": delta,
|
|
872
948
|
"context": context,
|
|
873
949
|
"snapshots": snapshots,
|
|
950
|
+
"wordcount": wordcount,
|
|
874
951
|
}
|
|
875
952
|
|
|
876
953
|
|
|
@@ -891,6 +968,8 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
|
|
|
891
968
|
next_state["last_committed_chapter"] = chapter
|
|
892
969
|
next_state["state_revision"] += 1
|
|
893
970
|
next_state["characters"].update(transaction["snapshots"])
|
|
971
|
+
if transaction["wordcount"] is not None:
|
|
972
|
+
next_state["wordcount_records"][str(chapter)] = transaction["wordcount"]
|
|
894
973
|
|
|
895
974
|
next_context = transaction["context"]
|
|
896
975
|
# 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
|
|
@@ -1013,7 +1092,7 @@ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
|
|
|
1013
1092
|
)
|
|
1014
1093
|
|
|
1015
1094
|
|
|
1016
|
-
def
|
|
1095
|
+
def _initialize_locked(project: Path, document: object) -> dict[str, Any]:
|
|
1017
1096
|
tracking = tracking_root(project)
|
|
1018
1097
|
require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
|
|
1019
1098
|
state = normalize_initial_document(document)
|
|
@@ -1036,11 +1115,16 @@ def initialize(project: Path, document: object) -> dict[str, Any]:
|
|
|
1036
1115
|
return state
|
|
1037
1116
|
|
|
1038
1117
|
|
|
1039
|
-
def
|
|
1118
|
+
def initialize(project: Path, document: object) -> dict[str, Any]:
|
|
1119
|
+
with project_write_lock(project):
|
|
1120
|
+
return _initialize_locked(project, document)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _apply_transaction_locked(project: Path, document: object) -> dict[str, Any]:
|
|
1040
1124
|
tracking = tracking_root(project)
|
|
1041
1125
|
require_no_retired_tracking_paths(tracking)
|
|
1042
1126
|
state = load_state(project)
|
|
1043
|
-
transaction = normalize_transaction(state, document)
|
|
1127
|
+
transaction = normalize_transaction(project, state, document)
|
|
1044
1128
|
next_state = merge_transaction(state, transaction)
|
|
1045
1129
|
|
|
1046
1130
|
delta_payload = render_delta(
|
|
@@ -1067,6 +1151,11 @@ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
|
|
|
1067
1151
|
return next_state
|
|
1068
1152
|
|
|
1069
1153
|
|
|
1154
|
+
def apply_transaction(project: Path, document: object) -> dict[str, Any]:
|
|
1155
|
+
with project_write_lock(project):
|
|
1156
|
+
return _apply_transaction_locked(project, document)
|
|
1157
|
+
|
|
1158
|
+
|
|
1070
1159
|
def check_project(project: Path) -> dict[str, Any]:
|
|
1071
1160
|
tracking = tracking_root(project)
|
|
1072
1161
|
require_no_retired_tracking_paths(tracking)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small deterministic wordcount core shared by storyctl and tracking."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
WORDCOUNT_SCHEMA = "story-wordcount-result/v1"
|
|
13
|
+
MEASUREMENT_SCHEMA = "story-wordcount-measurement/v1"
|
|
14
|
+
CHECKPOINT_SCHEMA = "story-wordcount-checkpoint/v1"
|
|
15
|
+
METRIC = "visible_chars_v1"
|
|
16
|
+
RESOLUTIONS = frozenset({"within_user_band", "accepted_current_length"})
|
|
17
|
+
|
|
18
|
+
_WHITE_SPACE_CODEPOINTS = frozenset(
|
|
19
|
+
[*range(0x0009, 0x000E)]
|
|
20
|
+
+ [
|
|
21
|
+
0x0020, 0x0085, 0x00A0, 0x1680, *range(0x2000, 0x200B),
|
|
22
|
+
0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
_FRONTMATTER_KEY_RE = re.compile(r"^[A-Za-z_\u3400-\u9FFF][^:\n]{0,80}:[ \t]*(?:.*)$")
|
|
26
|
+
_LEADING_BLANK_RE = re.compile(r"^[\u0009\u0020\u3000]*$")
|
|
27
|
+
_ATX_HEADING_RE = re.compile(r"^[\u0009\u0020]{0,3}#{1,6}[\u0009\u0020]+\S")
|
|
28
|
+
_POSITIVE_INTEGER_RE = re.compile(r"^[1-9]\d*$")
|
|
29
|
+
_TARGET_LINE_RE = re.compile(r"^[ \t>*-]*字数目标[ \t]*[::][ \t]*([1-9]\d*)[ \t]*(?:字)?[ \t]*$", re.MULTILINE)
|
|
30
|
+
_METRIC_LINE_RE = re.compile(r"^[ \t>*-]*字数口径[ \t]*[::][ \t]*([A-Za-z0-9_-]+)[ \t]*$", re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WordcountError(ValueError):
|
|
34
|
+
"""Expected deterministic wordcount contract failure."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def require(condition: bool, message: str) -> None:
|
|
38
|
+
if not condition:
|
|
39
|
+
raise WordcountError(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_newlines(value: str) -> str:
|
|
43
|
+
return value.replace("\r\n", "\n").replace("\r", "\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def strip_recognizable_frontmatter(value: str) -> str:
|
|
47
|
+
lines = value.split("\n")
|
|
48
|
+
if not lines or lines[0] != "---":
|
|
49
|
+
return value
|
|
50
|
+
closing = next((index for index in range(1, min(len(lines), 201)) if lines[index] in {"---", "..."}), -1)
|
|
51
|
+
if closing < 2 or not any(_FRONTMATTER_KEY_RE.match(line) for line in lines[1:closing]):
|
|
52
|
+
return value
|
|
53
|
+
return "\n".join(lines[closing + 1 :])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def visible_body(value: str) -> str:
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
raise TypeError("body must be a string")
|
|
59
|
+
text = normalize_newlines(value)
|
|
60
|
+
if text.startswith("\ufeff"):
|
|
61
|
+
text = text[1:]
|
|
62
|
+
lines = strip_recognizable_frontmatter(text).split("\n")
|
|
63
|
+
while lines and _LEADING_BLANK_RE.match(lines[0]):
|
|
64
|
+
lines.pop(0)
|
|
65
|
+
if lines and _ATX_HEADING_RE.match(lines[0]):
|
|
66
|
+
lines.pop(0)
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def count_visible_chars(value: str) -> int:
|
|
71
|
+
return sum(ord(character) not in _WHITE_SPACE_CODEPOINTS for character in visible_body(value))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_target(value: Any) -> int:
|
|
75
|
+
raw = str(value if value is not None else "")
|
|
76
|
+
if not _POSITIVE_INTEGER_RE.fullmatch(raw):
|
|
77
|
+
raise WordcountError("target must be a positive integer")
|
|
78
|
+
target = int(raw)
|
|
79
|
+
require(target <= 9_007_199_254_740_991, "target exceeds Number.MAX_SAFE_INTEGER")
|
|
80
|
+
return target
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_wordcount_bands(value: Any) -> dict[str, dict[str, int]]:
|
|
84
|
+
target = parse_target(value)
|
|
85
|
+
return {
|
|
86
|
+
"internal": {"min": (target * 88 + 99) // 100, "max": target * 112 // 100},
|
|
87
|
+
"user": {"min": (target * 85 + 99) // 100, "max": target * 115 // 100},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def invalid_wordcount_result(
|
|
92
|
+
reason: str, *, chapter: Any = None, case_id: Any = None,
|
|
93
|
+
target: int | None = None, actual: int | None = None,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
97
|
+
"target": target, "actual": actual, "internal_band": None, "user_band": None,
|
|
98
|
+
"signed_error_pct": None, "absolute_error_pct": None,
|
|
99
|
+
"status": "invalid", "invalid_reason": reason,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def measure_wordcount(value: str, *, chapter: Any = None, case_id: Any = None) -> dict[str, Any]:
|
|
104
|
+
try:
|
|
105
|
+
actual = count_visible_chars(value)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return {
|
|
108
|
+
"schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
|
|
109
|
+
"case_id": case_id, "actual": None, "status": "invalid", "invalid_reason": "INVALID_BODY",
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
"schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
|
|
113
|
+
"case_id": case_id, "actual": actual, "status": "measured", "invalid_reason": None,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def evaluate_wordcount(
|
|
118
|
+
value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
try:
|
|
121
|
+
target = parse_target(target_value)
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return invalid_wordcount_result("INVALID_TARGET", chapter=chapter, case_id=case_id)
|
|
124
|
+
try:
|
|
125
|
+
actual = count_visible_chars(value)
|
|
126
|
+
except (TypeError, ValueError):
|
|
127
|
+
return invalid_wordcount_result("INVALID_BODY", chapter=chapter, case_id=case_id, target=target)
|
|
128
|
+
if actual == 0:
|
|
129
|
+
return invalid_wordcount_result("EMPTY_BODY", chapter=chapter, case_id=case_id, target=target, actual=actual)
|
|
130
|
+
bands = compute_wordcount_bands(target)
|
|
131
|
+
internal_pass = bands["internal"]["min"] <= actual <= bands["internal"]["max"]
|
|
132
|
+
user_pass = bands["user"]["min"] <= actual <= bands["user"]["max"]
|
|
133
|
+
status = "internal_pass" if internal_pass else (
|
|
134
|
+
"borderline" if user_pass else ("under" if actual < bands["user"]["min"] else "over")
|
|
135
|
+
)
|
|
136
|
+
signed_error = (actual - target) / target
|
|
137
|
+
return {
|
|
138
|
+
"schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
139
|
+
"target": target, "actual": actual,
|
|
140
|
+
"internal_band": {**bands["internal"], "status": "pass" if internal_pass else "fail"},
|
|
141
|
+
"user_band": {**bands["user"], "status": "pass" if user_pass else "fail"},
|
|
142
|
+
"signed_error_pct": signed_error, "absolute_error_pct": abs(signed_error),
|
|
143
|
+
"status": status, "invalid_reason": None,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def checkpoint_wordcount(
|
|
148
|
+
value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
target = parse_target(target_value)
|
|
151
|
+
actual = count_visible_chars(value)
|
|
152
|
+
user = compute_wordcount_bands(target)["user"]
|
|
153
|
+
return {
|
|
154
|
+
"schema": CHECKPOINT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
155
|
+
"target": target, "actual": actual, "user_band": user,
|
|
156
|
+
"remaining_user_range": {
|
|
157
|
+
"min": max(0, user["min"] - actual),
|
|
158
|
+
"max": max(0, user["max"] - actual),
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def target_from_outline(value: str) -> int:
|
|
164
|
+
text = normalize_newlines(value)
|
|
165
|
+
if text.startswith("\ufeff"):
|
|
166
|
+
text = text[1:]
|
|
167
|
+
targets = list(dict.fromkeys(_TARGET_LINE_RE.findall(text)))
|
|
168
|
+
metrics = list(dict.fromkeys(_METRIC_LINE_RE.findall(text)))
|
|
169
|
+
require(len(targets) == 1, "字数目标 must appear exactly once with one value")
|
|
170
|
+
require(metrics == [METRIC], f"字数口径 must appear exactly once as {METRIC}")
|
|
171
|
+
return parse_target(targets[0])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
|
|
175
|
+
pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\- ].*)?\.md$"
|
|
176
|
+
match = re.match(pattern, name)
|
|
177
|
+
return int(match.group(1)) if match else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def find_chapter_file(directory: Path, chapter: int, *, outline: bool) -> Path:
|
|
181
|
+
require(isinstance(chapter, int) and not isinstance(chapter, bool) and chapter >= 1, "chapter must be >= 1")
|
|
182
|
+
require(directory.is_dir(), f"chapter directory is missing: {directory}")
|
|
183
|
+
matches = sorted(
|
|
184
|
+
path for path in directory.iterdir()
|
|
185
|
+
if path.is_file() and _chapter_number_from_name(path.name, outline=outline) == chapter
|
|
186
|
+
)
|
|
187
|
+
label = "outline" if outline else "body"
|
|
188
|
+
require(len(matches) == 1, f"chapter {chapter} must have exactly one {label} file")
|
|
189
|
+
return matches[0]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_project_wordcount_record(project: Path, chapter: int, *, resolution: str) -> dict[str, Any]:
|
|
193
|
+
require(resolution in RESOLUTIONS, f"unsupported wordcount resolution: {resolution}")
|
|
194
|
+
root = project.resolve()
|
|
195
|
+
outline_path = find_chapter_file(root / "大纲", chapter, outline=True)
|
|
196
|
+
body_path = find_chapter_file(root / "正文", chapter, outline=False)
|
|
197
|
+
try:
|
|
198
|
+
target = target_from_outline(outline_path.read_text(encoding="utf-8"))
|
|
199
|
+
body_bytes = body_path.read_bytes()
|
|
200
|
+
body = body_bytes.decode("utf-8")
|
|
201
|
+
except (OSError, UnicodeError) as exc:
|
|
202
|
+
raise WordcountError(f"unable to read chapter files: {exc}") from exc
|
|
203
|
+
result = evaluate_wordcount(body, target, chapter=chapter)
|
|
204
|
+
require(result["status"] != "invalid", f"invalid chapter wordcount: {result['invalid_reason']}")
|
|
205
|
+
in_user_band = result["status"] in {"internal_pass", "borderline"}
|
|
206
|
+
require(
|
|
207
|
+
(resolution == "within_user_band" and in_user_band)
|
|
208
|
+
or (resolution == "accepted_current_length" and not in_user_band),
|
|
209
|
+
"wordcount resolution does not match the current length",
|
|
210
|
+
)
|
|
211
|
+
return {
|
|
212
|
+
"metric": METRIC,
|
|
213
|
+
"target": result["target"],
|
|
214
|
+
"actual": result["actual"],
|
|
215
|
+
"status": result["status"],
|
|
216
|
+
"resolution": resolution,
|
|
217
|
+
"body_sha256": hashlib.sha256(body_bytes).hexdigest(),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def normalize_wordcount_record(value: object) -> dict[str, Any]:
|
|
222
|
+
require(isinstance(value, dict), "wordcount record must be an object")
|
|
223
|
+
require(
|
|
224
|
+
set(value) == {"metric", "target", "actual", "status", "resolution", "body_sha256"},
|
|
225
|
+
"wordcount record fields are invalid",
|
|
226
|
+
)
|
|
227
|
+
require(value.get("metric") == METRIC, "wordcount metric is unsupported")
|
|
228
|
+
target = parse_target(value.get("target"))
|
|
229
|
+
actual = value.get("actual")
|
|
230
|
+
require(isinstance(actual, int) and not isinstance(actual, bool) and actual >= 1, "wordcount actual is invalid")
|
|
231
|
+
expected = evaluate_wordcount("字" * actual, target)
|
|
232
|
+
require(value.get("status") == expected["status"], "wordcount status does not match target and actual")
|
|
233
|
+
resolution = value.get("resolution")
|
|
234
|
+
require(resolution in RESOLUTIONS, "wordcount resolution is unsupported")
|
|
235
|
+
in_user_band = expected["status"] in {"internal_pass", "borderline"}
|
|
236
|
+
require((resolution == "within_user_band") == in_user_band, "wordcount resolution does not match status")
|
|
237
|
+
digest = value.get("body_sha256")
|
|
238
|
+
require(isinstance(digest, str) and re.fullmatch(r"[0-9a-f]{64}", digest) is not None, "body_sha256 is invalid")
|
|
239
|
+
return dict(value)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_current_wordcount_record(project: Path, chapter: int, value: object) -> dict[str, Any]:
|
|
243
|
+
record = normalize_wordcount_record(value)
|
|
244
|
+
current = build_project_wordcount_record(project, chapter, resolution=record["resolution"])
|
|
245
|
+
require(record == current, "wordcount record is stale for the current body or target")
|
|
246
|
+
return record
|
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
name: story-review
|
|
3
3
|
version: 1.1.1
|
|
4
4
|
description: "多视角对抗式审查。full/lean 模式在已部署 reviewer agents 时并行 spawn;缺失/异常 agents 或 spawn 失败时自动降级 solo,参考文件不可读时使用内置 rubric fallback。触发方式:/story-review、/审查、「审查一下」「帮我审一下」。"
|
|
5
|
-
metadata: {"openclaw":{"source":"https://github.com/
|
|
5
|
+
metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
|
|
6
6
|
---
|
|
7
7
|
# story-review:多视角对抗式审查
|
|
8
8
|
|
|
9
|
-
> Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version:
|
|
9
|
+
> Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 29` 不一致时(标记缺失、字段缺失/非整数、小于或大于 29)**照常按文件存在性检查并 spawn**,但只检查当前运行时的 canonical 目录;同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 29)` 并提示重新运行 `/story-setup` 后新开会话;大于 29 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
|
|
10
10
|
|
|
11
11
|
你是审查协调器。你的职责是找出小说文本中的结构、角色、文字、设定问题,并给出可执行修改建议。
|
|
12
12
|
|
|
13
13
|
**执行铁律:审查是找问题,不是验证正确性。**
|
|
14
14
|
|
|
15
|
+
## 作者习惯边界
|
|
16
|
+
|
|
17
|
+
若作者记忆 state 已存在,审查前用 `scripts/author_memory_commit.py query` 获取本次相关 active 条目(总输出 ≤2KB)。它们只能帮助解释意图和组织报告,不能降低 rubric 严重度、把事实冲突判为无问题或跳过平台门禁;当前请求仍优先。完整规则见 [references/author-memory.md](references/author-memory.md)。
|
|
18
|
+
|
|
19
|
+
用户对报告格式或协作方式作出稳定声明时,在本轮审查完成后用 `record` 记录并回传回执;重复修正/推断先待确认,一次性要求不记录。审查发现、工具告警和助手建议本身绝不自动学习。
|
|
20
|
+
|
|
15
21
|
---
|
|
16
22
|
|
|
17
23
|
## Review Mode 选择
|
|
@@ -28,17 +34,18 @@ metadata: {"openclaw":{"source":"https://github.com/worldwonderer/oh-story-claud
|
|
|
28
34
|
1. **确定请求模式**:解析用户输入中的 `full`、`lean`、`solo`;未指定时目标模式为 `full`。
|
|
29
35
|
2. **确认是否允许 spawn**:如果当前已经在子代理/Agent 内执行,不再递归 spawn,直接降级为 `solo`。
|
|
30
36
|
3. **识别 ZCode 能力边界**:如果当前运行于 ZCode 且项目使用 `.zcode/`,ZCode 3.3.4 不执行项目/plugin custom agents;不要因为磁盘上存在其他端的 agent 文件就尝试同名 spawn,直接降级 `solo` 并报告 `Fallback: project custom agents unavailable -> solo`。
|
|
31
|
-
4. **检查核心 Agent
|
|
32
|
-
-
|
|
33
|
-
- full
|
|
34
|
-
- lean
|
|
37
|
+
4. **检查核心 Agent 部署状态**(只检查当前运行时的 canonical 目录,不因其他端文件存在而误判):
|
|
38
|
+
- Claude Code 检查 `.claude/agents/`,OpenCode 检查 `.opencode/agents/`,Codex 检查 `.codex/agents/`,Antigravity 检查 `.agents/agents/`
|
|
39
|
+
- full 必需 agent:`story-architect`、`character-designer`、`narrative-writer`、`consistency-checker`
|
|
40
|
+
- lean 必需 agent:`story-architect`、`consistency-checker`
|
|
35
41
|
- 对每个必需 Agent 文件:
|
|
36
42
|
- **Claude Code agent(`.claude/agents/`)**:读取 frontmatter,确认 `name:` 与 subagent_type 完全一致;frontmatter 缺失、不可解析或 name 不匹配时视为 malformed agent。
|
|
37
43
|
- **OpenCode agent(`.opencode/agents/`)**:文件名即 agent 名(OpenCode 不要求在 frontmatter 中写 `name:`),读取 frontmatter 确认 `mode: subagent` 和 `permission` 字段存在且可解析即可;frontmatter 缺失或不可解析视为 malformed。
|
|
38
44
|
- **Codex agent(`.codex/agents/`)**:文件名为 `{agent}.toml`,TOML 必须可解析,且包含 `name`、`description`、`developer_instructions`;`name` 必须与目标 agent 完全一致。
|
|
45
|
+
- **Antigravity agent(`.agents/agents/`)**:路径为 `.agents/agents/agent-name/agent.md`(`agent-name` 为目标 agent 名),frontmatter 必须可解析,且 `name` 与目标 agent 一致、`mainAgent: false`、`subagent: true`、`tools` 非空;缺失或不匹配视为 malformed。
|
|
39
46
|
- 如果目标模式所需任一文件缺失或 malformed,**不要尝试 spawn 缺失/异常 Agent**;自动降级为 `solo`,并在报告开头写明:`Fallback: missing agents -> solo` 或 `Fallback: malformed agents -> solo`,列出问题文件,建议用户运行 `/story-setup`。
|
|
40
|
-
5. **确认 Agent/
|
|
41
|
-
6. **运行时失败降级**:如果任何 Agent spawn 返回失败、`subagent_type` / `agent_type` 不可用、frontmatter/TOML 运行时解析失败或子 Agent 无法启动,停止继续 spawn,改用 `solo` 重新审查,并报告 `Fallback: spawn failed -> solo` 与失败的
|
|
47
|
+
5. **确认 Agent 工具可用**:Claude/OpenCode/Codex 需要当前运行时的子 Agent/Task 调用能力,Antigravity 需要 `invoke_subagent`;不可用时直接降级为 `solo`,报告 `Fallback: agent tool unavailable -> solo`。
|
|
48
|
+
6. **运行时失败降级**:如果任何 Agent spawn 返回失败、`subagent_type` / `agent_type` / `TypeName` 不可用、frontmatter/TOML 运行时解析失败或子 Agent 无法启动,停止继续 spawn,改用 `solo` 重新审查,并报告 `Fallback: spawn failed -> solo` 与失败的 agent 名;不要把部分成功的 Agent 结果当成 full/lean 结论。
|
|
42
49
|
7. **确定实际模式**:报告中必须同时列出 `Requested Mode` 与 `Effective Mode`。
|
|
43
50
|
|
|
44
51
|
---
|
|
@@ -67,16 +74,16 @@ Rubric Source: file | embedded fallback
|
|
|
67
74
|
3. `{项目根}/.codex/skills/{规范路径}`(Codex 项目内安装)
|
|
68
75
|
4. `{项目根}/.zcode/skills/{规范路径}`(ZCode 项目内安装)
|
|
69
76
|
5. `{项目根}/skills/{规范路径}`(OpenClaw / Reasonix / generic 部署,也是本仓库开发环境)
|
|
70
|
-
6. `{项目根}/.agents/skills/{规范路径}`(
|
|
77
|
+
6. `{项目根}/.agents/skills/{规范路径}`(Antigravity 项目内真实 skill root;Codex / Reasonix 也可能扫描此目录或其 symlink)
|
|
71
78
|
7. 当前运行时加载本 skill 的目录,或其可访问的全局 skill 搜索路径中同名 `{skill-name}/...` 目录
|
|
72
79
|
|
|
73
|
-
> 靠前几层不存在是正常的,不是部署损坏。`/story-setup`
|
|
80
|
+
> 靠前几层不存在是正常的,不是部署损坏。`/story-setup` 会为 Antigravity 把 13 个 skill 真实复制到 `.agents/skills/`,为 ZCode 复制到 `.zcode/skills/`,并为 OpenClaw / Reasonix / generic 复制到 `skills/`。Codex 项目部署不复制 skill 本体,本 skill 由 Codex 从 skill root 加载,references 通常命中第 6 或第 7 层。不要手工把 `references/` 复制进 `.codex/skills/`——手工副本不受 story-setup 管理,升级后会静默变旧。
|
|
74
81
|
|
|
75
82
|
规范路径如下;禁止只写裸文件名,禁止跨 skill 误读其他 skill 的 references:
|
|
76
83
|
|
|
77
84
|
| 用途 | 规范路径 |
|
|
78
85
|
|---|---|
|
|
79
|
-
| 通用质量清单 | `story-review/references/quality
|
|
86
|
+
| 通用质量清单 | `story-review/references/review-quality.md` |
|
|
80
87
|
| 通用内容评分 rubric | `story-review/references/quality-rubric.md` |
|
|
81
88
|
| 去 AI 味方法 | `story-review/references/anti-ai-writing.md` |
|
|
82
89
|
| 剧情循环/高潮公式 | `story-review/references/plot-core-methods.md` |
|
|
@@ -86,6 +93,8 @@ Rubric Source: file | embedded fallback
|
|
|
86
93
|
| 平台 rubric | `story-review/references/rubrics/{fanqie,qidian,zhihu}.md` |
|
|
87
94
|
| 标点预检脚本 | `story-review/scripts/normalize-punctuation.js` |
|
|
88
95
|
| AI句式预检脚本 | `story-review/scripts/check-ai-patterns.js` |
|
|
96
|
+
| 作者习惯协议 | `story-review/references/author-memory.md` |
|
|
97
|
+
| 作者习惯事务脚本 | `story-review/scripts/author_memory_commit.py` |
|
|
89
98
|
|
|
90
99
|
### 内置审查基准包(路径不可读时必用)
|
|
91
100
|
|
|
@@ -174,7 +183,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
174
183
|
- 这三个预检脚本只读;`story-review` **不修改正文、设定或大纲文件**,需要自动修复正文时建议转 `/story-deslop`。full / lean 模式只有下方「追踪文件维护」允许修改 `追踪/`;分批审查的所有模式都可按上方契约写 **.story-review/state.md**,solo 除该状态外不写项目内容。
|
|
175
184
|
- 默认 `--quote-mode keep`,不把知乎盐言短篇的 `「」` 当作问题;只有项目明确指定引号风格时才检查对应转换建议。
|
|
176
185
|
|
|
177
|
-
**story-explorer 预查询(可选)**。仅当 `Effective Mode` 仍为 `full`/`lean`、当前允许 spawn
|
|
186
|
+
**story-explorer 预查询(可选)**。仅当 `Effective Mode` 仍为 `full`/`lean`、当前允许 spawn 且当前运行时的 Agent 工具可用时,才可在对应 canonical agent 目录下确认 `story-explorer` 已部署并 spawn;Antigravity 检查 `.agents/agents/story-explorer/agent.md`,用 `invoke_subagent` + `TypeName: "story-explorer"`。`solo` 或子代理递归保护场景下不得 spawn,只能直接读取/检索。Prompt 示例:
|
|
178
187
|
|
|
179
188
|
```text
|
|
180
189
|
项目目录:{dir}
|
|
@@ -209,7 +218,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
209
218
|
|
|
210
219
|
## Phase 2:并行 Spawn Agent(full/lean 模式)
|
|
211
220
|
|
|
212
|
-
|
|
221
|
+
使用当前运行时的 Agent 工具并行调用(Codex 原生子代理使用 `agent_type`,Claude Code 兼容面使用 `subagent_type`,Antigravity 使用 `invoke_subagent` + 同名 `TypeName`;实际字段以当前 CLI 暴露的工具为准)。每个 Agent 不继承父对话上下文,prompt 必须自包含项目路径、审查范围、文件路径、必要摘录、审查基准包摘要、Rubric Source 和统一 Findings Schema。
|
|
213
222
|
|
|
214
223
|
**调用规则**:执行 Phase 0 后,只有实际模式仍是 full/lean 时才 spawn。不要 spawn 缺失 Agent。
|
|
215
224
|
|
|
@@ -226,7 +235,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
226
235
|
Rubric Source: file | embedded fallback
|
|
227
236
|
相关文件路径:{设定/大纲/细纲文件路径}
|
|
228
237
|
继承的开放项(分批审查必填,无则写「无」):{从 追踪/伏笔.md 提取的、预计回收章 ≤ 本批末章的已埋未回收钩子,连同上一批未解决 findings 摘要}
|
|
229
|
-
可选补充参考:本 Skill 的 `story-review/references/quality
|
|
238
|
+
可选补充参考:本 Skill 的 `story-review/references/review-quality.md`、`story-review/references/plot-core-methods.md`;若不可读,不影响审查。
|
|
230
239
|
检查项:
|
|
231
240
|
1. 这一章是否推进了故事主题?
|
|
232
241
|
2. 大纲结构是否完整(钩子/爽点/悬念)?
|
|
@@ -289,7 +298,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
289
298
|
审查基准包摘要:{Phase 1 形成的 rubric / fallback 摘要,必须内联}
|
|
290
299
|
Rubric Source: file | embedded fallback
|
|
291
300
|
AI 味 / 禁用词摘要:{从 anti-ai-writing、banned-words 或内置 fallback 提取,必须内联}
|
|
292
|
-
可选补充参考:本 Skill 的 `story-review/references/anti-ai-writing.md`、`story-review/references/banned-words.md`、`story-review/references/quality
|
|
301
|
+
可选补充参考:本 Skill 的 `story-review/references/anti-ai-writing.md`、`story-review/references/banned-words.md`、`story-review/references/review-quality.md`;若不可读,不影响审查。
|
|
293
302
|
检查项:
|
|
294
303
|
1. 是否存在禁用词/套话/陈词滥调,或“像/好像/仿佛/如同”式比喻成片堆叠?
|
|
295
304
|
2. 是否出现 AI 写作指纹、8 种 AI 写作模式(含模式 8 解释腔/上帝视角/安排感)或章末总结体?
|
|
@@ -321,7 +330,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
321
330
|
继承的开放项(分批审查必填,无则写「无」):{从 追踪/伏笔.md 提取的、预计回收章 ≤ 本批末章的已埋未回收伏笔,连同上一批未解决 findings 摘要}
|
|
322
331
|
审查基准包摘要:{Phase 1 形成的 rubric / fallback 摘要,必须内联}
|
|
323
332
|
Rubric Source: file | embedded fallback
|
|
324
|
-
可选补充参考:本 Skill 的 `story-review/references/quality
|
|
333
|
+
可选补充参考:本 Skill 的 `story-review/references/review-quality.md`;若不可读,不影响事实冲突扫描。
|
|
325
334
|
检查项:
|
|
326
335
|
1. 角色属性是否前后一致?
|
|
327
336
|
2. 世界规则是否被违反?
|
|
@@ -344,7 +353,7 @@ full/lean 模式下,主会话必须把“审查基准包摘要”直接写进
|
|
|
344
353
|
|
|
345
354
|
1. 收集实际执行的 reviewer VERDICT 和 FINDINGS。
|
|
346
355
|
2. 合并去重:按 `severity` 排序(S1 > S2 > S3 > S4),同级内按影响范围排序。
|
|
347
|
-
3. **可选事实核查**:如果审查内容涉及需要验证的外部事实(历史年代、地理方位、职业细节等),只有在 `Effective Mode` 仍为 `full`/`lean`、当前不是子 Agent
|
|
356
|
+
3. **可选事实核查**:如果审查内容涉及需要验证的外部事实(历史年代、地理方位、职业细节等),只有在 `Effective Mode` 仍为 `full`/`lean`、当前不是子 Agent、当前运行时的 Agent 工具可用且对应 canonical agent 目录下的 `story-researcher` 已部署时,才可额外 spawn;Antigravity 检查 `.agents/agents/story-researcher/agent.md`,用 `invoke_subagent` + `TypeName: "story-researcher"`。`solo`、missing/malformed/stale/spawn failed 降级或子代理递归保护场景下不得 spawn,只能在报告中标记“需人工事实核查”。
|
|
348
357
|
4. **分歧呈现**:如果 reviewer 间有冲突意见,明确呈现分歧让用户裁决;不要自动妥协。
|
|
349
358
|
5. 输出综合审查报告。报告必须列出实际模式、fallback 原因、使用的 rubric、Rubric Source、审查范围和证据不足项。
|
|
350
359
|
|
|
@@ -282,6 +282,7 @@ AI 喜欢把事情凑成三个以显"完整"。-> 砍到只剩最有力的一条
|
|
|
282
282
|
| 升华式收尾 | 结尾对仗拔高、金句点题 -> 用一个动作或一句留白收住,把"意思"压进画面里 |
|
|
283
283
|
| 抽象命运/开端收束 | 「命运终于露出獠牙」「早已布好的棋局」「这一刻终于明白」「属于他的反击才刚刚开始」-> 改成角色当下可见的文件、动作、对话或物理后果;`check-ai-patterns.js` 报 `abstract-summary-tic` 时优先处理 |
|
|
284
284
|
| 套词密度过高 | 仿佛/一丝/一抹/深吸一口气/平静无波/指节泛白等成串复现(`cliche-density-tic`)-> 不是同义词轮换,整段回到角色当下证据:文件、动作、对话、物理后果 |
|
|
285
|
+
| 套式反应细节 | 指尖轻叩、袖口里攥紧、指节泛白、目光移开、“语气平静得像在念……”等反应成片(`stock-reaction-tic`)-> 逐处做删除测试;只标注情绪而不改变选择、关系、物件或动作结果的删掉,不换部位和同义动作;有伤势、动作失败或情节后果的身体细节可留 |
|
|
285
286
|
| 比喻密度过高 | 像/好像/仿佛/如同等比喻标记成片复现(`metaphor-density-tic`)-> 保留最能传递信息或情绪的一两个,其余改回具体动作、物件、声音、后果;不要换成新比喻 |
|
|
286
287
|
| 系统公告公文腔过密 | 方括号规则/面板/公告行里硬规则词成片(`system-notice-formality-tic`)-> 保留为角色看见的屏幕/公告/规则载体;只在载体内部白话化部分硬词,或补角色当场看懂的具体后果,不改成叙述者解释 |
|
|
287
288
|
|
|
@@ -575,7 +576,7 @@ AI 喜欢把事情凑成三个以显"完整"。-> 砍到只剩最有力的一条
|
|
|
575
576
|
- [ ] **标点不压平**:没有把质问、爆发、犹豫全部压成句号;也没有随机堆砌 `?`/`!`,或用 `……`/`——` 硬造停顿
|
|
576
577
|
- [ ] **Show Don't Tell**:用行为代替形容词,用细节代替总结
|
|
577
578
|
- [ ] **句长达标**:叙述默认是逗号长句(逗号之间 8-12 字、整句 20-30 字,规则 3);短句只作偶尔的孤立重拍,用完回到逗号长句;没有连着的 ≤5 字碎片,没有通篇短句像提纲
|
|
578
|
-
- [ ] **detector advisory 逐条复核**:`micro-action-tic` / `abstract-summary-tic` / `cliche-density-tic` / `metaphor-density-tic` / `reasoning-chain-tic` / `system-notice-formality-tic` / `overcompressed-prose-tic` / `low-connective-density-tic` / `action-list-tic` 命中时按脚本给出的修法处理:先通读判断是不是机械复现,确属再改;功能性写法保留或标 `[需复核]`,不做同义词轮换、不机械注水
|
|
579
|
+
- [ ] **detector advisory 逐条复核**:`micro-action-tic` / `stock-reaction-tic` / `abstract-summary-tic` / `cliche-density-tic` / `metaphor-density-tic` / `reasoning-chain-tic` / `system-notice-formality-tic` / `overcompressed-prose-tic` / `low-connective-density-tic` / `action-list-tic` 命中时按脚本给出的修法处理:先通读判断是不是机械复现,确属再改;功能性写法保留或标 `[需复核]`,不做同义词轮换、不机械注水
|
|
579
580
|
- [ ] **不做硬指标投机**:不为反检测强制每句换行、50-60 字一行、对话 50%-60%、英文点省略号,或把 `地/得` 全改成 `的`
|
|
580
581
|
- [ ] **任务卡点服从原文边界**:抽象总结若改成角色办事被卡住,必须来自原文已有任务/证据/手续/物件缺口;不新增原文没有的事件链
|
|
581
582
|
- [ ] **去AI三遍法执行**:轻度只做Pass1,中度做Pass1+2,重度完整三遍
|