@oh-story/dsh 0.1.4 → 0.1.5
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 +2692 -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,7 +2,7 @@
|
|
|
2
2
|
name: story-long-analyze
|
|
3
3
|
version: 1.0.0
|
|
4
4
|
description: "长篇网文拆文。深度拆解爆款长篇小说的黄金三章、人设架构、爽点设计、节奏控制。单一深度拆解管道:跑完黄金三章(Stage 1)后产出快速预览报告并询问是否继续全量拆解,确认后从 Stage 2 续跑逐章摘要、聚合分析、设定关系、汇总报告,全程产物落盘 拆文库/{书名}/。触发方式:/story-long-analyze、/长篇拆文、「帮我拆这本书」「拆这本书」「分析黄金三章」「深度拆解」「完整拆解」「系统拆解」或提供小说文本文件路径——全部进入同一管道。"
|
|
5
|
-
metadata: {"openclaw":{"source":"https://github.com/
|
|
5
|
+
metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
|
|
6
6
|
---
|
|
7
7
|
# story-long-analyze:长篇网文拆文
|
|
8
8
|
|
|
@@ -12,9 +12,9 @@ metadata: {"openclaw":{"source":"https://github.com/worldwonderer/oh-story-claud
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
> Agent
|
|
15
|
+
> Agent 兼容性:只检查当前运行时的 canonical 目录:Claude `.claude/agents/{agent}.md`、OpenCode `.opencode/agents/{agent}.md`、Codex `.codex/agents/{agent}.toml`、Antigravity `.agents/agents/agent-name/agent.md`(`agent-name` 为目标 agent 名),不得因其他端文件存在而误判。Codex 使用同名 `agent_type`;Antigravity 使用 `invoke_subagent` + `TypeName`。对应运行时未暴露 custom-agent registry / `invoke_subagent` 或返回未知 agent 时,必须降级 solo/direct。检测到 `.zcode/` 时同样直接 solo/direct,因为 ZCode 3.3.4 不执行项目 custom agents;报告 `Fallback: project custom agents unavailable -> solo`。Claude/OpenCode 兼容面保留 `subagent_type`。
|
|
16
16
|
>
|
|
17
|
-
> Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version:
|
|
17
|
+
> Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 29` 不一致时(标记缺失、字段缺失/非整数、小于或大于 29)**照常按文件存在性检查并 spawn**,同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 29)` 并提示重新运行 `/story-setup` 后新开会话;大于 29 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
|
|
18
18
|
|
|
19
19
|
## 拆解边界声明(主线程同样适用)
|
|
20
20
|
|
|
@@ -267,7 +267,7 @@ Agent(
|
|
|
267
267
|
|
|
268
268
|
以下任一情况,Stage 2 自动退回串行模式,由主线程逐章处理(质量不受影响,只是改为串行、速度略慢)。**两条路径的要求是同一份**:串行时概要写法、情节点白描、原文引用精选规则和输出自检都按 [output-templates.md](references/output-templates.md)「Stage 2 章节摘要+情节点」执行;上面的机械硬检查串行同样要跑。串行没有 sonnet 升级重试这条路——硬检查命中时由主线程按失败项重写本章摘要 1 次,仍不过按 `⚠️ 跳过` 记入 `_progress.md` 「失败记录」表。
|
|
269
269
|
|
|
270
|
-
- **agent
|
|
270
|
+
- **agent 未部署**:当前运行时的 canonical agent 目录未找到 `chapter-extractor.md` 或 Codex 同名 TOML。项目 agent 通常不随写作仓库预置,应重新运行 `/story-setup` 完成当前适配器部署,不跨 Skill 读取模板源。
|
|
271
271
|
- **环境不支持 spawn 子代理**:本 skill 正运行在某个子代理上下文中,无法再起下一层 agent。
|
|
272
272
|
|
|
273
273
|
### Stage 2 收尾:合并章节摘要(_章节摘要汇总.md)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: story-long-scan
|
|
3
3
|
version: 1.0.0
|
|
4
4
|
description: "长篇网文扫榜。分析起点、番茄、晋江等平台排行榜数据,提炼市场趋势与热门题材。触发方式:/story-long-scan、/长篇扫榜、「长篇什么火」「起点排行」。"
|
|
5
|
-
metadata: {"openclaw":{"source":"https://github.com/
|
|
5
|
+
metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
|
|
6
6
|
---
|
|
7
7
|
# story-long-scan:长篇网文扫榜
|
|
8
8
|
|