Skip to content

Audit 2026-11: render.py, batch.py and sequence.py #458

Open

Description

@kajisho5

From a full audit of the tree at 47a962a (3.5.0 + #452) on 2026-11-12. Each finding was checked against the code, reproduced where it can be run, and then checked again by a second reviewer who tried to refute it. Findings that were refuted, already decided in docs/design-decisions.md, or already filed are left out. The severity is the second reviewer's. The reproduction and suggested fix for each one are in the audit record (ask on this issue to have one posted). render.py's output-is-input guard (any project input) is fixed in #456.

42 findings: 2 high, 16 medium, 24 low. Tick a box and name the PR when a fix is merged.

  • high scripts/batch.py:309 (A79): batch.py project recipes resolve every relative path in the project against the item's temporary work directory
  • high scripts/batch.py:555 (A275): batch.py --watch processes a source that is still being copied, reports the truncated output ok (verified for TS and every project recipe), and once the source settles refuses every later pass with output_exists, so the truncated deliverable is never replaced
  • medium references/scripts.md:1084 (A86): references/scripts.md says a pack's -o names the directory, but render.py writes the pack into the parent of -o
  • medium scripts/batch.py:62 (A80): batch.py's cache key for a project recipe ignores the files the project names, so an edited cue/music/brand file is served from the stale cache
  • medium scripts/batch.py:208 (A83): batch.py names a .webm source's final output .webm, which the re-encoding steps cannot write (the J: dry-run honesty, process handling, faststart and batch/render orchestration (audit round 2) #415 default-name rule is not applied)
  • medium scripts/batch.py:505 (A81): batch.py --watch computes recipe_key once, so a project.json edited during the watch is still served from the cache
  • medium scripts/batch.py:634 (A276): batch.py keys every item at the start of a pass, so a source that finishes copying while an earlier item is processing is processed in full but cached under its partial key, and every later --watch pass then fails the correct output with output_exists
  • medium scripts/render.py:359 (A85): A template pack (--template a,b / all) silently drops --stop-after, --cache, --from, --work, --keep, --export-timeline and --audit-log
  • medium scripts/render.py:983 (A96): An executed plan reports its output relative to the plan's cwd (after os.chdir), not to where the caller is
  • medium scripts/render.py:1240 (A77): render.py's default work directory is named by PID only and reused with exist_ok, so a later run with the same PID deletes an earlier run's kept preview and --keep intermediates
  • medium scripts/render.py:1352 (A93): A clip's own snap.to is never validated: a typo ("beat") or an unsupported grid silently renders the cut unsnapped
  • medium scripts/render.py:1392 (A92): An audiogram project with several clips cuts all of them and renders only the first, without a word
  • medium scripts/render.py:1399 (A91): audiogram.image is passed to waveform.py unresolved, so a relative image resolves against the cwd instead of the project file
  • medium scripts/render.py:1638 (A84): render.py without an export stage copies an MP4 intermediate to a .webm/.mkv output and reports it verified
  • medium scripts/render.py:1640 (A87): --stop-after export does not stop: the chapters and check stages still run, and the validation that treats check as unread lets a non-object check crash
  • medium scripts/render.py:1652 (A260): render.py writes chapter titles raw into its line-per-chapter chapters.txt: a newline adds a chapter (still verified), and other line breaks fail the render in its last stage
  • medium scripts/sequence.py:125 (A108): sequence.py --dir . (run from inside the frames folder) is a traceback without -o
  • medium templates/audiogram.json:10 (A89): --template audiogram without --image silently drops the audiogram stage: the dry run completes and the real run fails at export with "input has no video stream"
  • low examples/brand.json:24 (A107): examples/brand.json carries a loudness block that no tool reads
  • low examples/make_demo.sh:3 (A106): The demo pipeline claims to run every script, but it runs 13 of the 42
  • low examples/make_demo.sh:55 (A105): examples/make_demo.sh cannot be re-run: no tool call carries --overwrite and set -e aborts on the first existing output
  • low references/scripts.md:975 (A103): references/scripts.md misstates render's normalize default and says --codec is forwarded and keyed
  • low scripts/_common/timeline.py:337 (A266): EDL source timecodes run past 24:00:00:00 for a source whose start timecode is near midnight
  • low scripts/_common/timeline.py:382 (A265): EDL event numbers overflow the CMX 3600 three-digit field at 1000 events
  • low scripts/_common/timeline.py:401 (A261): timeline.to_edl writes caller strings raw into CMX 3600 lines: a newline in a chapter title, file name or output name adds lines or events, and parse_error() accepts the file
  • low scripts/batch.py:53 (A277): file_key, documented as a content hash, misses a same-size, same-second rewrite outside the first and last MiB, so batch serves an old output as cached and render.py --cache serves an old stage artifact
  • low scripts/batch.py:442 (A104): batch.py ignores unknown recipe keys: a misspelled output_dir/suffix/glob silently uses the default
  • low scripts/batch.py:555 (A82): batch.py's glob is case-sensitive, so camera files (GOPR0001.MP4) under the documented "*.mp4" glob complete as an empty batch with no note
  • low scripts/render.py:407 (A263): render.py --template a,b pack table writes file names raw into a Markdown row: a ' | ' in the input name shifts every column, and report.py --pack marks passing destinations as failed
  • low scripts/render.py:681 (A101): INTERMEDIATE is never reset in main(), so a second render in the same process encodes its delivery stages at superfast
  • low scripts/render.py:1049 (A267): --export-timeline runs ffprobe once per clip even when every clip is the same file
  • low scripts/render.py:1073 (A264): A lone surrogate in a project string (a JSON \ud83d escape) is a UnicodeEncodeError traceback with empty stdout, a 0-byte timeline (an --overwrite target truncated), or a failure after a whole render
  • low scripts/render.py:1157 (A102): render.py --init and --list-templates print plain text on stdout under --json
  • low scripts/render.py:1240 (A98): Re-running render.py with the same --work refuses the stage files the previous run left there
  • low scripts/render.py:1261 (A88): Sections that validate_project skips under --stop-after are still read before their stage, so a non-object export/check is a traceback
  • low scripts/render.py:1377 (A97): Under --dry-run a clip's speed stage is planned from an invented 10 s duration, and nothing says the number is a placeholder
  • low scripts/render.py:1536 (A95): Project text values beginning with '-' ("-30%") break the stage's argparse, because render passes them as a separate token
  • low scripts/render.py:1653 (A99): A dry run with --work or --keep creates the work directory and writes chapters.txt into it
  • low scripts/render.py:1653 (A100): With --cache, the chapters stage of an inline chapters list never hits, because chapters.txt is rewritten each run and its mtime is in the key
  • low scripts/render.py:1655 (A94): A chapters value that is neither a list nor a path string is not validated before the first stage, contrary to the reference
  • low scripts/sequence.py:116 (A109): sequence.py accepts --width/--height of 0 or below and silently renders at the source size
  • low templates/audiogram.json:1 (A90): The audiogram template ignores --brand and --title although the audiogram stage and its captions take both

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions