@lemoncat7/dsh-partner 2.8.1 → 2.8.3
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/docs/THIRD_PARTY_LICENSES.md +10 -0
- package/docs/blocked-delivery-recovery.md +7 -0
- package/docs/concern-attention.md +19 -0
- package/docs/observation-contract.md +21 -0
- package/docs/pendant-loading.md +30 -0
- package/docs/releases/2.8.2.md +7 -0
- package/docs/releases/2.8.3.md +20 -0
- package/lib/agent-runtime.d.ts +7 -0
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +119 -35
- package/lib/agent-runtime.js.map +1 -1
- package/lib/api/features/pendant-api.d.ts.map +1 -1
- package/lib/api/features/pendant-api.js +11 -2
- package/lib/api/features/pendant-api.js.map +1 -1
- package/lib/api.d.ts +1 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +41 -6
- package/lib/api.js.map +1 -1
- package/lib/autonomy.d.ts.map +1 -1
- package/lib/autonomy.js +9 -2
- package/lib/autonomy.js.map +1 -1
- package/lib/channel-message.d.ts +2 -0
- package/lib/channel-message.d.ts.map +1 -1
- package/lib/channel-message.js.map +1 -1
- package/lib/channels/direct/message-format.js +127 -127
- package/lib/channels/direct/message-format.js.map +1 -1
- package/lib/channels/manager.d.ts.map +1 -1
- package/lib/channels/manager.js +4 -3
- package/lib/channels/manager.js.map +1 -1
- package/lib/channels/notification-delivery.d.ts.map +1 -1
- package/lib/channels/notification-delivery.js +10 -0
- package/lib/channels/notification-delivery.js.map +1 -1
- package/lib/channels/weixin/connect.d.ts.map +1 -1
- package/lib/client-api.d.ts +17 -2
- package/lib/client-api.d.ts.map +1 -1
- package/lib/client-api.js +8 -1
- package/lib/client-api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +2398 -1970
- package/lib/client.js.map +4 -4
- package/lib/client.pendant.js +4157 -0
- package/lib/collaboration/composition.js +1 -1
- package/lib/collaboration/composition.js.map +1 -1
- package/lib/concern-attention.d.ts +19 -0
- package/lib/concern-attention.d.ts.map +1 -0
- package/lib/concern-attention.js +36 -0
- package/lib/concern-attention.js.map +1 -0
- package/lib/concern-domain.d.ts +10 -1
- package/lib/concern-domain.d.ts.map +1 -1
- package/lib/concern-domain.js +4 -0
- package/lib/concern-domain.js.map +1 -1
- package/lib/concern-execution-notes.d.ts +18 -0
- package/lib/concern-execution-notes.d.ts.map +1 -0
- package/lib/concern-execution-notes.js +41 -0
- package/lib/concern-execution-notes.js.map +1 -0
- package/lib/concern-execution-receipt.d.ts +4 -0
- package/lib/concern-execution-receipt.d.ts.map +1 -0
- package/lib/concern-execution-receipt.js +28 -0
- package/lib/concern-execution-receipt.js.map +1 -0
- package/lib/concern-execution.d.ts +32 -0
- package/lib/concern-execution.d.ts.map +1 -0
- package/lib/concern-execution.js +112 -0
- package/lib/concern-execution.js.map +1 -0
- package/lib/concern-management-tool.d.ts.map +1 -1
- package/lib/concern-management-tool.js +21 -19
- package/lib/concern-management-tool.js.map +1 -1
- package/lib/concern-note-source.d.ts +7 -0
- package/lib/concern-note-source.d.ts.map +1 -0
- package/lib/concern-note-source.js +25 -0
- package/lib/concern-note-source.js.map +1 -0
- package/lib/concern-resources.d.ts +27 -0
- package/lib/concern-resources.d.ts.map +1 -0
- package/lib/concern-resources.js +36 -0
- package/lib/concern-resources.js.map +1 -0
- package/lib/concern-sources.d.ts +1 -1
- package/lib/concern-sources.d.ts.map +1 -1
- package/lib/concern-store.d.ts +10 -1
- package/lib/concern-store.d.ts.map +1 -1
- package/lib/concern-store.js +135 -36
- package/lib/concern-store.js.map +1 -1
- package/lib/concern-tool.d.ts +1 -0
- package/lib/concern-tool.d.ts.map +1 -1
- package/lib/concern-tool.js +8 -4
- package/lib/concern-tool.js.map +1 -1
- package/lib/decision/conversation.d.ts +11 -0
- package/lib/decision/conversation.d.ts.map +1 -0
- package/lib/decision/conversation.js +52 -0
- package/lib/decision/conversation.js.map +1 -0
- package/lib/decision/events.d.ts +16 -0
- package/lib/decision/events.d.ts.map +1 -0
- package/lib/decision/events.js +9 -0
- package/lib/decision/events.js.map +1 -0
- package/lib/domain.d.ts +2 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js.map +1 -1
- package/lib/heartbeat.d.ts +12 -10
- package/lib/heartbeat.d.ts.map +1 -1
- package/lib/heartbeat.js +55 -26
- package/lib/heartbeat.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/lib/memory-prompt-context.js +1 -0
- package/lib/memory-prompt-context.js.map +1 -1
- package/lib/memory-reflection.js +1 -1
- package/lib/observation-contract.d.ts +9 -0
- package/lib/observation-contract.d.ts.map +1 -0
- package/lib/observation-contract.js +19 -0
- package/lib/observation-contract.js.map +1 -0
- package/lib/observation-executor.d.ts +5 -0
- package/lib/observation-executor.d.ts.map +1 -1
- package/lib/observation-executor.js +23 -1
- package/lib/observation-executor.js.map +1 -1
- package/lib/observation-loop.d.ts +7 -0
- package/lib/observation-loop.d.ts.map +1 -1
- package/lib/observation-loop.js +65 -4
- package/lib/observation-loop.js.map +1 -1
- package/lib/observation-progress.d.ts +18 -0
- package/lib/observation-progress.d.ts.map +1 -0
- package/lib/observation-progress.js +57 -0
- package/lib/observation-progress.js.map +1 -0
- package/lib/observation-sources.d.ts +14 -0
- package/lib/observation-sources.d.ts.map +1 -0
- package/lib/observation-sources.js +12 -0
- package/lib/observation-sources.js.map +1 -0
- package/lib/pendant/badge-art.d.ts +9 -0
- package/lib/pendant/badge-art.d.ts.map +1 -0
- package/lib/pendant/badge-art.js +68 -0
- package/lib/pendant/badge-art.js.map +1 -0
- package/lib/pendant/card-image.d.ts.map +1 -1
- package/lib/pendant/card-image.js +11 -9
- package/lib/pendant/card-image.js.map +1 -1
- package/lib/pendant/renderer-loader.d.ts +9 -0
- package/lib/pendant/renderer-loader.d.ts.map +1 -0
- package/lib/pendant/renderer-loader.js +12 -0
- package/lib/pendant/renderer-loader.js.map +1 -0
- package/lib/pendant/renderer.d.ts +2 -6
- package/lib/pendant/renderer.d.ts.map +1 -1
- package/lib/pendant/renderer.js +1 -67
- package/lib/pendant/renderer.js.map +1 -1
- package/lib/pendant/static-card.d.ts +20 -0
- package/lib/pendant/static-card.d.ts.map +1 -0
- package/lib/pendant/static-card.js +55 -0
- package/lib/pendant/static-card.js.map +1 -0
- package/lib/pendant/widget.d.ts.map +1 -1
- package/lib/pendant/widget.js +9 -5
- package/lib/pendant/widget.js.map +1 -1
- package/lib/pendant-renderer.js +14 -14
- package/lib/requirements/service.d.ts.map +1 -1
- package/lib/requirements/service.js +2 -0
- package/lib/requirements/service.js.map +1 -1
- package/lib/requirements/worker.d.ts.map +1 -1
- package/lib/requirements/worker.js +1 -0
- package/lib/requirements/worker.js.map +1 -1
- package/lib/scheduler/continuations.d.ts.map +1 -1
- package/lib/scheduler/continuations.js +17 -3
- package/lib/scheduler/continuations.js.map +1 -1
- package/lib/skills/builtin.d.ts.map +1 -1
- package/lib/skills/builtin.js +2 -0
- package/lib/skills/builtin.js.map +1 -1
- package/lib/skills/drama-builtin.d.ts.map +1 -1
- package/lib/skills/drama-builtin.js +13 -1
- package/lib/skills/drama-builtin.js.map +1 -1
- package/lib/skills/story-builtins.d.ts +3 -0
- package/lib/skills/story-builtins.d.ts.map +1 -0
- package/lib/skills/story-builtins.js +72 -0
- package/lib/skills/story-builtins.js.map +1 -0
- package/lib/skills/viora-canvas-builtin.d.ts.map +1 -1
- package/lib/skills/viora-canvas-builtin.js +12 -1
- package/lib/skills/viora-canvas-builtin.js.map +1 -1
- package/lib/snapshot-cache.d.ts +7 -0
- package/lib/snapshot-cache.d.ts.map +1 -0
- package/lib/snapshot-cache.js +26 -0
- package/lib/snapshot-cache.js.map +1 -0
- package/lib/tasks/result.js +1 -1
- package/lib/tasks/result.js.map +1 -1
- package/lib/ui/memory/concern-board.d.ts +1 -1
- package/lib/ui/memory/concern-board.d.ts.map +1 -1
- package/lib/ui/memory/concern-board.js +14 -6
- package/lib/ui/memory/concern-board.js.map +1 -1
- package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
- package/lib/ui/memory/concern-deletion.js +2 -1
- package/lib/ui/memory/concern-deletion.js.map +1 -1
- package/lib/ui/memory/concern-editor.d.ts.map +1 -1
- package/lib/ui/memory/concern-editor.js +5 -3
- package/lib/ui/memory/concern-editor.js.map +1 -1
- package/lib/ui/memory/concern-execution-fields.d.ts +7 -0
- package/lib/ui/memory/concern-execution-fields.d.ts.map +1 -0
- package/lib/ui/memory/concern-execution-fields.js +8 -0
- package/lib/ui/memory/concern-execution-fields.js.map +1 -0
- package/lib/ui/memory/concern-execution-status.d.ts +8 -0
- package/lib/ui/memory/concern-execution-status.d.ts.map +1 -0
- package/lib/ui/memory/concern-execution-status.js +31 -0
- package/lib/ui/memory/concern-execution-status.js.map +1 -0
- package/lib/ui/memory/concern-feedback.d.ts +15 -0
- package/lib/ui/memory/concern-feedback.d.ts.map +1 -0
- package/lib/ui/memory/concern-feedback.js +13 -0
- package/lib/ui/memory/concern-feedback.js.map +1 -0
- package/lib/ui/memory/concern-panel.d.ts.map +1 -1
- package/lib/ui/memory/concern-panel.js +11 -3
- package/lib/ui/memory/concern-panel.js.map +1 -1
- package/package.json +7 -1
- package/resources/skills/novel-game-creation/manifest.json +393 -0
- package/resources/skills/novel-game-creation/upstream/LICENSE +21 -0
- package/resources/skills/novel-game-creation/upstream/README.md +420 -0
- package/resources/skills/novel-game-creation/upstream/README_ZH.md +421 -0
- package/resources/skills/novel-game-creation/upstream/VERSION +1 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/BUILD_BRIEF.md +219 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/.vercelignore +3 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/RUN.md +138 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/new-guofeng.css +1494 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/style.css +2508 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/index.html +18 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/assets.js +147 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/audio.js +332 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/data.js +7470 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/engine.js +13066 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/main.js +3051 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/text.js +32 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/package.json +11 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/vercel.json +23 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/art/generated-art.json +161 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/example.json +12 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/README.md +26 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/playable-model.json +99 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verification.json +41 -0
- package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verify_playable_model.mjs +152 -0
- package/resources/skills/novel-game-creation/upstream/manifest.json +498 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/SKILL.md +47 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/references/art-direction-method.md +40 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/SKILL.md +66 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/references/build-brief-contract.md +72 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/references/generative-media-pipeline.md +25 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/references/playable-model-contract.md +46 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-build/references/tts-production-contract.md +23 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-concept/SKILL.md +54 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-concept/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-concept/references/concept-method.md +47 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-qa/SKILL.md +39 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-qa/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/qa-contract.md +66 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/test-design-method.md +49 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-world-design/SKILL.md +50 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-world-design/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/narrative-design-method.md +35 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/numeric-design-method.md +22 -0
- package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/world-design-method.md +57 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/SKILL.md +68 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/SKILL.md +54 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/agents/openai.yaml +4 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/intake-method.md +50 -0
- package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/pipeline-contract.md +60 -0
- package/resources/skills/story-creation/manifest.json +1477 -0
- package/resources/skills/story-creation/upstream/LICENSE.upstream +21 -0
- package/resources/skills/story-creation/upstream/manifest.json +1861 -0
- package/resources/skills/story-creation/upstream/roles/chapter-extractor.md +254 -0
- package/resources/skills/story-creation/upstream/roles/character-designer.md +168 -0
- package/resources/skills/story-creation/upstream/roles/consistency-checker.md +192 -0
- package/resources/skills/story-creation/upstream/roles/narrative-writer.md +105 -0
- package/resources/skills/story-creation/upstream/roles/story-architect.md +139 -0
- package/resources/skills/story-creation/upstream/roles/story-explorer.md +262 -0
- package/resources/skills/story-creation/upstream/roles/story-researcher.md +303 -0
- package/resources/skills/story-creation/upstream/skills/browser-cdp/SKILL.md +175 -0
- package/resources/skills/story-creation/upstream/skills/story/SKILL.md +129 -0
- package/resources/skills/story-creation/upstream/skills/story/VERSION +1 -0
- package/resources/skills/story-creation/upstream/skills/story/references/author-memory-maintenance.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story/references/author-memory.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story/scripts/author_memory_commit.py +1635 -0
- package/resources/skills/story-creation/upstream/skills/story-cover/SKILL.md +336 -0
- package/resources/skills/story-creation/upstream/skills/story-cover/references/cover-styles.md +210 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/SKILL.md +274 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/agent-calls.md +5 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/anti-ai-writing.md +556 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/banned-words.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/deslop-gates.md +171 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/references/style-resolution.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/author_memory_commit.py +1635 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-ai-patterns.js +1586 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-degeneration.js +349 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/normalize-punctuation.js +415 -0
- package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/resources/skills/story-creation/upstream/skills/story-import/SKILL.md +127 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/character-state-reverse.md +82 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/deep-analysis.md +47 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/format-and-structure.md +168 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/import-report.md +64 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/import-special-cases.md +34 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/import-tracking.md +95 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/length-routing.md +134 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse.md +24 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/state-tracking.md +43 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-long.md +448 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-short.md +187 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-initialization.md +31 -0
- package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-transaction.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/resources/skills/story-creation/upstream/skills/story-import/scripts/check-outline-contract.js +520 -0
- package/resources/skills/story-creation/upstream/skills/story-import/scripts/tracking_commit.py +1691 -0
- package/resources/skills/story-creation/upstream/skills/story-import/scripts/wordcount_core.py +405 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/SKILL.md +140 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/author-facing.md +252 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/deconstruction-notes.md +270 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/final-checks.md +19 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/pipeline-ops.md +154 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-generator.md +76 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-protocol.md +133 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
- package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/SKILL.md +193 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/genre-trends.md +197 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/publishing-guide.md +114 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/reader-profiling.md +105 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/references/topic-decision.md +82 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/aggregate-rank.js +682 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/cdp-utils.js +242 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/ciweimao-rank-scraper.js +240 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/fanqie-rank-scraper.js +658 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/jjwxc-rank-scraper.js +400 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qidian-rank-scraper.js +592 -0
- package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qimao-rank-scraper.js +435 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/SKILL.md +136 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/agent-calls.md +47 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/anti-ai-writing.md +556 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/artifact-protocols.md +183 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/banned-words.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/benchmark-recall.md +16 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-basics.md +445 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-design-methods.md +428 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-relations.md +433 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/commercial-core-methods.md +502 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/cross-book-recall.md +88 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/deslop-gates.md +171 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/dialogue-mastery.md +288 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-arc-design.md +313 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-methods.md +209 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/female-audience-writing.md +188 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards.md +91 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-readers.md +235 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-hooks.md +225 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-quality.md +177 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-format.md +36 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-catalog.md +394 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-mechanics.md +415 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-reversal.md +335 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-suspense.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/opening-design.md +349 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-conflict.md +457 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-methods.md +287 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-rhythm.md +389 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-structure-theory.md +273 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-core-methods.md +522 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-emotion-system.md +354 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-frameworks.md +711 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-special-topics.md +622 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/project-files.md +114 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/reader-contract-and-progression.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/reference-index.md +86 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-combat-face.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-craft.md +327 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-genre-modules.md +510 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-resolution.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-transaction.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-chapter.md +104 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-daily.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-outline.md +157 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-revision.md +89 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-setup.md +170 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-volume.md +142 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/writer-prompt-fallback.md +24 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/references/writing-craft.md +442 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/author_memory_commit.py +1635 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_writer_prompt.py +611 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-ai-patterns.js +1586 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-degeneration.js +349 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-contract.js +520 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-copy.js +313 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/normalize-punctuation.js +415 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/outline_view.py +286 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/storyctl.py +561 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/tracking_commit.py +1691 -0
- package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/wordcount_core.py +405 -0
- package/resources/skills/story-creation/upstream/skills/story-review/SKILL.md +231 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/agent-prompts.md +184 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/anti-ai-writing.md +556 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/banned-words.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/batch-review.md +19 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/character-relations.md +433 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/dialogue-mastery.md +288 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/plot-core-methods.md +522 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/quality-rubric.md +55 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/review-quality.md +246 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/review-tracking.md +10 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/fanqie.md +18 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/qidian.md +19 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/zhihu.md +20 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/solo.md +52 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/style-resolution.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-initialization.md +31 -0
- package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-transaction.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/author_memory_commit.py +1635 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-ai-patterns.js +1586 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-degeneration.js +349 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/normalize-punctuation.js +415 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/style-whitelist.js +39 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/tracking_commit.py +1691 -0
- package/resources/skills/story-creation/upstream/skills/story-review/scripts/wordcount_core.py +405 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/SKILL.md +165 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-quality.md +72 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/anti-ai-writing.md +556 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/banned-words.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-basics.md +445 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-design-methods.md +428 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-relations.md +433 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/deslop-gates.md +171 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/dialogue-mastery.md +288 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotional-arc-design.md +313 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/format-and-structure.md +168 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards.md +91 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-readers.md +235 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-chapter-hooks.md +225 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-catalog.md +394 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-mechanics.md +415 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-quality.md +42 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-reversal.md +335 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-suspense.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/opening-design.md +349 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-conflict.md +457 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-methods.md +287 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-rhythm.md +389 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/plot-core-methods.md +522 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-chapter-hooks.md +225 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-emotional-methods.md +179 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-genre-formulas.md +417 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-paragraph-hooks.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-quality.md +39 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-reversal.md +355 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-suspense.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-combat-face.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-genre-modules.md +510 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/writing-craft.md +442 -0
- package/resources/skills/story-creation/upstream/skills/story-setup/references/diagnostics.md +38 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/SKILL.md +224 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-basics.md +453 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-design.md +438 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-relations.md +395 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-paragraph-hooks.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-reader-profile.md +245 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-report-style.md +45 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-writing-techniques.md +477 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/deconstruction-examples.md +79 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/output-contract.md +127 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/real-market-data.md +90 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/source-story-quality.md +221 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
- package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/zhihu-style.md +54 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/SKILL.md +182 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/references/real-market-data.md +90 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/references/topic-match.md +38 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/aggregate-rank.js +682 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/cdp-utils.js +242 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/dz-browse-scraper.js +232 -0
- package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/heiyan-booklist-scraper.js +345 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/SKILL.md +136 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/agent-calls.md +13 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory.md +117 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/banned-words.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/benchmark-recall.md +40 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/cross-book-recall.md +62 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/dialogue-mastery.md +288 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/emotional-methods.md +179 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +159 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +156 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +163 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +167 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +178 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +166 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +159 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +151 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +153 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +210 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-formulas.md +417 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-techniques.md +490 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-chapter.md +225 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-paragraph.md +146 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/output-contract.md +127 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/reference-index.md +23 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/scene-craft.md +40 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-craft.md +147 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-deslop.md +133 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-format.md +199 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-prose-quality.md +224 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-reversal.md +355 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-suspense.md +352 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/style-resolution.md +27 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/submission-craft.md +35 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/villain-and-reveal.md +153 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-design.md +85 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-draft.md +116 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-revision.md +99 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/references/writing-workflow.md +50 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/author_memory_commit.py +1635 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-ai-patterns.js +1586 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-degeneration.js +349 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-delivery-contract.js +241 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-outline-copy.js +313 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/normalize-punctuation.js +415 -0
- package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/resources/skills/video-recap-production/manifest.json +673 -0
- package/resources/skills/video-recap-production/upstream/CHANGELOG.md +371 -0
- package/resources/skills/video-recap-production/upstream/LICENSE +21 -0
- package/resources/skills/video-recap-production/upstream/README.en.md +468 -0
- package/resources/skills/video-recap-production/upstream/README.md +459 -0
- package/resources/skills/video-recap-production/upstream/manifest.json +839 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/SKILL.md +112 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/audio-modes.md +63 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/foreground-compose.md +118 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/SOURCE.md +15 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/narration-adoption.md +103 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/packaging.md +45 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/pair-media.md +74 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/source-score.md +147 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/subtitle-track.md +202 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/artifacts.py +47 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble.py +591 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble_constants.py +51 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_contract.py +318 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_settings.py +138 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_automation.py +195 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_mix.py +465 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/export_jianying.py +254 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/builders.py +718 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/model.py +105 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/optional.py +23 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/templates.py +37 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/timeline_contract.py +223 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/tracks.py +72 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/writer.py +383 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/lib.py +218 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/media.py +190 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/narration_audio.py +413 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/packaging.py +122 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/pair_media.py +216 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/render_preflight.py +33 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_score.py +469 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_subtitles.py +423 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/core.py +330 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/render.py +82 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline.py +217 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline_emit.py +167 -0
- package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/visual_render.py +448 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/SKILL.md +108 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/references/shot-review.md +50 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut.py +41 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_cli.py +308 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_contract.py +424 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_render.py +304 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/lib.py +114 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/media_geometry.py +238 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narration_mapping.py +71 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/sentence_boundaries.py +585 -0
- package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/shot_review.py +357 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/SKILL.md +260 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/references/audio-routing.md +126 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/references/config-playbook.md +65 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/references/data-schema.md +545 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/references/resource-library.md +151 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/references/shift-left-qc-schema.md +23 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/doctor.py +556 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/final_qc.py +527 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/lib.py +220 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/library.py +509 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/materials.py +406 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/mimo_qc.py +33 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/project_binding.py +286 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_client.py +13 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_contract.py +9 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_evidence.py +287 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_observations.py +175 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_payload.py +206 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_report.py +318 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_runner.py +130 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc_contract.py +320 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap.py +9 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_cli.py +209 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_inspect.py +452 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_review.py +70 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runner.py +696 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runtime.py +260 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_source.py +278 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_stage_qc.py +160 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_timeline.py +621 -0
- package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/resource_lock.py +162 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/SKILL.md +251 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/references/creative-editing-playbook.md +229 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/references/promotional-copy.md +68 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/references/research-guide.md +75 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/agent_text.py +301 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/deslop_qc.py +211 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/evidence_bundle.py +440 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/lib.py +254 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/narration_lint.py +707 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review.py +40 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_grounding.py +238 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_response.py +669 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_runner.py +186 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/speech_ownership.py +143 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/timeline_fusion.py +108 -0
- package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/validate.py +175 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/SKILL.md +77 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/data-schema.md +193 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/prompt-templates.md +37 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/research-guide.md +74 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/agent_text.py +301 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr.py +279 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/brief.py +9 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/builder.py +402 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/context.py +317 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/inputs.py +268 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/timeline.py +326 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/consolidate.py +596 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/deslop_qc.py +211 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/detect.py +504 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/extract.py +62 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/lib.py +419 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/narration_lint.py +707 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/speech_ownership.py +143 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/storyboard.py +445 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/timeline_fusion.py +108 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understand.py +9 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_brief.py +190 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_cache.py +345 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_runner.py +387 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_storyboard.py +167 -0
- package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/vlm.py +674 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/SKILL.md +91 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/references/index-tts.md +31 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/dub.py +623 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/lib.py +352 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/fish_audio.py +91 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/tts_audio.py +102 -0
- package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/voiceover.py +771 -0
- package/resources/skills/viora-canvas-production/manifest.json +5 -1
- package/resources/skills/viora-canvas-production/references/story-handoff.md +49 -0
|
@@ -0,0 +1,1635 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Maintain evidence-backed author preferences and deterministic Markdown views.
|
|
3
|
+
|
|
4
|
+
The language model supplies compact semantic transactions. This tool validates
|
|
5
|
+
and applies them in memory, renders every derived view, and writes the JSON state
|
|
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.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import copy
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import stat
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
INPUT_SCHEMA_VERSION = 1
|
|
30
|
+
STATE_SCHEMA_VERSION = 1
|
|
31
|
+
STATE_MAX_BYTES = 2 * 1024 * 1024
|
|
32
|
+
PROFILE_MAX_BYTES = 12288
|
|
33
|
+
PENDING_MAX_BYTES = 12288
|
|
34
|
+
JOURNAL_MAX_BYTES = 24576
|
|
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
|
+
}
|
|
58
|
+
|
|
59
|
+
KINDS = ("prose_style", "story_design", "workflow", "delivery", "interaction")
|
|
60
|
+
KIND_TITLES = {
|
|
61
|
+
"prose_style": "文风与表达",
|
|
62
|
+
"story_design": "故事设计",
|
|
63
|
+
"workflow": "创作流程",
|
|
64
|
+
"delivery": "交付格式",
|
|
65
|
+
"interaction": "协作方式",
|
|
66
|
+
}
|
|
67
|
+
SCOPE_LEVELS = ("global", "genre", "book", "workflow")
|
|
68
|
+
STATUSES = ("active", "pending", "conflict", "rejected", "superseded")
|
|
69
|
+
CONFIDENCE_LEVELS = ("low", "medium", "high")
|
|
70
|
+
IMPORTANCE_LEVELS = ("low", "medium", "high")
|
|
71
|
+
# 作者记忆只记作者明确表达的偏好。repeated_correction / inferred_pattern 两条
|
|
72
|
+
# 由 agent 主动推断写入的管道已经移除(#436):它们只在攒待确认清单的审阅负
|
|
73
|
+
# 担,不是记忆质量;文档也明说不装全量消息 hook,隐式捕获本就承诺不了完整
|
|
74
|
+
# 性。SOURCES 里保留这两个值只为存量 state 仍能通过校验、仍能 decide/forget,
|
|
75
|
+
# 新写入一律按 WRITE_SOURCES 校验。
|
|
76
|
+
SOURCES = (
|
|
77
|
+
"explicit_user",
|
|
78
|
+
"accepted_suggestion",
|
|
79
|
+
"repeated_correction",
|
|
80
|
+
"inferred_pattern",
|
|
81
|
+
"manual",
|
|
82
|
+
)
|
|
83
|
+
WRITE_SOURCES = ("explicit_user", "accepted_suggestion", "manual")
|
|
84
|
+
RANK = {"low": 0, "medium": 1, "high": 2}
|
|
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
|
+
|
|
98
|
+
|
|
99
|
+
class AuthorMemoryError(ValueError):
|
|
100
|
+
"""Expected validation or state error."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def require(condition: bool, message: str) -> None:
|
|
104
|
+
if not condition:
|
|
105
|
+
raise AuthorMemoryError(message)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def as_mapping(value: object, label: str) -> dict[str, Any]:
|
|
109
|
+
require(isinstance(value, dict), f"{label} must be a JSON object")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def as_list(value: object, label: str) -> list[Any]:
|
|
114
|
+
require(isinstance(value, list), f"{label} must be a JSON array")
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def as_int(value: object, label: str, *, minimum: int = 0) -> int:
|
|
119
|
+
require(isinstance(value, int) and not isinstance(value, bool), f"{label} must be an integer")
|
|
120
|
+
require(value >= minimum, f"{label} must be >= {minimum}")
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -> None:
|
|
125
|
+
unknown = set(mapping) - allowed
|
|
126
|
+
require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def clean_text(value: object, label: str, *, max_bytes: int = 768) -> str:
|
|
130
|
+
require(isinstance(value, str), f"{label} must be a string")
|
|
131
|
+
cleaned = " ".join(value.replace("|", "|").split())
|
|
132
|
+
require(bool(cleaned), f"{label} must not be empty")
|
|
133
|
+
require(len(cleaned.encode("utf-8")) <= max_bytes, f"{label} exceeds {max_bytes} bytes")
|
|
134
|
+
return cleaned
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def optional_text(value: object, label: str, *, max_bytes: int = 768) -> str | None:
|
|
138
|
+
if value is None:
|
|
139
|
+
return None
|
|
140
|
+
return clean_text(value, label, max_bytes=max_bytes)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def choice(value: object, allowed: tuple[str, ...], label: str) -> str:
|
|
144
|
+
require(isinstance(value, str) and value in allowed, f"{label} must be one of: {', '.join(allowed)}")
|
|
145
|
+
return value
|
|
146
|
+
|
|
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
|
+
|
|
166
|
+
def clean_id_list(value: object, label: str, *, maximum: int = 32) -> list[str]:
|
|
167
|
+
raw = as_list(value, label)
|
|
168
|
+
require(len(raw) <= maximum, f"{label} may contain at most {maximum} items")
|
|
169
|
+
result: list[str] = []
|
|
170
|
+
for index, item in enumerate(raw):
|
|
171
|
+
item_id = clean_text(item, f"{label}[{index}]", max_bytes=32)
|
|
172
|
+
require(is_item_id(item_id), f"{label}[{index}] is not an author-memory id")
|
|
173
|
+
if item_id not in result:
|
|
174
|
+
result.append(item_id)
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def emit(document: object, *, error: bool = False) -> None:
|
|
179
|
+
payload = json.dumps(document, ensure_ascii=False, sort_keys=True)
|
|
180
|
+
stream = sys.stderr if error else sys.stdout
|
|
181
|
+
stream.flush()
|
|
182
|
+
stream.buffer.write((payload + "\n").encode("utf-8"))
|
|
183
|
+
stream.buffer.flush()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def json_payload(document: object) -> str:
|
|
187
|
+
return json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def read_json(path: Path) -> object:
|
|
191
|
+
try:
|
|
192
|
+
require(path.stat().st_size <= STATE_MAX_BYTES, f"{path} exceeds {STATE_MAX_BYTES} bytes")
|
|
193
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
194
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
195
|
+
raise AuthorMemoryError(f"unable to read JSON {path}: {exc}") from exc
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def atomic_write_text(path: Path, payload: str) -> None:
|
|
199
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o644
|
|
201
|
+
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
202
|
+
temporary = Path(temporary_name)
|
|
203
|
+
try:
|
|
204
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
205
|
+
handle.write(payload)
|
|
206
|
+
handle.flush()
|
|
207
|
+
os.fsync(handle.fileno())
|
|
208
|
+
os.chmod(temporary, mode)
|
|
209
|
+
os.replace(temporary, path)
|
|
210
|
+
finally:
|
|
211
|
+
temporary.unlink(missing_ok=True)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def write_if_changed(path: Path, payload: str) -> None:
|
|
215
|
+
try:
|
|
216
|
+
if path.read_text(encoding="utf-8") == payload:
|
|
217
|
+
return
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
pass
|
|
220
|
+
atomic_write_text(path, payload)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Stores
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Store:
|
|
229
|
+
"""一个 state 文件的落点:项目级(工作区)或书级(书目录)。"""
|
|
230
|
+
|
|
231
|
+
__slots__ = ("kind", "root", "book")
|
|
232
|
+
|
|
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] = {
|
|
399
|
+
"schema_version": STATE_SCHEMA_VERSION,
|
|
400
|
+
"state_revision": 0,
|
|
401
|
+
"next_item_number": 1,
|
|
402
|
+
"items": {},
|
|
403
|
+
"journal": [],
|
|
404
|
+
"applied_transactions": {},
|
|
405
|
+
}
|
|
406
|
+
if book is not None:
|
|
407
|
+
state["book"] = book
|
|
408
|
+
return state
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# Validation
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def normalize_scope(value: object, label: str) -> dict[str, str | None]:
|
|
417
|
+
scope = as_mapping(value, label)
|
|
418
|
+
require_known_keys(scope, {"level", "value"}, label)
|
|
419
|
+
level = choice(scope.get("level"), SCOPE_LEVELS, f"{label}.level")
|
|
420
|
+
raw_value = scope.get("value")
|
|
421
|
+
if level == "global":
|
|
422
|
+
require(raw_value is None, f"{label}.value must be null for global scope")
|
|
423
|
+
normalized_value = None
|
|
424
|
+
else:
|
|
425
|
+
normalized_value = clean_text(raw_value, f"{label}.value", max_bytes=180)
|
|
426
|
+
return {"level": level, "value": normalized_value}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def normalize_evidence(value: object, label: str) -> dict[str, str | None]:
|
|
430
|
+
evidence = as_mapping(value, label)
|
|
431
|
+
require_known_keys(evidence, {"quote", "source_ref"}, label)
|
|
432
|
+
return {
|
|
433
|
+
"quote": clean_text(evidence.get("quote"), f"{label}.quote", max_bytes=768),
|
|
434
|
+
"source_ref": optional_text(evidence.get("source_ref"), f"{label}.source_ref", max_bytes=240),
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def normalize_item(value: object, label: str) -> dict[str, Any]:
|
|
439
|
+
item = as_mapping(value, label)
|
|
440
|
+
allowed = {
|
|
441
|
+
"id", "kind", "scope", "assertion", "confidence", "importance", "status", "source",
|
|
442
|
+
"reason", "conflicts_with", "confirmation_count", "evidence", "created_revision",
|
|
443
|
+
"updated_revision", "superseded_by",
|
|
444
|
+
}
|
|
445
|
+
require_known_keys(item, allowed, label)
|
|
446
|
+
item_id = clean_text(item.get("id"), f"{label}.id", max_bytes=32)
|
|
447
|
+
require(is_item_id(item_id), f"{label}.id is invalid")
|
|
448
|
+
evidence = [normalize_evidence(entry, f"{label}.evidence[{index}]") for index, entry in enumerate(as_list(item.get("evidence"), f"{label}.evidence"))]
|
|
449
|
+
require(bool(evidence), f"{label}.evidence must not be empty")
|
|
450
|
+
status = choice(item.get("status"), STATUSES, f"{label}.status")
|
|
451
|
+
conflicts = clean_id_list(item.get("conflicts_with"), f"{label}.conflicts_with")
|
|
452
|
+
superseded_by = optional_text(item.get("superseded_by"), f"{label}.superseded_by", max_bytes=32)
|
|
453
|
+
if superseded_by is not None:
|
|
454
|
+
require(is_item_id(superseded_by), f"{label}.superseded_by is invalid")
|
|
455
|
+
return {
|
|
456
|
+
"id": item_id,
|
|
457
|
+
"kind": choice(item.get("kind"), KINDS, f"{label}.kind"),
|
|
458
|
+
"scope": normalize_scope(item.get("scope"), f"{label}.scope"),
|
|
459
|
+
"assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=LEGACY_ASSERTION_MAX_BYTES),
|
|
460
|
+
"confidence": choice(item.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
|
|
461
|
+
"importance": choice(item.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
|
|
462
|
+
"status": status,
|
|
463
|
+
"source": choice(item.get("source"), SOURCES, f"{label}.source"),
|
|
464
|
+
"reason": clean_text(item.get("reason"), f"{label}.reason", max_bytes=480),
|
|
465
|
+
"conflicts_with": conflicts,
|
|
466
|
+
"confirmation_count": as_int(item.get("confirmation_count"), f"{label}.confirmation_count", minimum=1),
|
|
467
|
+
"evidence": evidence,
|
|
468
|
+
"created_revision": as_int(item.get("created_revision"), f"{label}.created_revision", minimum=1),
|
|
469
|
+
"updated_revision": as_int(item.get("updated_revision"), f"{label}.updated_revision", minimum=1),
|
|
470
|
+
"superseded_by": superseded_by,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
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]:
|
|
481
|
+
state = as_mapping(value, "state")
|
|
482
|
+
allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions", "book"}
|
|
483
|
+
require_known_keys(state, allowed, "state")
|
|
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
|
|
496
|
+
revision = as_int(state.get("state_revision"), "state.state_revision")
|
|
497
|
+
next_number = as_int(state.get("next_item_number"), "state.next_item_number", minimum=1)
|
|
498
|
+
raw_items = as_mapping(state.get("items"), "state.items")
|
|
499
|
+
items: dict[str, Any] = {}
|
|
500
|
+
max_number = 0
|
|
501
|
+
for raw_id, raw_item in raw_items.items():
|
|
502
|
+
normalized = normalize_item(raw_item, f"state.items.{raw_id}")
|
|
503
|
+
require(raw_id == normalized["id"], f"state.items key {raw_id} does not match item id")
|
|
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))
|
|
507
|
+
require(normalized["created_revision"] <= normalized["updated_revision"] <= revision, f"state.items.{raw_id} revision is ahead of state")
|
|
508
|
+
items[raw_id] = normalized
|
|
509
|
+
require(next_number > max_number, "state.next_item_number must be greater than every allocated item id")
|
|
510
|
+
for item_id, item in items.items():
|
|
511
|
+
for conflict_id in item["conflicts_with"]:
|
|
512
|
+
require(conflict_id in items and conflict_id != item_id, f"state.items.{item_id} has an invalid conflict id")
|
|
513
|
+
if item["superseded_by"] is not None:
|
|
514
|
+
require(item["superseded_by"] in items and item["superseded_by"] != item_id, f"state.items.{item_id} has an invalid superseded_by id")
|
|
515
|
+
if item["status"] == "active":
|
|
516
|
+
require(not item["conflicts_with"], f"active item {item_id} cannot retain conflicts")
|
|
517
|
+
if item["status"] == "pending":
|
|
518
|
+
require(not item["conflicts_with"], f"pending item {item_id} cannot retain conflicts")
|
|
519
|
+
if item["status"] == "conflict":
|
|
520
|
+
require(bool(item["conflicts_with"]), f"conflict item {item_id} must reference an active item")
|
|
521
|
+
require(all(items[conflict_id]["status"] == "active" for conflict_id in item["conflicts_with"]), f"conflict item {item_id} must reference only active items")
|
|
522
|
+
if item["status"] != "superseded":
|
|
523
|
+
require(item["superseded_by"] is None, f"only superseded item {item_id} may set superseded_by")
|
|
524
|
+
journal = as_list(state.get("journal"), "state.journal")
|
|
525
|
+
require(len(journal) == revision, "state.journal length must equal state.state_revision")
|
|
526
|
+
journal_revisions: dict[str, int] = {}
|
|
527
|
+
for index, entry in enumerate(journal):
|
|
528
|
+
mapping = as_mapping(entry, f"state.journal[{index}]")
|
|
529
|
+
require_known_keys(mapping, {"revision", "transaction_id", "committed_at", "summaries"}, f"state.journal[{index}]")
|
|
530
|
+
entry_revision = as_int(mapping.get("revision"), f"state.journal[{index}].revision", minimum=1)
|
|
531
|
+
require(entry_revision == index + 1, f"state.journal[{index}].revision must be {index + 1}")
|
|
532
|
+
transaction_id = clean_text(mapping.get("transaction_id"), f"state.journal[{index}].transaction_id", max_bytes=128)
|
|
533
|
+
require(transaction_id not in journal_revisions, f"state.journal repeats transaction_id {transaction_id}")
|
|
534
|
+
journal_revisions[transaction_id] = entry_revision
|
|
535
|
+
clean_text(mapping.get("committed_at"), f"state.journal[{index}].committed_at", max_bytes=64)
|
|
536
|
+
summaries = as_list(mapping.get("summaries"), f"state.journal[{index}].summaries")
|
|
537
|
+
require(bool(summaries), f"state.journal[{index}].summaries must not be empty")
|
|
538
|
+
for summary_index, summary in enumerate(summaries):
|
|
539
|
+
clean_text(summary, f"state.journal[{index}].summaries[{summary_index}]", max_bytes=768)
|
|
540
|
+
transactions = as_mapping(state.get("applied_transactions"), "state.applied_transactions")
|
|
541
|
+
require(set(transactions) == set(journal_revisions), "state.applied_transactions must match state.journal transaction ids")
|
|
542
|
+
for transaction_id, record in transactions.items():
|
|
543
|
+
clean_text(transaction_id, "state.applied_transactions key", max_bytes=128)
|
|
544
|
+
mapping = as_mapping(record, f"state.applied_transactions.{transaction_id}")
|
|
545
|
+
require_known_keys(mapping, {"revision", "digest", "item_ids"}, f"state.applied_transactions.{transaction_id}")
|
|
546
|
+
transaction_revision = as_int(mapping.get("revision"), f"state.applied_transactions.{transaction_id}.revision", minimum=1)
|
|
547
|
+
require(transaction_revision == journal_revisions[transaction_id], f"state.applied_transactions.{transaction_id}.revision does not match journal")
|
|
548
|
+
digest = clean_text(mapping.get("digest"), f"state.applied_transactions.{transaction_id}.digest", max_bytes=64)
|
|
549
|
+
require(len(digest) == 64 and all(char in "0123456789abcdef" for char in digest), f"state.applied_transactions.{transaction_id}.digest is invalid")
|
|
550
|
+
item_ids = clean_id_list(mapping.get("item_ids"), f"state.applied_transactions.{transaction_id}.item_ids")
|
|
551
|
+
require(bool(item_ids), f"state.applied_transactions.{transaction_id}.item_ids must not be empty")
|
|
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")
|
|
553
|
+
result = {
|
|
554
|
+
"schema_version": STATE_SCHEMA_VERSION,
|
|
555
|
+
"state_revision": revision,
|
|
556
|
+
"next_item_number": next_number,
|
|
557
|
+
"items": items,
|
|
558
|
+
"journal": copy.deepcopy(journal),
|
|
559
|
+
"applied_transactions": copy.deepcopy(transactions),
|
|
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)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def normalize_preference(value: object, label: str, *, allow_status: bool) -> dict[str, Any]:
|
|
573
|
+
preference = as_mapping(value, label)
|
|
574
|
+
allowed = {"kind", "scope", "assertion", "quote", "source_ref", "source", "confidence", "importance", "reason"}
|
|
575
|
+
if allow_status:
|
|
576
|
+
allowed |= {"status", "conflicts_with"}
|
|
577
|
+
require_known_keys(preference, allowed, label)
|
|
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")
|
|
585
|
+
status = choice(preference.get("status"), ("active", "pending", "conflict"), f"{label}.status") if allow_status else "active"
|
|
586
|
+
conflicts = clean_id_list(preference.get("conflicts_with", []), f"{label}.conflicts_with") if allow_status else []
|
|
587
|
+
if status == "active":
|
|
588
|
+
require(not conflicts, f"{label}.conflicts_with must be empty for active status")
|
|
589
|
+
elif status == "conflict":
|
|
590
|
+
require(bool(conflicts), f"{label}.conflicts_with is required for conflict status")
|
|
591
|
+
else:
|
|
592
|
+
require(not conflicts, f"{label}.conflicts_with is only valid for conflict status")
|
|
593
|
+
return {
|
|
594
|
+
"kind": choice(preference.get("kind"), KINDS, f"{label}.kind"),
|
|
595
|
+
"scope": normalize_scope(preference.get("scope"), f"{label}.scope"),
|
|
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),
|
|
599
|
+
"quote": clean_text(preference.get("quote"), f"{label}.quote", max_bytes=768),
|
|
600
|
+
"source_ref": optional_text(preference.get("source_ref"), f"{label}.source_ref", max_bytes=240),
|
|
601
|
+
"source": source,
|
|
602
|
+
"confidence": choice(preference.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
|
|
603
|
+
"importance": choice(preference.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
|
|
604
|
+
"status": status,
|
|
605
|
+
"reason": clean_text(preference.get("reason"), f"{label}.reason", max_bytes=480),
|
|
606
|
+
"conflicts_with": conflicts,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def normalize_transaction(value: object) -> dict[str, Any]:
|
|
611
|
+
transaction = as_mapping(value, "transaction")
|
|
612
|
+
require_known_keys(transaction, {"schema_version", "transaction_id", "expected_state_revision", "operations"}, "transaction")
|
|
613
|
+
require(transaction.get("schema_version") == INPUT_SCHEMA_VERSION, f"transaction.schema_version must be {INPUT_SCHEMA_VERSION}")
|
|
614
|
+
transaction_id = clean_text(transaction.get("transaction_id"), "transaction.transaction_id", max_bytes=128)
|
|
615
|
+
operations = as_list(transaction.get("operations"), "transaction.operations")
|
|
616
|
+
require(1 <= len(operations) <= 32, "transaction.operations must contain 1-32 operations")
|
|
617
|
+
normalized_operations: list[dict[str, Any]] = []
|
|
618
|
+
for index, raw_operation in enumerate(operations):
|
|
619
|
+
label = f"transaction.operations[{index}]"
|
|
620
|
+
operation = as_mapping(raw_operation, label)
|
|
621
|
+
action = operation.get("action")
|
|
622
|
+
if action == "remember":
|
|
623
|
+
require_known_keys(operation, {"action", "preference"}, label)
|
|
624
|
+
normalized_operations.append({"action": action, "preference": normalize_preference(operation.get("preference"), f"{label}.preference", allow_status=True)})
|
|
625
|
+
elif action == "decide":
|
|
626
|
+
require_known_keys(operation, {"action", "item_id", "decision", "quote", "reason"}, label)
|
|
627
|
+
normalized_operations.append({
|
|
628
|
+
"action": action,
|
|
629
|
+
"item_id": clean_id_list([operation.get("item_id")], f"{label}.item_id", maximum=1)[0],
|
|
630
|
+
"decision": choice(operation.get("decision"), ("activate", "reject"), f"{label}.decision"),
|
|
631
|
+
"quote": clean_text(operation.get("quote"), f"{label}.quote", max_bytes=768),
|
|
632
|
+
"reason": clean_text(operation.get("reason"), f"{label}.reason", max_bytes=480),
|
|
633
|
+
})
|
|
634
|
+
elif action == "replace":
|
|
635
|
+
require_known_keys(operation, {"action", "old_ids", "preference"}, label)
|
|
636
|
+
old_ids = clean_id_list(operation.get("old_ids"), f"{label}.old_ids")
|
|
637
|
+
require(bool(old_ids), f"{label}.old_ids must not be empty")
|
|
638
|
+
normalized_operations.append({"action": action, "old_ids": old_ids, "preference": normalize_preference(operation.get("preference"), f"{label}.preference", allow_status=False)})
|
|
639
|
+
elif action == "forget":
|
|
640
|
+
require_known_keys(operation, {"action", "item_id", "quote", "reason"}, label)
|
|
641
|
+
normalized_operations.append({
|
|
642
|
+
"action": action,
|
|
643
|
+
"item_id": clean_id_list([operation.get("item_id")], f"{label}.item_id", maximum=1)[0],
|
|
644
|
+
"quote": clean_text(operation.get("quote"), f"{label}.quote", max_bytes=768),
|
|
645
|
+
"reason": clean_text(operation.get("reason"), f"{label}.reason", max_bytes=480),
|
|
646
|
+
})
|
|
647
|
+
else:
|
|
648
|
+
raise AuthorMemoryError(f"{label}.action must be one of: remember, decide, replace, forget")
|
|
649
|
+
return {
|
|
650
|
+
"schema_version": INPUT_SCHEMA_VERSION,
|
|
651
|
+
"transaction_id": transaction_id,
|
|
652
|
+
"expected_state_revision": as_int(transaction.get("expected_state_revision"), "transaction.expected_state_revision"),
|
|
653
|
+
"operations": normalized_operations,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def normalize_record_event(value: object) -> dict[str, Any]:
|
|
658
|
+
event = as_mapping(value, "event")
|
|
659
|
+
require_known_keys(event, {"schema_version", "event_id", "operation"}, "event")
|
|
660
|
+
require(event.get("schema_version") == INPUT_SCHEMA_VERSION, f"event.schema_version must be {INPUT_SCHEMA_VERSION}")
|
|
661
|
+
event_id = clean_text(event.get("event_id"), "event.event_id", max_bytes=120)
|
|
662
|
+
normalized = normalize_transaction({
|
|
663
|
+
"schema_version": INPUT_SCHEMA_VERSION,
|
|
664
|
+
"transaction_id": f"record:{event_id}",
|
|
665
|
+
"expected_state_revision": 0,
|
|
666
|
+
"operations": [event.get("operation")],
|
|
667
|
+
})
|
|
668
|
+
return {"event_id": event_id, "operation": normalized["operations"][0]}
|
|
669
|
+
|
|
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
|
+
|
|
724
|
+
def transaction_digest(transaction: dict[str, Any]) -> str:
|
|
725
|
+
canonical = json.dumps(transaction, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
726
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
727
|
+
|
|
728
|
+
|
|
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"]
|
|
733
|
+
value = {
|
|
734
|
+
"kind": preference["kind"],
|
|
735
|
+
"scope": {"level": scope["level"], "value": None if scope["value"] is None else scope["value"].casefold()},
|
|
736
|
+
"assertion": preference["assertion"].casefold(),
|
|
737
|
+
}
|
|
738
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
739
|
+
|
|
740
|
+
|
|
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}"
|
|
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"])
|
|
778
|
+
return {
|
|
779
|
+
"id": next_item_id(state),
|
|
780
|
+
"kind": preference["kind"],
|
|
781
|
+
"scope": copy.deepcopy(preference["scope"]),
|
|
782
|
+
"assertion": preference["assertion"],
|
|
783
|
+
"confidence": preference["confidence"],
|
|
784
|
+
"importance": preference["importance"],
|
|
785
|
+
"status": preference["status"],
|
|
786
|
+
"source": preference["source"],
|
|
787
|
+
"reason": preference["reason"],
|
|
788
|
+
"conflicts_with": list(preference["conflicts_with"]),
|
|
789
|
+
"confirmation_count": 1,
|
|
790
|
+
"evidence": [{"quote": preference["quote"], "source_ref": preference["source_ref"]}],
|
|
791
|
+
"created_revision": revision,
|
|
792
|
+
"updated_revision": revision,
|
|
793
|
+
"superseded_by": None,
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def best_level(first: str, second: str) -> str:
|
|
798
|
+
return first if RANK[first] >= RANK[second] else second
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def add_evidence(item: dict[str, Any], quote: str, source_ref: str | None) -> None:
|
|
802
|
+
evidence = {"quote": quote, "source_ref": source_ref}
|
|
803
|
+
if evidence not in item["evidence"]:
|
|
804
|
+
item["evidence"].append(evidence)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def require_item(state: dict[str, Any], item_id: str, label: str) -> dict[str, Any]:
|
|
808
|
+
require(item_id in state["items"], f"{label} references unknown item {item_id}")
|
|
809
|
+
return state["items"][item_id]
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def apply_remember(state: dict[str, Any], preference: dict[str, Any], revision: int) -> str:
|
|
813
|
+
require_scope_fits_store(state, preference["scope"])
|
|
814
|
+
for conflict_id in preference["conflicts_with"]:
|
|
815
|
+
conflict = require_item(state, conflict_id, "remember")
|
|
816
|
+
require(conflict["status"] == "active", f"remember conflict {conflict_id} must be active")
|
|
817
|
+
preference_fingerprint = fingerprint(preference)
|
|
818
|
+
for item in state["items"].values():
|
|
819
|
+
if item["status"] not in {"active", "pending", "conflict"} or fingerprint(item) != preference_fingerprint:
|
|
820
|
+
continue
|
|
821
|
+
require(not (item["status"] == "conflict" and preference["status"] == "active"), f"conflict item {item['id']} must be resolved with replace or rejected")
|
|
822
|
+
require(not (item["status"] == "active" and preference["status"] == "conflict"), f"active item {item['id']} cannot be recategorized as its own conflict")
|
|
823
|
+
add_evidence(item, preference["quote"], preference["source_ref"])
|
|
824
|
+
item["confirmation_count"] += 1
|
|
825
|
+
item["confidence"] = best_level(item["confidence"], preference["confidence"])
|
|
826
|
+
item["importance"] = best_level(item["importance"], preference["importance"])
|
|
827
|
+
item["updated_revision"] = revision
|
|
828
|
+
item["reason"] = preference["reason"]
|
|
829
|
+
if item["status"] == "pending" and preference["status"] == "active":
|
|
830
|
+
item["status"] = "active"
|
|
831
|
+
elif item["status"] == "pending" and preference["status"] == "conflict":
|
|
832
|
+
item["status"] = "conflict"
|
|
833
|
+
item["conflicts_with"] = list(preference["conflicts_with"])
|
|
834
|
+
elif item["status"] == "conflict" and preference["status"] == "conflict":
|
|
835
|
+
item["conflicts_with"] = sorted(set(item["conflicts_with"]) | set(preference["conflicts_with"]))
|
|
836
|
+
return f"强化 {item['id']}:{item['assertion']}"
|
|
837
|
+
item = allocate_item(state, preference, revision)
|
|
838
|
+
state["items"][item["id"]] = item
|
|
839
|
+
return f"新增 {item['id']}({item['status']}):{item['assertion']}"
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def apply_decide(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
|
|
843
|
+
item = require_item(state, operation["item_id"], "decide")
|
|
844
|
+
require(item["status"] in {"pending", "conflict"}, f"decide requires pending/conflict item, got {item['status']}")
|
|
845
|
+
if operation["decision"] == "activate":
|
|
846
|
+
require(item["status"] == "pending" and not item["conflicts_with"], "conflict candidates must be activated with replace")
|
|
847
|
+
item["status"] = "active"
|
|
848
|
+
verb = "确认"
|
|
849
|
+
else:
|
|
850
|
+
item["status"] = "rejected"
|
|
851
|
+
verb = "拒绝"
|
|
852
|
+
add_evidence(item, operation["quote"], None)
|
|
853
|
+
item["reason"] = operation["reason"]
|
|
854
|
+
item["updated_revision"] = revision
|
|
855
|
+
return f"{verb} {item['id']}:{item['assertion']}"
|
|
856
|
+
|
|
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
|
+
|
|
875
|
+
def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
|
|
876
|
+
old_items = [require_item(state, item_id, "replace") for item_id in operation["old_ids"]]
|
|
877
|
+
for item in old_items:
|
|
878
|
+
require(item["status"] in {"active", "conflict", "pending"}, f"replace target {item['id']} is already {item['status']}")
|
|
879
|
+
replacement = allocate_item(state, operation["preference"], revision)
|
|
880
|
+
replacement["status"] = "active"
|
|
881
|
+
replacement["conflicts_with"] = []
|
|
882
|
+
state["items"][replacement["id"]] = replacement
|
|
883
|
+
for item in old_items:
|
|
884
|
+
item["status"] = "superseded"
|
|
885
|
+
item["superseded_by"] = replacement["id"]
|
|
886
|
+
item["updated_revision"] = revision
|
|
887
|
+
released = release_conflicts(state, {item["id"] for item in old_items}, revision)
|
|
888
|
+
replaced = ", ".join(item["id"] for item in old_items)
|
|
889
|
+
suffix = f";{released} 个其他冲突候选退回待确认" if released else ""
|
|
890
|
+
return f"用 {replacement['id']} 替代 {replaced}:{replacement['assertion']}{suffix}"
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def apply_forget(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
|
|
894
|
+
item = require_item(state, operation["item_id"], "forget")
|
|
895
|
+
require(item["status"] in {"active", "pending", "conflict"}, f"forget target {item['id']} is already {item['status']}")
|
|
896
|
+
item["status"] = "superseded"
|
|
897
|
+
item["superseded_by"] = None
|
|
898
|
+
item["reason"] = operation["reason"]
|
|
899
|
+
item["updated_revision"] = revision
|
|
900
|
+
add_evidence(item, operation["quote"], None)
|
|
901
|
+
released = release_conflicts(state, {item["id"]}, revision)
|
|
902
|
+
suffix = f";{released} 个冲突候选退回待确认" if released else ""
|
|
903
|
+
return f"忘记 {item['id']}:{item['assertion']}{suffix}"
|
|
904
|
+
|
|
905
|
+
|
|
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]]:
|
|
941
|
+
applied = state["applied_transactions"].get(transaction["transaction_id"])
|
|
942
|
+
if applied is not None:
|
|
943
|
+
require(applied["digest"] == digest, "transaction_id was already used with different content")
|
|
944
|
+
return state, [f"事务已应用于修订 {applied['revision']},本次为幂等重放"]
|
|
945
|
+
require(transaction["expected_state_revision"] == state["state_revision"], f"stale state revision: expected {transaction['expected_state_revision']}, current {state['state_revision']}")
|
|
946
|
+
updated = copy.deepcopy(state)
|
|
947
|
+
revision = updated["state_revision"] + 1
|
|
948
|
+
summaries: list[str] = []
|
|
949
|
+
for operation in transaction["operations"]:
|
|
950
|
+
if operation["action"] == "remember":
|
|
951
|
+
summaries.append(apply_remember(updated, operation["preference"], revision))
|
|
952
|
+
elif operation["action"] == "decide":
|
|
953
|
+
summaries.append(apply_decide(updated, operation, revision))
|
|
954
|
+
elif operation["action"] == "replace":
|
|
955
|
+
summaries.append(apply_replace(updated, operation, revision))
|
|
956
|
+
else:
|
|
957
|
+
summaries.append(apply_forget(updated, operation, revision))
|
|
958
|
+
return seal_transaction(updated, transaction["transaction_id"], digest, summaries, store=store), summaries
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ---------------------------------------------------------------------------
|
|
962
|
+
# Derived views
|
|
963
|
+
# ---------------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def scope_label(scope: dict[str, str | None]) -> str:
|
|
967
|
+
if scope["level"] == "global":
|
|
968
|
+
return "全局"
|
|
969
|
+
labels = {"genre": "题材", "book": "本书", "workflow": "流程"}
|
|
970
|
+
return f"{labels[scope['level']]}:{scope['value']}"
|
|
971
|
+
|
|
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
|
+
|
|
980
|
+
def render_profile(state: dict[str, Any]) -> str:
|
|
981
|
+
lines = [
|
|
982
|
+
"# 作者画像",
|
|
983
|
+
"",
|
|
984
|
+
"<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
|
|
985
|
+
"",
|
|
986
|
+
f"> 状态修订:{state['state_revision']}。{store_caption(state)}仅列出已确认偏好;当前明确要求、本书设定与硬性门禁优先。",
|
|
987
|
+
"",
|
|
988
|
+
]
|
|
989
|
+
active = [item for item in state["items"].values() if item["status"] == "active"]
|
|
990
|
+
for kind in KINDS:
|
|
991
|
+
lines.extend([f"## {KIND_TITLES[kind]}", ""])
|
|
992
|
+
items = sorted((item for item in active if item["kind"] == kind), key=lambda item: id_number(item["id"]))
|
|
993
|
+
if not items:
|
|
994
|
+
lines.extend(["- 暂无", ""])
|
|
995
|
+
continue
|
|
996
|
+
for item in items:
|
|
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
|
+
)
|
|
1003
|
+
lines.append("")
|
|
1004
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def render_pending(state: dict[str, Any]) -> str:
|
|
1008
|
+
lines = [
|
|
1009
|
+
"# 待确认的作者习惯",
|
|
1010
|
+
"",
|
|
1011
|
+
"<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
|
|
1012
|
+
"",
|
|
1013
|
+
f"> 状态修订:{state['state_revision']}。{store_caption(state)}待确认项不参与创作约束,也不应打断当前任务。",
|
|
1014
|
+
"",
|
|
1015
|
+
]
|
|
1016
|
+
items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item: id_number(item["id"]))
|
|
1017
|
+
if not items:
|
|
1018
|
+
lines.extend(["暂无待确认项。", ""])
|
|
1019
|
+
for item in items:
|
|
1020
|
+
lines.extend([
|
|
1021
|
+
f"## {item['id']} · {'冲突' if item['status'] == 'conflict' else '待确认'}",
|
|
1022
|
+
"",
|
|
1023
|
+
f"- 候选习惯:{item['assertion']}",
|
|
1024
|
+
f"- 范围:{scope_label(item['scope'])}",
|
|
1025
|
+
f"- 原话:“{item['evidence'][-1]['quote']}”",
|
|
1026
|
+
f"- 依据:{item['reason']}",
|
|
1027
|
+
f"- 置信度 / 重要度:{item['confidence']} / {item['importance']}",
|
|
1028
|
+
])
|
|
1029
|
+
if item["conflicts_with"]:
|
|
1030
|
+
lines.append(f"- 冲突对象:{', '.join(item['conflicts_with'])}")
|
|
1031
|
+
lines.append("")
|
|
1032
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def render_journal(state: dict[str, Any]) -> str:
|
|
1036
|
+
lines = [
|
|
1037
|
+
"# 作者记忆变更记录",
|
|
1038
|
+
"",
|
|
1039
|
+
"<!-- 由 author_memory_commit.py 生成,请勿手改;最近记录在前。 -->",
|
|
1040
|
+
"",
|
|
1041
|
+
f"> {store_caption(state)}",
|
|
1042
|
+
"",
|
|
1043
|
+
]
|
|
1044
|
+
if not state["journal"]:
|
|
1045
|
+
lines.extend(["暂无变更。", ""])
|
|
1046
|
+
for entry in reversed(state["journal"][-100:]):
|
|
1047
|
+
lines.extend([f"## r{entry['revision']} · {entry['committed_at']}", "", f"- 事务:`{entry['transaction_id']}`"])
|
|
1048
|
+
lines.extend(f"- {summary}" for summary in entry["summaries"])
|
|
1049
|
+
lines.append("")
|
|
1050
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def render_views(state: dict[str, Any]) -> dict[str, str]:
|
|
1054
|
+
views = {
|
|
1055
|
+
"作者画像.md": render_profile(state),
|
|
1056
|
+
"待确认.md": render_pending(state),
|
|
1057
|
+
"变更记录.md": render_journal(state),
|
|
1058
|
+
}
|
|
1059
|
+
limits = {"作者画像.md": PROFILE_MAX_BYTES, "待确认.md": PENDING_MAX_BYTES, "变更记录.md": JOURNAL_MAX_BYTES}
|
|
1060
|
+
for name, payload in views.items():
|
|
1061
|
+
require(len(payload.encode("utf-8")) <= limits[name], f"{name} exceeds {limits[name]} bytes; consolidate old memory first")
|
|
1062
|
+
return views
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def write_snapshot(store: Store, state: dict[str, Any]) -> None:
|
|
1066
|
+
views = render_views(state)
|
|
1067
|
+
state_payload = json_payload(state)
|
|
1068
|
+
require(len(state_payload.encode("utf-8")) <= STATE_MAX_BYTES, f"_author-memory-state.json exceeds {STATE_MAX_BYTES} bytes")
|
|
1069
|
+
for name, payload in views.items():
|
|
1070
|
+
write_if_changed(store.root / name, payload)
|
|
1071
|
+
# State is the authority and therefore the last commit point.
|
|
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] + "…"
|
|
1086
|
+
|
|
1087
|
+
|
|
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 读不出来,放在任何读取之前才能让只读查询也自愈。"""
|
|
1321
|
+
require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
|
|
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
|
|
1330
|
+
|
|
1331
|
+
|
|
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)
|
|
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)
|
|
1348
|
+
digest = transaction_digest(transaction)
|
|
1349
|
+
updated, summaries = apply_transaction(state, transaction, digest, store=store)
|
|
1350
|
+
replayed = updated is state
|
|
1351
|
+
# 幂等重放时也重写快照,修复缺失或过期的派生视图。
|
|
1352
|
+
write_snapshot(store, updated)
|
|
1353
|
+
project_state, book_state, load_warnings = visible_states(store, updated, workspace, book_root, book)
|
|
1354
|
+
return {
|
|
1355
|
+
"ok": True,
|
|
1356
|
+
"command": "commit",
|
|
1357
|
+
"revision": updated["state_revision"],
|
|
1358
|
+
"transaction_id": transaction["transaction_id"],
|
|
1359
|
+
"replayed": replayed,
|
|
1360
|
+
"item_ids": updated["applied_transactions"][transaction["transaction_id"]]["item_ids"],
|
|
1361
|
+
"summaries": summaries,
|
|
1362
|
+
"warnings": load_warnings + query_budget_warnings(project_state, book_state),
|
|
1363
|
+
**store_fields(store),
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
|
|
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)
|
|
1369
|
+
event = normalize_record_event(read_json(input_path))
|
|
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)
|
|
1374
|
+
transaction_id = f"record:{event['event_id']}"
|
|
1375
|
+
applied = state["applied_transactions"].get(transaction_id)
|
|
1376
|
+
expected_revision = applied["revision"] - 1 if applied is not None else state["state_revision"]
|
|
1377
|
+
transaction = {
|
|
1378
|
+
"schema_version": INPUT_SCHEMA_VERSION,
|
|
1379
|
+
"transaction_id": transaction_id,
|
|
1380
|
+
"expected_state_revision": expected_revision,
|
|
1381
|
+
"operations": [event["operation"]],
|
|
1382
|
+
}
|
|
1383
|
+
digest = transaction_digest(transaction)
|
|
1384
|
+
updated, summaries = apply_transaction(state, transaction, digest, store=store)
|
|
1385
|
+
replayed = updated is state
|
|
1386
|
+
write_snapshot(store, updated)
|
|
1387
|
+
record = updated["applied_transactions"][transaction_id]
|
|
1388
|
+
item_ids = record["item_ids"]
|
|
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)
|
|
1391
|
+
return {
|
|
1392
|
+
"ok": True,
|
|
1393
|
+
"command": "record",
|
|
1394
|
+
"revision": updated["state_revision"],
|
|
1395
|
+
"applied_revision": record["revision"],
|
|
1396
|
+
"event_id": event["event_id"],
|
|
1397
|
+
"replayed": replayed,
|
|
1398
|
+
"item_ids": item_ids,
|
|
1399
|
+
"receipt": receipt,
|
|
1400
|
+
"summaries": summaries,
|
|
1401
|
+
"warnings": load_warnings + query_budget_warnings(project_state, book_state),
|
|
1402
|
+
**store_fields(store),
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def command_query(
|
|
1407
|
+
workspace: Path,
|
|
1408
|
+
book_root: Path | None,
|
|
1409
|
+
kinds: list[str] | None,
|
|
1410
|
+
book: str | None,
|
|
1411
|
+
genre: str | None,
|
|
1412
|
+
workflow: str | None,
|
|
1413
|
+
) -> dict[str, Any]:
|
|
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": []}
|
|
1423
|
+
requested_scopes = {
|
|
1424
|
+
"genre": optional_text(genre, "query.genre", max_bytes=180),
|
|
1425
|
+
"workflow": optional_text(workflow, "query.workflow", max_bytes=180),
|
|
1426
|
+
}
|
|
1427
|
+
# 装不下的**跳过而不中断**(一条长的不挡后面的短条),漏下的 ID 报进
|
|
1428
|
+
# omitted_ids:非空=记忆超编该整理了,不是「没有更多了」。写入端的
|
|
1429
|
+
# warnings 与这里共用 build_query_result 同一个信封,估算即实况。
|
|
1430
|
+
result, _ = merged_query(project_state, book_state, set(kinds), requested_scopes)
|
|
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")
|
|
1432
|
+
return result
|
|
1433
|
+
|
|
1434
|
+
|
|
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")
|
|
1438
|
+
views = render_views(state)
|
|
1439
|
+
for name, expected in views.items():
|
|
1440
|
+
view_path = store.root / name
|
|
1441
|
+
require(view_path.exists(), f"missing derived view: {view_path}")
|
|
1442
|
+
require(view_path.read_text(encoding="utf-8") == expected, f"derived view is stale or edited: {view_path}")
|
|
1443
|
+
counts = {status: sum(1 for item in state["items"].values() if item["status"] == status) for status in STATUSES}
|
|
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)
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1590
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
1591
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1592
|
+
|
|
1593
|
+
def add_location(child: argparse.ArgumentParser) -> None:
|
|
1594
|
+
child.add_argument("--workspace", required=True, type=Path)
|
|
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)
|
|
1604
|
+
query = subparsers.add_parser("query")
|
|
1605
|
+
add_location(query)
|
|
1606
|
+
query.add_argument("--kind", action="append", choices=KINDS)
|
|
1607
|
+
query.add_argument("--genre")
|
|
1608
|
+
query.add_argument("--workflow")
|
|
1609
|
+
return parser
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def main() -> int:
|
|
1613
|
+
args = build_parser().parse_args()
|
|
1614
|
+
try:
|
|
1615
|
+
if args.command == "init":
|
|
1616
|
+
result = command_init(args.workspace, args.book_root, args.book)
|
|
1617
|
+
elif args.command == "commit":
|
|
1618
|
+
result = command_commit(args.workspace, args.book_root, args.book, args.input)
|
|
1619
|
+
elif args.command == "record":
|
|
1620
|
+
result = command_record(args.workspace, args.book_root, args.book, args.input)
|
|
1621
|
+
elif args.command == "query":
|
|
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)
|
|
1625
|
+
else:
|
|
1626
|
+
result = command_check(args.workspace, args.book_root, args.book)
|
|
1627
|
+
emit(result)
|
|
1628
|
+
return 0
|
|
1629
|
+
except AuthorMemoryError as exc:
|
|
1630
|
+
emit({"ok": False, "error": str(exc)}, error=True)
|
|
1631
|
+
return 2
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
if __name__ == "__main__":
|
|
1635
|
+
raise SystemExit(main())
|