@oh-story/dsh 0.1.9 → 0.1.11
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 +14 -14
- package/lib/client.js +209 -73
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +248 -168
- package/lib/drama/skills/short-drama/SKILL.md +16 -4
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
- package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
- package/lib/drama/skills/short-drama-edit/SKILL.md +65 -44
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +6 -4
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +26 -67
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +31 -56
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +42 -78
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +44 -92
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +14 -16
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1272 -125
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +113 -9
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
- package/lib/drama/skills/short-drama-review/SKILL.md +4 -5
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
- package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +24 -35
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +23 -11
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +139 -34
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +15 -12
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +29 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +52 -39
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -21
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +9 -23
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +14 -26
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -2
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
- package/lib/drama/skills/short-drama-write/SKILL.md +10 -7
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
- package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
- package/lib/index.js +346 -98
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
- package/lib/oh-story/manifest.json +607 -327
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +15 -13
- package/lib/oh-story/roles/narrative-writer.md +36 -38
- package/lib/oh-story/roles/story-architect.md +9 -61
- package/lib/oh-story/roles/story-explorer.md +20 -125
- package/lib/oh-story/roles/story-researcher.md +0 -1
- package/lib/oh-story/skills/story/SKILL.md +15 -9
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story/references/author-memory.md +46 -94
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +59 -73
- package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +12 -11
- package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +46 -94
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +22 -5
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +64 -584
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
- package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
- package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
- package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +98 -92
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +15 -11
- package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +132 -27
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +478 -15
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +81 -270
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +252 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +3 -3
- package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
- package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +138 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +55 -128
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +6 -6
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +45 -194
- package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +49 -81
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +12 -11
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +24 -202
- package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +46 -94
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +7 -11
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +7 -15
- package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
- package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/project-files.md +19 -13
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +13 -5
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +5 -6
- package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +81 -121
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +41 -139
- package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +57 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -253
- package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +19 -65
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +226 -25
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +132 -27
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +7 -4
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +304 -46
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +478 -15
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-review/SKILL.md +31 -302
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +12 -11
- package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-review/references/author-memory.md +46 -94
- package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-review/references/review-quality.md +6 -11
- package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
- package/lib/oh-story/skills/story-review/references/solo.md +52 -0
- package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +15 -11
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +478 -15
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +71 -452
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +12 -11
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +22 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
- 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/long-emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
- package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +19 -65
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +56 -110
- package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/lib/oh-story/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
- package/lib/oh-story/skills/story-short-scan/SKILL.md +35 -65
- package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +23 -175
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +46 -94
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -47
- package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +21 -13
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +116 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +99 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +16 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +53 -1
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +60 -1
- package/lib/video-recap/README.en.md +348 -137
- package/lib/video-recap/README.md +340 -136
- package/lib/video-recap/manifest.json +406 -251
- package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
- package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
- package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
- package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
- package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
- package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
- package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
- package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
- package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
- package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
- package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
- package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
- package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
- package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
- package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
- package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
- package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
- package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
- package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
- package/lib/video-recap/skills/video-script/SKILL.md +16 -13
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
- package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
- package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
- package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
- package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
- package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/lib/video-recap/skills/{video-script/scripts/agent_brief.py → video-understanding/scripts/briefing/builder.py} +38 -35
- package/lib/video-recap/skills/video-understanding/scripts/{brief_context.py → briefing/context.py} +4 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
- package/lib/video-recap/skills/{video-script/scripts/brief_timeline.py → video-understanding/scripts/briefing/timeline.py} +5 -6
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
- package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
- package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -530
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -559
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +0 -678
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
- package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +0 -348
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +0 -327
- /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
- /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
- /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
The language model supplies compact semantic transactions. This tool validates
|
|
5
5
|
and applies them in memory, renders every derived view, and writes the JSON state
|
|
6
|
-
last as the commit point. Author memory
|
|
7
|
-
|
|
6
|
+
last as the commit point. Author memory lives in two kinds of store: the
|
|
7
|
+
project-level store under the workspace holds global / genre / workflow items
|
|
8
|
+
(`AP` ids); each book keeps its own book-level store under the book directory
|
|
9
|
+
(`BP` ids) so memory travels with the book. Both stay separate from each book's
|
|
10
|
+
story-continuity tracking. When the book root is the workspace itself, the
|
|
11
|
+
book-level store moves into a `书级/` subdirectory so the two never share a file.
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from __future__ import annotations
|
|
@@ -29,6 +33,28 @@ PROFILE_MAX_BYTES = 12288
|
|
|
29
33
|
PENDING_MAX_BYTES = 12288
|
|
30
34
|
JOURNAL_MAX_BYTES = 24576
|
|
31
35
|
QUERY_MAX_BYTES = 2048
|
|
36
|
+
ASSERTION_MAX_BYTES = 120 # 新建条目的断言限一句话;解释进 reason(不进 query 载荷)。
|
|
37
|
+
OMITTED_IDS_MAX = 20 # omitted_ids 封顶,omitted 保留真实总数——漏项列表不许把载荷本身挤炸。
|
|
38
|
+
LEGACY_ASSERTION_MAX_BYTES = 768 # 存量条目的读取上限;强化老条目不受新上限约束。
|
|
39
|
+
|
|
40
|
+
# query 的输出是要原样贴进执行 agent prompt 的注入载荷,QUERY_MAX_BYTES
|
|
41
|
+
# 是它在 prompt 里的注意力预算,不该放大。防「作者以为载入了、实际被静默
|
|
42
|
+
# 截断挤掉」靠三层:①新建条目的断言限 ASSERTION_MAX_BYTES,从源头短(强化
|
|
43
|
+
# 已有条目不受限,否则存量长断言再也无法被确认,只会派生重复条目);②写入
|
|
44
|
+
# 端按下列任务组合(与 references/author-memory-maintenance.md 的映射表同包跟版)估算
|
|
45
|
+
# 最坏查询情形——全局条目+各 scope 维度上最重的单一切片(一次查询只带一
|
|
46
|
+
# 个 book/genre/workflow,不同书的条目不会同现;切片按 casefold 归并,与
|
|
47
|
+
# same_scope_value 同一口径,轻重按 compact 字节+列表分隔符算,与真实载荷
|
|
48
|
+
# 同一把尺),装不下时在返回的 warnings 里点名将被略过的条目、指向「整理作
|
|
49
|
+
# 者记忆」,写入本身永不因注入预算失败;③查询按 重要度→本书例外→最近更新
|
|
50
|
+
# 排序装填,被略过的恒是重要度较低的条目,漏下的 ID 按同一优先级顺序报进
|
|
51
|
+
# omitted_ids。
|
|
52
|
+
QUERY_COMBOS: dict[str, tuple[str, ...]] = {
|
|
53
|
+
"正文初稿/续写": ("prose_style", "story_design"),
|
|
54
|
+
"去AI味/改写": ("prose_style",),
|
|
55
|
+
"设定/大纲": ("story_design", "workflow", "interaction"),
|
|
56
|
+
"审稿": ("delivery", "interaction", "prose_style"),
|
|
57
|
+
}
|
|
32
58
|
|
|
33
59
|
KINDS = ("prose_style", "story_design", "workflow", "delivery", "interaction")
|
|
34
60
|
KIND_TITLES = {
|
|
@@ -42,6 +68,11 @@ SCOPE_LEVELS = ("global", "genre", "book", "workflow")
|
|
|
42
68
|
STATUSES = ("active", "pending", "conflict", "rejected", "superseded")
|
|
43
69
|
CONFIDENCE_LEVELS = ("low", "medium", "high")
|
|
44
70
|
IMPORTANCE_LEVELS = ("low", "medium", "high")
|
|
71
|
+
# 作者记忆只记作者明确表达的偏好。repeated_correction / inferred_pattern 两条
|
|
72
|
+
# 由 agent 主动推断写入的管道已经移除(#436):它们只在攒待确认清单的审阅负
|
|
73
|
+
# 担,不是记忆质量;文档也明说不装全量消息 hook,隐式捕获本就承诺不了完整
|
|
74
|
+
# 性。SOURCES 里保留这两个值只为存量 state 仍能通过校验、仍能 decide/forget,
|
|
75
|
+
# 新写入一律按 WRITE_SOURCES 校验。
|
|
45
76
|
SOURCES = (
|
|
46
77
|
"explicit_user",
|
|
47
78
|
"accepted_suggestion",
|
|
@@ -49,8 +80,21 @@ SOURCES = (
|
|
|
49
80
|
"inferred_pattern",
|
|
50
81
|
"manual",
|
|
51
82
|
)
|
|
83
|
+
WRITE_SOURCES = ("explicit_user", "accepted_suggestion", "manual")
|
|
52
84
|
RANK = {"low": 0, "medium": 1, "high": 2}
|
|
53
85
|
|
|
86
|
+
# 两级 store(#435):项目级存 global/genre/workflow,ID 前缀 AP,位于
|
|
87
|
+
# {工作区}/.story/作者记忆/;书级只存该书的 book 条目,ID 前缀 BP,位于
|
|
88
|
+
# {书}/.story/作者记忆/,书归档、迁移时记忆随书走。ID 前缀就是路由键——
|
|
89
|
+
# decide/forget 看 item_id 前缀,remember/replace 看 scope.level,一份事务
|
|
90
|
+
# 只写一个 store。项目级 store 里升级前写入的存量 book 条目不再参与查询与估算,
|
|
91
|
+
# 用 migrate 搬进书目录后才回来——不做双读,双读会让迁移永远没人做。
|
|
92
|
+
STORE_PREFIX = {"project": "AP", "book": "BP"}
|
|
93
|
+
ID_PREFIXES = tuple(STORE_PREFIX.values())
|
|
94
|
+
# 单书布局(书根就是工作区)下书级 store 的子目录:两级 store 的默认落点在这种
|
|
95
|
+
# 布局里是同一个 state 文件,书级改住 {工作区}/.story/作者记忆/书级/。
|
|
96
|
+
SINGLE_ROOT_BOOK_DIR = "书级"
|
|
97
|
+
|
|
54
98
|
|
|
55
99
|
class AuthorMemoryError(ValueError):
|
|
56
100
|
"""Expected validation or state error."""
|
|
@@ -101,13 +145,31 @@ def choice(value: object, allowed: tuple[str, ...], label: str) -> str:
|
|
|
101
145
|
return value
|
|
102
146
|
|
|
103
147
|
|
|
148
|
+
def is_item_id(value: object) -> bool:
|
|
149
|
+
return (
|
|
150
|
+
isinstance(value, str)
|
|
151
|
+
and len(value) >= 3
|
|
152
|
+
and value[:2] in ID_PREFIXES
|
|
153
|
+
and value[2:].isdecimal()
|
|
154
|
+
and int(value[2:]) >= 1
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def id_number(item_id: str) -> int:
|
|
159
|
+
return int(item_id[2:])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def id_store(item_id: str) -> str:
|
|
163
|
+
return "book" if item_id.startswith(STORE_PREFIX["book"]) else "project"
|
|
164
|
+
|
|
165
|
+
|
|
104
166
|
def clean_id_list(value: object, label: str, *, maximum: int = 32) -> list[str]:
|
|
105
167
|
raw = as_list(value, label)
|
|
106
168
|
require(len(raw) <= maximum, f"{label} may contain at most {maximum} items")
|
|
107
169
|
result: list[str] = []
|
|
108
170
|
for index, item in enumerate(raw):
|
|
109
171
|
item_id = clean_text(item, f"{label}[{index}]", max_bytes=32)
|
|
110
|
-
require(
|
|
172
|
+
require(is_item_id(item_id), f"{label}[{index}] is not an author-memory id")
|
|
111
173
|
if item_id not in result:
|
|
112
174
|
result.append(item_id)
|
|
113
175
|
return result
|
|
@@ -158,16 +220,182 @@ def write_if_changed(path: Path, payload: str) -> None:
|
|
|
158
220
|
atomic_write_text(path, payload)
|
|
159
221
|
|
|
160
222
|
|
|
161
|
-
|
|
162
|
-
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Stores
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
163
226
|
|
|
164
227
|
|
|
165
|
-
|
|
166
|
-
|
|
228
|
+
class Store:
|
|
229
|
+
"""一个 state 文件的落点:项目级(工作区)或书级(书目录)。"""
|
|
167
230
|
|
|
231
|
+
__slots__ = ("kind", "root", "book")
|
|
168
232
|
|
|
169
|
-
def
|
|
170
|
-
|
|
233
|
+
def __init__(self, kind: str, root: Path, book: str | None) -> None:
|
|
234
|
+
self.kind = kind
|
|
235
|
+
self.root = root
|
|
236
|
+
self.book = book
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def prefix(self) -> str:
|
|
240
|
+
return STORE_PREFIX[self.kind]
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def state_path(self) -> Path:
|
|
244
|
+
return self.root / "_author-memory-state.json"
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def label(self) -> str:
|
|
248
|
+
return "项目级" if self.kind == "project" else f"书级({self.book})"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def project_store(workspace: Path) -> Store:
|
|
252
|
+
return Store("project", workspace.resolve() / ".story" / "作者记忆", None)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def peek_book_name(state_path: Path) -> str | None:
|
|
256
|
+
"""不校验整份 state,只取书名——书级 store 一旦建立,书名以 state 为准,
|
|
257
|
+
书目录改名不影响。"""
|
|
258
|
+
if not state_path.exists():
|
|
259
|
+
return None
|
|
260
|
+
document = read_json(state_path)
|
|
261
|
+
name = document.get("book") if isinstance(document, dict) else None
|
|
262
|
+
return name if isinstance(name, str) and name.strip() else None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def enclosing_workspace(workspace: Path) -> Path | None:
|
|
266
|
+
"""--workspace 指到的其实是多书工作区里的一本书时,返回真正的创作工作区。
|
|
267
|
+
|
|
268
|
+
认三种迹象,任一成立即是:①工作区的上一层目录叫 长篇 / 短篇(story-setup 的书
|
|
269
|
+
目录约定,返回再上一层);②某个祖先目录有 .active-book;③某个祖先目录有项目级
|
|
270
|
+
store(不带 book 字段的 state)——但工作区自己已有项目级 store 时不认这一条,最
|
|
271
|
+
近的项目级 store 就是它自己,免得主目录里一份误建的 store 挡住正常的单书工作区。
|
|
272
|
+
读不出来的祖先 state 不算迹象。"""
|
|
273
|
+
resolved = workspace.resolve()
|
|
274
|
+
if resolved.parent.name in {"长篇", "短篇"}:
|
|
275
|
+
return resolved.parent.parent
|
|
276
|
+
own = project_store(resolved).state_path
|
|
277
|
+
owns_project_store = own.exists() and peek_book_name(own) is None
|
|
278
|
+
for ancestor in resolved.parents:
|
|
279
|
+
if (ancestor / ".active-book").exists():
|
|
280
|
+
return ancestor
|
|
281
|
+
if owns_project_store:
|
|
282
|
+
continue
|
|
283
|
+
state_path = project_store(ancestor).state_path
|
|
284
|
+
try:
|
|
285
|
+
document = read_json(state_path) if state_path.is_file() else None
|
|
286
|
+
except AuthorMemoryError:
|
|
287
|
+
document = None
|
|
288
|
+
if isinstance(document, dict) and "book" not in document:
|
|
289
|
+
return ancestor
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def same_directory(first: Path, second: Path) -> bool:
|
|
294
|
+
# samefile 认得大小写不敏感文件系统(macOS APFS 默认)上只差大小写的同一目录。
|
|
295
|
+
if first.exists() and second.exists():
|
|
296
|
+
return os.path.samefile(first, second)
|
|
297
|
+
return first.resolve() == second.resolve()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def is_single_root(workspace: Path, book_root: Path | None) -> bool:
|
|
301
|
+
"""单书布局:书根就是工作区(正文/、大纲/、追踪/ 直接在工作区根)。
|
|
302
|
+
|
|
303
|
+
同一目录还可能是多书工作区里的一本书被误传成了 --workspace(书目录自己也含
|
|
304
|
+
.story/作者记忆/)。那时按单书布局处理会把书级 state 挪进 书级/、在原处补一份
|
|
305
|
+
空的项目级 state,此后正确的调用全部失败,所以一律报错、零写入。"""
|
|
306
|
+
if book_root is None or not same_directory(book_root, workspace):
|
|
307
|
+
return False
|
|
308
|
+
outer = enclosing_workspace(workspace)
|
|
309
|
+
require(
|
|
310
|
+
outer is None,
|
|
311
|
+
f"{workspace} 是创作工作区 {outer} 里的一本书,不是单书工作区:--workspace 应传 {outer},"
|
|
312
|
+
f"这个目录放 --book-root",
|
|
313
|
+
)
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def book_memory_root(workspace: Path, book_root: Path) -> Path:
|
|
318
|
+
root = book_root.resolve() / ".story" / "作者记忆"
|
|
319
|
+
# 单书布局下两级 store 的默认落点是同一个 state 文件;书级改住子目录,
|
|
320
|
+
# 两份 state、两套派生视图、两条修订线仍各自独立。
|
|
321
|
+
return root / SINGLE_ROOT_BOOK_DIR if is_single_root(workspace, book_root) else root
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def sole_legacy_book_name(project: Store) -> str | None:
|
|
325
|
+
"""项目级 store 里存量 book 条目只指向一本书时返回该书名(单书布局的升级默认)。"""
|
|
326
|
+
if not project.state_path.exists():
|
|
327
|
+
return None
|
|
328
|
+
document = read_json(project.state_path)
|
|
329
|
+
items = document.get("items") if isinstance(document, dict) else None
|
|
330
|
+
names: dict[str, str] = {}
|
|
331
|
+
for item in (items.values() if isinstance(items, dict) else ()):
|
|
332
|
+
scope = item.get("scope") if isinstance(item, dict) else None
|
|
333
|
+
if (
|
|
334
|
+
isinstance(scope, dict) and scope.get("level") == "book"
|
|
335
|
+
and isinstance(scope.get("value"), str) and scope["value"].strip()
|
|
336
|
+
and item.get("status") in {"active", "pending", "conflict"}
|
|
337
|
+
):
|
|
338
|
+
names.setdefault(scope["value"].casefold(), scope["value"])
|
|
339
|
+
return next(iter(names.values())) if len(names) == 1 else None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def book_store(workspace: Path, book_root: Path, book: str | None) -> Store:
|
|
343
|
+
require(book_root.exists() and book_root.is_dir(), f"book root does not exist: {book_root}")
|
|
344
|
+
resolved = book_root.resolve()
|
|
345
|
+
root = book_memory_root(workspace, book_root)
|
|
346
|
+
name = optional_text(book, "book", max_bytes=180)
|
|
347
|
+
stored = peek_book_name(root / "_author-memory-state.json")
|
|
348
|
+
if name is None:
|
|
349
|
+
name = stored
|
|
350
|
+
elif stored is not None:
|
|
351
|
+
require(
|
|
352
|
+
name.casefold() == stored.casefold(),
|
|
353
|
+
f"--book「{name}」与 {root} 里记录的书「{stored}」不一致",
|
|
354
|
+
)
|
|
355
|
+
if name is None and is_single_root(workspace, book_root):
|
|
356
|
+
# 单书工作区的目录名常常不是书名;升级前的本书条目只指向一本书时,以它为准,
|
|
357
|
+
# 否则 migrate 会按目录名找不到存量、静默迁移零条。
|
|
358
|
+
name = sole_legacy_book_name(project_store(workspace))
|
|
359
|
+
if name is None:
|
|
360
|
+
name = clean_text(resolved.name, "book root name", max_bytes=180)
|
|
361
|
+
return Store("book", root, name)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def relocate_misplaced_book_state(workspace: Path, book_root: Path | None) -> None:
|
|
365
|
+
"""单书布局的自愈:旧版把书级 state(带 state.book)写在了项目级位置,此后项目级
|
|
366
|
+
读写一律失败。带 --book-root {工作区} 运行任一命令时,把它原子移进书级子目录,
|
|
367
|
+
再在项目级位置补一份空 state 重建视图。state 内容不变,不推进任何修订。"""
|
|
368
|
+
if not is_single_root(workspace, book_root):
|
|
369
|
+
return
|
|
370
|
+
project = project_store(workspace)
|
|
371
|
+
misplaced = peek_book_name(project.state_path)
|
|
372
|
+
if misplaced is None:
|
|
373
|
+
return
|
|
374
|
+
target = Store("book", book_memory_root(workspace, book_root), misplaced)
|
|
375
|
+
require(
|
|
376
|
+
not target.state_path.exists(),
|
|
377
|
+
f"{project.state_path} 与 {target.state_path} 都是书级 state,无法自动归位;"
|
|
378
|
+
f"保留修订较新的一份移到 {target.state_path},另一份备份后移走再重跑",
|
|
379
|
+
)
|
|
380
|
+
state = validate_state(read_json(project.state_path), store=target)
|
|
381
|
+
target.root.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
os.replace(project.state_path, target.state_path)
|
|
383
|
+
write_snapshot(target, state)
|
|
384
|
+
write_snapshot(project, empty_state())
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def resolve_target_store(kind: str, workspace: Path, book_root: Path | None, book: str | None) -> Store:
|
|
388
|
+
if kind == "project":
|
|
389
|
+
return project_store(workspace)
|
|
390
|
+
require(
|
|
391
|
+
book_root is not None,
|
|
392
|
+
"book 级条目须传 --book-root {书目录}——记忆随书存放在 {书}/.story/作者记忆/,不再写进工作区的项目级 store",
|
|
393
|
+
)
|
|
394
|
+
return book_store(workspace, book_root, book)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def empty_state(book: str | None = None) -> dict[str, Any]:
|
|
398
|
+
state: dict[str, Any] = {
|
|
171
399
|
"schema_version": STATE_SCHEMA_VERSION,
|
|
172
400
|
"state_revision": 0,
|
|
173
401
|
"next_item_number": 1,
|
|
@@ -175,6 +403,14 @@ def empty_state() -> dict[str, Any]:
|
|
|
175
403
|
"journal": [],
|
|
176
404
|
"applied_transactions": {},
|
|
177
405
|
}
|
|
406
|
+
if book is not None:
|
|
407
|
+
state["book"] = book
|
|
408
|
+
return state
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# Validation
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
178
414
|
|
|
179
415
|
|
|
180
416
|
def normalize_scope(value: object, label: str) -> dict[str, str | None]:
|
|
@@ -208,19 +444,19 @@ def normalize_item(value: object, label: str) -> dict[str, Any]:
|
|
|
208
444
|
}
|
|
209
445
|
require_known_keys(item, allowed, label)
|
|
210
446
|
item_id = clean_text(item.get("id"), f"{label}.id", max_bytes=32)
|
|
211
|
-
require(
|
|
447
|
+
require(is_item_id(item_id), f"{label}.id is invalid")
|
|
212
448
|
evidence = [normalize_evidence(entry, f"{label}.evidence[{index}]") for index, entry in enumerate(as_list(item.get("evidence"), f"{label}.evidence"))]
|
|
213
449
|
require(bool(evidence), f"{label}.evidence must not be empty")
|
|
214
450
|
status = choice(item.get("status"), STATUSES, f"{label}.status")
|
|
215
451
|
conflicts = clean_id_list(item.get("conflicts_with"), f"{label}.conflicts_with")
|
|
216
452
|
superseded_by = optional_text(item.get("superseded_by"), f"{label}.superseded_by", max_bytes=32)
|
|
217
453
|
if superseded_by is not None:
|
|
218
|
-
require(
|
|
454
|
+
require(is_item_id(superseded_by), f"{label}.superseded_by is invalid")
|
|
219
455
|
return {
|
|
220
456
|
"id": item_id,
|
|
221
457
|
"kind": choice(item.get("kind"), KINDS, f"{label}.kind"),
|
|
222
458
|
"scope": normalize_scope(item.get("scope"), f"{label}.scope"),
|
|
223
|
-
"assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=
|
|
459
|
+
"assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=LEGACY_ASSERTION_MAX_BYTES),
|
|
224
460
|
"confidence": choice(item.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
|
|
225
461
|
"importance": choice(item.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
|
|
226
462
|
"status": status,
|
|
@@ -235,11 +471,28 @@ def normalize_item(value: object, label: str) -> dict[str, Any]:
|
|
|
235
471
|
}
|
|
236
472
|
|
|
237
473
|
|
|
238
|
-
def
|
|
474
|
+
def scope_fits_store(scope: dict[str, str | None], book: str | None) -> bool:
|
|
475
|
+
if book is None:
|
|
476
|
+
return True # 项目级:存量 book 条目仍合法,只为老库能通过校验并被 migrate 搬走
|
|
477
|
+
return scope["level"] == "book" and (scope["value"] or "").casefold() == book.casefold()
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def validate_state(value: object, *, store: Store) -> dict[str, Any]:
|
|
239
481
|
state = as_mapping(value, "state")
|
|
240
|
-
allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions"}
|
|
482
|
+
allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions", "book"}
|
|
241
483
|
require_known_keys(state, allowed, "state")
|
|
242
484
|
require(state.get("schema_version") == STATE_SCHEMA_VERSION, f"state.schema_version must be {STATE_SCHEMA_VERSION}")
|
|
485
|
+
if store.kind == "book":
|
|
486
|
+
book = clean_text(state.get("book"), "state.book", max_bytes=180)
|
|
487
|
+
require(store.book is not None and book.casefold() == store.book.casefold(), f"state.book「{book}」与目标书「{store.book}」不一致")
|
|
488
|
+
else:
|
|
489
|
+
require(
|
|
490
|
+
"book" not in state,
|
|
491
|
+
"project-level state must not carry state.book:这份其实是书级 state。--workspace 指到了"
|
|
492
|
+
"长篇/、短篇/ 下的书目录时,改传创作工作区根、书目录放 --book-root;只有书根就是工作区"
|
|
493
|
+
"(单书布局)时,带 --book-root {工作区} 重跑任一命令即自动移进 .story/作者记忆/书级/",
|
|
494
|
+
)
|
|
495
|
+
book = None
|
|
243
496
|
revision = as_int(state.get("state_revision"), "state.state_revision")
|
|
244
497
|
next_number = as_int(state.get("next_item_number"), "state.next_item_number", minimum=1)
|
|
245
498
|
raw_items = as_mapping(state.get("items"), "state.items")
|
|
@@ -248,7 +501,9 @@ def validate_state(value: object) -> dict[str, Any]:
|
|
|
248
501
|
for raw_id, raw_item in raw_items.items():
|
|
249
502
|
normalized = normalize_item(raw_item, f"state.items.{raw_id}")
|
|
250
503
|
require(raw_id == normalized["id"], f"state.items key {raw_id} does not match item id")
|
|
251
|
-
|
|
504
|
+
require(raw_id.startswith(store.prefix), f"state.items.{raw_id} does not belong to the {store.label} store(前缀应为 {store.prefix})")
|
|
505
|
+
require(scope_fits_store(normalized["scope"], book), f"state.items.{raw_id} scope does not belong to book「{book}」")
|
|
506
|
+
max_number = max(max_number, id_number(raw_id))
|
|
252
507
|
require(normalized["created_revision"] <= normalized["updated_revision"] <= revision, f"state.items.{raw_id} revision is ahead of state")
|
|
253
508
|
items[raw_id] = normalized
|
|
254
509
|
require(next_number > max_number, "state.next_item_number must be greater than every allocated item id")
|
|
@@ -295,7 +550,7 @@ def validate_state(value: object) -> dict[str, Any]:
|
|
|
295
550
|
item_ids = clean_id_list(mapping.get("item_ids"), f"state.applied_transactions.{transaction_id}.item_ids")
|
|
296
551
|
require(bool(item_ids), f"state.applied_transactions.{transaction_id}.item_ids must not be empty")
|
|
297
552
|
require(all(item_id in items for item_id in item_ids), f"state.applied_transactions.{transaction_id}.item_ids references an unknown item")
|
|
298
|
-
|
|
553
|
+
result = {
|
|
299
554
|
"schema_version": STATE_SCHEMA_VERSION,
|
|
300
555
|
"state_revision": revision,
|
|
301
556
|
"next_item_number": next_number,
|
|
@@ -303,6 +558,15 @@ def validate_state(value: object) -> dict[str, Any]:
|
|
|
303
558
|
"journal": copy.deepcopy(journal),
|
|
304
559
|
"applied_transactions": copy.deepcopy(transactions),
|
|
305
560
|
}
|
|
561
|
+
if book is not None:
|
|
562
|
+
result["book"] = book
|
|
563
|
+
return result
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def load_state(store: Store) -> dict[str, Any] | None:
|
|
567
|
+
if not store.state_path.exists():
|
|
568
|
+
return None
|
|
569
|
+
return validate_state(read_json(store.state_path), store=store)
|
|
306
570
|
|
|
307
571
|
|
|
308
572
|
def normalize_preference(value: object, label: str, *, allow_status: bool) -> dict[str, Any]:
|
|
@@ -311,12 +575,17 @@ def normalize_preference(value: object, label: str, *, allow_status: bool) -> di
|
|
|
311
575
|
if allow_status:
|
|
312
576
|
allowed |= {"status", "conflicts_with"}
|
|
313
577
|
require_known_keys(preference, allowed, label)
|
|
314
|
-
|
|
578
|
+
raw_source = preference.get("source")
|
|
579
|
+
require(
|
|
580
|
+
raw_source not in {"repeated_correction", "inferred_pattern"},
|
|
581
|
+
f"{label}.source「{raw_source}」已不再写入:作者记忆只记作者明确表达的偏好,"
|
|
582
|
+
f"不从重复修改或成稿轨迹推断;范围含糊的原话用 explicit_user 并置 status=pending",
|
|
583
|
+
)
|
|
584
|
+
source = choice(raw_source, WRITE_SOURCES, f"{label}.source")
|
|
315
585
|
status = choice(preference.get("status"), ("active", "pending", "conflict"), f"{label}.status") if allow_status else "active"
|
|
316
586
|
conflicts = clean_id_list(preference.get("conflicts_with", []), f"{label}.conflicts_with") if allow_status else []
|
|
317
587
|
if status == "active":
|
|
318
588
|
require(not conflicts, f"{label}.conflicts_with must be empty for active status")
|
|
319
|
-
require(source not in {"repeated_correction", "inferred_pattern"}, f"{label} inferred evidence must remain pending")
|
|
320
589
|
elif status == "conflict":
|
|
321
590
|
require(bool(conflicts), f"{label}.conflicts_with is required for conflict status")
|
|
322
591
|
else:
|
|
@@ -324,7 +593,9 @@ def normalize_preference(value: object, label: str, *, allow_status: bool) -> di
|
|
|
324
593
|
return {
|
|
325
594
|
"kind": choice(preference.get("kind"), KINDS, f"{label}.kind"),
|
|
326
595
|
"scope": normalize_scope(preference.get("scope"), f"{label}.scope"),
|
|
327
|
-
|
|
596
|
+
# 这里按存量上限收;ASSERTION_MAX_BYTES 只在真正新建条目时校验
|
|
597
|
+
# (require_new_item_assertion),好让存量长断言仍能被强化。
|
|
598
|
+
"assertion": clean_text(preference.get("assertion"), f"{label}.assertion", max_bytes=LEGACY_ASSERTION_MAX_BYTES),
|
|
328
599
|
"quote": clean_text(preference.get("quote"), f"{label}.quote", max_bytes=768),
|
|
329
600
|
"source_ref": optional_text(preference.get("source_ref"), f"{label}.source_ref", max_bytes=240),
|
|
330
601
|
"source": source,
|
|
@@ -397,25 +668,115 @@ def normalize_record_event(value: object) -> dict[str, Any]:
|
|
|
397
668
|
return {"event_id": event_id, "operation": normalized["operations"][0]}
|
|
398
669
|
|
|
399
670
|
|
|
671
|
+
# ---------------------------------------------------------------------------
|
|
672
|
+
# Routing: which store does an operation belong to?
|
|
673
|
+
# ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def scope_store_kind(scope: dict[str, str | None]) -> str:
|
|
677
|
+
return "book" if scope["level"] == "book" else "project"
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def store_kind_label(kind: str) -> str:
|
|
681
|
+
return "书级(BP)" if kind == "book" else "项目级(AP)"
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def operation_store_kind(operation: dict[str, Any], label: str) -> str:
|
|
685
|
+
action = operation["action"]
|
|
686
|
+
if action in {"decide", "forget"}:
|
|
687
|
+
return id_store(operation["item_id"])
|
|
688
|
+
preference = operation["preference"]
|
|
689
|
+
kind = scope_store_kind(preference["scope"])
|
|
690
|
+
if action == "remember":
|
|
691
|
+
for conflict_id in preference["conflicts_with"]:
|
|
692
|
+
require(
|
|
693
|
+
id_store(conflict_id) == kind,
|
|
694
|
+
f"{label}.conflicts_with 只能引用同一 store 的条目({conflict_id} 在{store_kind_label(id_store(conflict_id))}):"
|
|
695
|
+
f"本书例外不算与全局规则冲突,直接 remember 为 book 条目即可",
|
|
696
|
+
)
|
|
697
|
+
return kind
|
|
698
|
+
for old_id in operation["old_ids"]:
|
|
699
|
+
require(
|
|
700
|
+
id_store(old_id) == kind,
|
|
701
|
+
f"{label} replace 不能跨 store:{old_id} 在{store_kind_label(id_store(old_id))},新条目范围属于{store_kind_label(kind)};"
|
|
702
|
+
f"跨 store 改版拆成 forget+remember,存量 book 条目先 migrate",
|
|
703
|
+
)
|
|
704
|
+
return kind
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def transaction_store_kind(transaction: dict[str, Any]) -> str:
|
|
708
|
+
kinds = {
|
|
709
|
+
operation_store_kind(operation, f"transaction.operations[{index}]")
|
|
710
|
+
for index, operation in enumerate(transaction["operations"])
|
|
711
|
+
}
|
|
712
|
+
require(
|
|
713
|
+
len(kinds) == 1,
|
|
714
|
+
"一份事务只能写一个 store:项目级(global/genre/workflow 范围或 AP 编号)与书级(book 范围或 BP 编号)的操作要分开提交",
|
|
715
|
+
)
|
|
716
|
+
return kinds.pop()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# Transactions
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
400
724
|
def transaction_digest(transaction: dict[str, Any]) -> str:
|
|
401
725
|
canonical = json.dumps(transaction, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
402
726
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
403
727
|
|
|
404
728
|
|
|
405
729
|
def fingerprint(preference: dict[str, Any]) -> str:
|
|
730
|
+
"""同一条偏好的身份:kind+scope+断言,scope.value 与 same_scope_value 同样按
|
|
731
|
+
casefold 比——否则「Urban」「urban」会各建一条,两条同断言一起挤进 prompt。"""
|
|
732
|
+
scope = preference["scope"]
|
|
406
733
|
value = {
|
|
407
734
|
"kind": preference["kind"],
|
|
408
|
-
"scope":
|
|
735
|
+
"scope": {"level": scope["level"], "value": None if scope["value"] is None else scope["value"].casefold()},
|
|
409
736
|
"assertion": preference["assertion"].casefold(),
|
|
410
737
|
}
|
|
411
738
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
412
739
|
|
|
413
740
|
|
|
414
|
-
def
|
|
415
|
-
|
|
741
|
+
def require_new_item_assertion(preference: dict[str, Any]) -> None:
|
|
742
|
+
"""新建条目的断言限一句话。强化已有条目走不到这里——存量长断言必须还能
|
|
743
|
+
被确认,否则作者重申老偏好只会派生一条重复条目,库反而更挤。"""
|
|
744
|
+
size = len(preference["assertion"].encode("utf-8"))
|
|
745
|
+
require(
|
|
746
|
+
size <= ASSERTION_MAX_BYTES,
|
|
747
|
+
f"新条目的 assertion {size} 字节,超出 {ASSERTION_MAX_BYTES} 字节上限——"
|
|
748
|
+
f"断言限一句话,需要解释的背景写进 reason。若这是对已有条目的重申,"
|
|
749
|
+
f"原样使用该条目的 assertion 即可强化(不受本上限约束);"
|
|
750
|
+
f"若确实是几条互不依赖的偏好,才拆成几条分别记录。",
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def require_scope_fits_store(state: dict[str, Any], scope: dict[str, str | None]) -> None:
|
|
755
|
+
book = state.get("book")
|
|
756
|
+
if book is None:
|
|
757
|
+
require(
|
|
758
|
+
scope["level"] != "book",
|
|
759
|
+
"book 级条目须传 --book-root {书目录}——记忆随书存放在 {书}/.story/作者记忆/,不再写进工作区的项目级 store",
|
|
760
|
+
)
|
|
761
|
+
else:
|
|
762
|
+
require(
|
|
763
|
+
scope_fits_store(scope, book),
|
|
764
|
+
f"book 条目的范围「{scope['value']}」与本书 store「{book}」不一致——只有这本书的条目才住在这个书目录",
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def next_item_id(state: dict[str, Any]) -> str:
|
|
769
|
+
prefix = STORE_PREFIX["book"] if state.get("book") is not None else STORE_PREFIX["project"]
|
|
770
|
+
item_id = f"{prefix}{state['next_item_number']:03d}"
|
|
416
771
|
state["next_item_number"] += 1
|
|
772
|
+
return item_id
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def allocate_item(state: dict[str, Any], preference: dict[str, Any], revision: int) -> dict[str, Any]:
|
|
776
|
+
require_new_item_assertion(preference)
|
|
777
|
+
require_scope_fits_store(state, preference["scope"])
|
|
417
778
|
return {
|
|
418
|
-
"id":
|
|
779
|
+
"id": next_item_id(state),
|
|
419
780
|
"kind": preference["kind"],
|
|
420
781
|
"scope": copy.deepcopy(preference["scope"]),
|
|
421
782
|
"assertion": preference["assertion"],
|
|
@@ -449,6 +810,7 @@ def require_item(state: dict[str, Any], item_id: str, label: str) -> dict[str, A
|
|
|
449
810
|
|
|
450
811
|
|
|
451
812
|
def apply_remember(state: dict[str, Any], preference: dict[str, Any], revision: int) -> str:
|
|
813
|
+
require_scope_fits_store(state, preference["scope"])
|
|
452
814
|
for conflict_id in preference["conflicts_with"]:
|
|
453
815
|
conflict = require_item(state, conflict_id, "remember")
|
|
454
816
|
require(conflict["status"] == "active", f"remember conflict {conflict_id} must be active")
|
|
@@ -493,6 +855,23 @@ def apply_decide(state: dict[str, Any], operation: dict[str, Any], revision: int
|
|
|
493
855
|
return f"{verb} {item['id']}:{item['assertion']}"
|
|
494
856
|
|
|
495
857
|
|
|
858
|
+
def release_conflicts(state: dict[str, Any], removed_ids: set[str], revision: int) -> int:
|
|
859
|
+
"""被撤下的 active 条目不再是任何候选的冲突对象;冲突对象清空的候选退回 pending。"""
|
|
860
|
+
released = 0
|
|
861
|
+
for candidate in state["items"].values():
|
|
862
|
+
if candidate["status"] != "conflict":
|
|
863
|
+
continue
|
|
864
|
+
retained = [item_id for item_id in candidate["conflicts_with"] if item_id not in removed_ids]
|
|
865
|
+
if retained == candidate["conflicts_with"]:
|
|
866
|
+
continue
|
|
867
|
+
candidate["conflicts_with"] = retained
|
|
868
|
+
candidate["updated_revision"] = revision
|
|
869
|
+
if not retained:
|
|
870
|
+
candidate["status"] = "pending"
|
|
871
|
+
released += 1
|
|
872
|
+
return released
|
|
873
|
+
|
|
874
|
+
|
|
496
875
|
def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
|
|
497
876
|
old_items = [require_item(state, item_id, "replace") for item_id in operation["old_ids"]]
|
|
498
877
|
for item in old_items:
|
|
@@ -505,19 +884,7 @@ def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: in
|
|
|
505
884
|
item["status"] = "superseded"
|
|
506
885
|
item["superseded_by"] = replacement["id"]
|
|
507
886
|
item["updated_revision"] = revision
|
|
508
|
-
|
|
509
|
-
released = 0
|
|
510
|
-
for candidate in state["items"].values():
|
|
511
|
-
if candidate["status"] != "conflict":
|
|
512
|
-
continue
|
|
513
|
-
retained = [item_id for item_id in candidate["conflicts_with"] if item_id not in old_ids]
|
|
514
|
-
if retained == candidate["conflicts_with"]:
|
|
515
|
-
continue
|
|
516
|
-
candidate["conflicts_with"] = retained
|
|
517
|
-
candidate["updated_revision"] = revision
|
|
518
|
-
if not retained:
|
|
519
|
-
candidate["status"] = "pending"
|
|
520
|
-
released += 1
|
|
887
|
+
released = release_conflicts(state, {item["id"] for item in old_items}, revision)
|
|
521
888
|
replaced = ", ".join(item["id"] for item in old_items)
|
|
522
889
|
suffix = f";{released} 个其他冲突候选退回待确认" if released else ""
|
|
523
890
|
return f"用 {replacement['id']} 替代 {replaced}:{replacement['assertion']}{suffix}"
|
|
@@ -531,20 +898,46 @@ def apply_forget(state: dict[str, Any], operation: dict[str, Any], revision: int
|
|
|
531
898
|
item["reason"] = operation["reason"]
|
|
532
899
|
item["updated_revision"] = revision
|
|
533
900
|
add_evidence(item, operation["quote"], None)
|
|
534
|
-
released =
|
|
535
|
-
for candidate in state["items"].values():
|
|
536
|
-
if candidate["status"] != "conflict" or item["id"] not in candidate["conflicts_with"]:
|
|
537
|
-
continue
|
|
538
|
-
candidate["conflicts_with"] = [conflict_id for conflict_id in candidate["conflicts_with"] if conflict_id != item["id"]]
|
|
539
|
-
candidate["updated_revision"] = revision
|
|
540
|
-
if not candidate["conflicts_with"]:
|
|
541
|
-
candidate["status"] = "pending"
|
|
542
|
-
released += 1
|
|
901
|
+
released = release_conflicts(state, {item["id"]}, revision)
|
|
543
902
|
suffix = f";{released} 个冲突候选退回待确认" if released else ""
|
|
544
903
|
return f"忘记 {item['id']}:{item['assertion']}{suffix}"
|
|
545
904
|
|
|
546
905
|
|
|
547
|
-
def
|
|
906
|
+
def committed_now() -> str:
|
|
907
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def seal_transaction(
|
|
911
|
+
updated: dict[str, Any],
|
|
912
|
+
transaction_id: str,
|
|
913
|
+
digest: str,
|
|
914
|
+
summaries: list[str],
|
|
915
|
+
*,
|
|
916
|
+
store: Store,
|
|
917
|
+
) -> dict[str, Any]:
|
|
918
|
+
"""把本次修订写进 journal / applied_transactions,并整份校验。"""
|
|
919
|
+
revision = updated["state_revision"] + 1
|
|
920
|
+
updated["state_revision"] = revision
|
|
921
|
+
updated["journal"].append({
|
|
922
|
+
"revision": revision,
|
|
923
|
+
"transaction_id": transaction_id,
|
|
924
|
+
"committed_at": committed_now(),
|
|
925
|
+
"summaries": summaries,
|
|
926
|
+
})
|
|
927
|
+
item_ids = sorted(
|
|
928
|
+
(item_id for item_id, item in updated["items"].items() if item["updated_revision"] == revision),
|
|
929
|
+
key=id_number,
|
|
930
|
+
)
|
|
931
|
+
require(bool(item_ids), "transaction did not update any author-memory item")
|
|
932
|
+
updated["applied_transactions"][transaction_id] = {
|
|
933
|
+
"revision": revision,
|
|
934
|
+
"digest": digest,
|
|
935
|
+
"item_ids": item_ids,
|
|
936
|
+
}
|
|
937
|
+
return validate_state(updated, store=store)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest: str, *, store: Store) -> tuple[dict[str, Any], list[str]]:
|
|
548
941
|
applied = state["applied_transactions"].get(transaction["transaction_id"])
|
|
549
942
|
if applied is not None:
|
|
550
943
|
require(applied["digest"] == digest, "transaction_id was already used with different content")
|
|
@@ -562,25 +955,12 @@ def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest
|
|
|
562
955
|
summaries.append(apply_replace(updated, operation, revision))
|
|
563
956
|
else:
|
|
564
957
|
summaries.append(apply_forget(updated, operation, revision))
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
"summaries": summaries,
|
|
572
|
-
})
|
|
573
|
-
item_ids = sorted(
|
|
574
|
-
(item_id for item_id, item in updated["items"].items() if item["updated_revision"] == revision),
|
|
575
|
-
key=lambda item_id: int(item_id[2:]),
|
|
576
|
-
)
|
|
577
|
-
require(bool(item_ids), "transaction did not update any author-memory item")
|
|
578
|
-
updated["applied_transactions"][transaction["transaction_id"]] = {
|
|
579
|
-
"revision": revision,
|
|
580
|
-
"digest": digest,
|
|
581
|
-
"item_ids": item_ids,
|
|
582
|
-
}
|
|
583
|
-
return validate_state(updated), summaries
|
|
958
|
+
return seal_transaction(updated, transaction["transaction_id"], digest, summaries, store=store), summaries
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ---------------------------------------------------------------------------
|
|
962
|
+
# Derived views
|
|
963
|
+
# ---------------------------------------------------------------------------
|
|
584
964
|
|
|
585
965
|
|
|
586
966
|
def scope_label(scope: dict[str, str | None]) -> str:
|
|
@@ -590,24 +970,36 @@ def scope_label(scope: dict[str, str | None]) -> str:
|
|
|
590
970
|
return f"{labels[scope['level']]}:{scope['value']}"
|
|
591
971
|
|
|
592
972
|
|
|
973
|
+
def store_caption(state: dict[str, Any]) -> str:
|
|
974
|
+
book = state.get("book")
|
|
975
|
+
if book is None:
|
|
976
|
+
return "项目级记忆(全局、题材、流程);各书的书级记忆住在各自书目录的 .story/作者记忆/。"
|
|
977
|
+
return f"本书「{book}」的书级记忆;全局、题材、流程记忆住在工作区的 .story/作者记忆/。"
|
|
978
|
+
|
|
979
|
+
|
|
593
980
|
def render_profile(state: dict[str, Any]) -> str:
|
|
594
981
|
lines = [
|
|
595
982
|
"# 作者画像",
|
|
596
983
|
"",
|
|
597
984
|
"<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
|
|
598
985
|
"",
|
|
599
|
-
f"> 状态修订:{state['state_revision']}
|
|
986
|
+
f"> 状态修订:{state['state_revision']}。{store_caption(state)}仅列出已确认偏好;当前明确要求、本书设定与硬性门禁优先。",
|
|
600
987
|
"",
|
|
601
988
|
]
|
|
602
989
|
active = [item for item in state["items"].values() if item["status"] == "active"]
|
|
603
990
|
for kind in KINDS:
|
|
604
991
|
lines.extend([f"## {KIND_TITLES[kind]}", ""])
|
|
605
|
-
items = sorted((item for item in active if item["kind"] == kind), key=lambda item:
|
|
992
|
+
items = sorted((item for item in active if item["kind"] == kind), key=lambda item: id_number(item["id"]))
|
|
606
993
|
if not items:
|
|
607
994
|
lines.extend(["- 暂无", ""])
|
|
608
995
|
continue
|
|
609
996
|
for item in items:
|
|
610
|
-
|
|
997
|
+
# 必须显示 importance:它决定超编时谁留在 prompt 里,而「整理作者
|
|
998
|
+
# 记忆」只以本文件为输入——不显示就无从判断该退役哪条。
|
|
999
|
+
lines.append(
|
|
1000
|
+
f"- **{item['id']}**〔{scope_label(item['scope'])}|重要 {item['importance']}"
|
|
1001
|
+
f"|把握 {item['confidence']}|确认 {item['confirmation_count']} 次〕{item['assertion']}"
|
|
1002
|
+
)
|
|
611
1003
|
lines.append("")
|
|
612
1004
|
return "\n".join(lines).rstrip() + "\n"
|
|
613
1005
|
|
|
@@ -618,10 +1010,10 @@ def render_pending(state: dict[str, Any]) -> str:
|
|
|
618
1010
|
"",
|
|
619
1011
|
"<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
|
|
620
1012
|
"",
|
|
621
|
-
f"> 状态修订:{state['state_revision']}
|
|
1013
|
+
f"> 状态修订:{state['state_revision']}。{store_caption(state)}待确认项不参与创作约束,也不应打断当前任务。",
|
|
622
1014
|
"",
|
|
623
1015
|
]
|
|
624
|
-
items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item:
|
|
1016
|
+
items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item: id_number(item["id"]))
|
|
625
1017
|
if not items:
|
|
626
1018
|
lines.extend(["暂无待确认项。", ""])
|
|
627
1019
|
for item in items:
|
|
@@ -630,7 +1022,7 @@ def render_pending(state: dict[str, Any]) -> str:
|
|
|
630
1022
|
"",
|
|
631
1023
|
f"- 候选习惯:{item['assertion']}",
|
|
632
1024
|
f"- 范围:{scope_label(item['scope'])}",
|
|
633
|
-
f"-
|
|
1025
|
+
f"- 原话:“{item['evidence'][-1]['quote']}”",
|
|
634
1026
|
f"- 依据:{item['reason']}",
|
|
635
1027
|
f"- 置信度 / 重要度:{item['confidence']} / {item['importance']}",
|
|
636
1028
|
])
|
|
@@ -646,6 +1038,8 @@ def render_journal(state: dict[str, Any]) -> str:
|
|
|
646
1038
|
"",
|
|
647
1039
|
"<!-- 由 author_memory_commit.py 生成,请勿手改;最近记录在前。 -->",
|
|
648
1040
|
"",
|
|
1041
|
+
f"> {store_caption(state)}",
|
|
1042
|
+
"",
|
|
649
1043
|
]
|
|
650
1044
|
if not state["journal"]:
|
|
651
1045
|
lines.extend(["暂无变更。", ""])
|
|
@@ -668,40 +1062,295 @@ def render_views(state: dict[str, Any]) -> dict[str, str]:
|
|
|
668
1062
|
return views
|
|
669
1063
|
|
|
670
1064
|
|
|
671
|
-
def write_snapshot(
|
|
672
|
-
root = memory_root(workspace)
|
|
1065
|
+
def write_snapshot(store: Store, state: dict[str, Any]) -> None:
|
|
673
1066
|
views = render_views(state)
|
|
674
1067
|
state_payload = json_payload(state)
|
|
675
1068
|
require(len(state_payload.encode("utf-8")) <= STATE_MAX_BYTES, f"_author-memory-state.json exceeds {STATE_MAX_BYTES} bytes")
|
|
676
1069
|
for name, payload in views.items():
|
|
677
|
-
write_if_changed(root / name, payload)
|
|
1070
|
+
write_if_changed(store.root / name, payload)
|
|
678
1071
|
# State is the authority and therefore the last commit point.
|
|
679
|
-
write_if_changed(state_path
|
|
1072
|
+
write_if_changed(store.state_path, state_payload)
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# ---------------------------------------------------------------------------
|
|
1076
|
+
# Query & budget
|
|
1077
|
+
# ---------------------------------------------------------------------------
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def same_scope_value(item_value: str | None, requested: str | None) -> bool:
|
|
1081
|
+
return requested is not None and item_value is not None and item_value.casefold() == requested.casefold()
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def summarize_assertion(assertion: str, *, limit: int = 14) -> str:
|
|
1085
|
+
return assertion if len(assertion) <= limit else assertion[:limit] + "…"
|
|
680
1086
|
|
|
681
1087
|
|
|
682
|
-
def
|
|
1088
|
+
def compact_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
1089
|
+
"""query 载荷只带这四个字段——估算与真实输出必须同一把尺。"""
|
|
1090
|
+
return {"id": item["id"], "kind": item["kind"], "scope": item["scope"], "assertion": item["assertion"]}
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def compact_bytes(item: dict[str, Any]) -> int:
|
|
1094
|
+
return len(json.dumps(compact_item(item), ensure_ascii=False).encode("utf-8"))
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
SCOPE_RANK = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def query_sort_key(item: dict[str, Any]) -> tuple[int, int, int, int, int]:
|
|
1101
|
+
"""重要度→本书例外→最近更新→确认次数→编号。
|
|
1102
|
+
|
|
1103
|
+
重要度必须排在 scope 之前:超编时先丢的应当是不重要的条目,而不是「凡
|
|
1104
|
+
全局一律先丢」。scope 在前会让任意数量的 low 本书琐事挤掉 high 的全局
|
|
1105
|
+
铁律——那恰恰是作者最不愿意丢的那一类。同重要度之内才按本书例外优先。
|
|
1106
|
+
两个 store 的修订号互不可比,但 book 条目只来自书级 store、其余只来自项目
|
|
1107
|
+
级,同 scope 必同 store,所以按修订号比「最近更新」在合并后仍成立。
|
|
1108
|
+
"""
|
|
1109
|
+
return (
|
|
1110
|
+
-RANK[item["importance"]],
|
|
1111
|
+
SCOPE_RANK[item["scope"]["level"]],
|
|
1112
|
+
-item["updated_revision"],
|
|
1113
|
+
-item["confirmation_count"],
|
|
1114
|
+
id_number(item["id"]),
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def fit_items(
|
|
1119
|
+
sorted_items: list[dict[str, Any]],
|
|
1120
|
+
revision: int,
|
|
1121
|
+
*,
|
|
1122
|
+
extra: dict[str, Any] | None = None,
|
|
1123
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
1124
|
+
"""按 query 输出信封把条目装进 QUERY_MAX_BYTES:装不下的跳过而不中断
|
|
1125
|
+
(一条长的不挡后面的短条),漏下的 ID 报进 omitted_ids(封顶
|
|
1126
|
+
OMITTED_IDS_MAX 条,omitted 保留真实总数)。返回 (结果文档, 全部漏下 ID)。
|
|
1127
|
+
|
|
1128
|
+
漏项恒按候选优先级排序,不按被丢弃的先后:收尾回吐的条目优先级高于循环
|
|
1129
|
+
里跳过的,若按追加顺序排,omitted_ids 的封顶正好会把最该报的那条切掉。
|
|
1130
|
+
`extra` 是同样计入信封的附加字段(book_revision 等)。
|
|
1131
|
+
"""
|
|
1132
|
+
result: dict[str, Any] = {
|
|
1133
|
+
"ok": True,
|
|
1134
|
+
"command": "query",
|
|
1135
|
+
"initialized": True,
|
|
1136
|
+
"revision": revision,
|
|
1137
|
+
"items": [],
|
|
1138
|
+
"omitted": 0,
|
|
1139
|
+
"omitted_ids": [],
|
|
1140
|
+
}
|
|
1141
|
+
result.update(extra or {})
|
|
1142
|
+
order = {item["id"]: index for index, item in enumerate(sorted_items)}
|
|
1143
|
+
kept: list[dict[str, Any]] = []
|
|
1144
|
+
dropped: set[str] = set()
|
|
1145
|
+
|
|
1146
|
+
def ordered_omitted() -> list[str]:
|
|
1147
|
+
return sorted(dropped, key=order.__getitem__)
|
|
1148
|
+
|
|
1149
|
+
def envelope_bytes() -> int:
|
|
1150
|
+
omitted = ordered_omitted()
|
|
1151
|
+
result["items"] = [compact_item(item) for item in kept]
|
|
1152
|
+
result["omitted"] = len(omitted)
|
|
1153
|
+
result["omitted_ids"] = omitted[:OMITTED_IDS_MAX]
|
|
1154
|
+
return len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8"))
|
|
1155
|
+
|
|
1156
|
+
for item in sorted_items:
|
|
1157
|
+
kept.append(item)
|
|
1158
|
+
if envelope_bytes() > QUERY_MAX_BYTES:
|
|
1159
|
+
kept.pop()
|
|
1160
|
+
dropped.add(item["id"])
|
|
1161
|
+
# omitted 计数落定后包可能恰好贴边超出一两个字节,回吐条目直到装下。
|
|
1162
|
+
while kept and envelope_bytes() > QUERY_MAX_BYTES:
|
|
1163
|
+
dropped.add(kept.pop()["id"])
|
|
1164
|
+
envelope_bytes()
|
|
1165
|
+
return result, ordered_omitted()
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def build_query_result(
|
|
1169
|
+
candidates: list[dict[str, Any]],
|
|
1170
|
+
project_state: dict[str, Any] | None,
|
|
1171
|
+
book_state: dict[str, Any] | None,
|
|
1172
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
1173
|
+
"""真实查询与写入端估算共用的唯一信封构造:候选已按优先级排好,这里补上
|
|
1174
|
+
book_revision 等附加字段再装填。两处信封差一个字段就是一次假阴性——回执
|
|
1175
|
+
说没事、query 照样丢条。"""
|
|
1176
|
+
extra: dict[str, Any] = {}
|
|
1177
|
+
if book_state is not None:
|
|
1178
|
+
extra["book_revision"] = book_state["state_revision"]
|
|
1179
|
+
revision = project_state["state_revision"] if project_state is not None else 0
|
|
1180
|
+
return fit_items(candidates, revision, extra=extra)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def merged_query(
|
|
1184
|
+
project_state: dict[str, Any] | None,
|
|
1185
|
+
book_state: dict[str, Any] | None,
|
|
1186
|
+
kinds: set[str],
|
|
1187
|
+
requested_scopes: dict[str, str | None],
|
|
1188
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
1189
|
+
"""真实查询的纯函数部分:项目级按 kind/scope 过滤(存量 book 条目不参与,
|
|
1190
|
+
migrate 后才回来),书级整个 store 就是这本书的,只按 status/kind 过滤;
|
|
1191
|
+
合并后按 重要度→本书例外→最近更新 装填。"""
|
|
1192
|
+
|
|
1193
|
+
def relevant(item: dict[str, Any]) -> bool:
|
|
1194
|
+
if item["status"] != "active" or item["kind"] not in kinds:
|
|
1195
|
+
return False
|
|
1196
|
+
level = item["scope"]["level"]
|
|
1197
|
+
if level == "global":
|
|
1198
|
+
return True
|
|
1199
|
+
return level != "book" and same_scope_value(item["scope"]["value"], requested_scopes[level])
|
|
1200
|
+
|
|
1201
|
+
candidates: list[dict[str, Any]] = []
|
|
1202
|
+
if project_state is not None:
|
|
1203
|
+
candidates.extend(item for item in project_state["items"].values() if relevant(item))
|
|
1204
|
+
if book_state is not None:
|
|
1205
|
+
candidates.extend(active_of_kinds(book_state, tuple(kinds)))
|
|
1206
|
+
candidates.sort(key=query_sort_key)
|
|
1207
|
+
return build_query_result(candidates, project_state, book_state)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def slice_weight(items: list[dict[str, Any]]) -> int:
|
|
1211
|
+
"""切片在真实载荷里的占位:compact 字节+每条在 JSON 数组里的分隔符。
|
|
1212
|
+
|
|
1213
|
+
只比 compact 字节会挑错切片——条目多、单条短的切片字节和更小,实际占位
|
|
1214
|
+
却更大,于是估算判「装得下」而真实查询溢出(warnings 假阴性)。
|
|
1215
|
+
"""
|
|
1216
|
+
return sum(compact_bytes(item) + 2 for item in items)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def active_of_kinds(state: dict[str, Any] | None, kinds: tuple[str, ...]) -> list[dict[str, Any]]:
|
|
1220
|
+
if state is None:
|
|
1221
|
+
return []
|
|
1222
|
+
return [item for item in state["items"].values() if item["status"] == "active" and item["kind"] in kinds]
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def worst_case_items(
|
|
1226
|
+
project_state: dict[str, Any] | None,
|
|
1227
|
+
book_state: dict[str, Any] | None,
|
|
1228
|
+
kinds: tuple[str, ...],
|
|
1229
|
+
) -> list[dict[str, Any]]:
|
|
1230
|
+
"""某个任务组合的最坏查询候选:全局条目+各 scope 维度上最重的单一切片。
|
|
1231
|
+
|
|
1232
|
+
一次查询只带一个 book/genre/workflow,不同书的条目不会同现,所以按切片
|
|
1233
|
+
取最重而不是全加起来,多书工作区才不会被粗算误伤。切片按 casefold 归并,
|
|
1234
|
+
与 same_scope_value 同一口径——否则只差大小写的同名书在这里算两个切片、
|
|
1235
|
+
在真实查询里却合成一个,估算就成了下界。
|
|
1236
|
+
|
|
1237
|
+
book 维度就是当前可见的书级 store(整个 store 是这本书的一片),所以写书级
|
|
1238
|
+
条目时「本书+全局」是精确的;写项目级条目而没传 --book-root 时看不到任何
|
|
1239
|
+
书级 store,估算只覆盖项目级。项目级里的存量 book 条目不参与查询,也不参与
|
|
1240
|
+
估算。
|
|
1241
|
+
"""
|
|
1242
|
+
pool = [item for item in active_of_kinds(project_state, kinds) if item["scope"]["level"] != "book"]
|
|
1243
|
+
worst = [item for item in pool if item["scope"]["level"] == "global"]
|
|
1244
|
+
slices: dict[str, dict[str, list[dict[str, Any]]]] = {"genre": {}, "workflow": {}}
|
|
1245
|
+
for item in pool:
|
|
1246
|
+
level = item["scope"]["level"]
|
|
1247
|
+
if level != "global":
|
|
1248
|
+
slices[level].setdefault((item["scope"]["value"] or "").casefold(), []).append(item)
|
|
1249
|
+
for level in ("genre", "workflow"):
|
|
1250
|
+
if slices[level]:
|
|
1251
|
+
worst.extend(max(slices[level].values(), key=slice_weight))
|
|
1252
|
+
worst.extend(active_of_kinds(book_state, kinds))
|
|
1253
|
+
worst.sort(key=query_sort_key)
|
|
1254
|
+
return worst
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def legacy_book_items(project_state: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
1258
|
+
"""项目级 store 里升级前写入、还活着的 book 条目——只有 migrate 会碰它们。"""
|
|
1259
|
+
if project_state is None:
|
|
1260
|
+
return []
|
|
1261
|
+
return [
|
|
1262
|
+
item for item in project_state["items"].values()
|
|
1263
|
+
if item["scope"]["level"] == "book" and item["status"] in {"active", "pending", "conflict"}
|
|
1264
|
+
]
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def query_budget_warnings(project_state: dict[str, Any] | None, book_state: dict[str, Any] | None) -> list[str]:
|
|
1268
|
+
"""写入回执的预算提醒:每个任务组合按最坏查询情形试装,装不下的点名。
|
|
1269
|
+
|
|
1270
|
+
写入永不因注入预算失败;提醒指向「整理作者记忆」。点名带断言首句——只给
|
|
1271
|
+
编号的话,作者不打开 作者画像.md 就无从判断丢的是什么。
|
|
1272
|
+
"""
|
|
1273
|
+
warnings: list[str] = []
|
|
1274
|
+
for task, kinds in QUERY_COMBOS.items():
|
|
1275
|
+
worst = worst_case_items(project_state, book_state, kinds)
|
|
1276
|
+
_, omitted = build_query_result(worst, project_state, book_state)
|
|
1277
|
+
if not omitted:
|
|
1278
|
+
continue
|
|
1279
|
+
assertions = {item["id"]: item["assertion"] for item in worst}
|
|
1280
|
+
shown = ";".join(f"{item_id}「{summarize_assertion(assertions[item_id])}」" for item_id in omitted[:3])
|
|
1281
|
+
more = f" 等 {len(omitted)} 条" if len(omitted) > 3 else ""
|
|
1282
|
+
warnings.append(
|
|
1283
|
+
f"「{task}」最坏查询装不下 {len(omitted)} 条,它们不会进入 prompt:"
|
|
1284
|
+
f"{shown}{more}——超出 {QUERY_MAX_BYTES} 字节注入预算。"
|
|
1285
|
+
f"说「整理作者记忆」可合并同义条、退役过时条。"
|
|
1286
|
+
)
|
|
1287
|
+
return warnings
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def visible_states(
|
|
1291
|
+
store: Store,
|
|
1292
|
+
updated: dict[str, Any],
|
|
1293
|
+
workspace: Path,
|
|
1294
|
+
book_root: Path | None,
|
|
1295
|
+
book: str | None,
|
|
1296
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[str]]:
|
|
1297
|
+
"""写入后可见的两级状态:刚写的那份用内存里的,另一份只读加载(没有就 None)。
|
|
1298
|
+
|
|
1299
|
+
此时写入已经落盘,另一份读不出来(书目录不存在、--book 与书级记录不符、state
|
|
1300
|
+
损坏)只能降级为提醒:若在这里报错,调用方会告诉作者「没记住」,换个 event_id
|
|
1301
|
+
重试就派生重复条目。"""
|
|
1302
|
+
try:
|
|
1303
|
+
if store.kind == "project":
|
|
1304
|
+
other = load_state(book_store(workspace, book_root, book)) if book_root is not None else None
|
|
1305
|
+
return updated, other, []
|
|
1306
|
+
return load_state(project_store(workspace)), updated, []
|
|
1307
|
+
except AuthorMemoryError as exc:
|
|
1308
|
+
other_label = "书级" if store.kind == "project" else "项目级"
|
|
1309
|
+
notice = f"已写入{store.label} store;但{other_label} store 读取失败,本次预算提醒没算上它:{exc}"
|
|
1310
|
+
return (updated, None, [notice]) if store.kind == "project" else (None, updated, [notice])
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
# ---------------------------------------------------------------------------
|
|
1314
|
+
# Commands
|
|
1315
|
+
# ---------------------------------------------------------------------------
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def prepare_workspace(workspace: Path, book_root: Path | None = None) -> None:
|
|
1319
|
+
"""每个命令的入口:校验工作区,并在单书布局下先把误放的书级 state 归位——
|
|
1320
|
+
归位之前项目级 store 读不出来,放在任何读取之前才能让只读查询也自愈。"""
|
|
683
1321
|
require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1322
|
+
relocate_misplaced_book_state(workspace, book_root)
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def store_fields(store: Store) -> dict[str, Any]:
|
|
1326
|
+
fields: dict[str, Any] = {"store": store.kind, "root": str(store.root)}
|
|
1327
|
+
if store.book is not None:
|
|
1328
|
+
fields["book"] = store.book
|
|
1329
|
+
return fields
|
|
691
1330
|
|
|
692
1331
|
|
|
693
|
-
def
|
|
694
|
-
|
|
695
|
-
|
|
1332
|
+
def command_init(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
|
|
1333
|
+
prepare_workspace(workspace, book_root)
|
|
1334
|
+
store = book_store(workspace, book_root, book) if book_root is not None else project_store(workspace)
|
|
1335
|
+
state = load_state(store)
|
|
1336
|
+
if state is None:
|
|
1337
|
+
state = empty_state(store.book)
|
|
1338
|
+
write_snapshot(store, state)
|
|
1339
|
+
return {"ok": True, "command": "init", "revision": state["state_revision"], **store_fields(store)}
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def command_commit(workspace: Path, book_root: Path | None, book: str | None, input_path: Path) -> dict[str, Any]:
|
|
1343
|
+
prepare_workspace(workspace, book_root)
|
|
696
1344
|
transaction = normalize_transaction(read_json(input_path))
|
|
1345
|
+
store = resolve_target_store(transaction_store_kind(transaction), workspace, book_root, book)
|
|
1346
|
+
require(store.state_path.exists(), f"{store.label} author memory is not initialized; run init first")
|
|
1347
|
+
state = load_state(store)
|
|
697
1348
|
digest = transaction_digest(transaction)
|
|
698
|
-
updated, summaries = apply_transaction(state, transaction, digest)
|
|
1349
|
+
updated, summaries = apply_transaction(state, transaction, digest, store=store)
|
|
699
1350
|
replayed = updated is state
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
# Repair missing or stale views during an idempotent retry.
|
|
704
|
-
write_snapshot(workspace, state)
|
|
1351
|
+
# 幂等重放时也重写快照,修复缺失或过期的派生视图。
|
|
1352
|
+
write_snapshot(store, updated)
|
|
1353
|
+
project_state, book_state, load_warnings = visible_states(store, updated, workspace, book_root, book)
|
|
705
1354
|
return {
|
|
706
1355
|
"ok": True,
|
|
707
1356
|
"command": "commit",
|
|
@@ -710,14 +1359,18 @@ def command_commit(workspace: Path, input_path: Path) -> dict[str, Any]:
|
|
|
710
1359
|
"replayed": replayed,
|
|
711
1360
|
"item_ids": updated["applied_transactions"][transaction["transaction_id"]]["item_ids"],
|
|
712
1361
|
"summaries": summaries,
|
|
1362
|
+
"warnings": load_warnings + query_budget_warnings(project_state, book_state),
|
|
1363
|
+
**store_fields(store),
|
|
713
1364
|
}
|
|
714
1365
|
|
|
715
1366
|
|
|
716
|
-
def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
|
|
717
|
-
|
|
1367
|
+
def command_record(workspace: Path, book_root: Path | None, book: str | None, input_path: Path) -> dict[str, Any]:
|
|
1368
|
+
prepare_workspace(workspace, book_root)
|
|
718
1369
|
event = normalize_record_event(read_json(input_path))
|
|
719
|
-
|
|
720
|
-
state =
|
|
1370
|
+
store = resolve_target_store(operation_store_kind(event["operation"], "event.operation"), workspace, book_root, book)
|
|
1371
|
+
state = load_state(store)
|
|
1372
|
+
if state is None:
|
|
1373
|
+
state = empty_state(store.book)
|
|
721
1374
|
transaction_id = f"record:{event['event_id']}"
|
|
722
1375
|
applied = state["applied_transactions"].get(transaction_id)
|
|
723
1376
|
expected_revision = applied["revision"] - 1 if applied is not None else state["state_revision"]
|
|
@@ -728,12 +1381,13 @@ def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
|
|
|
728
1381
|
"operations": [event["operation"]],
|
|
729
1382
|
}
|
|
730
1383
|
digest = transaction_digest(transaction)
|
|
731
|
-
updated, summaries = apply_transaction(state, transaction, digest)
|
|
1384
|
+
updated, summaries = apply_transaction(state, transaction, digest, store=store)
|
|
732
1385
|
replayed = updated is state
|
|
733
|
-
write_snapshot(
|
|
1386
|
+
write_snapshot(store, updated)
|
|
734
1387
|
record = updated["applied_transactions"][transaction_id]
|
|
735
1388
|
item_ids = record["item_ids"]
|
|
736
1389
|
receipt = f"Author Memory Receipt: r{record['revision']} · {', '.join(item_ids)}"
|
|
1390
|
+
project_state, book_state, load_warnings = visible_states(store, updated, workspace, book_root, book)
|
|
737
1391
|
return {
|
|
738
1392
|
"ok": True,
|
|
739
1393
|
"command": "record",
|
|
@@ -744,104 +1398,212 @@ def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
|
|
|
744
1398
|
"item_ids": item_ids,
|
|
745
1399
|
"receipt": receipt,
|
|
746
1400
|
"summaries": summaries,
|
|
1401
|
+
"warnings": load_warnings + query_budget_warnings(project_state, book_state),
|
|
1402
|
+
**store_fields(store),
|
|
747
1403
|
}
|
|
748
1404
|
|
|
749
1405
|
|
|
750
|
-
def same_scope_value(item_value: str | None, requested: str | None) -> bool:
|
|
751
|
-
return requested is not None and item_value is not None and item_value.casefold() == requested.casefold()
|
|
752
|
-
|
|
753
|
-
|
|
754
1406
|
def command_query(
|
|
755
1407
|
workspace: Path,
|
|
1408
|
+
book_root: Path | None,
|
|
756
1409
|
kinds: list[str] | None,
|
|
757
1410
|
book: str | None,
|
|
758
1411
|
genre: str | None,
|
|
759
1412
|
workflow: str | None,
|
|
760
1413
|
) -> dict[str, Any]:
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1414
|
+
prepare_workspace(workspace, book_root)
|
|
1415
|
+
require(
|
|
1416
|
+
bool(kinds),
|
|
1417
|
+
"query 必须显式传 --kind(按调用方 skill 写好的 query 命令选类型,映射表见 references/author-memory-maintenance.md),不再默认返回全部类型",
|
|
1418
|
+
)
|
|
1419
|
+
project_state = load_state(project_store(workspace))
|
|
1420
|
+
book_state = load_state(book_store(workspace, book_root, book)) if book_root is not None else None
|
|
1421
|
+
if project_state is None and book_state is None:
|
|
1422
|
+
return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "omitted": 0, "omitted_ids": []}
|
|
767
1423
|
requested_scopes = {
|
|
768
|
-
"book": optional_text(book, "query.book", max_bytes=180),
|
|
769
1424
|
"genre": optional_text(genre, "query.genre", max_bytes=180),
|
|
770
1425
|
"workflow": optional_text(workflow, "query.workflow", max_bytes=180),
|
|
771
1426
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
level = item["scope"]["level"]
|
|
777
|
-
return level == "global" or same_scope_value(item["scope"]["value"], requested_scopes[level])
|
|
778
|
-
|
|
779
|
-
scope_rank = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
|
|
780
|
-
candidates = sorted(
|
|
781
|
-
(item for item in state["items"].values() if relevant(item)),
|
|
782
|
-
key=lambda item: (
|
|
783
|
-
scope_rank[item["scope"]["level"]],
|
|
784
|
-
-RANK[item["importance"]],
|
|
785
|
-
-item["confirmation_count"],
|
|
786
|
-
int(item["id"][2:]),
|
|
787
|
-
),
|
|
788
|
-
)
|
|
789
|
-
result: dict[str, Any] = {
|
|
790
|
-
"ok": True,
|
|
791
|
-
"command": "query",
|
|
792
|
-
"initialized": True,
|
|
793
|
-
"revision": state["state_revision"],
|
|
794
|
-
"items": [],
|
|
795
|
-
"omitted": len(candidates),
|
|
796
|
-
}
|
|
797
|
-
for item in candidates:
|
|
798
|
-
compact = {
|
|
799
|
-
"id": item["id"],
|
|
800
|
-
"kind": item["kind"],
|
|
801
|
-
"scope": item["scope"],
|
|
802
|
-
"assertion": item["assertion"],
|
|
803
|
-
}
|
|
804
|
-
result["items"].append(compact)
|
|
805
|
-
result["omitted"] = len(candidates) - len(result["items"])
|
|
806
|
-
payload = json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n"
|
|
807
|
-
if len(payload.encode("utf-8")) > QUERY_MAX_BYTES:
|
|
808
|
-
result["items"].pop()
|
|
809
|
-
result["omitted"] += 1
|
|
810
|
-
break
|
|
1427
|
+
# 装不下的**跳过而不中断**(一条长的不挡后面的短条),漏下的 ID 报进
|
|
1428
|
+
# omitted_ids:非空=记忆超编该整理了,不是「没有更多了」。写入端的
|
|
1429
|
+
# warnings 与这里共用 build_query_result 同一个信封,估算即实况。
|
|
1430
|
+
result, _ = merged_query(project_state, book_state, set(kinds), requested_scopes)
|
|
811
1431
|
require(len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")) <= QUERY_MAX_BYTES, "query result exceeds its fixed byte budget")
|
|
812
1432
|
return result
|
|
813
1433
|
|
|
814
1434
|
|
|
815
|
-
def
|
|
816
|
-
|
|
817
|
-
require(
|
|
818
|
-
state = validate_state(read_json(path))
|
|
1435
|
+
def check_store(store: Store) -> dict[str, Any]:
|
|
1436
|
+
state = load_state(store)
|
|
1437
|
+
require(state is not None, f"{store.label} author memory is not initialized")
|
|
819
1438
|
views = render_views(state)
|
|
820
|
-
root = memory_root(workspace)
|
|
821
1439
|
for name, expected in views.items():
|
|
822
|
-
view_path = root / name
|
|
1440
|
+
view_path = store.root / name
|
|
823
1441
|
require(view_path.exists(), f"missing derived view: {view_path}")
|
|
824
1442
|
require(view_path.read_text(encoding="utf-8") == expected, f"derived view is stale or edited: {view_path}")
|
|
825
1443
|
counts = {status: sum(1 for item in state["items"].values() if item["status"] == status) for status in STATUSES}
|
|
826
|
-
return {"
|
|
1444
|
+
return {"revision": state["state_revision"], "counts": counts}
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def command_check(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
|
|
1448
|
+
prepare_workspace(workspace, book_root)
|
|
1449
|
+
project = project_store(workspace)
|
|
1450
|
+
result: dict[str, Any] = {"ok": True, "command": "check"}
|
|
1451
|
+
if book_root is not None:
|
|
1452
|
+
store = book_store(workspace, book_root, book)
|
|
1453
|
+
result["book"] = {"name": store.book, "root": str(store.root), **check_store(store)}
|
|
1454
|
+
if project.state_path.exists():
|
|
1455
|
+
result["project"] = {"root": str(project.root), **check_store(project)}
|
|
1456
|
+
else:
|
|
1457
|
+
result["project"] = {"root": str(project.root), **check_store(project)}
|
|
1458
|
+
# 兼容旧调用:顶层 revision / counts 指项目级 store。
|
|
1459
|
+
primary = result.get("project") or result["book"]
|
|
1460
|
+
result["revision"] = primary["revision"]
|
|
1461
|
+
result["counts"] = primary["counts"]
|
|
1462
|
+
return result
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
MIGRATE_PREFIX = "migrate:"
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def migration_digest(item: dict[str, Any]) -> str:
|
|
1469
|
+
"""迁移事务的摘要只看条目不可变的身份:编号、类型、范围、断言。证据会随强化
|
|
1470
|
+
增长、状态会随 decide/forget 变化,它们若进摘要,中途失败后作者动过条目就会
|
|
1471
|
+
永远「already used with different content」。"""
|
|
1472
|
+
return transaction_digest({"id": item["id"], "kind": item["kind"], "scope": item["scope"], "assertion": item["assertion"]})
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def migrated_item(source: dict[str, Any], new_id: str, revision: int, id_map: dict[str, str]) -> dict[str, Any]:
|
|
1476
|
+
"""把项目级的存量 book 条目原样搬进书级 store:断言、证据、确认次数、重要度
|
|
1477
|
+
全部保留,只换编号与修订。冲突对象只保留同书已迁移的(重映射为 BP),
|
|
1478
|
+
引用全局条目的冲突关系不再成立——本书例外按优先级覆盖全局,不算冲突——
|
|
1479
|
+
对象清空的候选退回 pending。"""
|
|
1480
|
+
item = copy.deepcopy(source)
|
|
1481
|
+
item["id"] = new_id
|
|
1482
|
+
item["created_revision"] = revision
|
|
1483
|
+
item["updated_revision"] = revision
|
|
1484
|
+
item["superseded_by"] = None
|
|
1485
|
+
item["conflicts_with"] = [id_map[old] for old in source["conflicts_with"] if old in id_map]
|
|
1486
|
+
if item["status"] == "conflict" and not item["conflicts_with"]:
|
|
1487
|
+
item["status"] = "pending"
|
|
1488
|
+
return item
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def migration_records(state: dict[str, Any]) -> dict[str, str]:
|
|
1492
|
+
"""书级 store 里「源 AP 编号 → 已建 BP 编号」;每个源条目一笔 migrate:APxxx 事务。"""
|
|
1493
|
+
records: dict[str, str] = {}
|
|
1494
|
+
for transaction_id, record in state["applied_transactions"].items():
|
|
1495
|
+
if transaction_id.startswith(MIGRATE_PREFIX) and is_item_id(transaction_id[len(MIGRATE_PREFIX):]):
|
|
1496
|
+
records[transaction_id[len(MIGRATE_PREFIX):]] = record["item_ids"][0]
|
|
1497
|
+
return records
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def apply_single(state: dict[str, Any], transaction_id: str, digest: str, mutate, summaries: list[str], *, store: Store) -> dict[str, Any]:
|
|
1501
|
+
updated = copy.deepcopy(state)
|
|
1502
|
+
mutate(updated, updated["state_revision"] + 1)
|
|
1503
|
+
return seal_transaction(updated, transaction_id, digest, summaries, store=store)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def command_migrate(workspace: Path, book_root: Path | None, book: str | None) -> dict[str, Any]:
|
|
1507
|
+
"""把项目级 store 里某本书的存量 book 条目搬进该书目录的书级 store。
|
|
1508
|
+
|
|
1509
|
+
书级每个源条目一笔事务(migrate:APxxx,摘要只看不可变字段),所以每个源条目
|
|
1510
|
+
至多迁一次,与本次批次里还有哪些条目无关、源条目状态怎么变都不会锁死;项目级
|
|
1511
|
+
随后一笔事务把这批源条目标 superseded 并注明去向。先写书级再写项目级,中途
|
|
1512
|
+
失败直接重跑:书级已有记录的直接复用编号,只补项目级。
|
|
1513
|
+
"""
|
|
1514
|
+
prepare_workspace(workspace, book_root)
|
|
1515
|
+
require(book_root is not None, "migrate 须传 --book-root {书目录}:书名到书目录的映射由调用方给出")
|
|
1516
|
+
project = project_store(workspace)
|
|
1517
|
+
store = book_store(workspace, book_root, book)
|
|
1518
|
+
assert store.book is not None
|
|
1519
|
+
project_state = load_state(project)
|
|
1520
|
+
book_state = load_state(store)
|
|
1521
|
+
|
|
1522
|
+
def result(migrated: list[dict[str, str]]) -> dict[str, Any]:
|
|
1523
|
+
return {
|
|
1524
|
+
"ok": True,
|
|
1525
|
+
"command": "migrate",
|
|
1526
|
+
"migrated": migrated,
|
|
1527
|
+
"project_revision": project_state["state_revision"] if project_state is not None else 0,
|
|
1528
|
+
"revision": book_state["state_revision"] if book_state is not None else 0,
|
|
1529
|
+
"warnings": query_budget_warnings(project_state, book_state),
|
|
1530
|
+
**store_fields(store),
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if project_state is None:
|
|
1534
|
+
return result([])
|
|
1535
|
+
legacy = sorted(
|
|
1536
|
+
(item for item in legacy_book_items(project_state) if same_scope_value(item["scope"]["value"], store.book)),
|
|
1537
|
+
key=lambda item: id_number(item["id"]),
|
|
1538
|
+
)
|
|
1539
|
+
if book_state is None:
|
|
1540
|
+
if not legacy:
|
|
1541
|
+
return result([])
|
|
1542
|
+
book_state = empty_state(store.book)
|
|
1543
|
+
id_map = migration_records(book_state)
|
|
1544
|
+
|
|
1545
|
+
# 1) 书级:还没迁的源条目各建一条 BP;已迁的复用编号。
|
|
1546
|
+
for item in legacy:
|
|
1547
|
+
if item["id"] in id_map:
|
|
1548
|
+
continue
|
|
1549
|
+
transaction_id = f"{MIGRATE_PREFIX}{item['id']}"
|
|
1550
|
+
new_id = f"{STORE_PREFIX['book']}{book_state['next_item_number']:03d}"
|
|
1551
|
+
|
|
1552
|
+
preview = migrated_item(item, new_id, 1, id_map)
|
|
1553
|
+
|
|
1554
|
+
def create(updated: dict[str, Any], revision: int, source=item, new_id=new_id) -> None:
|
|
1555
|
+
updated["next_item_number"] += 1
|
|
1556
|
+
updated["items"][new_id] = migrated_item(source, new_id, revision, id_map)
|
|
1557
|
+
|
|
1558
|
+
book_state = apply_single(
|
|
1559
|
+
book_state, transaction_id, migration_digest(item), create,
|
|
1560
|
+
[f"迁移 {item['id']} → {new_id}({preview['status']}):{item['assertion']}"],
|
|
1561
|
+
store=store,
|
|
1562
|
+
)
|
|
1563
|
+
id_map[item["id"]] = new_id
|
|
1564
|
+
# 幂等重跑也重写快照,修复缺失或过期的派生视图。
|
|
1565
|
+
write_snapshot(store, book_state)
|
|
1566
|
+
|
|
1567
|
+
# 2) 项目级:这批源条目标 superseded 并注明去向。
|
|
1568
|
+
migrated = [{"from": item["id"], "to": id_map[item["id"]]} for item in legacy]
|
|
1569
|
+
if legacy:
|
|
1570
|
+
transaction_id = f"{MIGRATE_PREFIX}{store.book.casefold()}:r{project_state['state_revision'] + 1}"
|
|
1571
|
+
digest = transaction_digest({"transaction_id": transaction_id, "migrated": migrated})
|
|
1572
|
+
|
|
1573
|
+
def move_out(updated: dict[str, Any], revision: int) -> None:
|
|
1574
|
+
for item in legacy:
|
|
1575
|
+
target = updated["items"][item["id"]]
|
|
1576
|
+
target["status"] = "superseded"
|
|
1577
|
+
target["superseded_by"] = None
|
|
1578
|
+
target["conflicts_with"] = []
|
|
1579
|
+
target["reason"] = f"已迁移到书级 store「{store.book}」:{id_map[item['id']]}"
|
|
1580
|
+
target["updated_revision"] = revision
|
|
1581
|
+
release_conflicts(updated, set(id_map), revision)
|
|
1582
|
+
|
|
1583
|
+
summaries = [f"迁出 {pair['from']} → {pair['to']}:{project_state['items'][pair['from']]['assertion']}" for pair in migrated]
|
|
1584
|
+
project_state = apply_single(project_state, transaction_id, digest, move_out, summaries, store=project)
|
|
1585
|
+
write_snapshot(project, project_state)
|
|
1586
|
+
return result(migrated)
|
|
827
1587
|
|
|
828
1588
|
|
|
829
1589
|
def build_parser() -> argparse.ArgumentParser:
|
|
830
1590
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
831
1591
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
832
|
-
|
|
833
|
-
|
|
1592
|
+
|
|
1593
|
+
def add_location(child: argparse.ArgumentParser) -> None:
|
|
834
1594
|
child.add_argument("--workspace", required=True, type=Path)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1595
|
+
child.add_argument("--book-root", type=Path, help="书目录;书级条目(scope.level=book / BP 编号)的落点")
|
|
1596
|
+
child.add_argument("--book", help="书名;传了 --book-root 时默认取 store 记录的书名或目录名")
|
|
1597
|
+
|
|
1598
|
+
for command in ("init", "check", "migrate"):
|
|
1599
|
+
add_location(subparsers.add_parser(command))
|
|
1600
|
+
for command in ("commit", "record"):
|
|
1601
|
+
child = subparsers.add_parser(command)
|
|
1602
|
+
add_location(child)
|
|
1603
|
+
child.add_argument("--input", required=True, type=Path)
|
|
841
1604
|
query = subparsers.add_parser("query")
|
|
842
|
-
query
|
|
1605
|
+
add_location(query)
|
|
843
1606
|
query.add_argument("--kind", action="append", choices=KINDS)
|
|
844
|
-
query.add_argument("--book")
|
|
845
1607
|
query.add_argument("--genre")
|
|
846
1608
|
query.add_argument("--workflow")
|
|
847
1609
|
return parser
|
|
@@ -851,15 +1613,17 @@ def main() -> int:
|
|
|
851
1613
|
args = build_parser().parse_args()
|
|
852
1614
|
try:
|
|
853
1615
|
if args.command == "init":
|
|
854
|
-
result = command_init(args.workspace)
|
|
1616
|
+
result = command_init(args.workspace, args.book_root, args.book)
|
|
855
1617
|
elif args.command == "commit":
|
|
856
|
-
result = command_commit(args.workspace, args.input)
|
|
1618
|
+
result = command_commit(args.workspace, args.book_root, args.book, args.input)
|
|
857
1619
|
elif args.command == "record":
|
|
858
|
-
result = command_record(args.workspace, args.input)
|
|
1620
|
+
result = command_record(args.workspace, args.book_root, args.book, args.input)
|
|
859
1621
|
elif args.command == "query":
|
|
860
|
-
result = command_query(args.workspace, args.kind, args.book, args.genre, args.workflow)
|
|
1622
|
+
result = command_query(args.workspace, args.book_root, args.kind, args.book, args.genre, args.workflow)
|
|
1623
|
+
elif args.command == "migrate":
|
|
1624
|
+
result = command_migrate(args.workspace, args.book_root, args.book)
|
|
861
1625
|
else:
|
|
862
|
-
result = command_check(args.workspace)
|
|
1626
|
+
result = command_check(args.workspace, args.book_root, args.book)
|
|
863
1627
|
emit(result)
|
|
864
1628
|
return 0
|
|
865
1629
|
except AuthorMemoryError as exc:
|