@oh-story/dsh 0.1.8 → 0.1.9
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 +11 -11
- package/lib/client.js +35 -14
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- 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/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -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 +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- 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 +3 -3
- 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 +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- 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 +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -22,7 +22,7 @@ const path = require('path')
|
|
|
22
22
|
const FIELDS = [
|
|
23
23
|
'核心事件', '字数目标', '字数口径', '阶段位置', '单元ID/位置', '目标情绪',
|
|
24
24
|
'主角目标/关键选择', '章节定位', '本章结构公式', '章首钩子', '爽点',
|
|
25
|
-
'本章禁止提前释放', '契约风险',
|
|
25
|
+
'本章标价', '闭环状态', '本章禁止提前释放', '写手自由区', '契约风险',
|
|
26
26
|
]
|
|
27
27
|
const SUBSECTIONS = ['内容概括', '情节安排', '人物关系和出场顺序', '情节细化']
|
|
28
28
|
const FIVE_ACT = ['起因', '发展', '转折', '高潮', '结尾']
|
|
@@ -46,11 +46,11 @@ function readUtf8(file) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function makeCheck(id, ok, file, evidence, expected, repair) {
|
|
49
|
+
function makeCheck(id, ok, file, evidence, expected, repair, severity = 'blocking') {
|
|
50
50
|
return {
|
|
51
51
|
id,
|
|
52
52
|
ok,
|
|
53
|
-
severity
|
|
53
|
+
severity,
|
|
54
54
|
file,
|
|
55
55
|
evidence,
|
|
56
56
|
expected,
|
|
@@ -65,7 +65,47 @@ function parseTableRow(line) {
|
|
|
65
65
|
return trimmed.slice(1, -1).split('|').map((cell) => cell.replace(/\*\*/g, '').replace(/`/g, '').trim())
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function
|
|
68
|
+
function resolveBookRoot(file, projectRoot) {
|
|
69
|
+
if (projectRoot) return path.resolve(projectRoot)
|
|
70
|
+
const parent = path.dirname(path.resolve(file))
|
|
71
|
+
if (path.basename(parent) === '大纲') return path.dirname(parent)
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 项目引用须写目录路径;裸文件名可能指向 skill reference,不猜测其归属。
|
|
76
|
+
const REF_DIR_PREFIX = /^(?:设定|大纲|追踪|正文|读者笔记|对标)\//
|
|
77
|
+
|
|
78
|
+
function checkSettingRefs(text, name, file, projectRoot) {
|
|
79
|
+
const bookRoot = resolveBookRoot(file, projectRoot)
|
|
80
|
+
if (!bookRoot) {
|
|
81
|
+
return makeCheck('outline.setting-refs-exist', true, name, '未能定位书目录(细纲不在 大纲/ 下且未传 --project),跳过', '细纲引用的项目内文件真实存在', '无需修复。')
|
|
82
|
+
}
|
|
83
|
+
const refs = new Set()
|
|
84
|
+
const pattern = /`([^`\n]+?\.md)[^`\n]*`/g
|
|
85
|
+
let match
|
|
86
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
87
|
+
const token = match[1].trim().replace(/\\/g, '/')
|
|
88
|
+
if (token.includes('{') || token.includes('}')) continue
|
|
89
|
+
if (REF_DIR_PREFIX.test(token) || /^\.{1,2}\//.test(token)) refs.add(token)
|
|
90
|
+
}
|
|
91
|
+
const missing = []
|
|
92
|
+
for (const token of refs) {
|
|
93
|
+
const base = /^\.{1,2}\//.test(token) ? path.dirname(path.resolve(file)) : bookRoot
|
|
94
|
+
try {
|
|
95
|
+
if (!fs.statSync(path.resolve(base, token)).isFile()) missing.push(token)
|
|
96
|
+
} catch { missing.push(token) }
|
|
97
|
+
}
|
|
98
|
+
return makeCheck(
|
|
99
|
+
'outline.setting-refs-exist',
|
|
100
|
+
missing.length === 0,
|
|
101
|
+
name,
|
|
102
|
+
missing.length ? `引用的路径不是可用文件:${missing.join('、')}` : `${refs.size} 处项目内引用全部存在`,
|
|
103
|
+
'细纲引用的每个项目内 .md 文件都真实存在——不许把槽位指向查无实据的权威文件',
|
|
104
|
+
'修正引用路径;或按「创作自主权分级」先把缺的设定落档(B 级直接补、C 级进《供给单》提案),再在细纲引用。'
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function verify(file, projectRoot = null) {
|
|
69
109
|
const name = path.basename(file)
|
|
70
110
|
const read = readUtf8(file)
|
|
71
111
|
const checks = []
|
|
@@ -133,21 +173,82 @@ function verify(file) {
|
|
|
133
173
|
let header = null
|
|
134
174
|
for (const line of lines) {
|
|
135
175
|
const cells = parseTableRow(line)
|
|
136
|
-
|
|
176
|
+
// 四列是旧表(# / 情节点 / 功能标签 / 执行边界),五列是现行模板(中间多一列「分辨率」)。
|
|
177
|
+
// 两种都收:功能标签固定在第 3 列,执行边界一律取最后一列,旧细纲不因加列而失效。
|
|
178
|
+
if (cells && (cells.length === 4 || cells.length === 5) && PLOT_HEADER_FIRST.test(cells[0])) {
|
|
137
179
|
header = cells
|
|
138
180
|
break
|
|
139
181
|
}
|
|
140
182
|
}
|
|
141
|
-
const headerOk = Boolean(header)
|
|
183
|
+
const headerOk = Boolean(header)
|
|
184
|
+
&& header[2].includes('功能标签')
|
|
185
|
+
&& header[header.length - 1].includes('执行边界')
|
|
186
|
+
&& (header.length === 4 || header[3].includes('分辨率'))
|
|
142
187
|
checks.push(makeCheck(
|
|
143
188
|
'outline.plotpoint-table',
|
|
144
189
|
headerOk,
|
|
145
190
|
name,
|
|
146
|
-
header ? `表头:${header.join(' | ')}` : '未找到 | # | 情节点 | 功能标签 | 执行边界 | 表头',
|
|
147
|
-
'
|
|
148
|
-
'
|
|
191
|
+
header ? `表头:${header.join(' | ')}` : '未找到 | # | 情节点 | 功能标签 | [分辨率] | 执行边界 | 表头',
|
|
192
|
+
'情节细化使用五列表格:# / 情节点(谁做了什么) / 功能标签 / 分辨率 / 执行边界(四列旧表仍收,但新建细纲按五列模板走)',
|
|
193
|
+
'只把情节点序列改成五列表格,逐点补功能标签、分辨率与执行边界;不增删情节点本身。'
|
|
149
194
|
))
|
|
150
195
|
|
|
196
|
+
// 「放」半边:五列表的执行边界必须至少一个点解除限制(模板既有要求),
|
|
197
|
+
// 全是禁令的情节点表实测会把整章压成同一个温度(015 章十一个点全「不许」)。
|
|
198
|
+
if (headerOk && header.length === 5) {
|
|
199
|
+
const rows = []
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const cells = parseTableRow(line)
|
|
202
|
+
if (cells && cells.length === 5 && /^\d+$/.test(cells[0])) rows.push(cells)
|
|
203
|
+
}
|
|
204
|
+
if (rows.length) {
|
|
205
|
+
const released = rows.filter((cells) => {
|
|
206
|
+
const match = cells[4].match(/(?:^|[。;;\s])放\s*[::]([^]*?)(?=(?:禁|放)\s*[::]|$)/)
|
|
207
|
+
if (!match) return false
|
|
208
|
+
const value = match[1].replace(/[\s。;;,,、\[\]【】]/g, '')
|
|
209
|
+
return value.length > 0 && !/^(?:无|暂无|待补充|待定|无缺口)$/.test(value)
|
|
210
|
+
})
|
|
211
|
+
checks.push(makeCheck(
|
|
212
|
+
'outline.plotpoint-release',
|
|
213
|
+
released.length > 0,
|
|
214
|
+
name,
|
|
215
|
+
`${rows.length} 个情节点里 ${released.length} 个写了「放」`,
|
|
216
|
+
'执行边界写成「禁+放」两半,每章至少一个点写了「放」——放行是解除限制,不是义务',
|
|
217
|
+
'给最该出彩的一两个点补「放」半边(谁可以出声、谁可以失态、允不允许当场标价),不改情节点本身。'
|
|
218
|
+
))
|
|
219
|
+
if (released.length > 0 && rows.length >= 3 && released.length * 3 < rows.length) {
|
|
220
|
+
checks.push(makeCheck(
|
|
221
|
+
'outline.plotpoint-release-ratio',
|
|
222
|
+
false,
|
|
223
|
+
name,
|
|
224
|
+
`${rows.length} 个情节点里只有 ${released.length} 个写了「放」(不足三分之一)`,
|
|
225
|
+
'建议至少三分之一的情节点写「放」半边,避免整章只剩禁令',
|
|
226
|
+
'按人工判断酌情补「放」;本项不阻断。',
|
|
227
|
+
'advisory'
|
|
228
|
+
))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 章级「禁止提前释放」只写本章特有的三五条;条目过多多半是把卷级常任禁忌逐章复读了。
|
|
234
|
+
const releaseMatch = text.match(/^\s*[-*+]\s*\*{0,2}本章禁止提前释放\*{0,2}\s*[::]\s*(.*)$/m)
|
|
235
|
+
if (releaseMatch) {
|
|
236
|
+
const items = releaseMatch[1].split(/[、;;,,\/]/).map((s) => s.trim()).filter(Boolean)
|
|
237
|
+
if (items.length > 5) {
|
|
238
|
+
checks.push(makeCheck(
|
|
239
|
+
'outline.release-brevity',
|
|
240
|
+
false,
|
|
241
|
+
name,
|
|
242
|
+
`本章禁止提前释放列了 ${items.length} 条`,
|
|
243
|
+
'章级只写本章特有的三五条;卷级常任禁忌写在卷纲「本卷禁碰的终局底牌」,不逐章复读',
|
|
244
|
+
'把与卷纲常任重复的条目删掉,只留本章特有;本项不阻断。',
|
|
245
|
+
'advisory'
|
|
246
|
+
))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
checks.push(checkSettingRefs(text, name, file, projectRoot))
|
|
251
|
+
|
|
151
252
|
const targetMatch = text.match(/字数目标\s*[::]\s*(?:约\s*)?([\d,,]+)/)
|
|
152
253
|
const target = targetMatch ? Number(targetMatch[1].replace(/[,,]/g, '')) : null
|
|
153
254
|
const caliberOk = new RegExp(`字数口径\\s*[::]\\s*${CALIBER}`).test(text)
|
|
@@ -164,7 +265,8 @@ function verify(file) {
|
|
|
164
265
|
}
|
|
165
266
|
|
|
166
267
|
function report(file, checks) {
|
|
167
|
-
const failures = checks.filter((check) => !check.ok)
|
|
268
|
+
const failures = checks.filter((check) => !check.ok && check.severity === 'blocking')
|
|
269
|
+
const advisories = checks.filter((check) => !check.ok && check.severity === 'advisory')
|
|
168
270
|
return {
|
|
169
271
|
schema_version: 1,
|
|
170
272
|
verifier: 'story-long-write.outline-contract',
|
|
@@ -172,6 +274,7 @@ function report(file, checks) {
|
|
|
172
274
|
ok: failures.length === 0,
|
|
173
275
|
checks,
|
|
174
276
|
failures,
|
|
277
|
+
advisories,
|
|
175
278
|
repair_scope: failures.map((failure) => ({
|
|
176
279
|
id: failure.id,
|
|
177
280
|
file: failure.file,
|
|
@@ -200,13 +303,65 @@ function resolveChapter(project, chapter) {
|
|
|
200
303
|
return { file: path.join(dir, hit) }
|
|
201
304
|
}
|
|
202
305
|
|
|
306
|
+
// --supply:批末验证《供给单》已落卷纲——定位单元卡块,确认其中有「供给自查」小节。
|
|
307
|
+
function verifySupply(volumeFile, unitId) {
|
|
308
|
+
const read = readUtf8(volumeFile)
|
|
309
|
+
if (!read.ok) {
|
|
310
|
+
return { schema_version: 1, verifier: 'story-long-write.outline-supply', file: path.resolve(volumeFile), unit: unitId, ok: false, evidence: read.error || '卷纲文件为空' }
|
|
311
|
+
}
|
|
312
|
+
// 只检查目标单元内的标题,不把正文提及或代码示例当成小节。
|
|
313
|
+
let fence = null
|
|
314
|
+
const lines = read.text.split(/\r?\n/).map((line) => {
|
|
315
|
+
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/)
|
|
316
|
+
if (marker) {
|
|
317
|
+
if (!fence) fence = marker[1]
|
|
318
|
+
else if (marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = null
|
|
319
|
+
return ''
|
|
320
|
+
}
|
|
321
|
+
return fence ? '' : line.replace(/\*\*/g, '')
|
|
322
|
+
})
|
|
323
|
+
const headings = []
|
|
324
|
+
for (let index = 0; index < lines.length; index++) {
|
|
325
|
+
const heading = lines[index].match(/^(#{1,6})\s+(.+)$/)
|
|
326
|
+
if (heading) headings.push({ index, level: heading[1].length, title: heading[2] })
|
|
327
|
+
}
|
|
328
|
+
const card = headings.find((heading) => {
|
|
329
|
+
const id = heading.title.match(/剧情单元\s+([A-Za-z][\w-]*)/)
|
|
330
|
+
if (id) return id[1] === unitId
|
|
331
|
+
const end = headings.find((next) => next.index > heading.index)?.index ?? lines.length
|
|
332
|
+
return lines.slice(heading.index + 1, end).some((line) => {
|
|
333
|
+
const field = line.match(/^\s*[-*+]\s*单元ID\s*[::]\s*([A-Za-z][\w-]*)\s*$/)
|
|
334
|
+
return field?.[1] === unitId
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
const end = card ? (headings.find((next) => next.index > card.index && next.level <= card.level)?.index ?? lines.length) : 0
|
|
338
|
+
const ok = Boolean(card) && headings.some((heading) => heading.index > card.index && heading.index < end && /^供给自查(?:\s|[((]|$)/.test(heading.title))
|
|
339
|
+
if (!card) {
|
|
340
|
+
return { schema_version: 1, verifier: 'story-long-write.outline-supply', file: path.resolve(volumeFile), unit: unitId, ok: false, evidence: `卷纲中未找到剧情单元 ${unitId}` }
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
schema_version: 1,
|
|
344
|
+
verifier: 'story-long-write.outline-supply',
|
|
345
|
+
file: path.resolve(volumeFile),
|
|
346
|
+
unit: unitId,
|
|
347
|
+
ok,
|
|
348
|
+
evidence: ok ? '单元卡含「供给自查」小节' : `剧情单元 ${unitId} 的卡内没有「供给自查」小节——每批出细纲前须产出《供给单》(含「无缺口」情形),见 workflow-setup.md「按剧情批出细纲」步骤 3`,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
203
352
|
function parseArgs(argv) {
|
|
204
353
|
const files = []
|
|
205
354
|
let project = null
|
|
206
355
|
let chapter = null
|
|
356
|
+
let supply = null
|
|
207
357
|
for (let index = 0; index < argv.length; index++) {
|
|
208
358
|
const arg = argv[index]
|
|
209
359
|
if (arg === '--json') continue
|
|
360
|
+
if (arg === '--supply') {
|
|
361
|
+
if (index + 2 >= argv.length) return null
|
|
362
|
+
supply = { volumeFile: argv[++index], unitId: argv[++index] }
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
210
365
|
if (arg === '--project' || arg === '--chapter') {
|
|
211
366
|
if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) return null
|
|
212
367
|
const value = argv[++index]
|
|
@@ -217,6 +372,10 @@ function parseArgs(argv) {
|
|
|
217
372
|
if (arg.startsWith('--')) return null
|
|
218
373
|
files.push(arg)
|
|
219
374
|
}
|
|
375
|
+
if (supply) {
|
|
376
|
+
if (project || chapter || files.length) return null
|
|
377
|
+
return { supply }
|
|
378
|
+
}
|
|
220
379
|
if (project || chapter) {
|
|
221
380
|
if (!project || !chapter || files.length || !/^\d+$/.test(chapter)) return null
|
|
222
381
|
return { project, chapter }
|
|
@@ -228,9 +387,14 @@ function parseArgs(argv) {
|
|
|
228
387
|
function main(argv) {
|
|
229
388
|
const parsed = parseArgs(argv)
|
|
230
389
|
if (!parsed) {
|
|
231
|
-
process.stderr.write('用法: node scripts/check-outline-contract.js --json <细纲路径...> | --json --project <书目录> --chapter N
|
|
390
|
+
process.stderr.write('用法: node scripts/check-outline-contract.js --json <细纲路径...> | --json --project <书目录> --chapter N | --json --supply <卷纲路径> <单元ID>\n')
|
|
232
391
|
return 2
|
|
233
392
|
}
|
|
393
|
+
if (parsed.supply) {
|
|
394
|
+
const supplyReport = verifySupply(parsed.supply.volumeFile, parsed.supply.unitId)
|
|
395
|
+
process.stdout.write(`${JSON.stringify(supplyReport, null, 2)}\n`)
|
|
396
|
+
return supplyReport.ok ? 0 : 1
|
|
397
|
+
}
|
|
234
398
|
let targets = parsed.files
|
|
235
399
|
if (!targets) {
|
|
236
400
|
const resolved = resolveChapter(parsed.project, parsed.chapter)
|
|
@@ -240,7 +404,7 @@ function main(argv) {
|
|
|
240
404
|
}
|
|
241
405
|
targets = [resolved.file]
|
|
242
406
|
}
|
|
243
|
-
const reports = targets.map((file) => verify(file))
|
|
407
|
+
const reports = targets.map((file) => verify(file, parsed.project || null))
|
|
244
408
|
const ok = reports.every((entry) => entry.ok)
|
|
245
409
|
process.stdout.write(`${JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2)}\n`)
|
|
246
410
|
return ok ? 0 : 1
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
* 收尾复扫用 `正文/第XXX章_*.md` 这类通配传多章时,多出来的正文不能被当成细纲吞掉
|
|
32
32
|
* ——那会让首个文件比错对象、其余文件根本不检,静默退 0 报「干净」。
|
|
33
33
|
*
|
|
34
|
-
* 退出码:0 =
|
|
35
|
-
*
|
|
34
|
+
* 退出码:0 = 干净或未自动发现细纲而跳过;1 = 有重合待复核;
|
|
35
|
+
* 2 = 参数错误、输入不可读(含已发现的细纲)或非预期异常。
|
|
36
|
+
* 干净时静默;自动发现不到细纲时明确报告跳过,避免与「已检查且干净」混淆。
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
39
|
'use strict'
|
|
@@ -42,19 +43,59 @@ const path = require('path')
|
|
|
42
43
|
const MIN_RUN = 16 // 判定阈值:连续重合 >15 字,即 >=16
|
|
43
44
|
const REPORT_TOP = 8 // 最多列出的片段数
|
|
44
45
|
|
|
45
|
-
function
|
|
46
|
+
function readTextFile(p) {
|
|
46
47
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
48
|
+
const stat = fs.statSync(p)
|
|
49
|
+
if (!stat.isFile()) return { ok: false, reason: '不是普通文件' }
|
|
50
|
+
return { ok: true, text: fs.readFileSync(p, 'utf8').replace(/^/, '') }
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error && error.code === 'ENOENT') return { ok: false, reason: '不存在' }
|
|
53
|
+
if (error && error.code) return { ok: false, reason: `无法读取(${error.code})` }
|
|
54
|
+
return { ok: false, reason: `无法读取:${error instanceof Error ? error.message : String(error)}` }
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
function reportInputError(kind, file, reason) {
|
|
59
|
+
process.stderr.write(`错误: 无法读取${kind} "${file}":${reason}。\n`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reportUsageError(message) {
|
|
63
|
+
process.stderr.write(`错误: ${message}。\n`)
|
|
64
|
+
process.stderr.write('用法: node check-outline-copy.js [--outline <细纲路径>] <正文路径...>\n')
|
|
65
|
+
return 2
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/** 只留汉字——剥掉标点/加粗/【】后比对,防止细纲标注造成假阴性 */
|
|
54
69
|
function hanOnly(s) {
|
|
55
70
|
return s.replace(/[^一-鿿]/g, '')
|
|
56
71
|
}
|
|
57
72
|
|
|
73
|
+
// 写后回填的完成记录不是誊抄来源。标题块包含子标题;字段块止于下一字段或标题。
|
|
74
|
+
function stripCompletionRecord(outline) {
|
|
75
|
+
let headingLevel = 0
|
|
76
|
+
let inField = false
|
|
77
|
+
return outline.split('\n').filter((line) => {
|
|
78
|
+
const heading = line.match(/^ {0,3}(#{1,6})[\t ]+(.*)$/)
|
|
79
|
+
if (headingLevel) {
|
|
80
|
+
if (!heading || heading[1].length > headingLevel) return false
|
|
81
|
+
headingLevel = 0
|
|
82
|
+
}
|
|
83
|
+
if (inField) {
|
|
84
|
+
if (!heading && !/^[-*+][\t ]+/.test(line)) return false
|
|
85
|
+
inField = false
|
|
86
|
+
}
|
|
87
|
+
if (heading && heading[2].includes('实际完成情况')) {
|
|
88
|
+
headingLevel = heading[1].length
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
if (/^[-*+][\t ]+(?:\*\*|__)?实际完成情况[^::]*[::]/.test(line)) {
|
|
92
|
+
inField = true
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
return true
|
|
96
|
+
}).join('\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
/**
|
|
59
100
|
* 抽出细纲「复沓锚句」字段下的原话,一行一条。
|
|
60
101
|
* 只认这一个字段,不扫情节点序列——锚句集中在固定区块,情节点保持只写「要发生什么」。
|
|
@@ -129,7 +170,9 @@ function findOutline(proseFile) {
|
|
|
129
170
|
const fm = file.match(/^细纲_第0*(\d+)章.*\.md$/)
|
|
130
171
|
if (fm && fm[1] === chapter) return path.join(dir, file)
|
|
131
172
|
}
|
|
132
|
-
} catch {
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code !== 'ENOENT') throw error
|
|
175
|
+
}
|
|
133
176
|
return null
|
|
134
177
|
}
|
|
135
178
|
|
|
@@ -137,34 +180,56 @@ function main() {
|
|
|
137
180
|
const proseFiles = []
|
|
138
181
|
let explicitOutline = null
|
|
139
182
|
const argv = process.argv.slice(2)
|
|
183
|
+
let positionalOnly = false
|
|
140
184
|
for (let k = 0; k < argv.length; k++) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
185
|
+
const arg = argv[k]
|
|
186
|
+
if (!positionalOnly && arg === '--') {
|
|
187
|
+
positionalOnly = true
|
|
188
|
+
} else if (!positionalOnly && arg === '--outline') {
|
|
189
|
+
if (explicitOutline !== null) return reportUsageError('--outline 不能重复指定')
|
|
190
|
+
const value = argv[k + 1]
|
|
191
|
+
if (!value || value.startsWith('--')) return reportUsageError('--outline 缺少路径')
|
|
192
|
+
explicitOutline = value
|
|
193
|
+
k++
|
|
194
|
+
} else if (!positionalOnly && arg.startsWith('--')) {
|
|
195
|
+
return reportUsageError(`未知选项: ${arg}`)
|
|
196
|
+
} else {
|
|
197
|
+
proseFiles.push(arg)
|
|
198
|
+
}
|
|
147
199
|
}
|
|
148
|
-
|
|
200
|
+
if (!proseFiles.length) return reportUsageError('缺少正文路径')
|
|
201
|
+
|
|
202
|
+
// 逐个文件独立判定;输入错误优先于重合发现
|
|
149
203
|
let status = 0
|
|
150
204
|
for (const proseFile of proseFiles) {
|
|
151
|
-
|
|
205
|
+
status = Math.max(status, checkOne(proseFile, explicitOutline))
|
|
152
206
|
}
|
|
153
207
|
return status
|
|
154
208
|
}
|
|
155
209
|
|
|
156
210
|
function checkOne(proseFile, explicitOutline) {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
211
|
+
const proseResult = readTextFile(proseFile)
|
|
212
|
+
if (!proseResult.ok) {
|
|
213
|
+
reportInputError('正文', proseFile, proseResult.reason)
|
|
214
|
+
return 2
|
|
215
|
+
}
|
|
216
|
+
const prose = proseResult.text
|
|
159
217
|
|
|
160
218
|
const outlineFile = explicitOutline || findOutline(proseFile)
|
|
161
|
-
if (!outlineFile)
|
|
162
|
-
|
|
163
|
-
|
|
219
|
+
if (!outlineFile) {
|
|
220
|
+
process.stdout.write(`细纲照搬检测:跳过 "${proseFile}"(未自动发现细纲)。\n`)
|
|
221
|
+
return 0
|
|
222
|
+
}
|
|
223
|
+
const outlineResult = readTextFile(outlineFile)
|
|
224
|
+
if (!outlineResult.ok) {
|
|
225
|
+
reportInputError(explicitOutline ? '显式细纲' : '自动发现的细纲', outlineFile, outlineResult.reason)
|
|
226
|
+
return 2
|
|
227
|
+
}
|
|
228
|
+
const outline = outlineResult.text
|
|
164
229
|
|
|
165
230
|
// 正文去掉标题行后比对
|
|
166
231
|
const P = hanOnly(prose.replace(/^#.*$/gm, ''))
|
|
167
|
-
const O = hanOnly(outline)
|
|
232
|
+
const O = hanOnly(stripCompletionRecord(outline))
|
|
168
233
|
if (P.length < MIN_RUN || O.length < MIN_RUN) return 0
|
|
169
234
|
|
|
170
235
|
// 复沓锚句列出的原话允许逐字落地,命中后计入豁免、不判誊抄
|
|
@@ -236,6 +301,8 @@ function checkOne(proseFile, explicitOutline) {
|
|
|
236
301
|
|
|
237
302
|
try {
|
|
238
303
|
process.exit(main())
|
|
239
|
-
} catch {
|
|
240
|
-
|
|
304
|
+
} catch (error) {
|
|
305
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
306
|
+
process.stderr.write(`错误: 细纲照搬检测异常:${message}。\n`)
|
|
307
|
+
process.exit(2)
|
|
241
308
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const { loadStyleWhitelist, styleSpans } = require('./style-whitelist.js');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
|
|
7
8
|
const USAGE = `Usage: node normalize-punctuation.js [--check] [--quote-mode keep|ascii|yan] <file...>
|
|
@@ -9,6 +10,7 @@ const USAGE = `Usage: node normalize-punctuation.js [--check] [--quote-mode keep
|
|
|
9
10
|
Normalize正文 punctuation deterministically:
|
|
10
11
|
- replace ellipses, em dashes, and double hyphens with Chinese punctuation
|
|
11
12
|
- remove markdown divider lines (---) from正文
|
|
13
|
+
- preserve pause punctuation covered by book-local .deslop-whitelist literals
|
|
12
14
|
- keep quote style by default; convert quotes only when explicitly requested
|
|
13
15
|
`;
|
|
14
16
|
|
|
@@ -61,7 +63,10 @@ for (const file of options.files) {
|
|
|
61
63
|
continue;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
let whitelist;
|
|
67
|
+
try { whitelist = loadStyleWhitelist(fullPath); }
|
|
68
|
+
catch (error) { die(`${file}: unable to read .deslop-whitelist (${error.message})`); }
|
|
69
|
+
const result = normalizeDocument(input, options.quoteMode, whitelist);
|
|
65
70
|
totalFindings += result.findings.length;
|
|
66
71
|
|
|
67
72
|
if (options.check) {
|
|
@@ -94,7 +99,7 @@ function die(message) {
|
|
|
94
99
|
process.exit(2);
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function normalizeDocument(input, quoteMode) {
|
|
102
|
+
function normalizeDocument(input, quoteMode, whitelist) {
|
|
98
103
|
const { lines, endings } = splitLinesKeepingEndings(input);
|
|
99
104
|
|
|
100
105
|
const findings = [];
|
|
@@ -159,7 +164,7 @@ function normalizeDocument(input, quoteMode) {
|
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
const commentOpenBefore = commentOpen;
|
|
162
|
-
const punctuationResult = normalizePausePunctuation(line, lineNo, commentOpen);
|
|
167
|
+
const punctuationResult = normalizePausePunctuation(line, lineNo, commentOpen, whitelist);
|
|
163
168
|
findings.push(...punctuationResult.findings);
|
|
164
169
|
line = punctuationResult.line;
|
|
165
170
|
commentOpen = punctuationResult.commentOpen;
|
|
@@ -237,7 +242,7 @@ function isClosingFence(line, fence) {
|
|
|
237
242
|
// 一遍归一化留不干净,再跑一遍还会改已定稿的正文;所以反复归一化到不动点。
|
|
238
243
|
// 每遍至少把一个 `…/./—/-` 换成非停顿字符,字符数严格递减,必然收敛。
|
|
239
244
|
// findings 只留第一遍:同一处不重复计数,column 也仍然是原行的偏移。
|
|
240
|
-
function normalizePausePunctuation(line, lineNo, commentOpen) {
|
|
245
|
+
function normalizePausePunctuation(line, lineNo, commentOpen, whitelist) {
|
|
241
246
|
let current = line;
|
|
242
247
|
let findings = null;
|
|
243
248
|
let commentOpenAfter = commentOpen;
|
|
@@ -245,7 +250,7 @@ function normalizePausePunctuation(line, lineNo, commentOpen) {
|
|
|
245
250
|
for (;;) {
|
|
246
251
|
const comments = htmlCommentSpans(current, commentOpen);
|
|
247
252
|
commentOpenAfter = comments.open;
|
|
248
|
-
const pass = normalizePausePunctuationPass(current, lineNo, comments.spans);
|
|
253
|
+
const pass = normalizePausePunctuationPass(current, lineNo, comments.spans.concat(styleSpans(current, whitelist)));
|
|
249
254
|
if (findings === null) findings = pass.findings;
|
|
250
255
|
if (pass.line === current) break;
|
|
251
256
|
current = pass.line;
|