@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.
- package/README.md +12 -12
- package/lib/drama/manifest.json +77 -247
- package/lib/drama/skills/short-drama/SKILL.md +9 -1
- package/lib/drama/skills/short-drama/references/creator-documents.md +51 -23
- package/lib/drama/skills/short-drama/references/reference-roles.md +1 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +442 -129
- package/lib/drama/skills/short-drama/scripts/project_tool.py +54 -1
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -6
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +9 -3
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +3 -3
- package/lib/drama/skills/short-drama-assets/references/location-and-view.md +4 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +2 -2
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +19 -4
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +1 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +1 -1
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +28 -6
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +1 -5
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +0 -4
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +9 -9
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +43 -13
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +5 -5
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +5 -5
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +27 -3
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +6 -8
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +16 -1
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +23 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +2 -2
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -2
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +7 -3
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +1 -1
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +94 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +13 -4
- package/lib/drama/skills/short-drama-write/scripts/selftest.py +3 -77
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/novel-to-game/README.md +20 -34
- package/lib/novel-to-game/README_ZH.md +20 -34
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +2 -6
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +33 -39
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +2 -2
- package/lib/novel-to-game/manifest.json +46 -46
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +1 -1
- package/lib/novel-to-game/skills/game-build/references/baked-sprite-workflow.md +2 -3
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +6 -7
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +2 -2
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +1 -4
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +1 -3
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +1 -3
- package/lib/novel-to-game/skills/game-concept/SKILL.md +1 -1
- package/lib/novel-to-game/skills/game-qa/SKILL.md +3 -4
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +1 -1
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +3 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +13 -14
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +1 -2
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +2 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +0 -8
- package/lib/oh-story/manifest.json +49 -49
- package/lib/oh-story/skills/story/SKILL.md +2 -2
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/SKILL.md +5 -5
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-tracking.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +2 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -2
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-review/SKILL.md +8 -7
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +7 -7
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -2
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +6 -1
- package/lib/oh-story/skills/story-setup/SKILL.md +6 -6
- package/lib/oh-story/skills/story-short-write/SKILL.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +1 -1
- package/package.json +21 -21
- package/lib/drama/skills/short-drama-assets/assets/character-look.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/assets/continuity.example.jsonl +0 -4
- package/lib/drama/skills/short-drama-assets/assets/decisions.example.jsonl +0 -5
- package/lib/drama/skills/short-drama-assets/assets/location-view.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/assets/occurrences.example.jsonl +0 -5
- package/lib/drama/skills/short-drama-assets/assets/prop-state.example.jsonl +0 -3
- package/lib/drama/skills/short-drama-assets/examples/minimal/characters.jsonl +0 -2
- package/lib/drama/skills/short-drama-assets/examples/minimal/looks.jsonl +0 -2
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +0 -267
- package/lib/drama/skills/short-drama-assets/scripts/selftest.py +0 -89
- package/lib/drama/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +0 -2
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +0 -443
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +0 -95
- package/lib/drama/skills/short-drama-review/assets/finding-template.jsonl +0 -2
- package/lib/drama/skills/short-drama-review/assets/supersession-decision.example.json +0 -42
- package/lib/drama/skills/short-drama-review/assets/verdict-template.json +0 -32
- package/lib/drama/skills/short-drama-review/examples/minimal-findings.jsonl +0 -2
- package/lib/drama/skills/short-drama-review/examples/minimal-verdict.json +0 -34
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +0 -325
- package/lib/drama/skills/short-drama-review/scripts/selftest.py +0 -87
- package/lib/drama/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +0 -12
- package/lib/drama/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-storyboard/scripts/selftest.py +0 -191
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +0 -824
- package/lib/drama/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +0 -21
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +0 -2
- package/lib/drama/skills/short-drama-video-prompts/assets/performance.fragment.json +0 -38
- package/lib/drama/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +0 -2
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +0 -329
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +0 -451
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +0 -327
- package/lib/drama/skills/short-drama-video-prompts/scripts/selftest.py +0 -82
- 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,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())
|