- 2026-10-12 -- A mapping template's option names the section a UI lists it under (
core_utils/engines/shots/manifest/mapping/_mapping.pyMapping.option_specsgainssection, newMapping.option_sections;_spec.pyvalidates it;default.jsongroups its nine options as Sheet / Objects / Shots).option_sectionsgroups the specs by section in first-appearance order, wherever the file puts them, so a UI draws one titled group per section with no code per option (the Shot Manifest's CSV Mapping popup). A template read without validation that writes"section": nulllists that option under"", neverNone.- Test(s):
test_shots_manifest_core.pynewtest_an_option_names_the_section_a_ui_groups_it_under(+ the null section, red:None != ''),test_the_shipped_template_groups_every_option(red: nosectionkey);test_specs_are_shaped_for_a_widget_factorypins the new key;test_surface_snapshot.pysnapshot regenerated forPlannedInsertandSHOT_STATUS_LEVELS.
- Test(s):
- 2026-10-12 -- Shot Manifest: on a built timeline a step with no shot gets one in sheet order, the later shots moving out of its way, and a range typed for an unbuilt step moves with them (
core_utils/engines/shots/manifest/manifest_engine.pynewShotManifest.plan_insertions,insert_planned,ride_inserts, hooks_sequencer,_new_shot_duration,_built_metadata;manifest_model.pynewPlannedInsert;shot_sequencer.pynewShotSequencer.insert_placement,insert_shot). A Build with Hide Missing Shots off placed each unbuilt step at a stale resolved range or zero-length at its predecessor's end, so the new shots lay over the built ones -- overlapping, zero-length, out of sheet order (2026-10-12: "very broken and out of order results"; SOL_REPLACE_ASSEMBLY: 37 new shots, dozens of overlaps, 9 zero-length).plan_insertionssays where each goes before anything moves: after its previous step's shot and that shot's split tails (or at the head), sized as a first build sizes it, each placed byShotSequencer.insert_placement-- the one placement ruleinsert_shotnow places by too, read on the timeline the earlier inserts leave -- so every plan's bounds are where its shot lands once all the inserts are in (a later insert placed before an earlier one, on a timeline holding its shots out of sheet order, moved the earlier one past its plan). A copy of the arithmetic had drifted: content trailing a moved last shot was read where it was, not where its shot went (30 frames trailing each shot: A04 planned at (280, 330), landed at (310, 360)).insert_shotmoves the shots after the insert by position, not every shot starting at the next one's frame, where the shot before the insert ends at or before the next one's start: a zero-length shot on that frame moved past the insert (A01, A02, Z, A03 for A01, Z, A02, A03); where the two overlap it keeps the frame rule, so overlapping shots sharing a start move together, each with its keys (by position the first stayed, its keys gone on with the second); every other timeline moves as before.insert_placementsays which rows move, and takes astarta caller knows (asinsert_shotdoes).insert_plannedinserts through the_sequencer()hook (a DCC host's: keys and audio ride) and records each shot as a first build does -- bound to its step, the step's metadata and the name it gave (BUILD_NAME), which it never wrote, soedited_by_handnever saw an inserted shot renamed by hand. Each insert goes after the nearest row before it on the plan's timeline that exists when it is made (PlannedInsert.follows;after_shot_id,after_step_id): part of a plan goes where the plan put it -- an insert after one left out took the timeline's head. A range typed in the timeline's current frames is content (maintainer 2026-10-12):ride_insertsmoves every one starting at or after an insert'spivotby itspush-- the typed A04 (220, 270) landed over A03, moved to [170, 270], and now lands at (280, 330). A step with a typed range (plan_insertions(typed=)) is no insert but a row of the plan's timeline at that range, moved by the inserts before it, and the next new step follows it -- an untyped step after one typed past the last shot appended after the last SHOT, over it (X at (570, 620) on P's (570, 610));insert_planned(typed=)starts such an insert after where the range lands. A step whose name the store refuses is left for the build to report. The no-range duration rule is_new_shot_duration, shared with_compute_plan.- Test(s):
test_shots_manifest_core.pynewTestInsertInSheetOrder(12; red: noplan_insertions, thentest_an_inserted_shot_carries_what_a_first_build_writes(nobuild_name),test_content_trailing_a_moved_last_shot_moves_with_it,test_a_shot_sharing_the_insert_point_keeps_its_place,test_part_of_a_plan_keeps_its_place_in_the_sheet,test_every_plan_is_where_its_shot_lands,test_a_typed_range_rides_the_inserts_before_it,test_a_typed_step_is_not_inserted,test_a_step_after_a_typed_one_follows_where_that_one_lands(red: X at (570, 620)),test_a_typed_step_between_shots_is_followed_and_ridden), newTestInsertPlanMatchesTheInserts(150 seeded random timelines -- zero-length shots, overlapping shots sharing a start, unpaired shots, trailing content, typed ranges -- plan against the inserts, red before the shared rule);test_shot_sequencer.pynewtest_insert_moves_the_shots_after_it_by_position(red: Z moved),test_overlapping_shots_sharing_a_start_move_together(red: B1 left behind),test_insert_placement_is_where_insert_shot_puts_it,test_insert_at_a_start_the_caller_knows. Full suite 7249 passed, 0 failed (38 skipped).
- Test(s):
- 2026-10-12 --
AudioUtils.memo_duration: a host's clip-length reader runs once per file as it is on disk (audio_utils/_audio_utils.py). Kept by the file's resolved path, modification time and size, and the frame rate: a replaced file reads again; a file that cannot be stat'ed, or a reading of no length, is never kept. The Shot Manifest's insert preview plans on every table resolve and sizes each unbuilt audio step from its clip -- a WAV header read from a synced drive each time; mayatk'sAudioUtils.audio_duration_framesand blendertk's manifest reader go through it.- Test(s):
test_audio_utils.pynewTestMemoDuration(2; red: nomemo_duration).
- Test(s):
- 2026-10-12 --
SHOT_STATUS_LEVELSnames the severity each shot status is shown at (core_utils/engines/shots/shot_model.py;SHOT_PALETTEis built from it);ManifestEdits.hidden_objects()with no step lists every hidden object;StepStatus.HELP["edited"]names the Shot Manifest's Edits mode;StepStatus.ROLLUPranks by that severity (manifest_model.py). A UI can mark a status by shape as well as colour (the Shot Manifest's glyphs). The rollup rankedbehavior_conflict, an error, belowambiguous_objectandnot_in_shot, warnings: a step holding both was named by the warning while its row's icon showed the error.- Test(s):
test_shots_core.pynewtest_every_status_names_its_severity(red: noSHOT_STATUS_LEVELS);test_manifest_edits.pynewtest_every_hidden_object_wherever_it_sits;test_shots_manifest_core.pynewtest_the_rollup_names_the_worst_severity_first(red: the order).
- Test(s):