engine: sequence.inspect reports a reversed clip

Problem: a clip set to play backward (`clip.speedDuration` with
`reverse: true`) read back from `sequence.inspect` exactly like a
forward clip: same `speed`, same source range, nothing that said it was
reversed. An agent could set it and not confirm it.

Solution: clips gain `reverse` (bool) in `sequence.inspect`, named as
in the project file and in the command. Additive; existing keys are
unchanged. `docs/agents.md` lists it and says what it means for the
source range.

Tests: sequence_inspect_tests.rs reverses a demo clip through the
command and checks that only that clip reports `reverse: true`, and
none after undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
punkhop 2026-10-05 22:45:21 -07:00
parent 51ecd70763
commit 7eb4f1fc7b
3 changed files with 31 additions and 3 deletions

View File

@ -2714,7 +2714,7 @@ pub fn inspect_sequence(s: &Session, id: ItemId, q: &filmcraft_project::Sequence
"id": t.id.0, "name": t.name, "locked": t.locked, "syncLock": t.sync_lock, "enabled": t.enabled, "muted": t.muted, "solo": t.solo,
"items": t.items.iter().map(|i| json!({
"clip": i.id.0, "item": i.item.0, "name": i.name, "start": i.start.0, "duration": i.duration.0,
"startFrame": rate.frame_at(i.start), "durationFrames": rate.frame_at(i.duration), "sourceIn": i.source_in.0, "speed": i.speed,
"startFrame": rate.frame_at(i.start), "durationFrames": rate.frame_at(i.duration), "sourceIn": i.source_in.0, "speed": i.speed, "reverse": i.reverse,
"end": i.end().0, "endFrame": rate.frame_at(i.end()), "sourceOut": i.source_out().0, "gainDb": i.gain_db,
"enabled": i.enabled, "link": i.link, "label": i.label.name(),
"effects": i.effects.iter().map(|e| json!({"effect": e.effect, "enabled": e.enabled, "masks": e.masks.len(), "params": e.params.iter().map(|(k, p)| (k.clone(), json!({"value": format!("{:?}", p.value), "keyframes": p.keyframes.len()}))).collect::<serde_json::Map<_, _>>()})).collect::<Vec<_>>(),

View File

@ -1,5 +1,5 @@
//! `sequence.inspect` is how an agent checks its edits: it has to show where a clip ends in the
//! sequence and in its media, the clip's audio gain and what a marker says.
//! sequence and in its media, the clip's audio gain, whether it plays backward and what a marker says.
use serde_json::json;
@ -59,3 +59,31 @@ fn sequence_inspect_reports_clip_ends_gain_and_marker_comments() {
}
assert_eq!(seen, 2, "both edited clips were found in the readback");
}
/// A clip set to play backward read back exactly like a forward one: same `speed`, same source
/// range, and nothing that said it was reversed.
#[test]
fn sequence_inspect_reports_a_reversed_clip() {
let mut s = Session::default();
s.execute("file.openDemoProject", json!({})).unwrap();
let backward = s.active_sequence().unwrap().video_tracks[0].items[1].id;
let all = |js: &serde_json::Value| -> Vec<(u64, bool)> {
["video", "audio"]
.iter()
.flat_map(|k| js[*k].as_array().unwrap().iter())
.flat_map(|t| t["items"].as_array().unwrap().iter())
.map(|i| (i["clip"].as_u64().unwrap(), i["reverse"].as_bool().expect("every clip reports `reverse`")))
.collect()
};
let js = s.execute("sequence.inspect", json!({})).unwrap();
assert!(all(&js).iter().all(|(_, reversed)| !reversed), "nothing plays backward yet");
s.execute("sequence.linkedSelection", json!({"on": false})).unwrap();
s.execute("timeline.select", json!({"clips": [backward.0]})).unwrap();
s.execute("clip.speedDuration", json!({"speed": 100, "reverse": true})).unwrap();
let js = s.execute("sequence.inspect", json!({})).unwrap();
let reversed: Vec<u64> = all(&js).into_iter().filter(|(_, r)| *r).map(|(c, _)| c).collect();
assert_eq!(reversed, vec![backward.0], "only the reversed clip says so");
s.execute("edit.undo", json!({})).unwrap();
let js = s.execute("sequence.inspect", json!({})).unwrap();
assert!(all(&js).iter().all(|(_, reversed)| !reversed));
}

View File

@ -40,7 +40,7 @@ claude mcp add filmcraft-headless -- /abs/path/filmcraft/target/release/filmcraf
| `command_list` | both | every command: id, label, menu, shortcut, params, enabled now (`filter`, `enabled_only`) |
| `command_run` | both | run a command `{id, params}`; edits are undoable |
| `project_inspect` | both | bins and items with ids, types, durations; active sequence |
| `sequence_inspect` | both | the active sequence: tracks, clips (`start` / `end` and `sourceIn` / `sourceOut` in ticks, frames, `speed`, `gainDb`), effects, transitions, markers (name and `comment`), playhead, selection |
| `sequence_inspect` | both | the active sequence: tracks, clips (`start` / `end` and `sourceIn` / `sourceOut` in ticks, frames, `speed`, `reverse`: the clip plays its `sourceIn` to `sourceOut` stretch backward, `gainDb`), effects, transitions, markers (name and `comment`), playhead, selection |
| `media_import` | both | import files by absolute path (`text`, one path per line) |
| `render_frame` | both | PNG of the program frame at `seconds` (headless renders; bridge screenshots the Program monitor) |
| `ui_inspect` | bridge | UI state: tool, workspace, panels, zoom, playback, fps |