@oh-story/dsh 0.1.3 → 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 +58 -12
- package/lib/client.js +2970 -537
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +218 -178
- package/lib/drama/skills/short-drama/SKILL.md +110 -117
- package/lib/drama/skills/short-drama/references/audience-reveal.md +14 -67
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +26 -77
- package/lib/drama/skills/short-drama/references/creator-documents.md +204 -0
- package/lib/drama/skills/short-drama/references/creator-workflow.md +73 -63
- package/lib/drama/skills/short-drama/references/knowhow-index.md +5 -0
- package/lib/drama/skills/short-drama/references/look-development.md +2 -3
- package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +14 -48
- package/lib/drama/skills/short-drama/references/production-form-profiles.md +1 -1
- package/lib/drama/skills/short-drama/references/reference-roles.md +32 -135
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +16 -19
- 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 +59 -170
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +18 -25
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +13 -15
- package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +11 -14
- 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 +6 -12
- package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +8 -11
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +21 -2
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +12 -105
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +12 -32
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +45 -155
- package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +39 -51
- package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +3 -4
- package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +15 -14
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +11 -15
- package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +4 -7
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +23 -23
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +8 -65
- 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 +67 -15
- 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/SKILL.md +61 -123
- package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +10 -15
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +15 -16
- package/lib/drama/skills/short-drama-review/references/project-calibration.md +7 -9
- package/lib/drama/skills/short-drama-review/references/review-method.md +8 -9
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +9 -13
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +104 -4
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +27 -19
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +4 -63
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +116 -238
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +9 -34
- package/lib/drama/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
- package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +5 -10
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +12 -7
- 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 +27 -62
- package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +16 -25
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +21 -23
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +8 -10
- package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +9 -13
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +16 -71
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +123 -230
- 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/camera-audio-continuity.md +10 -8
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +2 -2
- 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 +35 -27
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -11
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +13 -10
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +34 -63
- 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 +17 -73
- 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 +74 -268
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +7 -0
- package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +17 -43
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +15 -15
- package/lib/drama/skills/short-drama-write/references/script-craft.md +208 -21
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +11 -70
- 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 +971 -46
- 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 +419 -309
- package/lib/oh-story/roles/character-designer.md +4 -4
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +41 -42
- package/lib/oh-story/roles/story-architect.md +19 -23
- package/lib/oh-story/roles/story-explorer.md +8 -5
- 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 +21 -8
- 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-long-write/references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-long-write/references/{reversal-toolkit.md → 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 +5 -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 +17 -11
- 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 +85 -41
- 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-setup/references/agent-references/{genre-core-mechanics.md → 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-short-write/references/reversal-toolkit.md → story-setup/references/agent-references/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-setup/references/agent-references/{reversal-toolkit.md → 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 +9 -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 +5 -2
- 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/reference-tool.d.ts +6 -0
- package/lib/types/reference-tool.d.ts.map +1 -0
- package/lib/types/role-provider.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts +3 -1
- 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 +57 -41
- package/lib/drama/skills/short-drama/references/lifecycle-commands.md +0 -130
- 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
|
|