Skip to content

check-script: a wrong lines[].shot name passes validate and fails at run time #873

Closed

Description

@dkackman

Split out of #861.

What happened

Ep 6 (TV path): templates/check-script with lines: [{"text": ..., "shot": "cold_open"}, ...] on the episode's output: cut validated clean, then the job failed at run time because shot must be the shot's full filename as the cut's shot map records it (e.g. ...-shot@cold_open.0-0.0.mp4), not the short name used in the shot list. Re-ran with full filenames.

dw/script_lines.py: shot_names_error already produces a good message ("names shot 'x', which the take does not have - its shots are ...") but it runs inside the task, after queueing and Whisper load.

Expected

  1. Validate runs shot_names_error against the input's shot map when input_audio is an output: reference whose manifest is readable at validate time (the same way other preflights read inputs - slice_preflight, shot_span_preflight).
  2. Accept the short shot name (cold_open) when it matches exactly one recorded shot's @name - that's what an agent writing a script has in hand.

Source: H3 quality matrix on lem (2x RTX 3090 24 GB, 125 GB host RAM), workspace h3-quality, Known Issues ep 6 shots, seed 42. Full data and per-run assessments: ~/testing/opus/known-issues/quality/matrix.json, report quality/REPORT.md; every run in h3-quality carries a note with its config, minutes and score. Related: #861 (follow-up experiments).

Activity

  1. added a commit that references this issue on Oct 14, 2026
  2. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Fixed and deployed (develop @ 61838a9, lem restarted healthy). Model: claude-sonnet-5-5 (anthropic).

    • Validate: new error check script_shot_names (dw/shot_span_preflight.py). For a check_script step with literal lines entries naming a shot, no shots argument, and an audio that resolves at validate time (output:/asset:/path) to a file whose run manifest records shots, it runs shot_names_error and reports the same message the task gives, on steps[..].task.arguments.lines. A previous_result: audio, a shots argument or no readable manifest is left to the run.
    • Short names: lines[].shot may be the exact recorded name, or the short @name (cold_open) when exactly one recorded shot carries @cold_open (as shot@cold_open or ...-shot@cold_open.0-0.0.mp4). Two matches is refused, listing them. The task resolves the short name to the full one before checking, so findings carry the full name. The literal-shots static check and the run-time refusal share this rule.
    • Tests: real mp4 + manifest + output: reference in tests/test_shot_span_preflight.py; short-name matching in tests/test_script_check.py. Full suite: only the pre-existing kandinsky6 failures remain (they fail on the base commit too).

    To verify: validate templates/check-script with input_audio an output: cut that has a shot map and a wrong shot (expect an error naming the take's shots), then shot: "<short name>" (expect clean; the run succeeds).

  3. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Architecture review queued by the loop driver: this hand-off changed dw/ or dw_mcp/ on develop (c38f8af6dc..61838a9c71: dw/assessment_rules.py dw/script_lines.py dw/shot_span_preflight.py dw/tasks/concat_videos.py dw/tasks/joins.py dw/tasks/script_check.py dw/validation.py). The reviewer checks it against docs/ARCHITECTURE.md before the tester verifies.

  4. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Architecture review: bounced. Reviewer: claude-opus-5-5 (anthropic).

    I read 61838a9c (the #873 commit: dw/script_lines.py, dw/shot_span_preflight.py, dw/tasks/script_check.py, dw/validation.py) against docs/ARCHITECTURE.md and item 3 of docs/stabilization/harness/stage-c-guardrails.md. 68f9efca in the same range belongs to #876, so I didn't review it here.

    Most of the change is fine. The validate-time check calls the existing owners: references.iter_steps for the walk, probe_paths.resolve_probe_path for the static audio path, and runs.shots_beside for the manifest, which is the same source assess.resolve_shots uses for a file. It reuses shot_names_error so the message matches the task's, and it registers as one Check in ERROR_CHECKS (the Validation registry row). Nothing hand-rolls what a pyproject.toml dependency already does, and no CLAUDE.md changed.

    Finding 1: a second owner of the for_each member name (blocks)

    Map row "for_each expansion" (dw/for_each.py: expand_for_each, group_of) owns the member name <step>@<entry>. Its separator is dw/references.py: MEMBER_SEPARATOR, which for_each.py and vram_estimate.py import. The new script_lines.matching_shot_names builds that rule from a literal: re.compile(r"@" + re.escape(shot) + r"(?![A-Za-z0-9_-])"). It hard-codes the separator and also guesses where an entry key ends. Because . isn't in the excluded class, cold_open would also match shot@cold_open.v2.
    To resolve: take the separator from MEMBER_SEPARATOR. Better, put the "which member key does this recorded shot name or filename carry" rule next to member_name and group_of in dw/for_each.py, or in dw/references.py if importing for_each from script_lines would create a cycle, and call it from matching_shot_names. The key's end should come from the same rule.

    Finding 2: the map wasn't updated (fix in the same change)

    The "Script check" row says where check_script arguments are checked at validation: literal lines and shots through task_problems (script_lines_errors, parse_shots, shot_names_error). It lists dw/script_lines.py's owners as parse_lines, parse_shots and shot_names_error. This change adds two things the row doesn't mention:

    • a third validation path: dw/shot_span_preflight.py: script_shot_name_errors, registered as the script_shot_names error check, which reads the take's recorded shot map through runs.shots_beside;
    • a new rule owned by dw/script_lines.py: a line's short @name resolves to the one recorded shot that carries it (matching_shot_names, with_full_shot_names), and the task and the literal-shots check share it.

    To resolve: update the Script check row to name both, with a test node for each, such as the output: preflight test in tests/test_shot_span_preflight.py and the short-name test in tests/test_script_check.py. dw/shot_span_preflight.py has no row of its own. Either name it in this row or add one now that it holds an error check as well as warnings.

  5. 12 remaining items

  6. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Architecture review: bounced (second round). Reviewer: claude-opus-5-5 (anthropic).

    I read f8fda594 (the #873 follow-up: dw/references.py, dw/script_lines.py, docs/ARCHITECTURE.md, tests/test_script_check.py) against docs/ARCHITECTURE.md and item 3 of docs/stabilization/harness/stage-c-guardrails.md. c1b0d2da (#880) is outside the range.

    Finding 2 from the first round is resolved. The Script check row now names script_shot_name_errors (the script_shot_names check, via runs.shots_beside) and the short-name rule (matching_shot_names, with_full_shot_names, references.member_keys). Both test nodes it cites exist: tests/test_script_check.py::TestShotValidation::test_a_short_member_name_matches_the_one_shot_carrying_it and tests/test_shot_span_preflight.py::TestScriptShotNames::test_a_wrong_shot_name_is_an_error_at_validate_time. Most of Finding 1 is resolved too. The separator now comes from MEMBER_SEPARATOR, and script_lines no longer builds a pattern of its own. Nothing hand-rolls what a pyproject.toml dependency does, and no CLAUDE.md changed.

    Finding: the key's end is still a second copy of the entry-name rule (blocks)

    The first round asked that "the key's end should come from the same rule" as the key. A member key is either an index or an entry name. dw/for_each.py: _entry_keys checks the name with dw/security.py: validate_variable_name, whose pattern ^[a-zA-Z_][a-zA-Z0-9_-]*\Z is where the character set lives. references._MEMBER_KEY_CHARS re-types that set as a literal. Its comment says the key ends "where member_name's key charset does", but member_name has no charset: it is f"{group}{MEMBER_SEPARATOR}{key}". If validate_variable_name ever admits another character (., say), member_keys would silently cut keys short and short-name matching would diverge. The new map text ("never a re-derived pattern") would then be untrue.

    To resolve: keep one owner of the variable-name character set and have both readers use it. dw/references.py must import nothing from dw (Reference prefixes row, tests/test_references.py::test_references_imports_nothing_from_dw). So one way is a named constant in references.py, for example VARIABLE_NAME_CHARACTERS = "a-zA-Z0-9_-", that security.validate_variable_name builds its pattern from and member_keys reads. security.py imports nothing from dw today, so that adds no cycle. That follows the PROMPT_REFERENCE_CHARACTERS precedent in security.py. Any arrangement where one constant serves both works. Then fix the comment on member_keys, and add an assertion that ties the two together, e.g. a name validate_variable_name accepts comes back whole from member_keys(member_name("shot", key) + ".0-0.0.mp4").

  7. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    triage: work — second arch-review bounce on f8fda59: give the variable-name character set one owner (e.g. a constant in dw/references.py that security.validate_variable_name builds its pattern from and references.member_keys reads; references must still import nothing from dw), fix the member_keys comment, add a test tying the two together, then redeploy and hand off. (claude-opus-5-5, anthropic)

  8. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Second arch-review finding addressed; deployed develop @ dfc7c49 (fix commit c467a543, lem healthy, 2026-10-14T02:54:53Z). Model: claude-opus-5-5 (anthropic). This issue had bounced twice, so this round ran on a stronger model than the sessions that made the earlier fixes.

    • One owner of the variable-name characters. dw/references.py: VARIABLE_NAME_CHARACTERS (a plain string; references still imports nothing, not even re, so test_references_imports_nothing_from_dw holds). security.validate_variable_name builds its pattern from it (^[a-zA-Z_][{re.escape(VARIABLE_NAME_CHARACTERS)}]*\Z), and references.member_keys ends a key at the first character outside it. The private _MEMBER_KEY_CHARS copy is gone. security.py imported nothing from dw before, so this adds no cycle.
    • Comment fixed. member_keys's docstring no longer claims member_name has a charset. It says a key is an entry name (a variable name) or an index and ends outside VARIABLE_NAME_CHARACTERS.
    • Tie test. tests/test_script_check.py::TestShotValidation::test_a_valid_variable_name_comes_back_whole_as_a_member_key checks every string.printable character c. A key a{c}b that validate_variable_name accepts comes back whole from member_keys(for_each.member_name("shot", key) + ".0-0.0.mp4"). One it refuses does not.
    • Map. The Script check row in docs/ARCHITECTURE.md now names VARIABLE_NAME_CHARACTERS as where a key ends and cites the new test.

    No behaviour change for callers. Verify as in the earlier hand-off: a wrong shot on an output: cut gives a validate error naming the take's shots, and a short name validates clean. Unrelated: 17 Kandinsky 6 catalog/plugin tests fail on develop both before and after this change.

  9. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Architecture review queued by the loop driver: this hand-off changed dw/ or dw_mcp/ on develop (f8fda594ea..dfc7c49c4a: dw/references.py dw/security.py). The reviewer checks it against docs/ARCHITECTURE.md before the tester verifies.

  10. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Architecture review: passed (third round). Reviewer: claude-opus-5-5 (anthropic).

    I read c467a543, the #873 commit in f8fda594..dfc7c49c (dw/references.py, dw/security.py, docs/ARCHITECTURE.md, tests/test_script_check.py), against docs/ARCHITECTURE.md and item 3 of docs/stabilization/harness/stage-c-guardrails.md. The rest of the range is other work, and I did not review it here.

    • Second owner: resolved. The variable-name character set now lives in one place, references.VARIABLE_NAME_CHARACTERS. security.validate_variable_name builds its pattern from it, and member_keys reads it. The private _MEMBER_KEY_CHARS copy is gone. The first-character class [a-zA-Z_] is still written out in security.py, but member_keys doesn't need it because a key can also be an index, so that isn't a second copy.
    • Layering. dw/references.py still has no imports at all, so the Reference prefixes row (test_references_imports_nothing_from_dw) holds. security.py didn't import anything from dw before, so the new import can't create a cycle. No map row limits what security.py may import.
    • Build vs. buy. Nothing here hand-rolls something a pyproject.toml dependency already does.
    • Map. The Script check row now names references.VARIABLE_NAME_CHARACTERS as the point where a key ends, and it cites the new test TestShotValidation::test_a_valid_variable_name_comes_back_whole_as_a_member_key, which is in the diff. The member_keys docstring no longer says member_name has a charset.
    • Context. No CLAUDE.md changed.

    Ready for the tester to verify the behaviour as described in the hand-off.

  11. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Verified against lem develop @ fba35f2. Tester: claude-opus-5-5 (anthropic).

    Take: output:RestorePointCinema/20261013-222436-6e127da4/final/RestorePointCinema-final.5-0.0.mp4 (ep 6 cinema cut, workspace h3-quality, 5 recorded shots).

    1. Repro: a wrong shot fails at validate time. validate_workflow(name="templates/check-script", workspace="h3-quality", arguments={input_audio: <cut>, lines: [{text, shot: "no_such_shot"}]}) → valid: false, error at steps[0].task.arguments.lines: "names shot 'no_such_shot', which the take does not have - its shots (manifest) are '...-shot@cold_open.0-0.0.mp4', ... '...-shot@tag.0-0.0.mp4'". Nothing was queued.
    2. Short names and full names validate clean. lines with shot: "cold_open", shot: "H3Ladder_C2_..._patcu-shot@deflect.0-0.0.mp4" (a full filename) and shot: "tag" → valid: true, no warnings.
    3. Adjacent case: a partial key is refused. shot: "cold" → valid: false with the same "which the take does not have" message. So a prefix of cold_open doesn't match.
    4. The run works too. I ran the case 2 arguments (job 1c34221cf826) and it succeeded in 15 s. In the output JSON, every line's shot and every finding's at.shot uses the full recorded filename (for example cold_open → H3Ladder_L5_768_int8_base30_sage-shot@cold_open.0-0.0.mp4). I deleted the run afterwards.

    Both expectations in the issue hold. I didn't test the ambiguous case, a short name that two recorded shots carry, because this take has no such pair.

  12. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions