@oh-story/dsh 0.1.17 → 0.1.19

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.
Files changed (136) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +147 -96
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +77 -247
  5. package/lib/drama/skills/short-drama/SKILL.md +9 -1
  6. package/lib/drama/skills/short-drama/references/creator-documents.md +51 -23
  7. package/lib/drama/skills/short-drama/references/reference-roles.md +1 -1
  8. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +442 -129
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +54 -1
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -6
  11. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +9 -3
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +3 -3
  13. package/lib/drama/skills/short-drama-assets/references/location-and-view.md +4 -0
  14. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +2 -2
  15. package/lib/drama/skills/short-drama-develop/references/episode-design.md +19 -4
  16. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +1 -1
  17. package/lib/drama/skills/short-drama-edit/SKILL.md +1 -1
  18. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +28 -6
  19. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +1 -5
  21. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  22. package/lib/drama/skills/short-drama-produce/SKILL.md +2 -0
  23. package/lib/drama/skills/short-drama-review/SKILL.md +0 -4
  24. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +1 -1
  25. package/lib/drama/skills/short-drama-storyboard/SKILL.md +9 -9
  26. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +43 -13
  27. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +5 -5
  28. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +5 -5
  29. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +27 -3
  30. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -1
  31. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +6 -8
  32. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +16 -1
  33. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +23 -6
  34. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +2 -2
  35. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -1
  36. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -2
  37. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +1 -1
  38. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -1
  39. package/lib/drama/skills/short-drama-write/SKILL.md +7 -3
  40. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +1 -1
  41. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +94 -1
  42. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +13 -4
  43. package/lib/drama/skills/short-drama-write/scripts/selftest.py +3 -77
  44. package/lib/index.js +18 -14
  45. package/lib/index.js.map +2 -2
  46. package/lib/novel-to-game/README.md +20 -34
  47. package/lib/novel-to-game/README_ZH.md +20 -34
  48. package/lib/novel-to-game/VERSION +1 -1
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +2 -6
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +33 -39
  51. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +2 -2
  52. package/lib/novel-to-game/manifest.json +46 -46
  53. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +1 -1
  54. package/lib/novel-to-game/skills/game-build/references/baked-sprite-workflow.md +2 -3
  55. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +6 -7
  56. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +2 -2
  57. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +1 -4
  58. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +1 -3
  59. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +1 -3
  60. package/lib/novel-to-game/skills/game-concept/SKILL.md +1 -1
  61. package/lib/novel-to-game/skills/game-qa/SKILL.md +3 -4
  62. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +1 -1
  63. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +3 -2
  64. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +13 -14
  65. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +1 -2
  66. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +4 -4
  67. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +2 -3
  68. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +0 -8
  69. package/lib/oh-story/manifest.json +49 -49
  70. package/lib/oh-story/skills/story/SKILL.md +2 -2
  71. package/lib/oh-story/skills/story/VERSION +1 -1
  72. package/lib/oh-story/skills/story-deslop/SKILL.md +2 -2
  73. package/lib/oh-story/skills/story-import/SKILL.md +5 -5
  74. package/lib/oh-story/skills/story-import/references/deep-analysis.md +1 -1
  75. package/lib/oh-story/skills/story-import/references/import-report.md +1 -1
  76. package/lib/oh-story/skills/story-import/references/import-tracking.md +1 -1
  77. package/lib/oh-story/skills/story-import/references/outline-reverse.md +1 -1
  78. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +6 -1
  79. package/lib/oh-story/skills/story-long-analyze/SKILL.md +2 -2
  80. package/lib/oh-story/skills/story-long-write/SKILL.md +2 -2
  81. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +3 -3
  82. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -2
  83. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +6 -1
  84. package/lib/oh-story/skills/story-review/SKILL.md +8 -7
  85. package/lib/oh-story/skills/story-review/references/agent-prompts.md +7 -7
  86. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -2
  87. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +6 -1
  88. package/lib/oh-story/skills/story-setup/SKILL.md +6 -6
  89. package/lib/oh-story/skills/story-short-write/SKILL.md +2 -2
  90. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +3 -3
  91. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +2 -2
  92. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +1 -1
  93. package/lib/types/client/game-preview-state.d.ts +34 -0
  94. package/lib/types/client/game-preview-state.d.ts.map +1 -0
  95. package/lib/types/client/index.d.ts.map +1 -1
  96. package/lib/types/client/video-studio.d.ts +24 -0
  97. package/lib/types/client/video-studio.d.ts.map +1 -1
  98. package/lib/types/host-python.d.ts +10 -3
  99. package/lib/types/host-python.d.ts.map +1 -1
  100. package/lib/types/workspace-route.d.ts +6 -0
  101. package/lib/types/workspace-route.d.ts.map +1 -1
  102. package/package.json +21 -21
  103. package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +0 -3
  104. package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +0 -4
  105. package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +0 -5
  106. package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +0 -3
  107. package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +0 -5
  108. package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +0 -3
  109. package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +0 -2
  110. package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +0 -2
  111. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +0 -267
  112. package/lib/drama/skills/short-drama-assets/scripts/selftest.py +0 -89
  113. package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +0 -2
  114. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +0 -443
  115. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +0 -95
  116. package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +0 -2
  117. package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +0 -42
  118. package/lib/drama/skills/short-drama-review/assets/verdict-template.json +0 -32
  119. package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +0 -2
  120. package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +0 -34
  121. package/lib/drama/skills/short-drama-review/scripts/review_check.py +0 -325
  122. package/lib/drama/skills/short-drama-review/scripts/selftest.py +0 -87
  123. package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +0 -2
  124. package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +0 -12
  125. package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +0 -2
  126. package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +0 -191
  127. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +0 -824
  128. package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +0 -21
  129. package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +0 -2
  130. package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +0 -38
  131. package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +0 -2
  132. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +0 -329
  133. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +0 -451
  134. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +0 -327
  135. package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +0 -82
  136. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +0 -416
@@ -15,6 +15,12 @@ export declare function parseByteRange(value: string | undefined, size: number):
15
15
  export declare function assertCreativePath(path: string, kind: "text" | "media"): void;
16
16
  export declare function mediaMimeTypeForPath(path: string): string | undefined;
17
17
  export declare function gameRoot(path: string): boolean;
18
+ /**
19
+ * The game's name from its brief's first heading, e.g. `# PRODUCT_BRIEF — 《雪夜渡》` → `雪夜渡`.
20
+ * The Studio adds its own 书名号 and separators, so neither the file label nor the work's
21
+ * 书名号 belong in the name.
22
+ */
23
+ export declare function gameProjectTitle(content: string | undefined, fallback: string): string;
18
24
  /**
19
25
  * Emitted for EVERY preview response, not just HTML. previewContentType serves .svg as
20
26
  * image/svg+xml — an active document type — so a game that self-navigates its frame to a scripted
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-route.d.ts","sourceRoot":"","sources":["../../src/workspace-route.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAuCnD,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AA8HD,MAAM,WAAW,SAAS;IAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAE3E;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAcpG;AAwGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAW7E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErE;AAoHD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ9C;AAmMD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAiBnE;AA2JD,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAM7F"}
1
+ {"version":3,"file":"workspace-route.d.ts","sourceRoot":"","sources":["../../src/workspace-route.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAuCnD,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AA8HD,MAAM,WAAW,SAAS;IAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAE3E;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAcpG;AAwGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAW7E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErE;AAoHD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ9C;AAsED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStF;AA0HD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAiBnE;AA2JD,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAM7F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-story/dsh",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "A DSH plugin for fiction, short-drama, interactive-game, and video-recap production",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -98,26 +98,26 @@
98
98
  },
99
99
  "devDependencies": {
100
100
  "@deepseek-ai/cordis": "4.0.5",
101
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
102
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
103
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
104
- "@deepseek-ai/dsh-client-ui-chat": "0.2.1-rc.2",
105
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
106
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
107
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
108
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.2",
109
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
110
- "@deepseek-ai/dsh-client-ui-tool": "0.2.1-rc.2",
111
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
112
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
113
- "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
114
- "@deepseek-ai/dsh-session": "0.2.1-rc.2",
115
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
116
- "@deepseek-ai/dsh-skill": "0.2.1-rc.2",
117
- "@deepseek-ai/dsh-subagent": "0.2.1-rc.2",
118
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
119
- "@deepseek-ai/dsh-typert-registry": "0.2.1-rc.2",
120
- "@deepseek-ai/dsh-util-values": "0.2.1-rc.2",
101
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.4",
102
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.4",
103
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.4",
104
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.1-rc.4",
105
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.4",
106
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.4",
107
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.4",
108
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.4",
109
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.4",
110
+ "@deepseek-ai/dsh-client-ui-tool": "0.2.1-rc.4",
111
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.4",
112
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.4",
113
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.4",
114
+ "@deepseek-ai/dsh-session": "0.2.1-rc.4",
115
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.4",
116
+ "@deepseek-ai/dsh-skill": "0.2.1-rc.4",
117
+ "@deepseek-ai/dsh-subagent": "0.2.1-rc.4",
118
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.4",
119
+ "@deepseek-ai/dsh-typert-registry": "0.2.1-rc.4",
120
+ "@deepseek-ai/dsh-util-values": "0.2.1-rc.4",
121
121
  "@deepseek-ai/schemastery": "3.18.5",
122
122
  "@types/react": "18.3.28",
123
123
  "@types/react-dom": "18.3.7",
@@ -1,3 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"guhe-audition-02":{"owner":"creator","artifact":"输入/voice-refs/guhe-audition-02.wav"},"reference-observations":{"owner":"creator","artifact":"创作者决策/reference-observations.jsonl"},"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"cd-char-001":{"owner":"creator","artifact":"创作者决策/cd-char-001.json"},"characters":{"owner":"short-drama-assets","artifact":"设定集/characters.jsonl"},"cd-look-001":{"owner":"creator","artifact":"创作者决策/cd-look-001.json"}}}
2
- {"example_only":true,"destination":"设定集/characters.jsonl","schema_version":"1.0.0-draft","record_type":"character","character_id":"CHAR-GUHE","display_name":"顾禾","aliases":["小顾"],"identity_anchors":["窄长脸","左眉尾短缺口","自然卷黑发","肩背略前倾"],"persistent_performance_facts":["说话前常先观察对方手里的物件"],"voice_direction":{"language":"zh-CN","reference":{"slot_id":"VREF-GUHE-01","artifact_ref":{"src":"guhe-audition-02"},"role":"timbre","may_control":["音色质感","年龄与体量印象"],"must_not_control":["这条试音稿里的克制情绪","录音棚的混响与话筒距离","背景底噪"],"admission_status":"creator_described","reference_observation_ref":{"src":"reference-observations","record_id":"OBS-VREF-GUHE-01"},"unresolved_risks":[]},"selection_criteria":[{"kind":"register","text":"中低音区,比同剧其他成年男性低半档","counter_example":"低到需要压喉才发得出,就过头了"},{"kind":"rhythm","text":"句尾常收住而不拖长","counter_example":"每句都硬切断,听起来像念稿"}],"distinction":{"nearest_character_id":"CHAR-LINSHU","distinguishing_trait":"顾禾句尾收住;林舒句尾常带一段下滑的余音"},"pronunciation_notes":[{"term":"顾禾","accepted_reading":"Gù Hé"}],"not_voice_identity":["本场因淋雨造成的短暂气息不稳","某句台词的临时音量"]},"not_identity":["本集橙色雨衣","右肩湿痕","瞬时低头"],"source_refs":[{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A01"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-char-001","record_id":"CD-CHAR-001"}}}
3
- {"example_only":true,"destination":"设定集/looks.jsonl","schema_version":"1.0.0-draft","record_type":"look","look_id":"LOOK-GUHE-RAIN","character_ref":{"src":"characters","record_id":"CHAR-GUHE"},"base_look_id":"LOOK-GUHE-WORK","differences":{"wardrobe_layers":["工作装外加橙色旧雨衣"],"hair_styling":["低束,兜帽放下"],"makeup":[],"injury":[],"weathering":[]},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A01"},"validity":{"from":"EP003/SC004/BLK-EP003-SC004-A01","until":"EP003/SC006/BLK-EP003-SC006-A03"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-look-001","record_id":"CD-LOOK-001"}}}
@@ -1,4 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"props":{"owner":"short-drama-assets","artifact":"设定集/props.jsonl"},"prop-states":{"owner":"short-drama-assets","artifact":"设定集/prop-states.jsonl"},"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"locations":{"owner":"short-drama-assets","artifact":"设定集/locations.jsonl"}}}
2
- {"example_only":true,"schema_version":"1.0.0-draft","delta_id":"DELTA-TIN-OPEN","episode_id":"EP003","scene_id":"SC004","subject_ref":{"src":"props","record_id":"PROP-TIN-CASE"},"state_field":"condition.open","before":{"value":"closed","state_ref":{"src":"prop-states","record_id":"PSTATE-TIN-CLOSED-BADGE-IN"}},"after":{"value":"open","proposed_state_id":"PSTATE-TIN-OPEN-EMPTY"},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A03"},"effective_range":{"from":"EP003/SC004/BLK-EP003-SC004-A03","until":"open_ended"},"reconciliation":{"next_linked_start":"EP003/SC005/start","status":"must_match_or_revise"},"creator_acceptance":{"status":"proposed","decision_ref":null},"affected_binding_refs":[{"src":"props","record_id":"PROP-TIN-CASE"}],"affected_binding_locators":[{"owner":"short-drama-storyboard","artifact":"剧集/EP003/storyboard/shots.jsonl","selector":{"asset_or_variant_ids":["PROP-TIN-CASE","PROP-CERAMIC-BADGE-017"]},"status":"future_until_shots_exist"}],"destination":"剧集/EP003/assets/continuity.jsonl"}
3
- {"example_only":true,"schema_version":"1.0.0-draft","delta_id":"DELTA-BADGE-TRANSFER","episode_id":"EP003","scene_id":"SC004","subject_ref":{"src":"props","record_id":"PROP-CERAMIC-BADGE-017"},"state_field":"custody","before":{"owner":"CHAR-GUHE","holder":"CHAR-GUHE","hand":"right"},"after":{"owner":"CHAR-WEISHU","holder":"CHAR-WEISHU","hand":"right"},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A04"},"effective_range":{"from":"EP003/SC004/BLK-EP003-SC004-A04","until":"open_ended"},"reconciliation":{"next_linked_start":"EP003/SC005/start","status":"must_match_or_revise"},"creator_acceptance":{"status":"proposed","decision_ref":null},"affected_binding_refs":[{"src":"props","record_id":"PROP-CERAMIC-BADGE-017"}],"affected_binding_locators":[{"owner":"short-drama-storyboard","artifact":"剧集/EP003/storyboard/shots.jsonl","selector":{"asset_or_variant_ids":["PROP-CERAMIC-BADGE-017","CHAR-GUHE","CHAR-WEISHU"]},"status":"future_until_shots_exist"}],"destination":"剧集/EP003/assets/continuity.jsonl"}
4
- {"example_only":true,"schema_version":"1.0.0-draft","delta_id":"DELTA-OFFICE-BLACKOUT","episode_id":"EP003","scene_id":"SC004","subject_ref":{"src":"locations","record_id":"LOC-FERRY-OFFICE"},"state_field":"active_view","before":{"value":"VIEW-FERRY-OFFICE-NORTH-NIGHT"},"after":{"value":"VIEW-FERRY-OFFICE-NORTH-BLACKOUT"},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A04"},"effective_range":{"from":"EP003/SC004/BLK-EP003-SC004-A04","until":"open_ended"},"reconciliation":{"next_linked_start":"EP003/SC005/start","status":"must_match_or_revise"},"creator_acceptance":{"status":"proposed","decision_ref":null},"affected_binding_refs":[{"src":"locations","record_id":"LOC-FERRY-OFFICE"}],"affected_binding_locators":[{"owner":"short-drama-storyboard","artifact":"剧集/EP003/storyboard/shots.jsonl","selector":{"asset_or_variant_ids":["LOC-FERRY-OFFICE","VIEW-FERRY-OFFICE-NORTH-BLACKOUT"]},"status":"future_until_shots_exist"}],"destination":"剧集/EP003/assets/continuity.jsonl"}
@@ -1,5 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"occurrences":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/occurrences.jsonl"},"characters":{"owner":"short-drama-assets","artifact":"设定集/characters.jsonl"},"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"location-views":{"owner":"short-drama-assets","artifact":"设定集/location-views.jsonl"}}}
2
- {"example_only":true,"schema_version":"1.0.0-draft","decision_id":"DEC-OCC-001","occurrence_refs":[{"src":"occurrences","record_id":"OCC-EP003-SC004-001","authority":"candidate"}],"decision_kind":"new_variant","asset_kind":"character","proposed_binding":{"identity_id":"CHAR-GUHE","variant_id":"LOOK-GUHE-RAIN"},"base_ref":{"src":"characters","record_id":"CHAR-GUHE"},"persistent_evidence":["剧本沿用顾禾的身份和关系","已有永久识别锚点未改变"],"differences":["在工作装外加橙色旧雨衣","兜帽放下"],"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A01"},"validity":{"from":"EP003/SC004/BLK-EP003-SC004-A01","until":"EP003/SC006/BLK-EP003-SC006-A03"},"rationale":"人物持续身份不变;跨场服装需要精确绑定","creator_acceptance":{"status":"proposed","decision_ref":null},"downstream_impact":["character_image_prompt","affected_storyboard_bindings"],"destination":"剧集/EP003/assets/decisions.jsonl"}
3
- {"example_only":true,"schema_version":"1.0.0-draft","decision_id":"DEC-OCC-002","occurrence_refs":[{"src":"occurrences","record_id":"OCC-EP003-SC004-002","authority":"candidate"}],"decision_kind":"reuse","asset_kind":"location","proposed_binding":{"identity_id":"LOC-FERRY-OFFICE","variant_id":"VIEW-FERRY-OFFICE-NORTH-NIGHT"},"base_ref":{"src":"location-views","record_id":"VIEW-FERRY-OFFICE-NORTH-NIGHT"},"persistent_evidence":["北门、河窗和柜台的地理与已有 View 一致"],"differences":[],"cause_ref":null,"validity":{"from":"EP003/SC004","until":"EP003/SC004/BLK-EP003-SC004-A04"},"rationale":"同一空间、同一可复用方向与正常夜间光态","creator_acceptance":{"status":"proposed","decision_ref":null},"downstream_impact":["affected_storyboard_bindings"],"destination":"剧集/EP003/assets/decisions.jsonl"}
4
- {"example_only":true,"schema_version":"1.0.0-draft","decision_id":"DEC-OCC-003","occurrence_refs":[{"src":"occurrences","record_id":"OCC-EP003-SC004-003","authority":"candidate"}],"decision_kind":"new_asset","asset_kind":"prop","proposed_binding":{"identity_id":"PROP-TIN-CASE","variant_id":"PSTATE-TIN-CLOSED-BADGE-IN"},"base_ref":null,"persistent_evidence":["扁长黑铁匣","铰链盖","右前角永久凹陷","用于保存号牌"],"differences":[],"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"},"validity":{"from":"EP003/SC004","until":"open_ended"},"rationale":"现有 设定集 无同时匹配形制、唯一凹陷和叙事功能的 Prop","creator_acceptance":{"status":"proposed","decision_ref":null},"downstream_impact":["prop_image_prompt","affected_storyboard_bindings","continuity"],"destination":"剧集/EP003/assets/decisions.jsonl"}
5
- {"example_only":true,"schema_version":"1.0.0-draft","decision_id":"DEC-OCC-004","occurrence_refs":[{"src":"occurrences","record_id":"OCC-EP003-SC004-004","authority":"candidate"}],"decision_kind":"unresolved","asset_kind":"character","proposed_binding":null,"candidate_bindings":[{"identity_id":"CHAR-GUHE","support":"此前顾禾靠近北门","conflict":"最近动作主语已切换"},{"identity_id":"CHAR-WEISHU","support":"最近一句台词属于魏叔","conflict":"未写他走向北门"}],"persistent_evidence":[],"differences":[],"cause_ref":null,"validity":null,"rationale":"代词没有唯一先行项;不得以距离或最近姓名猜测","creator_question":"从北门离开的‘她’具体是谁?若为新人物,请补可识别称谓。","creator_acceptance":{"status":"pending_choice","decision_ref":null},"downstream_impact":["blocks_character_binding","blocks_dependent_storyboard"],"destination":"剧集/EP003/assets/decisions.jsonl"}
@@ -1,3 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"cd-loc-001":{"owner":"creator","artifact":"创作者决策/cd-loc-001.json"},"locations":{"owner":"short-drama-assets","artifact":"设定集/locations.jsonl"},"cd-view-001":{"owner":"creator","artifact":"创作者决策/cd-view-001.json"}}}
2
- {"example_only":true,"destination":"设定集/locations.jsonl","schema_version":"1.0.0-draft","record_type":"location","location_id":"LOC-FERRY-OFFICE","display_name":"旧渡站值班室","spatial_identity":{"shape":"狭长室内","zones":["柜台内工作区","柜台外等候区"],"entrances":[{"id":"north_door","connects_to":"LOC-FERRY-HALL"}],"fixed_anchors":["东侧河窗","西侧整面旧柜台","柜台后墙配电箱"],"materials":["磨损木柜台","灰白粉刷墙","旧金属窗框"]},"not_identity":["夜间","停电","顾禾站在北门","低机位"],"source_refs":[{"src":"screenplay-index","record_id":"BLK-EP003-SC004-H01"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-loc-001","record_id":"CD-LOC-001"}}}
3
- {"example_only":true,"destination":"设定集/location-views.jsonl","schema_version":"1.0.0-draft","record_type":"location_view","view_id":"VIEW-FERRY-OFFICE-NORTH-BLACKOUT","location_ref":{"src":"locations","record_id":"LOC-FERRY-OFFICE"},"base_view_id":"VIEW-FERRY-OFFICE-NORTH-NIGHT","orientation":{"from_zone":"柜台内工作区","toward":"北门","visible_fixed_anchors":["北门","东侧河窗","柜台后墙配电箱"]},"state_differences":{"dressing":[],"time":"night","weather":"heavy_rain_outside","light":["顶灯熄灭","河窗冷光保留"]},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A04"},"validity":{"from":"EP003/SC004/BLK-EP003-SC004-A04","until":"open_ended"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-view-001","record_id":"CD-VIEW-001"}}}
@@ -1,5 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"}}}
2
- {"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-001","episode_id":"EP003","scene_id":"SC004","asset_kind":"character","surface_form":"顾禾","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A01"},"source_blocks":["BLK-EP003-SC004-A01"],"extracted_projection":{"role":"derived_projection","visible_facts":["外穿橙色旧雨衣","兜帽放下"],"explicit_state":{"look_hint":"rain_shell","wetness":"dry_at_entry"}},"story_function":["enters_location","carries_case"],"production_disposition":"visual_asset_required","proposed_binding":{"character_id":"CHAR-GUHE","look_id":"LOOK-GUHE-RAIN"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-001"},"status":"future_until_decision_exists"},"status":"proposed"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
3
- {"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-002","episode_id":"EP003","scene_id":"SC004","asset_kind":"location","surface_form":"旧渡站值班室","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-H01"},"source_blocks":["BLK-EP003-SC004-H01","BLK-EP003-SC004-A01"],"extracted_projection":{"role":"derived_projection","visible_facts":["北门通向候船厅","东侧河窗","夜间顶灯亮"],"explicit_state":{"time":"night","light":"ceiling_light_on"}},"story_function":["action_space","later_blackout_setup"],"production_disposition":"visual_asset_required","proposed_binding":{"location_id":"LOC-FERRY-OFFICE","view_id":"VIEW-FERRY-OFFICE-NORTH-NIGHT"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-002"},"status":"future_until_decision_exists"},"status":"proposed"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
4
- {"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-003","episode_id":"EP003","scene_id":"SC004","asset_kind":"prop","surface_form":"铁皮匣","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"},"source_blocks":["BLK-EP003-SC004-A02"],"extracted_projection":{"role":"derived_projection","visible_facts":["顾禾将匣放上柜台","匣起初闭合"],"explicit_state":{"condition":"closed","holder":"CHAR-GUHE","location":"counter"}},"story_function":["contains_reveal_object","supports_readable_text_reveal"],"production_disposition":"hero_prop_required","proposed_binding":{"prop_id":"PROP-TIN-CASE","state_id":"PSTATE-TIN-CLOSED-BADGE-IN"},"reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-003"},"status":"future_until_decision_exists"},"status":"proposed"},"destination":"剧集/EP003/assets/occurrences.jsonl","continuity_delta_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/continuity.jsonl","selector":{"source_occurrence_id":"OCC-EP003-SC004-003"},"status":"future_until_delta_exists"}}
5
- {"example_only":true,"schema_version":"1.0.0-draft","occurrence_id":"OCC-EP003-SC004-004","episode_id":"EP003","scene_id":"SC004","asset_kind":"character","surface_form":"她","presence":"on_screen","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A05"},"source_blocks":["BLK-EP003-SC004-A05"],"extracted_projection":{"role":"derived_projection","visible_facts":["从北门离开"],"explicit_state":{}},"story_function":["exits_with_unknown_identity"],"production_disposition":"identity_resolution_required","proposed_binding":null,"candidate_bindings":["CHAR-GUHE","CHAR-WEISHU"],"ambiguity":"block 内无唯一先行项,两个已出场角色均可能","reconciliation":{"role":"derived_annotation","decision_locator":{"owner":"short-drama-assets","artifact":"剧集/EP003/assets/decisions.jsonl","selector":{"occurrence_id":"OCC-EP003-SC004-004"},"status":"future_until_decision_exists"},"status":"unresolved"},"continuity_delta_ref":null,"destination":"剧集/EP003/assets/occurrences.jsonl"}
@@ -1,3 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP003/screenplay-index.jsonl"},"cd-prop-001":{"owner":"creator","artifact":"创作者决策/cd-prop-001.json"},"props":{"owner":"short-drama-assets","artifact":"设定集/props.jsonl"},"cd-pstate-001":{"owner":"creator","artifact":"创作者决策/cd-pstate-001.json"}}}
2
- {"example_only":true,"destination":"设定集/props.jsonl","schema_version":"1.0.0-draft","record_type":"prop","prop_id":"PROP-TIN-CASE","display_name":"黑铁皮匣","identity_anchors":{"scale":"手掌至小臂长度的扁长匣","form":"铰链盖,长方体,右前角凹陷","materials":["黑色旧铁皮"],"function":"保存渡站号牌","permanent_marks":["右前角永久凹陷"]},"text_policy":{"mode":"exact_readable","text":"潮位 7","placement":"匣盖内侧","source_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A03"}},"source_refs":[{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-prop-001","record_id":"CD-PROP-001"}}}
3
- {"example_only":true,"destination":"设定集/prop-states.jsonl","schema_version":"1.0.0-draft","record_type":"prop_state","state_id":"PSTATE-TIN-CLOSED-BADGE-IN","prop_ref":{"src":"props","record_id":"PROP-TIN-CASE"},"condition":{"open":"closed","damage":"right_front_dent_only","powered":"not_applicable"},"custody":{"owner":"CHAR-GUHE","holder":"CHAR-GUHE","hand":null,"location":"under_ferry_office_counter"},"contents":["PROP-CERAMIC-BADGE-017"],"text_visibility":"hidden_while_closed","validity":{"from":"EP003/SC004/start","until":"EP003/SC004/BLK-EP003-SC004-A03"},"cause_ref":{"src":"screenplay-index","record_id":"BLK-EP003-SC004-A02"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"cd-pstate-001","record_id":"CD-PSTATE-001"}}}
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"input-script":{"owner":"creator","artifact":"input-script.md"},"decisions":{"owner":"creator","artifact":"decisions.json"}}}
2
- {"schema_version":"1.0.0-draft","record_type":"character","character_id":"CHAR-LIN","display_name":"林岚","aliases":[],"identity_anchors":["齐下颌黑发","左眉尾浅疤","站立时重心偏右"],"persistent_performance_facts":["回答前先看向出口"],"not_identity":["本场湿外套"],"source_refs":[{"src":"input-script","record_id":"SC001-A01"}],"creator_acceptance":{"status":"accepted","decision_ref":{"src":"decisions","record_id":"CD-CHAR-LIN"}}}
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"characters":{"owner":"short-drama-assets","artifact":"characters.jsonl"},"input-script":{"owner":"creator","artifact":"input-script.md"},"decisions":{"owner":"creator","artifact":"decisions.json"}}}
2
- {"schema_version":"1.0.0-draft","record_type":"look","look_id":"LOOK-LIN-RAIN","character_ref":{"src":"characters","record_id":"CHAR-LIN"},"base_look_id":"LOOK-LIN-BASE","differences":{"wardrobe_layers":["深蓝旧雨衣"],"weathering":["肩部雨水加深"]},"cause_ref":{"src":"input-script","record_id":"SC001-A01"},"validity":{"from":"SC001/A01","until":"SC002/end"},"creator_acceptance":{"status":"accepted","decision_ref":{"src":"decisions","record_id":"CD-LOOK-LIN-RAIN"}}}
@@ -1,267 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Validate the structural core of standalone character and look records."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import re
9
- import sys
10
- from pathlib import Path
11
- from typing import Any, NamedTuple
12
-
13
- MINIMUM_PYTHON = (3, 9)
14
- if sys.version_info < MINIMUM_PYTHON:
15
- raise SystemExit("asset_check.py requires Python 3.9 or newer")
16
-
17
- SKILL_ROOT = Path(__file__).resolve().parents[1]
18
- HASH_RE = re.compile(r"[0-9a-f]{64}")
19
- ACCEPTANCE_STATUSES = {"accepted", "proposed", "pending_choice"}
20
-
21
-
22
- # ---------------------------------------------------------------------------
23
- # REFERENCE RESOLVER -- reference implementation.
24
- #
25
- # Each skill checker carries its own copy of this block. The suite has no shared
26
- # library on purpose: a skill must stay runnable after copying only its own
27
- # directory, so duplicating these few lines across skills is the correct shape.
28
- # Copy the block verbatim; do not import it.
29
- # ---------------------------------------------------------------------------
30
-
31
- SOURCES_RECORD_TYPE = "sources"
32
- SOURCES_SCHEMA_VERSION = "1.0.0"
33
-
34
-
35
- class ResolvedRef(NamedTuple):
36
- """An upstream reference with its snapshot resolved, whichever form it used."""
37
-
38
- owner: str
39
- artifact: str
40
- record_id: str | None
41
- field: str | None
42
- authority: str | None
43
-
44
-
45
- class RefFinding(NamedTuple):
46
- """A structural defect in a reference object."""
47
-
48
- code: str
49
- location: str
50
- detail: str
51
-
52
-
53
- def load_sources(document: Any) -> dict[str, dict[str, Any]]:
54
- """Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
55
-
56
- Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
57
- ``.jsonl`` file, whose declaration lives on the first record.
58
- """
59
- if isinstance(document, list):
60
- document = document[0] if document else None
61
- if not isinstance(document, dict):
62
- return {}
63
- declared = document.get("sources")
64
- if not isinstance(declared, dict):
65
- return {}
66
- return {key: value for key, value in declared.items() if isinstance(value, dict)}
67
-
68
-
69
- def resolve_ref(
70
- ref: Any, sources: dict[str, dict[str, Any]], location: str
71
- ) -> tuple[ResolvedRef | None, RefFinding | None]:
72
- """Resolve a reference object written in either the compact or expanded form."""
73
- if not isinstance(ref, dict):
74
- return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
75
- src = ref.get("src")
76
- if isinstance(src, str):
77
- entry = sources.get(src)
78
- if entry is None:
79
- return None, RefFinding(
80
- "REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
81
- )
82
- owner, artifact = entry.get("owner"), entry.get("artifact")
83
- if not (isinstance(owner, str) and isinstance(artifact, str)):
84
- return None, RefFinding(
85
- "SOURCE_ENTRY_IS_INCOMPLETE", location, f"sources[{src!r}] needs owner/artifact"
86
- )
87
- elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
88
- owner, artifact = ref["owner"], ref["artifact"]
89
- else:
90
- return None, RefFinding(
91
- "REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
92
- )
93
- optional = {
94
- key: ref[key] for key in ("record_id", "field", "authority") if isinstance(ref.get(key), str)
95
- }
96
- return (
97
- ResolvedRef(
98
- owner,
99
- artifact,
100
- optional.get("record_id"),
101
- optional.get("field"),
102
- optional.get("authority"),
103
- ),
104
- None,
105
- )
106
-
107
-
108
- # ---------------------------------------------------------------------------
109
- # END REFERENCE RESOLVER
110
- # ---------------------------------------------------------------------------
111
-
112
-
113
- class ValidationError(ValueError):
114
- pass
115
-
116
-
117
- class RecordFile(NamedTuple):
118
- """One JSONL file: its upstream snapshot declaration and its records."""
119
-
120
- sources: dict[str, dict[str, Any]]
121
- records: list[dict[str, Any]]
122
-
123
-
124
- def resolve_input(value: str | Path) -> Path:
125
- path = Path(value).expanduser()
126
- if path.exists() or path.is_absolute():
127
- return path
128
- return SKILL_ROOT / path
129
-
130
-
131
- def load_jsonl(value: str | Path) -> RecordFile:
132
- path = resolve_input(value)
133
- parsed: list[dict[str, Any]] = []
134
- for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
135
- if not line.strip():
136
- continue
137
- try:
138
- record = json.loads(line)
139
- except json.JSONDecodeError as exc:
140
- raise ValidationError(f"{path}:{number}: invalid JSON") from exc
141
- if not isinstance(record, dict):
142
- raise ValidationError(f"{path}:{number}: each JSONL record must be an object")
143
- parsed.append(record)
144
- sources = load_sources(parsed)
145
- records = [
146
- record for record in parsed if record.get("record_type") != SOURCES_RECORD_TYPE
147
- ]
148
- if not records:
149
- raise ValidationError(f"{path}: no records")
150
- return RecordFile(sources, records)
151
-
152
-
153
- def require_text(record: dict[str, Any], key: str, label: str) -> str:
154
- value = record.get(key)
155
- if not isinstance(value, str) or not value.strip():
156
- raise ValidationError(f"{label}: {key} must be non-empty text")
157
- return value
158
-
159
-
160
- def require_list(record: dict[str, Any], key: str, label: str) -> list[Any]:
161
- value = record.get(key)
162
- if not isinstance(value, list) or not value:
163
- raise ValidationError(f"{label}: {key} must be a non-empty list")
164
- return value
165
-
166
-
167
- def validate_ref(
168
- value: Any, sources: dict[str, dict[str, Any]], label: str
169
- ) -> ResolvedRef:
170
- resolved, finding = resolve_ref(value, sources, label)
171
- if finding is not None:
172
- raise ValidationError(f"{label}: {finding.code}: {finding.detail}")
173
- assert resolved is not None
174
- if not resolved.record_id or not resolved.record_id.strip():
175
- raise ValidationError(f"{label}: record_id must be non-empty text")
176
- return resolved
177
-
178
-
179
- def validate_acceptance(
180
- value: Any, sources: dict[str, dict[str, Any]], label: str
181
- ) -> None:
182
- if not isinstance(value, dict) or value.get("status") not in ACCEPTANCE_STATUSES:
183
- raise ValidationError(f"{label}: invalid creator_acceptance status")
184
- decision_ref = value.get("decision_ref")
185
- if value["status"] == "accepted" or decision_ref is not None:
186
- resolved = validate_ref(decision_ref, sources, f"{label}.decision_ref")
187
- # A decision_ref pointing at a block or an asset record binds acceptance to
188
- # something that never recorded a creator decision.
189
- if not str(resolved.record_id).startswith("CD-"):
190
- raise ValidationError(
191
- f"{label}.decision_ref: record_id must be a creator decision starting with CD-"
192
- )
193
-
194
-
195
- def validate_records(characters: RecordFile, looks: RecordFile) -> dict[str, Any]:
196
- character_ids: set[str] = set()
197
- for index, record in enumerate(characters.records, 1):
198
- label = f"character[{index}]"
199
- character_id = require_text(record, "character_id", label)
200
- if not character_id.startswith("CHAR-"):
201
- raise ValidationError(f"{label}: character_id must start with CHAR-")
202
- if character_id in character_ids:
203
- raise ValidationError(f"{label}: duplicate character_id {character_id}")
204
- character_ids.add(character_id)
205
- require_text(record, "display_name", label)
206
- anchors = require_list(record, "identity_anchors", label)
207
- if any(not isinstance(item, str) or not item.strip() for item in anchors):
208
- raise ValidationError(f"{label}: identity_anchors must contain text")
209
- for ref_index, ref in enumerate(require_list(record, "source_refs", label), 1):
210
- validate_ref(ref, characters.sources, f"{label}.source_refs[{ref_index}]")
211
- validate_acceptance(record.get("creator_acceptance"), characters.sources, label)
212
-
213
- look_ids: set[str] = set()
214
- for index, record in enumerate(looks.records, 1):
215
- label = f"look[{index}]"
216
- look_id = require_text(record, "look_id", label)
217
- if not look_id.startswith("LOOK-"):
218
- raise ValidationError(f"{label}: look_id must start with LOOK-")
219
- if look_id in look_ids:
220
- raise ValidationError(f"{label}: duplicate look_id {look_id}")
221
- look_ids.add(look_id)
222
- character_ref = validate_ref(
223
- record.get("character_ref"), looks.sources, f"{label}.character_ref"
224
- )
225
- if character_ref.record_id not in character_ids:
226
- raise ValidationError(
227
- f"{label}: character_ref does not resolve: {character_ref.record_id}"
228
- )
229
- differences = record.get("differences")
230
- if not isinstance(differences, dict) or not any(differences.values()):
231
- raise ValidationError(f"{label}: differences must describe an observable change")
232
- validity = record.get("validity")
233
- if not isinstance(validity, dict) or not validity.get("from"):
234
- raise ValidationError(f"{label}: validity.from is required")
235
- validate_acceptance(record.get("creator_acceptance"), looks.sources, label)
236
-
237
- return {
238
- "status": "valid",
239
- "characters": len(characters.records),
240
- "looks": len(looks.records),
241
- # `source_declaration` is this file's own `sources` header resolving;
242
- # `look_binding` is a real cross-record lookup, and it is one only
243
- # because both files are handed to this checker.
244
- "checks": ["unique_ids", "acceptance_shape", "source_declaration", "look_binding"],
245
- }
246
-
247
-
248
- def validate_files(characters: str | Path, looks: str | Path) -> dict[str, Any]:
249
- return validate_records(load_jsonl(characters), load_jsonl(looks))
250
-
251
-
252
- def main() -> int:
253
- parser = argparse.ArgumentParser(description=__doc__)
254
- parser.add_argument("--characters", required=True)
255
- parser.add_argument("--looks", required=True)
256
- args = parser.parse_args()
257
- try:
258
- result = validate_files(args.characters, args.looks)
259
- except (OSError, ValidationError) as exc:
260
- print(f"asset check failed: {exc}", file=sys.stderr)
261
- return 2
262
- print(json.dumps(result, ensure_ascii=True, indent=2))
263
- return 0
264
-
265
-
266
- if __name__ == "__main__":
267
- raise SystemExit(main())
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Offline self-test for the standalone asset checker."""
3
-
4
- from __future__ import annotations
5
-
6
- import copy
7
- import sys
8
- from pathlib import Path
9
-
10
- from asset_check import SKILL_ROOT, RecordFile, ValidationError, load_jsonl, validate_records
11
-
12
- MINIMUM_PYTHON = (3, 9)
13
- if sys.version_info < MINIMUM_PYTHON:
14
- raise SystemExit("selftest.py requires Python 3.9 or newer")
15
-
16
-
17
- def require(condition: bool, message: str) -> None:
18
- if not condition:
19
- raise AssertionError(message)
20
-
21
-
22
- def expect_failure(characters: RecordFile, looks: RecordFile, marker: str) -> None:
23
- try:
24
- validate_records(characters, looks)
25
- except ValidationError as exc:
26
- if marker not in str(exc):
27
- raise AssertionError(f"expected {marker!r}, got {exc!s}") from exc
28
- else:
29
- raise AssertionError(f"expected validation failure containing {marker!r}")
30
-
31
-
32
- def edited(source: RecordFile, index: int) -> tuple[RecordFile, dict]:
33
- records = copy.deepcopy(source.records)
34
- return RecordFile(copy.deepcopy(source.sources), records), records[index]
35
-
36
-
37
- def main() -> int:
38
- example = Path(SKILL_ROOT, "examples/minimal")
39
- characters = load_jsonl(example / "characters.jsonl")
40
- looks = load_jsonl(example / "looks.jsonl")
41
- result = validate_records(characters, looks)
42
- require(result["characters"] == 1 and result["looks"] == 1, "valid fixture count")
43
-
44
- duplicate = copy.deepcopy(characters.records[0])
45
- expect_failure(
46
- RecordFile(characters.sources, [*characters.records, duplicate]),
47
- looks,
48
- "duplicate character_id",
49
- )
50
-
51
- broken_look, record = edited(looks, 0)
52
- record["character_ref"]["record_id"] = "CHAR-MISSING"
53
- expect_failure(characters, broken_look, "does not resolve")
54
-
55
- undeclared, record = edited(looks, 0)
56
- record["character_ref"]["src"] = "not-declared"
57
- expect_failure(characters, undeclared, "REF_SRC_IS_NOT_DECLARED")
58
-
59
- unbound, record = edited(looks, 0)
60
- record["character_ref"] = {"record_id": "CHAR-LIN"}
61
- expect_failure(characters, unbound, "REF_HAS_NO_UPSTREAM_BINDING")
62
-
63
- # A released project may still carry the snapshot inline on the reference.
64
- inline, record = edited(looks, 0)
65
- record["character_ref"] = {
66
- **inline.sources["characters"],
67
- "record_id": "CHAR-LIN",
68
- }
69
- inline.sources.pop("characters")
70
- require(validate_records(characters, inline)["status"] == "valid", "inline snapshot")
71
-
72
- candidate, record = edited(characters, 0)
73
- record["creator_acceptance"] = {"status": "proposed", "decision_ref": None}
74
- require(validate_records(candidate, looks)["status"] == "valid", "candidate status")
75
-
76
- invalid, record = edited(characters, 0)
77
- record["creator_acceptance"] = {"status": "approved", "decision_ref": None}
78
- expect_failure(invalid, looks, "invalid creator_acceptance status")
79
-
80
- misbound, record = edited(characters, 0)
81
- record["creator_acceptance"]["decision_ref"]["record_id"] = "SC001-A01"
82
- expect_failure(misbound, looks, "must be a creator decision starting with CD-")
83
-
84
- print("9 self-tests passed")
85
- return 0
86
-
87
-
88
- if __name__ == "__main__":
89
- raise SystemExit(main())
@@ -1,2 +0,0 @@
1
- {"record_type": "sources", "schema_version": "1.0.0", "sources": {"characters": {"owner": "short-drama-assets", "artifact": "characters.jsonl"}, "looks": {"owner": "short-drama-assets", "artifact": "looks.jsonl"}, "composition-reference": {"owner": "creator", "artifact": "composition-reference.png"}}}
2
- {"spec_id": "IMG-LIN-RAIN", "status": "candidate", "purpose": "character_sheet", "asset_binding": {"identity_ref": {"src": "characters", "record_id": "CHAR-LIN"}, "variant_ref": {"src": "looks", "record_id": "LOOK-LIN-RAIN"}}, "source_refs": [{"src": "characters", "record_id": "CHAR-LIN", "field": "/identity_anchors", "role": "identity_anchor"}], "reference_bindings": [{"slot_id": "REF-LIN-COMPOSITION", "order": 1, "artifact_ref": {"src": "composition-reference", "record_id": "REF-COMP-01"}, "role": "composition", "may_control": ["左右排版与留白"], "must_not_control": ["角色身份、服装、文字"], "admission_status": "creator_described", "reference_observation_ref": null, "unresolved_risks": []}], "intent": {"reuse_job": "保持角色在雨夜造型中的跨镜身份", "audience": "image production"}, "identity_or_form_anchors": ["齐下颌黑发", "左眉尾浅疤", "站立时重心偏右"], "variant_deltas": [{"field": "wardrobe", "observable_change": "深蓝旧雨衣,肩部被雨水加深", "valid_range": "SC001-SC002"}], "composition": {"view": "front and side reference sheet", "framing": "full body", "orientation": "neutral", "scale_relation": "consistent body proportions", "spatial_relations": []}, "appearance": {"materials": ["worn waterproof fabric"], "palette": "deep blue with muted skin tones", "lighting": "soft neutral studio light", "atmosphere": "clean reference presentation"}, "background": {"policy": "clean", "details": "plain light-gray background"}, "constraints": ["one coherent accepted look", "identity anchors remain visible"], "negative_constraints": ["no extra accessories", "no readable text"], "generic_prompt": "Character reference sheet for Lin Lan in one coherent rain look, jaw-length black hair, a shallow scar at the left eyebrow tail, weight resting slightly on the right leg, worn deep-blue raincoat with rain-darkened shoulders, front and side full-body views, neutral studio light, plain light-gray background, no extra accessories, no readable text.", "provenance": "creator_project"}