@oh-story/dsh 0.1.18 → 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 (125) hide show
  1. package/README.md +12 -12
  2. package/lib/drama/manifest.json +77 -247
  3. package/lib/drama/skills/short-drama/SKILL.md +9 -1
  4. package/lib/drama/skills/short-drama/references/creator-documents.md +51 -23
  5. package/lib/drama/skills/short-drama/references/reference-roles.md +1 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +442 -129
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +54 -1
  8. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -6
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +9 -3
  10. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +3 -3
  11. package/lib/drama/skills/short-drama-assets/references/location-and-view.md +4 -0
  12. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +2 -2
  13. package/lib/drama/skills/short-drama-develop/references/episode-design.md +19 -4
  14. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +1 -1
  15. package/lib/drama/skills/short-drama-edit/SKILL.md +1 -1
  16. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +28 -6
  17. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +1 -1
  18. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +1 -5
  19. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  20. package/lib/drama/skills/short-drama-produce/SKILL.md +2 -0
  21. package/lib/drama/skills/short-drama-review/SKILL.md +0 -4
  22. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +1 -1
  23. package/lib/drama/skills/short-drama-storyboard/SKILL.md +9 -9
  24. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +43 -13
  25. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +5 -5
  26. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +5 -5
  27. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +27 -3
  28. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -1
  29. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +6 -8
  30. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +16 -1
  31. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +23 -6
  32. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +2 -2
  33. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -1
  34. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -2
  35. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +1 -1
  36. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -1
  37. package/lib/drama/skills/short-drama-write/SKILL.md +7 -3
  38. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +1 -1
  39. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +94 -1
  40. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +13 -4
  41. package/lib/drama/skills/short-drama-write/scripts/selftest.py +3 -77
  42. package/lib/index.js +2 -2
  43. package/lib/index.js.map +1 -1
  44. package/lib/novel-to-game/README.md +20 -34
  45. package/lib/novel-to-game/README_ZH.md +20 -34
  46. package/lib/novel-to-game/VERSION +1 -1
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +2 -6
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +33 -39
  49. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +2 -2
  50. package/lib/novel-to-game/manifest.json +46 -46
  51. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +1 -1
  52. package/lib/novel-to-game/skills/game-build/references/baked-sprite-workflow.md +2 -3
  53. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +6 -7
  54. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +2 -2
  55. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +1 -4
  56. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +1 -3
  57. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +1 -3
  58. package/lib/novel-to-game/skills/game-concept/SKILL.md +1 -1
  59. package/lib/novel-to-game/skills/game-qa/SKILL.md +3 -4
  60. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +1 -1
  61. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +3 -2
  62. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +13 -14
  63. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +1 -2
  64. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +4 -4
  65. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +2 -3
  66. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +0 -8
  67. package/lib/oh-story/manifest.json +49 -49
  68. package/lib/oh-story/skills/story/SKILL.md +2 -2
  69. package/lib/oh-story/skills/story/VERSION +1 -1
  70. package/lib/oh-story/skills/story-deslop/SKILL.md +2 -2
  71. package/lib/oh-story/skills/story-import/SKILL.md +5 -5
  72. package/lib/oh-story/skills/story-import/references/deep-analysis.md +1 -1
  73. package/lib/oh-story/skills/story-import/references/import-report.md +1 -1
  74. package/lib/oh-story/skills/story-import/references/import-tracking.md +1 -1
  75. package/lib/oh-story/skills/story-import/references/outline-reverse.md +1 -1
  76. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +6 -1
  77. package/lib/oh-story/skills/story-long-analyze/SKILL.md +2 -2
  78. package/lib/oh-story/skills/story-long-write/SKILL.md +2 -2
  79. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +3 -3
  80. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -2
  81. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +6 -1
  82. package/lib/oh-story/skills/story-review/SKILL.md +8 -7
  83. package/lib/oh-story/skills/story-review/references/agent-prompts.md +7 -7
  84. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -2
  85. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +6 -1
  86. package/lib/oh-story/skills/story-setup/SKILL.md +6 -6
  87. package/lib/oh-story/skills/story-short-write/SKILL.md +2 -2
  88. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +3 -3
  89. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +2 -2
  90. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +1 -1
  91. package/package.json +21 -21
  92. package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +0 -3
  93. package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +0 -4
  94. package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +0 -5
  95. package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +0 -3
  96. package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +0 -5
  97. package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +0 -3
  98. package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +0 -2
  99. package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +0 -2
  100. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +0 -267
  101. package/lib/drama/skills/short-drama-assets/scripts/selftest.py +0 -89
  102. package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +0 -2
  103. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +0 -443
  104. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +0 -95
  105. package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +0 -2
  106. package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +0 -42
  107. package/lib/drama/skills/short-drama-review/assets/verdict-template.json +0 -32
  108. package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +0 -2
  109. package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +0 -34
  110. package/lib/drama/skills/short-drama-review/scripts/review_check.py +0 -325
  111. package/lib/drama/skills/short-drama-review/scripts/selftest.py +0 -87
  112. package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +0 -2
  113. package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +0 -12
  114. package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +0 -2
  115. package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +0 -191
  116. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +0 -824
  117. package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +0 -21
  118. package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +0 -2
  119. package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +0 -38
  120. package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +0 -2
  121. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +0 -329
  122. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +0 -451
  123. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +0 -327
  124. package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +0 -82
  125. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +0 -416
@@ -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"}