@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,325 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Validate standalone review findings and their verdict summary."""
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("review_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
- SEVERITIES = {"fatal", "error", "warning", "note"}
20
- STATUSES = {"open", "closed"}
21
- VERDICTS = {"APPROVE", "APPROVE_WITH_NOTES", "REVISE", "PROVISIONAL"}
22
- DISPOSITIONS = {"keep", "post_production", "targeted_edit", "resubmit", "rewrite", "not_applicable"}
23
-
24
-
25
- class ValidationError(ValueError):
26
- pass
27
-
28
-
29
- # ---------------------------------------------------------------------------
30
- # REFERENCE RESOLVER
31
- #
32
- # A file declares each upstream snapshot once, and every reference names the
33
- # declared snapshot plus the record. This skill carries its own copy so it stays
34
- # runnable after copying only this directory; do not import it from elsewhere.
35
- # ---------------------------------------------------------------------------
36
-
37
- SOURCES_RECORD_TYPE = "sources"
38
- SOURCES_SCHEMA_VERSION = "1.0.0"
39
-
40
-
41
- class ResolvedRef(NamedTuple):
42
- """An upstream reference with its snapshot resolved."""
43
-
44
- owner: str
45
- artifact: str
46
- record_id: str | None
47
- field: str | None
48
- authority: str | None
49
-
50
-
51
- class RefFinding(NamedTuple):
52
- """A structural defect in a reference object."""
53
-
54
- code: str
55
- location: str
56
- detail: str
57
-
58
-
59
- def load_sources(document: Any) -> dict[str, dict[str, Any]]:
60
- """Return the ``sources`` declaration of a parsed file, or ``{}`` if absent.
61
-
62
- Accepts a parsed ``.json`` document (a dict) or the parsed record list of a
63
- ``.jsonl`` file, whose declaration lives on the first record.
64
- """
65
- if isinstance(document, list):
66
- document = document[0] if document else None
67
- if not isinstance(document, dict):
68
- return {}
69
- declared = document.get("sources")
70
- if not isinstance(declared, dict):
71
- return {}
72
- return {key: value for key, value in declared.items() if isinstance(value, dict)}
73
-
74
-
75
- def resolve_ref(
76
- ref: Any, sources: dict[str, dict[str, Any]], location: str
77
- ) -> tuple[ResolvedRef | None, RefFinding | None]:
78
- """Resolve a reference object written in either the compact or expanded form."""
79
- if not isinstance(ref, dict):
80
- return None, RefFinding("REF_IS_NOT_AN_OBJECT", location, f"got {type(ref).__name__}")
81
- src = ref.get("src")
82
- if isinstance(src, str):
83
- entry = sources.get(src)
84
- if entry is None:
85
- return None, RefFinding(
86
- "REF_SRC_IS_NOT_DECLARED", location, f"src {src!r} has no sources entry"
87
- )
88
- owner, artifact = entry.get("owner"), entry.get("artifact")
89
- if not (isinstance(owner, str) and isinstance(artifact, str)):
90
- return None, RefFinding(
91
- "SOURCE_ENTRY_IS_INCOMPLETE", location, f"sources[{src!r}] needs owner/artifact"
92
- )
93
- elif all(isinstance(ref.get(key), str) for key in ("owner", "artifact")):
94
- owner, artifact = ref["owner"], ref["artifact"]
95
- else:
96
- return None, RefFinding(
97
- "REF_HAS_NO_UPSTREAM_BINDING", location, "needs src, or owner+artifact"
98
- )
99
- optional = {
100
- key: ref[key] for key in ("record_id", "field", "authority") if isinstance(ref.get(key), str)
101
- }
102
- return (
103
- ResolvedRef(
104
- owner,
105
- artifact,
106
- optional.get("record_id"),
107
- optional.get("field"),
108
- optional.get("authority"),
109
- ),
110
- None,
111
- )
112
-
113
-
114
- # ---------------------------------------------------------------------------
115
- # END REFERENCE RESOLVER
116
- # ---------------------------------------------------------------------------
117
-
118
-
119
- def split_sources(
120
- records: list[dict[str, Any]],
121
- ) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]:
122
- """Separate a leading ``sources`` header record from the payload records."""
123
- first = records[0] if records else None
124
- if isinstance(first, dict) and first.get("record_type") == SOURCES_RECORD_TYPE:
125
- return load_sources(first), records[1:]
126
- return {}, records
127
-
128
-
129
- def resolve_input(value: str | Path) -> Path:
130
- path = Path(value).expanduser()
131
- if path.exists() or path.is_absolute():
132
- return path
133
- return SKILL_ROOT / path
134
-
135
-
136
- def load_jsonl(value: str | Path) -> list[dict[str, Any]]:
137
- path = resolve_input(value)
138
- records: list[dict[str, Any]] = []
139
- for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
140
- if not line.strip():
141
- continue
142
- try:
143
- record = json.loads(line)
144
- except json.JSONDecodeError as exc:
145
- raise ValidationError(f"{path}:{number}: invalid JSON") from exc
146
- if not isinstance(record, dict):
147
- raise ValidationError(f"{path}:{number}: record must be an object")
148
- records.append(record)
149
- return records
150
-
151
-
152
- def load_object(value: str | Path) -> dict[str, Any]:
153
- path = resolve_input(value)
154
- try:
155
- document = json.loads(path.read_text(encoding="utf-8"))
156
- except json.JSONDecodeError as exc:
157
- raise ValidationError(f"{path}: invalid JSON") from exc
158
- if not isinstance(document, dict):
159
- raise ValidationError(f"{path}: verdict must be an object")
160
- return document
161
-
162
-
163
- def text(record: dict[str, Any], key: str, label: str, *, empty_ok: bool = False) -> str:
164
- value = record.get(key)
165
- if not isinstance(value, str) or (not empty_ok and not value.strip()):
166
- qualifier = "text" if empty_ok else "non-empty text"
167
- raise ValidationError(f"{label}: {key} must be {qualifier}")
168
- return value
169
-
170
-
171
- def validate_ref(
172
- value: Any,
173
- label: str,
174
- sources: dict[str, dict[str, Any]],
175
- *,
176
- record_optional: bool = False,
177
- ) -> None:
178
- resolved, defect = resolve_ref(value, sources, label)
179
- if resolved is None:
180
- detail = f"{defect.code}: {defect.detail}" if defect is not None else "unresolvable reference"
181
- raise ValidationError(f"{label}: {detail}")
182
- for key, field_value in (("owner", resolved.owner), ("artifact", resolved.artifact)):
183
- if not field_value.strip():
184
- raise ValidationError(f"{label}: {key} must be non-empty text")
185
- if not record_optional and not resolved.record_id and not resolved.field:
186
- raise ValidationError(f"{label}: record_id or field is required")
187
-
188
-
189
- def validate_findings(records: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
190
- sources, findings = split_sources(records)
191
- indexed: dict[str, dict[str, Any]] = {}
192
- for index, finding in enumerate(findings, 1):
193
- label = f"finding[{index}]"
194
- finding_id = text(finding, "finding_id", label)
195
- if finding_id in indexed:
196
- raise ValidationError(f"{label}: duplicate finding_id {finding_id}")
197
- indexed[finding_id] = finding
198
- for key in (
199
- "diagnostic_code",
200
- "scope",
201
- "classification",
202
- "enforcer",
203
- "evidence",
204
- "impact",
205
- "disposition_rationale",
206
- "owner_skill",
207
- ):
208
- text(finding, key, label)
209
- text(finding, "required_change", label, empty_ok=True)
210
- if finding.get("severity") not in SEVERITIES:
211
- raise ValidationError(f"{label}: invalid severity")
212
- if finding.get("status") not in STATUSES:
213
- raise ValidationError(f"{label}: invalid status")
214
- disposition = finding.get("disposition")
215
- if disposition not in DISPOSITIONS:
216
- raise ValidationError(
217
- f"{label}: invalid disposition {disposition!r}; "
218
- f"use one of {', '.join(sorted(DISPOSITIONS))}"
219
- )
220
- # A finding that blocks delivery has to say what to do about it,
221
- # whatever its disposition. REV-11 exempts dispositions that call for no
222
- # change, and routes every non-calibration finding to `not_applicable`
223
- # -- which is one of the exempt three. So the common case was exempt,
224
- # and an open `fatal` that told its owner nothing was stamped valid.
225
- # REV-02 requires the fix; this is where that requirement lands.
226
- if (
227
- finding.get("status") == "open"
228
- and finding.get("severity") in {"fatal", "error"}
229
- and not str(finding.get("required_change") or "").strip()
230
- ):
231
- raise ValidationError(
232
- f"{label}: an open {finding.get('severity')} finding blocks "
233
- f"delivery and must state its required change"
234
- )
235
- if disposition in {"targeted_edit", "resubmit", "rewrite"} and not finding["required_change"].strip():
236
- raise ValidationError(f"{label}: disposition requires required_change")
237
- refs = finding.get("evidence_refs")
238
- if not isinstance(refs, list) or not refs:
239
- raise ValidationError(f"{label}: evidence_refs must be a non-empty list")
240
- for ref_index, ref in enumerate(refs, 1):
241
- validate_ref(ref, f"{label}.evidence_refs[{ref_index}]", sources)
242
- validate_ref(finding.get("target_ref"), f"{label}.target_ref", sources)
243
- return indexed
244
-
245
-
246
- def validate_records(records: list[dict[str, Any]], verdict: dict[str, Any]) -> dict[str, Any]:
247
- indexed = validate_findings(records)
248
- _, findings = split_sources(records)
249
- verdict_sources = load_sources(verdict)
250
- text(verdict, "review_id", "verdict")
251
- scopes = verdict.get("scope")
252
- if not isinstance(scopes, list) or not scopes or any(not isinstance(item, str) or not item for item in scopes):
253
- raise ValidationError("verdict: scope must be a non-empty text list")
254
- artifacts = verdict.get("reviewed_artifacts")
255
- if not isinstance(artifacts, list) or not artifacts:
256
- raise ValidationError("verdict: reviewed_artifacts must be non-empty")
257
- for index, artifact in enumerate(artifacts, 1):
258
- validate_ref(
259
- artifact,
260
- f"verdict.reviewed_artifacts[{index}]",
261
- verdict_sources,
262
- record_optional=True,
263
- )
264
- validate_ref(
265
- verdict.get("findings_ref"), "verdict.findings_ref", verdict_sources, record_optional=True
266
- )
267
- if verdict.get("review_method") not in {"uninvolved_reviewer", "self_check"}:
268
- raise ValidationError("verdict: review_method must be uninvolved_reviewer or self_check")
269
- text(verdict, "reviewer", "verdict")
270
- if verdict.get("verdict") not in VERDICTS:
271
- raise ValidationError("verdict: invalid verdict")
272
-
273
- blockers = sorted(
274
- finding_id
275
- for finding_id, finding in indexed.items()
276
- if finding["status"] == "open" and finding["severity"] in {"fatal", "error"}
277
- )
278
- declared = verdict.get("blocking_findings")
279
- if not isinstance(declared, list) or sorted(declared) != blockers:
280
- raise ValidationError("verdict: blocking_findings must equal the open fatal/error findings")
281
- if verdict.get("open_blocker_count") != len(blockers):
282
- raise ValidationError("verdict: open_blocker_count does not match blocking_findings")
283
- decision = verdict["verdict"]
284
- if blockers and decision != "REVISE":
285
- raise ValidationError("verdict: open blockers require REVISE")
286
- if not blockers and decision == "REVISE":
287
- raise ValidationError("verdict: REVISE requires an open blocker")
288
-
289
- return {
290
- "status": "valid",
291
- "findings": len(findings),
292
- "open_blockers": len(blockers),
293
- "verdict": decision,
294
- # `evidence_declaration`: each evidence reference names a snapshot the
295
- # findings file declares. The records it points at live in artifacts
296
- # this checker never opens.
297
- "checks": [
298
- "finding_shape",
299
- "evidence_declaration",
300
- "blocker_count",
301
- "verdict_consistency",
302
- ],
303
- }
304
-
305
-
306
- def validate_files(findings: str | Path, verdict: str | Path) -> dict[str, Any]:
307
- return validate_records(load_jsonl(findings), load_object(verdict))
308
-
309
-
310
- def main() -> int:
311
- parser = argparse.ArgumentParser(description=__doc__)
312
- parser.add_argument("--findings", required=True)
313
- parser.add_argument("--verdict", required=True)
314
- args = parser.parse_args()
315
- try:
316
- result = validate_files(args.findings, args.verdict)
317
- except (OSError, ValidationError) as exc:
318
- print(f"review check failed: {exc}", file=sys.stderr)
319
- return 2
320
- print(json.dumps(result, ensure_ascii=True, indent=2))
321
- return 0
322
-
323
-
324
- if __name__ == "__main__":
325
- raise SystemExit(main())
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Offline self-test for the standalone review checker."""
3
-
4
- from __future__ import annotations
5
-
6
- import copy
7
- import sys
8
-
9
- from review_check import SKILL_ROOT, ValidationError, load_jsonl, load_object, validate_records
10
-
11
- MINIMUM_PYTHON = (3, 9)
12
- if sys.version_info < MINIMUM_PYTHON:
13
- raise SystemExit("selftest.py requires Python 3.9 or newer")
14
-
15
-
16
- def require(condition: bool, message: str) -> None:
17
- if not condition:
18
- raise AssertionError(message)
19
-
20
-
21
- def fail(findings: list[dict], verdict: dict, marker: str) -> None:
22
- try:
23
- validate_records(findings, verdict)
24
- except ValidationError as exc:
25
- require(marker in str(exc), f"expected {marker!r}, got {exc!s}")
26
- else:
27
- raise AssertionError(f"expected failure containing {marker!r}")
28
-
29
-
30
- def expand(records: list[dict], verdict: dict) -> tuple[list[dict], dict]:
31
- """Rewrite the fixture into the expanded reference form.
32
-
33
- Projects written before the ``sources`` declaration carry that form on disk,
34
- so the checker keeps resolving it.
35
- """
36
- sources = records[0]["sources"]
37
- expanded_findings = [copy.deepcopy(record) for record in records[1:]]
38
- for finding in expanded_findings:
39
- for ref in [*finding["evidence_refs"], finding["target_ref"]]:
40
- ref.update(sources[ref.pop("src")])
41
- expanded_verdict = copy.deepcopy(verdict)
42
- verdict_sources = expanded_verdict.pop("sources")
43
- for ref in [*expanded_verdict["reviewed_artifacts"], expanded_verdict["findings_ref"]]:
44
- ref.update(verdict_sources[ref.pop("src")])
45
- return expanded_findings, expanded_verdict
46
-
47
-
48
- def main() -> int:
49
- findings = load_jsonl(SKILL_ROOT / "examples/minimal-findings.jsonl")
50
- verdict = load_object(SKILL_ROOT / "examples/minimal-verdict.json")
51
- result = validate_records(findings, verdict)
52
- require(
53
- result["open_blockers"] == 1 and result["verdict"] == "REVISE",
54
- "valid fixture verdict",
55
- )
56
- require(result["findings"] == 1, "the sources header is not counted as a finding")
57
-
58
- expanded_findings, expanded_verdict = expand(findings, verdict)
59
- expanded = validate_records(expanded_findings, expanded_verdict)
60
- require(expanded == result, "expanded references validate to the same result")
61
-
62
- undeclared = copy.deepcopy(findings)
63
- undeclared[1]["target_ref"] = {"src": "unknown-source", "record_id": "SC001"}
64
- fail(undeclared, verdict, "REF_SRC_IS_NOT_DECLARED")
65
-
66
- unbound = copy.deepcopy(findings)
67
- unbound[1]["evidence_refs"][0] = {"role": "source", "record_id": "SC001-A01"}
68
- fail(unbound, verdict, "REF_HAS_NO_UPSTREAM_BINDING")
69
-
70
- wrong_count = copy.deepcopy(verdict)
71
- wrong_count["open_blocker_count"] = 0
72
- fail(findings, wrong_count, "open_blocker_count")
73
-
74
- false_approval = copy.deepcopy(verdict)
75
- false_approval["verdict"] = "APPROVE"
76
- fail(findings, false_approval, "open blockers require REVISE")
77
-
78
- broken_finding = copy.deepcopy(findings)
79
- broken_finding[1]["evidence_refs"] = []
80
- fail(broken_finding, verdict, "evidence_refs")
81
-
82
- print("8 self-tests passed")
83
- return 0
84
-
85
-
86
- if __name__ == "__main__":
87
- raise SystemExit(main())
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"<characters-or-props>":{"owner":"short-drama-assets","artifact":"设定集/<characters-or-props>.jsonl"},"<looks-or-prop-states>":{"owner":"short-drama-assets","artifact":"设定集/<looks-or-prop-states>.jsonl"},"location-views":{"owner":"short-drama-assets","artifact":"设定集/location-views.jsonl"},"locations":{"owner":"short-drama-assets","artifact":"设定集/locations.jsonl"},"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP001/screenplay-index.jsonl"},"short-drama":{"owner":"creator","artifact":"short-drama.json"}}}
2
- {"example_only":true,"audition_id":"AUD-EP001-SC001","status":"candidate","scene_ref":{"src":"screenplay-index","record_id":"BLK-EP001-SC001-H01","field":"/scene_id"},"visual_direction_ref":{"src":"short-drama","field":"/creator_authority/visual_direction"},"production_profile_ref":{"src":"short-drama","field":"/creator_authority/production_profile"},"location_binding":{"identity_ref":{"src":"locations","record_id":"LOC-<id>"},"variant_ref":{"src":"location-views","record_id":"VIEW-<id>"}},"asset_bindings":[{"identity_ref":{"src":"<characters-or-props>","record_id":"CHAR/PROP-<id>"},"variant_ref":{"src":"<looks-or-prop-states>","record_id":"LOOK/PSTATE-<id>"},"role":"<performance_owner/evidence/observer>"}],"reason_for_audition":"<为什么导演选择会显著改变体验>","approaches":[{"approach_id":"APPROACH-A","directing_hypothesis":"<表演/证据/主观/空间/声音等真正不同的命题>","audience_knowledge":"<何时知道什么>","audience_alignment":"<站在哪个人物一边>","performance_space":"<谁拥有主要表演通道,谁只承担差异化反应>","strongest_image":"<最强画面>","landing":"<最终落点>","tradeoffs":{"preserves":["<保住什么>"],"risks_losing":["<可能丢什么>"],"production_fit":"<与已接受制作形态怎样相容或冲突>"}}],"comparison_summary":"<真正分歧与不可同时获得的收益>"}
@@ -1,12 +0,0 @@
1
- {
2
- "revision_lineage": {
3
- "change": "split | merge | restore",
4
- "predecessor_shot_ids": [
5
- "SHOT-<same-file-prior-id>"
6
- ],
7
- "retired_shot_ids": [
8
- "SHOT-<same-file-retired-id>"
9
- ],
10
- "reason": "<导演职责怎样被拆开、合并或恢复>"
11
- }
12
- }
@@ -1,2 +0,0 @@
1
- {"record_type":"sources","schema_version":"1.0.0","sources":{"<characters-or-props>":{"owner":"short-drama-assets","artifact":"设定集/<characters-or-props>.jsonl"},"<looks-or-prop-states>":{"owner":"short-drama-assets","artifact":"设定集/<looks-or-prop-states>.jsonl"},"coverage-audition":{"owner":"short-drama-storyboard","artifact":"剧集/EP001/storyboard/coverage-auditions/SC001.jsonl"},"creator-decision":{"owner":"creator","artifact":"创作者决策/EP001-coverage-audition-SC001.json"},"location-views":{"owner":"short-drama-assets","artifact":"设定集/location-views.jsonl"},"locations":{"owner":"short-drama-assets","artifact":"设定集/locations.jsonl"},"screenplay-index":{"owner":"short-drama-write","artifact":"剧集/EP001/screenplay-index.jsonl"},"short-drama":{"owner":"creator","artifact":"short-drama.json"}}}
2
- {"example_only":true,"plan_id":"SVP-EP001-SC001","status":"candidate","scene_ref":{"src":"screenplay-index","record_id":"BLK-EP001-SC001-H01","field":"/scene_id"},"source_block_refs":[{"src":"screenplay-index","record_id":"BLK-EP001-SC001-A01"}],"visual_direction_ref":{"src":"short-drama","field":"/creator_authority/visual_direction"},"production_profile_ref":{"src":"short-drama","field":"/creator_authority/production_profile"},"location_binding":{"identity_ref":{"src":"locations","record_id":"LOC-<id>"},"variant_ref":{"src":"location-views","record_id":"VIEW-<id>"}},"asset_bindings":[{"identity_ref":{"src":"<characters-or-props>","record_id":"CHAR/PROP-<id>"},"variant_ref":{"src":"<looks-or-prop-states>","record_id":"LOOK/PSTATE-<id>"},"role":"<performance_owner/evidence/observer>"}],"source_audition_ref":{"src":"coverage-audition","record_id":"AUD-EP001-SC001"},"creator_selection_ref":{"src":"creator-decision","record_id":"CD-EP001-COVERAGE-AUDITION-SC001","field":"/selected_approach_id"},"dramatic_turn":"<本场不可逆的关系/信息/选择变化>","audience_alignment":{"start":"<开场站在哪一方或保持什么距离>","end":"<结束被移到哪里>"},"spatial_pressure":"<入口、距离、占位、遮挡、让路或退路怎样承载人物策略>","visual_progression":["<按注意、关系、信息、压力或节奏写推进,不预写 shot 数量>"],"camera_strategy":"<何时固定、移动、切近或停留,以及触发原因>","reveal_image":"<关键揭示怎样进入画面且服从信息权限>","reaction_landing":"<谁的选择、反应或空间后果承担结尾>","sound_strategy":{"opening_attention":"<先听见什么>","power_or_space_sound":"<代表当前空间/权力的声源>","withdrawal_or_silence":"<何时撤出/留白及对象>","return_or_distance_change":"<何时恢复或改变距离>","sound_bridge":"<与相邻场怎样连接或 none>"},"constraints":["<必须保持的剧本、资产与视觉方向事实>"],"creator_overrides":[]}
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Offline self-test for storyboard bookkeeping checks."""
3
-
4
- from __future__ import annotations
5
-
6
- import copy
7
- import sys
8
- import tempfile
9
- from pathlib import Path
10
- from typing import Any
11
-
12
- from storyboard_check import (
13
- check_boundary_entries,
14
- check_episode_duration,
15
- check_keyframe_boundaries,
16
- check_screenplay_coverage,
17
- )
18
-
19
- MINIMUM_PYTHON = (3, 9)
20
- if sys.version_info < MINIMUM_PYTHON:
21
- raise SystemExit("selftest.py requires Python 3.9 or newer")
22
-
23
-
24
- def require(condition: bool, message: str) -> None:
25
- if not condition:
26
- raise AssertionError(message)
27
-
28
-
29
- def codes(findings: list[dict[str, Any]]) -> set[Any]:
30
- return {item["code"] for item in findings}
31
-
32
-
33
- SHOT: dict[str, Any] = {
34
- "shot_id": "SHOT-001",
35
- "duration_seconds": 5,
36
- "start_boundary": "closed door",
37
- "end_boundary": "open door",
38
- }
39
- BOUNDED_SHOT: dict[str, Any] = {
40
- "shot_id": "SHOT-002",
41
- "duration_seconds": 5,
42
- "start_boundary": {
43
- "positions": ["站在柜台东侧(与上一镜相同)"],
44
- "facing": ["面向门口"],
45
- },
46
- "end_boundary": {"positions": ["退到门内一步"], "facing": ["面向柜台"]},
47
- }
48
- SHOTS_SOURCE: dict[str, Any] = {
49
- "owner": "short-drama-storyboard",
50
- "artifact": "剧集/EP001/storyboard/shots.jsonl",
51
- "hash": "a" * 64,
52
- }
53
- COVERAGE: dict[str, Any] = {
54
- "sources": {"shots": SHOTS_SOURCE},
55
- "dispositions": [{"shot_refs": [{"src": "shots", "record_id": "SHOT-001"}]}],
56
- "episode_duration": {
57
- "counted_shot_ids": ["SHOT-001"],
58
- "unresolved_durations": [],
59
- "shot_seconds_total": 5,
60
- "delta_seconds": 0,
61
- "disposition": "within_creator_tolerance",
62
- },
63
- }
64
- KEYFRAME_SOURCES: dict[str, Any] = {"shots": SHOTS_SOURCE}
65
- KEYFRAME: dict[str, Any] = {
66
- "keyframe_id": "KF-001-END",
67
- "boundary_role": "end",
68
- "boundary_ref": {"src": "shots", "record_id": "SHOT-001", "field": "/end_boundary"},
69
- }
70
- # A reference may also carry its whole snapshot inline instead of naming a
71
- # sources key; the checker resolves both to the same upstream binding.
72
- EXPANDED_COVERAGE: dict[str, Any] = {
73
- "dispositions": [
74
- {"shot_refs": [{**SHOTS_SOURCE, "record_id": "SHOT-001", "authority": "accepted"}]}
75
- ],
76
- "episode_duration": COVERAGE["episode_duration"],
77
- }
78
- EXPANDED_KEYFRAME: dict[str, Any] = {
79
- "keyframe_id": "KF-001-END",
80
- "boundary_role": "end",
81
- "boundary_ref": {**SHOTS_SOURCE, "record_id": "SHOT-001", "field": "/end_boundary"},
82
- }
83
-
84
-
85
- def test_screenplay_coverage_flags_gaps_and_double_claims() -> None:
86
- """Every screenplay block must be claimed by exactly one shot.
87
-
88
- Shots bind to the screenplay through the index, so coverage is measured in
89
- block IDs. Measuring it in prose lines instead reported every line of a
90
- correctly covered episode as unfilmed.
91
- """
92
- with tempfile.TemporaryDirectory() as directory:
93
- index = Path(directory) / "screenplay-index.jsonl"
94
- index.write_text(
95
- '{"record_type":"block","block_id":"BLK-A"}\n'
96
- '{"record_type":"block","block_id":"BLK-B"}\n'
97
- '{"record_type":"block","block_id":"BLK-C"}\n',
98
- encoding="utf-8",
99
- )
100
- sources = {
101
- "screenplay-index": {
102
- "owner": "short-drama-write",
103
- "artifact": "剧集/EP001/screenplay-index.jsonl",
104
- }
105
- }
106
- shots = [
107
- {"shot_id": "SH001", "source_refs": [{"src": "screenplay-index", "record_id": "BLK-A"}]},
108
- {"shot_id": "SH002", "source_refs": [{"src": "screenplay-index", "record_id": "BLK-B"}]},
109
- {"shot_id": "SH003", "source_refs": [{"src": "screenplay-index", "record_id": "BLK-B"}]},
110
- {"shot_id": "SH004", "source_refs": [{"src": "screenplay-index", "record_id": "BLK-Z"}]},
111
- ]
112
- found = {f["code"] for f in check_screenplay_coverage(shots, sources, index)}
113
- require("SHT01_BLOCK_UNCLAIMED" in found, "an unfilmed block must be reported")
114
- require("SHT01_BLOCK_CLAIMED_TWICE" in found, "a doubly claimed block must be reported")
115
- require(
116
- "SHT01_BLOCK_NOT_IN_SCREENPLAY" in found,
117
- "a shot claiming an absent block must be reported",
118
- )
119
-
120
-
121
- def test_screenplay_coverage_is_skipped_when_not_supplied() -> None:
122
- require(check_screenplay_coverage([], {}, None) == [], "no index means no claim")
123
-
124
-
125
- def main() -> int:
126
- require(check_episode_duration(COVERAGE, [SHOT], 5) == [], "valid duration")
127
- require(
128
- check_keyframe_boundaries([KEYFRAME], [SHOT], KEYFRAME_SOURCES) == [],
129
- "valid boundary",
130
- )
131
- require(check_episode_duration(EXPANDED_COVERAGE, [SHOT], 5) == [], "expanded duration")
132
- require(
133
- check_keyframe_boundaries([EXPANDED_KEYFRAME], [SHOT], {}) == [],
134
- "expanded boundary",
135
- )
136
-
137
- wrong_total = copy.deepcopy(COVERAGE)
138
- wrong_total["episode_duration"]["shot_seconds_total"] = 4
139
- require(
140
- "SHT16_TOTAL_IS_NOT_THE_SUM" in codes(check_episode_duration(wrong_total, [SHOT], 5)),
141
- "wrong duration total was not detected",
142
- )
143
-
144
- wrong_boundary = copy.deepcopy(KEYFRAME)
145
- wrong_boundary["boundary_ref"]["field"] = "/start_boundary"
146
- require(
147
- "SHT17_BOUNDARY_REF_DISAGREES_WITH_ROLE"
148
- in codes(check_keyframe_boundaries([wrong_boundary], [SHOT], KEYFRAME_SOURCES)),
149
- "wrong keyframe boundary was not detected",
150
- )
151
-
152
- undeclared = copy.deepcopy(COVERAGE)
153
- undeclared["sources"] = {}
154
- require(
155
- "REF_SRC_IS_NOT_DECLARED" in codes(check_episode_duration(undeclared, [SHOT], 5)),
156
- "a src without a sources entry was not detected",
157
- )
158
- require(
159
- "REF_SRC_IS_NOT_DECLARED"
160
- in codes(check_keyframe_boundaries([KEYFRAME], [SHOT], {})),
161
- "a keyframe src without a sources entry was not detected",
162
- )
163
-
164
- unbound = copy.deepcopy(COVERAGE)
165
- unbound["dispositions"][0]["shot_refs"] = [{"record_id": "SHOT-001"}]
166
- require(
167
- "REF_HAS_NO_UPSTREAM_BINDING" in codes(check_episode_duration(unbound, [SHOT], 5)),
168
- "a reference binding no snapshot was not detected",
169
- )
170
-
171
- # A boundary entry that only says "same as before" reads as written but tells
172
- # the next stage nothing; an absolute fact that mentions the previous shot in
173
- # passing is fine and must not be flagged.
174
- require(check_boundary_entries([BOUNDED_SHOT]) == [], "absolute boundary entries")
175
- relative = copy.deepcopy(BOUNDED_SHOT)
176
- relative["end_boundary"]["positions"] = ["(位置不变)"]
177
- require(
178
- "SHT05_BOUNDARY_ENTRY_IS_A_BACK_REFERENCE"
179
- in codes(check_boundary_entries([relative])),
180
- "a boundary entry that only points back was not detected",
181
- )
182
-
183
- test_screenplay_coverage_flags_gaps_and_double_claims()
184
- test_screenplay_coverage_is_skipped_when_not_supplied()
185
-
186
- print("13 self-tests passed")
187
- return 0
188
-
189
-
190
- if __name__ == "__main__":
191
- raise SystemExit(main())