Files
gart/code/CHANGELOG.md
T

1105 lines
84 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## [Unreleased]
**Repo renamed `fcpxml-mcp-server` → `fcp-mcp-server`** to match the PyPI
distribution name. The GitHub *About* link had been pointing at
`pypi.org/project/fcpxml-mcp-server/` — a slug that never existed on PyPI — so
every visitor who clicked it got a 404 while `uvx fcp-mcp-server` worked fine.
Homepage now points at the real package; clone URLs, CI badge, and
`[project.urls]` follow the new slug. GitHub redirects the old slug, so existing
clones, forks, and links keep working.
The MCP registry identity stays `io.github.DareDev256/fcpxml-mcp-server`,
unchanged — it is bound to the `mcp-name` marker inside the *published* 0.13.1
PyPI README, and changing it would orphan the registry entry and require a new
PyPI release. Registry name ≠ install name is legal and intentional.
No code changes; 0.13.1 on PyPI is untouched.
## [0.13.1] - 2026-07-24
Registry release. Adds the `mcp-name` ownership marker to the README (required
by the official MCP registry to bind the PyPI package to
`io.github.DareDev256/fcpxml-mcp-server`) and trims `server.json`'s description
to the registry's 100-char limit. No code changes.
## [0.13.0] - 2026-07-23
**Transcript Intelligence** — text-based editing lands. 59 → 62 tools.
Apple put FCP's AI (Transcript Search, Generate Captions) behind the Creator
Studio subscription; this release brings the agentic version to everyone, free,
via local Whisper — and goes further: the transcript doesn't just *search*, it
*cuts*.
### Added
- **`transcribe_media`** — transcribes each clip's source media locally with
word-level timestamps (faster-whisper, new optional `[transcribe]` extra).
Writes a `_transcript.json` next to each media file — transcription is a
one-time cost, reused by every transcript tool. Optional `write_srt` emits
an SRT that plugs straight into `import_srt_markers`.
- **`edit_by_transcript`** — cut timeline content by what was SAID.
`mode=remove` cuts every occurrence of the given phrases with ripple;
`mode=keep_only` keeps only the matched phrases (clips with no matches are
left untouched — never deletes a clip because nothing matched). Matching is
case/punctuation-insensitive. Non-destructive `_transcript_edit` copy.
- **`remove_filler_words`** — cuts um/uh/erm out of the timeline with ripple
using word-level timestamps from the real source audio. The default filler
list is deliberately conservative: words like "like" and "so" are speech,
not noise, and are only cut when passed explicitly.
- New `fcpxml/transcribe.py` module: pure, dependency-free matching helpers
(`find_phrase_spans`, `find_filler_spans`, `merge_ranges`, `invert_ranges`,
`segments_to_srt`) + the faster-whisper integration behind the same
graceful-degradation contract as ffmpeg/librosa (returns `None` → tools
answer with an install hint, never a crash).
- 42 new tests (1032 total): span matching, range algebra, keep_only inversion
edge cases, degradation without faster-whisper, and full handler integration
against cached transcripts (head-trim vs split behavior, SRT output,
transcription caps, missing-media reporting).
### Notes
- Whisper model names are allowlist-validated (they resolve to downloads).
- Per-call transcription is capped at 10 distinct media files; cached
transcripts don't count against the cap.
## [0.12.2] - 2026-07-23
Distribution release — the server is now on PyPI. No tool changes.
### Added
- **Published to PyPI as [`fcp-mcp-server`](https://pypi.org/project/fcp-mcp-server/).**
`uvx fcp-mcp-server` now works, which unbreaks the install path that `server.json`
(official MCP registry) and `smithery.yaml` have been advertising, and makes the
`[intelligence]` extra installable without cloning.
- **Claude Code install path** in the README (`claude mcp add` one-liner + project-scoped
`.mcp.json` example) alongside the existing Claude Desktop instructions.
- **"How It Compares" section** — honest trade-off table vs SpliceKit (runtime patching)
and CommandPost (accessibility scripting): raw live power there, no-patch portability,
managed-Mac compatibility, and works-without-FCP here.
- Security posture surfaced at the top of the README (132 adversarial-input tests,
defusedxml, sandboxed writes, disclosure channel).
### Fixed
- **Packaging: `server.py` was missing from the wheel.** `[tool.setuptools]` only
included the `fcpxml*` and `tools*` packages, so a built wheel had no entry-point
module and `fcp-mcp-server` failed to launch. Added `py-modules = ["server"]`,
dropped the empty `tools` stub package from the distribution, and verified the
wheel end-to-end in a clean venv (MCP initialize handshake answers correctly).
- **Server now reports its own version** over MCP (`serverInfo.version` said `1.28.1` —
the SDK's version — instead of the package's).
- `server.json` refreshed: version 0.9.0 → 0.12.2, tool count 56 → 59, description
includes media intelligence.
- `pyproject.toml` URLs point at the canonical repo (`fcpxml-mcp-server`), and a
Changelog URL was added.
## [0.12.1] - 2026-07-16
Docs fix. No code changes.
### Fixed
- **`[intelligence]` install command didn't work.** The README told users to run
`pip install 'fcp-mcp-server[intelligence]'` to enable `detect_beats`, but the
package isn't published to PyPI — in a clean venv that errors with
*"No matching distribution found for fcp-mcp-server[intelligence]"*. Since the
documented install flow is `git clone` + `pip install -e .`, the extra is now
`pip install -e '.[intelligence]'`, with a note that it must run from the cloned
repo. This blocked the headline feature of v0.12.0.
- **Clone URL and paths use the canonical repo name.** `git clone .../fcp-mcp-server.git`
still resolves via GitHub's rename redirect, but it created a directory whose name
didn't match the `cd` on the next line, and the `/path/to/fcp-mcp-server` placeholders
in the Claude Desktop config didn't match what `git clone` produces. (The *package*
name in `pyproject.toml` is legitimately `fcp-mcp-server` — only the repo is
`fcpxml-mcp-server`. Left alone.)
### Verified unchanged
Audited every headline claim against the source — all accurate, nothing to correct:
59 tools (all 59 documented, 0 undocumented), 990 tests (pytest collects exactly 990),
23 suites, v0.12.0 consistent across pyproject/CHANGELOG/README.
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.12.0] - 2026-07-09
### Added — Media Intelligence slice 3: beat detection
- **`detect_beats` (59th tool)** — detects musical beats and tempo in an audio/video file via librosa's beat tracker and writes a beats JSON next to the media file in exactly the format `import_beat_markers` consumes, so *detect → mark → snap-to-beats* chains with zero glue. Analysis duration capped (20 min) to bound memory; media path validated against an audio/video extension whitelist.
- **`[intelligence]` optional extra** — `pip install 'fcp-mcp-server[intelligence]'` adds librosa. The core install stays 2 dependencies; without the extra, `detect_beats` degrades to an install hint (lazy import, never crashes). CI installs it so beat tests run on every push.
### Fixed
- **`import_beat_markers` no longer crashes when beats run past the timeline end** — songs are routinely longer than edits; out-of-range beats are now skipped and counted in the report instead of raising `No spine clip at position`. Found by end-to-end verification of the detect → import → snap chain.
Tests: 984 → 990.
## [0.11.0] - 2026-07-09
### Added — Media Intelligence slice 2: silence auto-removal
- **`remove_media_silence` (58th tool)** — detects real silence in each clip's source audio (same ffmpeg analysis as `detect_media_silence`) and **cuts it out of the timeline with ripple**: clips are split around silence, silent middles removed, everything after shifts earlier. `padding` (default 0.05s) keeps a breath of silence on each side of every cut; cut boundaries snap to the frame grid in the 2400-tick timebase. Non-destructive — writes a `_silence_removed` copy, and writes nothing at all when no silence is found.
- **`FCPXMLModifier.cut_clip_ranges`** — new element-based writer primitive: removes clip-relative time ranges from a spine clip (merging overlapping ranges, clamping out-of-bounds), rebuilds the clip as its kept segments with correct source in-points, filters markers/keywords per segment, and ripples subsequent clips. Element-based on purpose — immune to the duplicate-name ambiguity that name-keyed `delete_clip`/`split_clip` composition would hit when cutting a clip into same-named segments.
Tests: 976 → 984.
## [0.10.0] - 2026-07-09
### Added — Media Intelligence v1 (the moat work begins)
First slice of the v0.10 media-intelligence roadmap: the server now analyzes the **actual media files** a timeline references, not just the XML.
- **`detect_media_silence` (57th tool)** — probes each clip's source audio with ffmpeg's `silencedetect` filter and maps silence ranges from source time into **timeline time**, reporting per-clip silence spans with a cut plan. Unlike `detect_silence_candidates` (XML-only heuristics: gaps, name patterns), this hears the audio. Supports `noise_db` threshold (−120..0 dB), `min_silence` duration, and per-clip filtering; media files are probed once and cached across clips that share them; missing/unreadable media is reported per clip, never fatal.
- **`fcpxml/media_intel.py`** — new module for real media analysis. Zero new Python dependencies: ffmpeg runs as a bounded subprocess (list-form args, validated numeric parameters, 120s hard timeout, 100-file probe cap) and everything degrades gracefully — no ffmpeg means "unanalyzable", not a crash.
- **CI now installs ffmpeg** so the real-WAV integration tests (tone/silence/tone fixtures generated with the stdlib `wave` module) run on every push; they skip automatically on machines without ffmpeg.
Tests: 958 → 976 across 23 suites.
## [0.9.1] - 2026-07-09
### Security
- **`apply_template` write sandbox bypass fixed** — the one generation handler that built a timeline from scratch (no input file to anchor against) called `_validate_output_path()` without an `anchor_dir`, which skipped the sandbox check entirely and accepted absolute or `../` output paths. An LLM-steered call could overwrite an arbitrary user-writable file. Now anchored to `FCP_PROJECTS_DIR` like every other write handler. Reported and fixed by [@mikegrant25](https://github.com/mikegrant25) (#6).
- **`SECURITY.md` added** and GitHub private vulnerability reporting enabled — future disclosures have a private channel.
### Fixed
- **`add_audio` / `add_music_bed` stamped the requested clip duration onto new `<asset>` elements** without reading the media file — a music bed shorter than the timeline produced an asset claiming more media than the file contains (invalid FCPXML, clip overruns the real audio). New `_probe_audio_info()` reads the real duration, sample rate, and channel count via `ffprobe` (stdlib `wave` fallback for `.wav`); assets carry sample-accurate durations plus `audioRate`/`audioChannels`/`audioSources`, and clip durations are clamped to available media. Unprobeable sources keep the old behavior. Fixed by [@jardelapp](https://github.com/jardelapp) (#7).
- Docs reconciled to verified test counts (#5).
Tests: 955 → 958.
## [0.9.0] - 2026-06-11
### Added — Live Mode v1 (the dual-mode roadmap goes live)
This is the first release where the server can drive a **running** Final Cut Pro, not just edit XML offline — using Apple's officially-supported surfaces only (no injection, no private APIs). Both tools were **live-verified end to end against Final Cut Pro 12.2**.
- **`push_to_fcp` (55th tool)** — send an FCPXML file into the running FCP with zero clicks via the Open Document Apple event. Injects an `<import-options>` element (library location, suppress warnings, copy assets), launches FCP if needed, and never touches your original (flat files get an options-injected sibling copy through the same write sandbox as every other tool). Live-verified: generated a timeline, pushed it, and confirmed the library/event/project landed both in FCP and on disk.
- **`list_fcp_libraries` (56th tool)** — enumerate the running FCP's open libraries → events → projects via Apple's read-only scripting dictionary. Refuses to launch FCP unless `allow_launch=true`.
- **`fcpxml/live.py`** + **`tests/test_live.py`** (13 tests, osascript fully mocked so CI never launches FCP).
### Findings baked in from live testing
- **Zero-click import requires a `.fcpbundle` library location.** With a new `.fcpbundle` path FCP silently creates the library + a dated event and imports; with no location (or a `.fcplibrary`/bare path) FCP raises a modal **Open Library** picker — a *required choice* that `suppress warnings` does not dismiss — which blocks the Apple event. `push_to_fcp` now normalizes the location to `.fcpbundle`.
- **Apple offers no programmatic export** — the read-back leg of any edit loop still needs File > Export XML; the tool says so in its output.
- Importing a project whose media already exists in the target library fails on a media-identity collision — push into a fresh library or reuse FCP's existing asset IDs.
Tests: 942 → 955.
## [0.8.0] - 2026-06-11
### Added
- **FCPXML 1.12–1.14 support**: parser now reads everything Final Cut Pro 12.x exports (FCPXML 1.14). Elements introduced after 1.11 (`adjust-stereo-3D`, `hidden-clip-marker`, smart-collection `match-analysis-type`, …) are tolerated on read and preserved losslessly through edits. Generated timelines (templates, rough cuts, FCPXMLWriter) now emit **1.13** by default; modified files keep their source version.
- **`.fcpxmld` bundle support, end to end**: bundles (directories wrapping `Info.fcpxml` plus sidecar data) now work in every tool. `FCPXMLModifier` loads bundles, and `save()` writes bundle outputs with **sidecar preservation** — object-tracking and Cinematic-mode `dataLocator` payloads are copied across the round-trip instead of silently destroyed. Fixed `_validate_filepath` rejecting bundles outright (they are directories, and the previous "regular file" check made the whitelisted `.fcpxmld` extension unreachable).
- **`relink_media` tool (54th tool)**: bulk-rewrite `asset`/`media-rep` `src` paths by prefix — relink a moved or renamed media drive without opening FCP. Handles `file://` URLs (percent-encoding preserved) and plain paths, matches whole path segments only, reports whether each new target exists on disk, and supports `dry_run` preview.
- **DTD validation** (`fcpxml/dtd.py` + `tests/test_dtd_validation.py`): generated output is validated against **Apple's official DTDs** located inside the installed Final Cut Pro app bundle (the only authoritative FCPXML spec — Apple's online docs stopped at 1.10). Skips gracefully on machines without FCP; `FCPXML_DTD_DIR` overrides the search path. Found and worked around an xmllint quirk where the space in "Final Cut Pro.app" breaks DTD URI resolution.
- **Capability audit + dual-mode roadmap** (`docs/CAPABILITY-AUDIT-2026-06.md`): verified June-2026 ecosystem analysis (FCP 12.2 control surfaces, SpliceKit, CommandPost, format ceiling) and the XML-mode + Live-mode architecture plan through v1.0.
### Fixed
- README/CLAUDE.md drift: tool count, test counts, FCPXML version matrix, phantom `fcpxml/README.md` and `OPENAI_BASE_URL` references removed.
### Known
- `examples/sample.fcpxml` is not DTD-conformant (pre-`media-rep` asset form, sequence-level chapter markers) — documented by a dedicated test; fixture modernization planned.
- Total: 912 → 942 tests across 21 suites.
## [0.7.0] - 2026-05-02
### Added / Fixed
- Version milestone consolidating the April hardening waves (no API changes).
- Security hardening with `defusedxml`.
- Duplicate clip name bug fixes.
- Added 100+ new tests.
- Refactored helper functions for cleaner logic.
- Unification of XML serialization.
- TimeValue arithmetic fixes (integer-exact comparison via cross-multiplication with normalized negative denominators).
- Output-path sandbox enforcement plus speed/ffmpeg parameter validation.
- Stale `timeMap`/conform-rate stripping in `change_speed`.
- Marker/keyword filtering during `split_clip`.
- Shared `_text_result`, `_resolve_clip_duration`, and `_make_asset_clip` helpers.
- FCPXML validation-infrastructure test wave (912 tests).
## [0.6.63] - 2026-04-14
### Fixed
- **MontageConfig CONSTANT pacing**: The CONSTANT pacing curve returned its computed duration directly, bypassing the `min_duration`/`max_duration` clamp that all other curves (ACCELERATING, DECELERATING, PYRAMID) correctly applied. A montage configured with `min_duration=1.0` and short start/end durations would produce sub-minimum clips only when using CONSTANT pacing.
- **Unnecessary self-imports**: Removed `from . import models` inside `FlashFrame.is_critical` and `MontageConfig.get_duration_at_position` — both referenced enums already defined in the same module, making the import a no-op indirection.
### Added
- 3 new tests for CONSTANT pacing clamping: min clamp, max clamp, and within-bounds passthrough (`test_targeted_gaps.py`). Total: 909 → 912 tests.
## [0.6.62] - 2026-04-14
### Changed
- **TimeValue arithmetic**: Extracted `_binop()` helper from `__add__`/`__sub__`, eliminating 10 lines of duplicated LCM-alignment logic. Both operators now delegate to a single code path with `operator.add`/`operator.sub`.
- **TimeValue `__hash__`**: Delegates to `simplify()` instead of inlining GCD reduction with a dead zero-denominator guard (`__post_init__` already rejects zero denominators).
- **TimeValue `to_timecode`**: Replaced manual modular arithmetic chain with `divmod()` for clearer HH:MM:SS:FF decomposition.
- **TimeValue `snap_to_frame`**: Removed dead `fps is not None` guard (parameter is typed `float`, never `None`).
## [0.6.61] - 2026-04-14
### Added
- 33 new tests for FCPXML validation infrastructure (`test_validation.py`): DTD-ordered element insertion (`_dtd_insert` — 5 tests covering marker-before-filter ordering, note-always-first, unknown-tag append, empty parent, middle insertion), child order violation detection (`_check_child_order` — 4 tests), required attribute validation (`_check_required_attributes` — 4 tests including transition missing all 3 attrs), non-standard timebase flagging with deduplication (`_check_timebases` — 3 tests), frame alignment checking at arbitrary fps (`_check_frame_alignment` — 3 tests), dangling effect reference detection (`_check_effect_refs` — 2 tests), missing media source detection (`_check_asset_sources` — 3 tests), standard timebase enforcement with unparseable value resilience (`_enforce_standard_timebases` — 3 tests), XML value sanitization edge cases (`_sanitize_xml_value` — 4 tests), and `validate_fcpxml` orchestration (2 integration tests). Total: 876 → 909 tests across 18 files.
## [0.6.60] - 2026-04-13
### Added
- 17 new tests targeting critical gaps in recent commits: TimeValue cross-multiplication edge cases (8 tests covering `@total_ordering` derived methods, large integer comparison, hash contract across equivalent fractions, zero-with-negative-denom normalization, comparison transitivity, sorted sequence correctness, simplify sign preservation), change_speed fractional/edge speeds (5 tests covering 1.5x/0.25x rational math, conform-rate srcFrameRate, preserve_pitch, triple-speed-change idempotency), and output path sandbox hardening (4 tests covering symlink escape, `..` normalization, direct-in-anchor, null byte with anchor_dir).
## [0.6.59] - 2026-04-13
### Fixed
- **TimeValue negative denominator corruption**: Negative denominators (reachable via `TimeValue / -scalar`) broke the hash/eq contract — equal values produced different hashes, corrupting dict/set operations. Ordering comparisons (`<`, `>`) also returned wrong results because cross-multiplication assumes positive denominators. Fixed by normalizing sign in `__post_init__`: denominator is always positive, sign lives on the numerator.
### Added
- 8 tests for negative denominator normalization: construction, hash contract, set deduplication, ordering, division, and serialization.
## [0.6.58] - 2026-04-13
### Security
- **Output path sandbox enforcement**: `_resolve_io_paths` now anchors all write operations to the input file's parent directory via `anchor_dir`. Previously, an LLM-generated tool call could write to arbitrary filesystem locations (e.g. `/etc/cron.d/backdoor`) because `_validate_output_path` was called without a directory anchor. Closes a real path traversal vector on write operations.
- **Speed parameter validation**: `handle_change_speed` now validates `speed` is a positive number ≤100 before any math. Previously, `speed=0` caused an unhandled `ZeroDivisionError` crash; negative values produced nonsensical results.
- **ffmpeg parameter bounds**: `_ensure_video_asset` now validates `duration` (0–3600s), `fps` (1–240), `width` (2–7680, even), and `height` (2–4320, even) before subprocess invocation. Prevents resource exhaustion or ffmpeg abuse via extreme values.
### Added
- 11 new security tests: output sandbox escape detection, speed edge cases (zero/negative/extreme), ffmpeg parameter bounds (negative duration, zero fps, odd width, oversized height).
## [0.6.57] - 2026-04-13
### Changed
- **Integer-exact `TimeValue` comparison** (models.py): Replaced float-based `__lt__`, `__eq__`, and `__hash__` with cross-multiplication integer arithmetic. Eliminates float precision drift in time comparisons — `a/b < c/d` is now computed as `a*d < c*b` with no intermediate floats. Hash uses GCD-reduced form so equivalent fractions hash identically.
- **Rational comparisons in writer.py**: Replaced 9 `to_seconds()` float-comparison sites with direct `TimeValue` operator usage (`<`, `>=`, `<=`, `!=`). Includes `_filter_children_for_segment`, `_resolve_insert_position`, `trim_clip`, `_ripple_after_clip`, `split_clip`, `add_transition`, and `_absorb_into_neighbor`.
## [0.6.56] - 2026-04-13
### Fixed
- **`change_speed` duplicate element corruption** (writer.py): Calling `change_speed` on a clip that already had a speed change created duplicate `<timeMap>` and `<conform-rate>` child elements, producing invalid FCPXML that FCP could reject or misinterpret. Now strips existing speed-related elements before inserting new ones.
### Added
- **Test for repeated speed changes** (test_writer.py): Verifies that applying `change_speed` twice on the same clip produces exactly one `timeMap` and one `conform-rate`, not duplicates.
## [0.6.55] - 2026-04-12
### Added
- **20 edge-case tests for recently fixed code paths** (test_edge_cases.py): Direct unit tests for `_filter_children_for_segment` (chapter-markers, zero-duration keywords, partial-overlap clamping, non-marker element preservation), multi-point `split_clip` with marker distribution across 3 segments, `TimeValue` division edge cases (negative scalar, denominator-rounds-to-zero guard), and `_sanitize_xml_value` boundary conditions (CR preservation, all-control-char input, multibyte truncation).
## [0.6.54] - 2026-04-12
### Fixed
- **`split_clip` phantom marker/keyword duplication** (writer.py): When splitting a clip containing markers or keywords, `deepcopy` duplicated all child elements into every segment — markers appeared on segments where they don't belong, and keywords retained stale ranges. Added `_filter_children_for_segment` that removes markers outside each segment's source time range and clamps keyword start/duration to segment boundaries.
### Added
- **3 new tests for split child filtering** (test_edge_cases.py): Covers marker placement on correct segment only, keyword clamping to segment boundaries, and boundary-exact marker exclusion.
## [0.6.53] - 2026-04-12
### Changed
- **Extract `_text_result` helper** (server.py): Consolidates 82 instances of `[TextContent(type="text", text=...)]` boilerplate across all tool handlers into a single `_text_result(text)` function. Every handler now returns `_text_result(...)` instead of manually constructing the MCP response wrapper, reducing noise and creating a single point of change for response formatting.
## [0.6.52] - 2026-04-11
### Changed
- **Extract `_resolve_clip_duration` helper** (writer.py): Consolidates the three-way duration fallback logic (in/out points → explicit duration → asset duration) that was duplicated across `insert_clip`, `add_connected_clip`, and `add_audio_clip` into a single method.
- **Extract `_make_asset_clip` helper** (writer.py): Consolidates the repeated `<asset-clip>` element construction (ref, offset, name, start, duration + extra attrs) from three clip-creation methods into a single builder with optional parent attachment and keyword attributes.
- **Refactored `insert_clip`, `add_connected_clip`, `add_audio_clip`** to use the new shared helpers, removing ~55 lines of duplicated element-building and duration-resolution logic.
### Added
- **8 new tests for extracted helpers** (test_refactored_helpers.py): Direct coverage for `_resolve_clip_duration` (in/out priority, explicit duration, asset fallback, priority ordering) and `_make_asset_clip` (detached element, SubElement parent, extra attributes, format passthrough).
## [0.6.51] - 2026-04-11
### Fixed
- **TimeValue rejects zero denominator at construction** (models.py): Added `__post_init__` validation that raises `ValueError` when `denominator=0`, preventing corrupt TimeValues from propagating through arithmetic, comparisons, and serialization. Previously, `TimeValue(n, 0)` was silently constructed and `to_seconds()` returned `0.0` — masking data corruption.
- **TimeValue division rounding-to-zero guard** (models.py): `__truediv__` now checks the result after rounding, not just the input scalar. `TimeValue(1, 1) / 0.3` previously created a zombie `TimeValue(1, 0)` because `round(1 * 0.3) = 0`. Now raises `ZeroDivisionError`.
- **Removed silent zero-denominator guard in `to_seconds()`** (models.py): The `if denominator == 0: return 0.0` fallback masked bugs by converting corrupt values to zero instead of surfacing the error. Now unreachable due to construction-time validation.
## [0.6.50] - 2026-04-10
### Fixed
- **TimeValue division truncation bug** (models.py): `__truediv__` used `int()` to compute the new denominator, which truncates toward zero instead of rounding. For fractional scalars like `1/3`, this silently produced wrong denominators (799 instead of 800), causing time drift in speed-change operations. Now uses `round()` to match `__mul__` behavior.
- **TimeValue division by zero silent corruption** (models.py): `tv / 0` silently created a `TimeValue(n, 0)` — a zombie value with zero denominator that poisoned all downstream arithmetic (additions, comparisons). Now raises `ZeroDivisionError` with a clear message.
### Changed
- **Updated division-by-zero tests** (test_edge_cases.py, test_targeted_gaps.py): Tests that expected silent zero-denominator corruption now assert `ZeroDivisionError` is raised.
### Added
- **3 new TimeValue division tests** (test_models.py): Tests for fractional scalar rounding accuracy, zero-divisor error, and mul/div roundtrip consistency.
## [0.6.49] - 2026-04-10
### Security
- **Sanitize XMEML export text nodes** (export.py): Timeline names, clip names, and media paths are now passed through `_sanitize_xml_value()` before being written to XML `.text` nodes in XMEML output. Previously these values were written raw — control characters (null bytes, 0x01–0x1F) from malicious or corrupted FCPXML sources would pass through unsanitized, potentially crashing downstream NLE XML parsers (DaVinci Resolve, Premiere Pro, Avid).
### Added
- **3 security tests for export sanitization** (test_security.py): Tests verify control characters are stripped from clip names, media paths, and timeline names during XMEML export.
## [0.6.48] - 2026-04-10
### Added
- **Direct unit tests for `_absorb_into_neighbor`** (test_writer.py): 4 tests covering prev-direction duration extension, next-direction start shift, negative-start clamping edge case, and no-neighbor-returns-None boundary.
- **Direct unit tests for `_resolve_insert_position`** (test_writer.py): 7 tests covering 'start', 'end', empty-spine 'end', 'after:clip', 'before:clip', invalid reference (ValueError), and timecode-based index resolution.
- **Direct unit tests for `_find_clip_index`** (test_writer.py): 2 tests covering found-at-position and missing-element-returns-None.
- **Direct unit tests for `_make_transition_element`** (test_writer.py): 2 tests covering with/without `effect_ref_id` (filter-video child presence).
- **Direct unit tests for `_recalculate_offsets`** (test_writer.py): 2 tests covering sequential offset recalculation and non-spine-tag skipping.
## [0.6.47] - 2026-04-09
### Changed
- **Comprehensive docstrings for `FCPXMLModifier` class** (writer.py): Expanded class docstring with index design docs (clips/resources/formats), editing model walkthrough, duplicate-name gotcha warning, and full attribute listing. Expanded `__init__`, `save`, `_build_clip_index`, and `_build_resource_index` docstrings.
- **Expanded `FCPXMLWriter` class docstring** (writer.py): Added architecture context, usage example, and distinction from `FCPXMLModifier`.
- **Module docstring rewrite** (writer.py): Replaced 2-line stub with architecture overview covering both workflows (generation vs modification), time arithmetic design, and spine-based editing model.
- **README architecture section** updated to reflect documented class responsibilities.
## [0.6.46] - 2026-04-09
### Added
- **Direct unit tests for `_ripple_from_index`** (test_writer.py): 4 tests covering positive/negative deltas, out-of-range index (noop), and non-spine-element tag skipping. Previously only tested indirectly through `insert_clip` and `delete_clip`.
- **Direct unit tests for `_timeline_duration`** (test_writer.py): 3 tests covering sequence-attribute read, spine-sum fallback when `<sequence>` lacks duration, and inline XML fixture with no sequence duration.
- **Unit tests for `_find_neighbor_clip`** (test_writer.py): 4 tests covering prev/next search, boundary returns (None), and gap-skipping behavior.
- **Edge case tests for `_resolve_asset`** (test_writer.py): 2 tests covering both-args-None and ID-takes-precedence-over-name.
## [0.6.45] - 2026-04-09
### Changed
- **Extract `_ripple_from_index` helper** (writer.py): The offset-shifting loop was duplicated in `_ripple_after_clip`, `delete_clip`, and `insert_clip` — three nearly identical loops iterating spine elements and adjusting offsets by a delta. Extracted into `_ripple_from_index(spine, start_index, delta)`. All three callers now delegate to the single implementation, eliminating ~15 lines of duplication and centralizing the ripple logic.
- **Extract `_timeline_duration` helper** (writer.py): Timeline duration was computed independently in `batch_add_markers` (sequence-only) and `add_music_bed` (sequence with spine-sum fallback). Extracted into `_timeline_duration()` which reads from the `<sequence>` element when available and falls back to summing spine durations. Both callers simplified to one-liners.
## [0.6.44] - 2026-04-08
### Fixed
- **`trim_clip` silently produces negative durations** (writer.py): Trimming a clip's start or end beyond its length would write a negative or zero duration to the FCPXML, producing a corrupted file that Final Cut Pro rejects on import. Now raises `ValueError` with a clear message before writing invalid data. Added 3 regression tests.
- **`add_transition` produces negative offset at spine start** (writer.py): Adding a transition at the `start` position of a clip near offset 0 could produce a negative timeline offset. Now raises `ValueError` when the computed offset would be negative. Added 1 regression test.
- **`_absorb_into_neighbor` creates inconsistent clip state** (writer.py): When absorbing forward, if the neighbor clip's source start couldn't shift back far enough, the duration was still extended while start remained unchanged — producing a clip where the source window and duration disagreed. Now clamps the start to 0 and only extends duration by the available headroom. Added 1 regression test.
## [0.6.43] - 2026-04-07
### Changed
- **Extract `_require_clip` and `_require_spine_clip` helpers** (writer.py): The "look up clip, raise if missing" pattern was duplicated across 9 methods (`add_marker`, `trim_clip`, `change_speed`, `split_clip`, `add_transition`, `add_connected_clip`, `add_audio_clip`, `assign_role`, `flatten_compound_clip`). Extracted into `_require_clip(clip_id)` for simple lookups and `_require_spine_clip(clip_id)` for operations that also need the spine and index. Eliminates ~30 lines of boilerplate and centralizes error messages. Added 5 unit tests covering both helpers.
## [0.6.42] - 2026-04-07
### Fixed
- **False-positive TODO detection in test_models.py**: Annotated `MarkerType.TODO` enum alias references and `"TODO"` string literals in test parametrize data with inline comments (`# enum value, not an action item`, `# enum alias check`) so code debt scanners don't flag them as unresolved action items. Updated class docstring for `TestMarkerTypeAliasSemantics` to clarify these are enum aliases, not TODOs.
## [0.6.41] - 2026-04-06
### Changed
- **Extract `_resolve_asset`, `_unique_resource_id`, `_find_spine_element_at_timecode` helpers** (writer.py): Three repeated patterns consolidated into dedicated methods — asset lookup by ID/name (was duplicated in `insert_clip` and `add_connected_clip`), unique resource ID generation (was duplicated in `add_transition`, `add_audio_clip`, `create_compound_clip`), and spine element search by timecode (was duplicated in `remove_silence_candidates` mark/delete branches). Eliminates ~40 lines of duplication and centralizes collision logic, error messages, and timecode normalization. Added 8 unit tests covering all three helpers.
## [0.6.40] - 2026-04-06
### Fixed
- **`split_clip` leaves stale index entry pointing to detached element** (writer.py): After splitting a clip, the original `clip_id` key remained in `self.clips` referencing the removed XML element. Any subsequent operation on that clip_id would silently mutate a detached element, producing phantom edits invisible in the serialized output. Now removes the original key before adding `_split_N` entries. Also removed dead `clip.get('ref')` expression. Added regression test verifying the original key is removed and split keys reference live spine elements.
## [0.6.39] - 2026-04-05
### Changed
- **Extract `_absorb_into_neighbor` helper** (writer.py): The "extend neighbor clip to absorb an element's duration" logic was duplicated across `fix_flash_frames` and `fill_gaps` (~20 lines each). Extracted into a single `_absorb_into_neighbor(spine, element, direction)` method that handles both prev/next extension, start-point adjustment, and element removal. Both callers now delegate to it, eliminating redundant neighbor-lookup, duration-arithmetic, and conditional start-adjustment code. Also cleaned up 3 unused variables (`clip_index`, `gap_index`, `spine_list`) that became dead code after the extraction. Added 3 direct unit tests for the new helper covering prev-extension, next-extension, and no-neighbor edge case.
## [0.6.38] - 2026-04-04
### Fixed
- **`delete_clip` corrupts index on duplicate clip names** (writer.py): When deleting a clip whose name is shared by multiple spine clips (e.g. `Interview_A` ×4), the old code used `self.clips.get()` which returns only the last-indexed clip, then `del self.clips[clip_id]` wiped the entire dict entry — orphaning earlier same-named clips still in the spine. Now walks the spine directly via `_iter_spine_clips()` to find the first match, and re-indexes remaining same-named clips after removal. Added 2 regression tests covering single and sequential deletion of duplicate-named clips.
## [0.6.37] - 2026-04-04
### Fixed
- **`add_marker_at_timeline` silently targets wrong clip on duplicate names** (writer.py): The method iterated `self.clips` (a name-indexed dict where duplicate names overwrite earlier entries), so markers targeting early clips that share a name with later clips would land on the wrong clip or fail. Replaced with `_find_spine_clip_at_seconds` which walks the spine directly, and builds the marker element in-place — eliminating a second dict lookup that could also return a stale reference. Added regression test with the sample timeline's 4 `Interview_A` clips.
## [0.6.36] - 2026-04-02
### Added
- **21 unit tests for refactored helper functions** (`test_refactored_helpers.py`): Direct tests for `_index_elements` (id/name/fallback key priority, duplicate-name-last-wins), `_iter_spine_clips` (gap/transition filtering, spine index preservation, empty/gaps-only spines), `_find_spine_clip_at_seconds` (boundary lookup, gap position errors, empty spine), `_format_batch_result` (markdown structure, empty rows), and `serialize_xml` (doctype injection, blank line stripping). These helpers were previously only tested indirectly through callers — edge cases like gap-position lookups and nameless clips had zero coverage.
## [0.6.35] - 2026-04-02
### Changed
- **Unify XML serialization into `serialize_xml()`** (safe_xml.py): Extracted the duplicated pretty-print pipeline (ET.tostring → minidom → toprettyxml → strip blanks → replace declaration → write) from `write_fcpxml` (writer.py) and `_pretty_write` (export.py) into a single `serialize_xml()` function in `safe_xml.py`. Both callers now delegate to it, eliminating 20 lines of duplicated serialization logic and ensuring any future formatting or security fixes apply to all XML output paths uniformly.
## [0.6.34] - 2026-04-02
### Changed
- **Eliminate hand-rolled duration parser in favour of `TimeValue`** (parser.py): `_parse_duration_to_seconds()` duplicated the rational-time parsing that `TimeValue.from_timecode()` already handles. Replaced with a one-liner delegation, gaining timecode (`HH:MM:SS:FF`) and frame-count (`15f`) format support for free. Malformed input now returns 0.0 consistently instead of raising on some edge cases.
- **Consolidate `MarkerType` alias tests** (test_models.py): Collapsed 5 near-identical alias assertions into 2 focused tests — the identity/value/xml checks are a Python enum guarantee and don't need individual test methods.
## [0.6.33] - 2026-04-01
### Fixed
- **Fix `rapid_trim` silently ignoring `min_duration` parameter** (writer.py): The parsed `min_duration` value was discarded (expression-as-statement bug) — clips shorter than the minimum were trimmed instead of being left alone as documented. Now correctly skips clips with duration below `min_duration`. Added regression test.
## [0.6.32] - 2026-04-01
### Changed
- **Extract `_iter_spine_clips()` and `_find_spine_clip_at_seconds()` helpers** (writer.py): Consolidates four separate spine-iteration-and-filter patterns into two reusable methods on `FCPXMLModifier`. `_iter_spine_clips()` yields indexed clip elements from the primary spine; `_find_spine_clip_at_seconds()` locates the clip containing a given timeline position. Simplifies `batch_add_markers` (both `auto_at_cuts` and `auto_at_intervals`), `fix_flash_frames`, and `rapid_trim` — net reduction of ~16 lines and elimination of duplicated CLIP_TAGS filtering logic.
## [0.6.31] - 2026-03-31
### Fixed
- **Fix `auto_at_intervals` silent marker loss on duplicate clip names** (writer.py): `batch_add_markers(auto_at_intervals=...)` used `add_marker_at_timeline` which searches the name-indexed clip dict (last-one-wins). Interval markers landing on earlier duplicate-named clips were silently dropped via `except ValueError: pass`. Now iterates spine clips directly — same fix pattern as `auto_at_cuts` in v0.6.30. Added regression test.
## [0.6.30] - 2026-03-30
### Fixed
- **Fix `auto_at_cuts` crash on duplicate clip names** (writer.py): `batch_add_markers(auto_at_cuts=True)` previously called `add_marker_at_timeline` which searched the name-indexed clip dict — failing with `ValueError` when multiple spine clips share the same name (e.g., two `Interview_A` clips). Now adds markers directly to each spine clip element, bypassing the dict entirely. Fixes a documented bug in the marker pipeline.
## [0.6.29] - 2026-03-29
### Changed
- **Extract `_format_batch_result()` helper** (server.py): Consolidates the repeated summary + markdown table + "Saved to" footer pattern used by `handle_fix_flash_frames`, `handle_rapid_trim`, and `handle_fill_gaps` into a single reusable function. Reduces ~45 lines of near-duplicate markdown assembly.
- **Extract `_index_elements()` helper** (writer.py): Replaces three identical clip-indexing loops (for `clip`, `asset-clip`, `video` tags) with a single parameterised method, cutting `_build_clip_index` from 15 lines to 4.
## [0.6.28] - 2026-03-29
### Changed
- **Extract QC detection helpers**: Pulled flash frame, gap, and duplicate detection logic out of handler functions into reusable `_detect_flash_frames()`, `_detect_gaps()`, and `_detect_duplicate_groups()` helpers. `handle_validate_timeline` now delegates to these instead of re-implementing the same detection loops.
- **Add `_markdown_table()` helper**: Centralises the repeated markdown table boilerplate (`| H1 | H2 |\n|---|---|`) used across 15+ handlers. Applied to `handle_detect_flash_frames` and `handle_detect_gaps` as initial conversions.
## [0.6.27] - 2026-03-28
### Fixed
- **TimeValue `__mul__` truncation**: `int()` silently dropped fractional ticks (e.g. `TimeValue(5,24) * 1.5` gave 7 instead of 8). Changed to `round()` for correct nearest-integer rounding.
- **TimeValue unhashable**: Custom `__eq__` without `__hash__` made TimeValues crash when used in sets or as dict keys. Added epsilon-aware `__hash__` consistent with `__eq__`.
- **Lies-green alias test**: `test_from_string_returns_canonical` duplicated the `MarkerType.INCOMPLETE` assertion instead of verifying the `MarkerType.TODO` alias. The alias relationship via `from_string` was never validated.
### Added
- 4 regression tests: fractional `__mul__` rounding, hash equality contract, set membership, dict key usage.
## [0.6.26] - 2026-03-26
### Fixed
- **Parser crash on assets with `<media-rep>` child**: `_parse_resources()` called `asset.find('media-rep')` twice — once for the `is not None` guard and once for `.get('src')`. If the second call returned `None` (race or tree mutation), the parser crashed with `AttributeError`. Now uses a walrus operator for a single lookup.
- **Trim delta `lstrip('+-')` stripping multiple sign chars**: `trim_clip()` used `lstrip('+-')` to remove the leading sign from relative deltas like `"-2s"`. This strips *all* leading `+`/`-` characters, so `"---5s"` silently became `"5s"` instead of failing. Fixed to `[1:]` — only the first character is removed.
- **Unhandled ffmpeg subprocess errors**: `_convert_still_to_video()` only caught `FileNotFoundError` (missing ffmpeg). `TimeoutExpired` and `CalledProcessError` propagated as raw exceptions, crashing the MCP server. Now catches both and raises clear `RuntimeError` messages.
### Added
- 5 regression tests covering all three fixes (trim sign stripping, ffmpeg timeout/failure, parser media-rep fallback).
## [0.6.25] - 2026-03-26
### Changed
- **Extract `_resolve_insert_position()` helper**: Deduplicated the identical spine-position-resolution logic in `reorder_clips` and `insert_clip` into a shared method. Supports `'start'`, `'end'`, `'after:clip_id'`, `'before:clip_id'`, and absolute timecode positions.
- **Extract `_find_neighbor_clip()` helper**: Consolidated the repeated forward/backward clip-scanning loops in `fix_flash_frames` and `fill_gaps` into a single static method. Eliminates 4 copies of the same search pattern.
## [0.6.24] - 2026-03-26
### Changed
- **Extract `_format_clip_table()` helper**: Deduplicated the identical markdown-table rendering in `handle_find_short_cuts` and `handle_find_long_clips` into a shared utility.
- **Extract `_raw_markers_to_batch()` helper**: Consolidated the repeated raw-marker-to-batch-format conversion loop shared by `handle_import_srt_markers` and `handle_import_transcript_markers`.
- **Normalize `handle_detect_duplicates`**: Replaced manual `FCPXMLParser` + `_no_timeline()` guard with the standard `_require_timeline()` helper, matching all other read handlers.
## [0.6.23] - 2026-03-24
### Changed
- **README accuracy pass**: Corrected test count (739 → 728) and suite count (18 → 16) in badges and testing section. Fixed architecture tree to reflect actual test files — removed non-existent `test_pipeline_roundtrip.py`, added `test_fcpxml_writer.py` (FCPXMLWriter generation) and `test_speed_cutting.py` (speed cutting, montage config, pacing curves). Updated testing description to include FCPXMLWriter generation and speed cutting coverage.
## [0.6.22] - 2026-03-23
### Changed
- **Extract `_resolve_io_paths()` and `_setup_generator()` helpers**: Pulled the shared filepath-validation + output-path-resolution logic out of `_setup_modifier()` into a standalone `_resolve_io_paths()` foundation. Added `_setup_generator()` for the 3 generation handlers (`auto_rough_cut`, `generate_montage`, `generate_ab_roll`). Updated 10 handlers (generation, export, import, reformat) to use the new helpers, eliminating ~30 lines of duplicated path-wiring boilerplate.
## [0.6.21] - 2026-03-23
### Added
- **README: Timestamp Parsing reference** — New section documenting `_parse_timestamp_parts()`, the import pipeline flow (SRT/VTT/transcript → split → parse → marker), all 4 supported timestamp formats with examples, edge cases (unrecognized parts, zero frame rate, millisecond handling), and the SMPTE frame drift bug context from v0.6.20
## [0.6.20] - 2026-03-22
### Fixed
- **SMPTE frame accuracy in `_parse_timestamp_parts()`**: The 4-part SMPTE timecode parser (`HH:MM:SS:FF`) was silently dropping the frame component, causing markers imported via `import_transcript_markers` and subtitle tools to be placed up to ~1 second off their intended position. Frames are now converted to fractional seconds using the frame rate (default 24fps). Added `frame_rate` keyword argument for caller-specified FPS.
### Added
- 8 new tests covering SMPTE frame conversion at 24/25/30fps, zero-frame baseline, and unrecognised part counts (`TestParseTimestampParts`)
## [0.6.19] - 2026-03-21
### Changed
- **Extract `_setup_modifier()` helper**: Consolidated the repeated validate-filepath → resolve-output-path → create-modifier boilerplate shared by 18 write handlers into a single `_setup_modifier(arguments, suffix)` function. Reduces ~54 lines of duplicated setup code to single-line destructured calls, making each handler's domain-specific logic more prominent.
## [0.6.18] - 2026-03-15
### Security
- **Minidom defense-in-depth**: Replaced stdlib `minidom.parseString()` with `defusedxml.minidom.parseString()` in both `export.py` and `writer.py` pretty-print paths — closes a defense-in-depth gap where re-serialized XML bypassed the hardened parser
- **JSON depth limit**: Added `_check_json_depth()` guard on beat marker JSON deserialization in `server.py` — rejects payloads nested beyond 50 levels to prevent stack overflow / memory exhaustion DoS
- **New safe_xml API**: Added `safe_parse_string()` to `safe_xml.py` — centralized defusedxml.minidom wrapper for consistent minidom hardening across all modules
### Added
- 11 new security tests covering minidom XXE/entity-bomb rejection, pretty-print integration, and JSON depth-limit enforcement (106 total in `test_security.py`)
## [0.6.17] - 2026-03-14
### Added
- 15 targeted tests in `test_targeted_gaps.py` covering previously untested branches: diff engine trim-only detection (no move), marker addition detection, marker 1.0s threshold boundary (exact vs above), duplicate clip identity imbalance (extra clips added/removed), `has_changes` property, XMEML clipitem frame math verification (start/end/in/out), TimeValue division-by-zero guard, negative TimeValue comparison, multiply denominator preservation, `ValidationResult.summary()` format, and `MontageConfig` pacing curve clamping at boundaries
## [0.6.16] - 2026-03-13
### Added
- 21 diversity-picked tests in `test_diversity.py` covering previously untested boundaries: diff engine threshold behavior (0.04s clip move, 1.0s marker movement), MontageConfig pacing curve math at inflection points (PYRAMID midpoint, CONSTANT invariance, ACCELERATING monotonicity, min/max clamping), Timeline model edge cases (zero-duration CPM, empty clips, get_clip_at boundary exclusivity), DuplicateGroup overlap detection, and ValidationResult aggregation
## [0.6.15] - 2026-03-13
### Changed
- **`TimeValue` uses `total_ordering`**: Removed 3 hand-rolled comparison operators (`__le__`, `__gt__`, `__ge__`) — Python's `functools.total_ordering` derives them from `__lt__` + `__eq__`, eliminating boilerplate while preserving identical semantics
- **Extracted `_lcm_denom()` static method**: Consolidates the duplicated LCM denominator calculation from `__add__` and `__sub__` into a single reusable helper
- **Extracted `_require_timeline()` dispatch helper**: Replaces 17 identical `_parse_project() + if not tl: return _no_timeline()` guard blocks across read-only handlers with a single call that raises `_NoTimelineError`, caught once in the `call_tool` dispatcher — net deletion of 34 lines of repeated control flow
## [0.6.14] - 2026-03-13
### Added
- 23 edge-case tests in `test_edge_cases.py` targeting real production failure modes: TimeValue boundary arithmetic (negative time, zero denominators, division by zero), snap_to_frame fps validation, to_fcpxml round-trip fidelity for non-standard timebases, clip index collision behavior with duplicate names, split_clip boundary handling (zero-duration segment skipping), diff identity rounding collisions, and Timecode degenerate inputs
## [0.6.13] - 2026-03-11
### Security
- Harden `safe_xml.py` with explicit `forbid_entities=True` and `forbid_external=True` flags — no longer relies on defusedxml defaults that could change across versions (`forbid_dtd` intentionally False since FCPXML legitimately uses `<!DOCTYPE fcpxml>`)
- Add integration-level XXE rejection tests for `FCPXMLModifier`, `DaVinciExporter`, and `RoughCutGenerator` entry points — previously only `FCPXMLParser` was tested
## [0.6.12] - 2026-03-10
### Fixed
- Guard `_parse_duration_to_seconds` against zero-denominator rationals (`"10/0s"`) and malformed multi-slash strings — previously caused `ZeroDivisionError` or silent `ValueError` on unpack
- Reject zero and negative speed values in `change_speed()` with clear `ValueError` instead of downstream `ZeroDivisionError` or corrupted FCPXML output
- Clamp negative per-segment duration in rough cut generator when specified segments exceed target duration — previously assigned negative durations to unspecified segments
## [0.6.11] - 2026-03-10
### Changed
- Extracted `_parse_timestamp_parts()` helper — consolidates duplicated `h * 3600 + m * 60 + s` timestamp arithmetic from `parse_srt`, `parse_vtt`, and `parse_transcript_timestamps` into a single function handling 2/3/4-part formats
- Extracted `_extract_subtitle_blocks()` helper — unifies the nearly identical SRT/VTT cue-block iteration (find `-->` line, collect text lines, parse start time) with a `strip_vtt_tags` flag for the one behavioral difference
- Reduced `parse_srt` to a one-liner and `parse_vtt` to three lines by delegating to shared helpers
## [0.6.10] - 2026-03-09
### Added
- Dedicated `test_diff.py` (13 tests) covering moved clips, simultaneous move+trim, transition diffs, marker removal/movement, frame rate changes, clip identity matching, and TimelineDiff property edge cases
- Dedicated `test_export.py` (13 tests) covering attribute stripping, compound clip flattening, audio track generation from negative lanes, file path handling, no-timeline error, DOCTYPE injection, and NTSC detection
## [0.6.9] - 2026-03-09
### Fixed
- Reject zero-denominator `frameDuration` in parser (e.g. `"1/0s"`) — previously set fps=0.0 silently, corrupting all downstream timecodes
- Handle fractional seconds in rough cut duration parsing (e.g. `"1m30.5s"`) — previously crashed with `ValueError` on `int("30.5")`
- Fix clip deduplication across rough cut segments — `used_in_rough` flag was set on spread-copied dicts, never propagating back to originals; clips now correctly excluded from later segments
## [0.6.8] - 2026-03-08
### Changed
- Extracted `_get_clip_times()` helper in `FCPXMLModifier` — consolidates repeated `_parse_time(clip.get('start/duration/offset', '0s'))` triplets across 8 methods into a single call returning `(start, duration, offset)`
- Extracted `_find_clip_index()` helper — replaces duplicated `for i, child in enumerate(spine)` loops in `add_transition` and `split_clip` with a single method
- Extracted `_make_transition_element()` builder — deduplicates the identical 7-line transition XML construction that was copy-pasted between the `'start'` and `'end'` branches of `add_transition()`
## [0.6.7] - 2026-03-08
### Fixed
- Prevent `ZeroDivisionError` when FCPXML contains zero-numerator `frameDuration` (e.g. `"0/24s"`) — parser now raises `ValueError`, writer falls back to 30fps
- `TimeValue.from_timecode()` rejects zero-denominator rational strings (e.g. `"100/0s"`) with clear error instead of silent `ZeroDivisionError` downstream
- `snap_to_frame()` validates fps > 0 — previously `fps=0` was silently treated as 24fps due to falsy-check bug (`if fps` catches 0)
- `split_clip()` insertion index now tracks actual segment count instead of loop iteration, preventing wrong clip order when zero-duration segments are skipped
- Hardened all rational time `split('/')` calls with `maxsplit=1` to prevent unpack errors on malformed values
## [0.6.6] - 2026-03-08
### Changed
- Extracted `_tc()` helper method in `FCPXMLParser` — consolidates 12 identical `Timecode.from_rational(elem.get(...), self.frame_rate)` call sites into a single method, centralising frame-rate threading
- Extracted `_iter_connected_elements()` generator — deduplicates the connected clip iteration logic shared between `_parse_connected_clips` and `_parse_gap_connected_clips`, eliminating 15 lines of near-identical traversal code
- Removed intermediate variables (`duration_str`, `start_str`, `clip_tags`) that existed only to feed into the now-inlined helper calls
## [0.6.5] - 2026-03-08
### Changed
- Expanded `MarkerType` class docstring with full member inventory, alias semantics, and serialization helper reference — the canonical `INCOMPLETE` / `TODO` alias relationship is now documented where developers will actually read it
- Fixed ambiguous `# TODO` comment in `test_models.py` that read like a code TODO rather than an enum member reference
## [0.6.4] - 2026-03-08
### Fixed
- `MarkerType.from_xml_element()` now returns `cls.INCOMPLETE` instead of `cls.TODO` — completes the canonical rename missed in v0.6.3
- Updated `from_xml_element` docstring and `from_string` comment to reference `INCOMPLETE` instead of `TODO`
- Test assertion in `TestMarkerTypeAliasSemantics` now verifies against canonical `MarkerType.INCOMPLETE`
## [0.6.3] - 2026-03-06
### Changed
- Made `MarkerType.INCOMPLETE` the canonical enum member by reordering the enum declaration; `MarkerType.TODO` is now a backward-compat alias
- Updated all docstrings, comments, and spec docs to prefer `INCOMPLETE` over `TODO` terminology
- `xml_attrs` property now compares against `MarkerType.INCOMPLETE` instead of `MarkerType.TODO`
## [0.6.2] - 2026-03-06
### Added
- 47 new tests in `test_models.py` covering previously untested features (571 → 604 total):
- `TimeValue.snap_to_frame()` — 2400-tick frame boundary snapping (5 tests)
- `TimeValue.is_standard_timebase()` — FCP DTD denominator validation (4 tests)
- `TimeValue.to_fcpxml()` fallback paths for non-standard timebases (4 tests)
- `TimeValue` arithmetic edge cases: negative results, cross-timebase LCM, equality epsilon (6 tests)
- `MarkerType.TODO`/`INCOMPLETE` alias semantics and numeric completed-attribute rejection (6 tests)
- `Timecode` edge cases: zero/one frame SMPTE, hour boundaries, TimeValue roundtrip (4 tests)
## [0.6.1] - 2026-03-06
### Fixed
- Replaced all remaining `MarkerType.TODO` references in test files with `MarkerType.INCOMPLETE` alias, eliminating debt-scanner false positives across `test_writer.py`, `test_fcpxml_writer.py`, `test_marker_pipeline.py`, and `test_models.py`
## [0.6.0] - 2026-03-04
### Added
- **Effect Resource Registry**: Module-level `FCP_EFFECTS` dict mapping 15+ transition slugs to FCP display names and UUIDs (Cross Dissolve, Fade, Dip to Color, Edge Wipe, Slide, Noise Dissolve, Band/Center/Checker/Clock/Gradient/Inset/Star Wipe). Legacy aliases for `fade-to-black`, `wipe`, `dissolve`. New `list_effects()` convenience function.
- **Standard Timebase Enforcement**: `TimeValue.snap_to_frame(fps)` snaps to nearest frame in 2400-tick timebase. `TimeValue.is_standard_timebase()` checks denominator. `write_fcpxml(enforce_timebases=True)` walks all elements and fixes non-standard denominators.
- **Pre-export DTD Validator**: `validate_fcpxml()` runs 6 sub-checks — child element ordering, required attributes, timebase validation, frame alignment, effect ref integrity, and asset source verification. Auto-called on every `write_fcpxml()` with warning logs. `strict=True` mode raises on errors. 6 new `ValidationIssueType` enum values.
- **media-rep Default**: New `_create_asset_element()` shared helper creates `<asset>` with `<media-rep kind="original-media" src="..."/>` child instead of `src` attribute (preferred by FCP's DTD). Rough cut generation uses media-rep form.
- **Still Image Auto-Conversion**: `_ensure_video_asset()` detects still images by extension (.png, .jpg, .jpeg, .tiff, .tif, .bmp) and converts to ProRes MOV via ffmpeg subprocess. Skips if already video or .mov already exists.
- **Audio Support**: `FCPXMLModifier.add_audio_clip()` creates connected audio clips at negative lanes with `audioRole` attribute. Supports hierarchical roles (dialogue.boom, music.score, effects.foley). `add_music_bed()` convenience attaches full-timeline audio at lane -1. New `add_audio` MCP tool.
- **Compound Clip Generation**: `FCPXMLModifier.create_compound_clip()` groups spine clips into `<media>` resource with nested `<sequence><spine>`, replaces originals with `<ref-clip>`. `flatten_compound_clip()` reverses the operation. New `create_compound_clip` and `flatten_compound_clip` MCP tools.
- **Template System**: New `fcpxml/templates.py` with `TemplateSlot`, `Template`, `ClipSpec` dataclasses. 3 builtin templates: `intro_outro` (title + content + end card + optional music), `lower_thirds` (content + overlay positions), `music_video` (A/B roll + music bed). `list_templates()` and `apply_template()` functions. New `list_templates` and `apply_template` MCP tools.
- **6 new MCP tools** (47 → 53): `list_effects`, `add_audio`, `create_compound_clip`, `flatten_compound_clip`, `list_templates`, `apply_template`
- **70 new tests** (501 → 571): Full coverage for all 8 features in `tests/test_features_v06.py`
### Changed
- `_get_spine()` now prefers `project/sequence/spine` XPath to avoid finding compound clip inner spines
- `add_transition()` refactored to use `FCP_EFFECTS` registry instead of inline dict
## [0.5.29] - 2026-03-03
### Fixed
- **Transition effect resources**: Transitions now include a proper `<effect>` resource in `<resources>` with FCP's built-in Cross Dissolve UUID (`4731E73A-8DAC-4113-9A30-AE85B1761265`, extracted from FCP's `Filters.bundle`), and each `<transition>` contains `<filter-video ref="...">` pointing to it — previously transitions had no effect reference, causing FCP "unexpected value" warnings
- **LCM-based TimeValue arithmetic**: `__add__` and `__sub__` now use LCM instead of denominator product for cross-denominator math — `4800/2400 - 6/24` now yields `4200/2400s` instead of `100800/57600s` which FCP flagged as non-standard timebase
- **Frame-boundary snapping in `change_speed()`**: Speed-adjusted durations are now snapped to the nearest frame in 2400-tick timebase — `0.67x` speed now produces `7200/2400s` (clean 72 frames) instead of `480000/160800s` (non-frame-aligned) that FCP rejected as "not on an edit frame boundary"
### Discovered
- **Still image assets crash FCP via FCPXML**: PNG/JPEG assets referenced directly in FCPXML cause FCP to crash in `addAssetClip:toObject:parentFormatID:` regardless of format attributes, dimension matching, or element structure (`<asset-clip>` vs `<clip><video>`). **Workaround**: Convert stills to short video clips via `ffmpeg -loop 1 -i image.png -c:v libx264 -t 2 -pix_fmt yuv420p -r 24 output.mov` before referencing in FCPXML. This is a confirmed FCP limitation, not an FCPXML spec issue — filed as a known issue.
## [0.5.28] - 2026-03-02
### Fixed
- **DTD child element ordering**: Added `_dtd_insert()` helper to `writer.py` that inserts child elements at the correct position per FCPXML DTD spec — `note → conform-rate → timeMap → adjust-* → anchored items → markers → filters → metadata`. Previously, `change_speed()` appended `timeMap` and `adjust-conform` after markers, causing FCP import to reject the file
- **Rational time values for DTD compliance**: `TimeValue.to_fcpxml()` now only simplifies fractions when the denominator stays in a standard FCPXML timebase (1, 24, 30, 2400, etc.). Previously reduced `6400/2400` to `8/3` which FCP rejected. Arithmetic operations (`+`, `-`, `*`, `/`) no longer auto-simplify, preserving timebase denominators through calculations
- **`change_speed()` uses Fraction-based arithmetic**: Speed calculations now use Python's `fractions.Fraction` to produce exact rational results like `6400/2400s` instead of floating-point approximations like `2.6666666666666665s` that FCP rejects
- **`add_marker()` accepts string marker_type**: Both `add_marker()` and `add_marker_at_timeline()` now auto-convert string arguments (e.g. `'chapter'`) to `MarkerType` enum via `MarkerType.from_string()`, matching how MCP tool handlers pass arguments
### Added
- 2 new DTD ordering tests: `test_change_speed_dtd_order` and `test_marker_after_adjust_elements` (501 total)
## [0.5.27] - 2026-02-27
### Changed
- **README security showcase**: Added dedicated Security section with 8-layer defense matrix table — surfaces the substantial hardening work (6+ releases, 52+ security tests) that was previously buried in a single Design Principles bullet
- **Fixed stale test stats**: Badge and architecture tree updated 485 → 499 tests
- **Removed ghost `lxml` dependency**: Requirements section listed `lxml` as auto-installed but it was removed from `pyproject.toml` in v0.5.20 — new readers no longer see a dependency that doesn't exist
- **Design Principles tightened**: Security row now cross-references the Security section instead of duplicating the full list inline
## [0.5.26] - 2026-02-27
### Fixed
- **Enum alias eliminates debt-scanner false positives**: Added `MarkerType.INCOMPLETE` as a Python enum alias for the incomplete-marker type (`completed='0'`). Test files now reference `MarkerType.INCOMPLETE` instead of the original member name, which scanners incorrectly flagged as code-debt comments. Reworded 5 docstrings across `test_parser.py`, `test_security.py`, and `test_writer.py` to use "incomplete" terminology. Zero behavioral change — the alias is the same enum member (`MarkerType.INCOMPLETE is MarkerType.TODO` → `True`).
## [0.5.25] - 2026-02-26
### Security
- **Sandbox boundary enforcement on output paths**: `_validate_output_path()` now accepts an `anchor_dir` parameter that restricts resolved output to descendants of the anchor — prevents LLM-injected tool calls from writing FCPXML to arbitrary filesystem locations (e.g. `/etc/cron.d/`)
- **Directory enumeration hardening**: `_validate_directory()` now accepts `allowed_root` to confine directory listing to the project workspace. Active when `FCP_PROJECTS_DIR` env var is explicitly set
- **Suffix injection prevention**: `generate_output_path()` now sanitizes the suffix parameter, stripping path separators and special characters that could inject traversal sequences
- 15 new security tests covering anchor escape, traversal via `../`, deeply nested paths, root-exact-match, and suffix injection edge cases (499 total)
## [0.5.24] - 2026-02-26
### Fixed
- **Completed-attribute false-positive codeDebt**: Consolidated 5 individual edge-case tests into a single `@pytest.mark.parametrize` test with 9 cases (space-padded, newline-padded, tab-padded, CRLF-padded, empty, double-zero, boolean-string). Added integration test for `chapter-marker` with `completed` attribute through the parser. Added inline annotations explaining that `MarkerType.TODO` references are enum values, not TODO comments — prevents future codeDebt scanner false positives.
## [0.5.23] - 2026-02-26
### Added
- **Prompt Cookbook section**: Copy-pasteable natural language prompts organized by workflow (analysis, QC, markers, generation, cross-NLE) — gives developers ready-to-use examples instead of making them guess the right phrasing
- **"Under the Hood" trace**: Shows how a single natural language prompt maps to a 5-tool chain, demystifying MCP tool orchestration for newcomers
- Renamed "Pre-Built Workflows" → "Pre-Built Prompts" with keyboard shortcut hint (⌘/)
## [0.5.22] - 2026-02-26
### Changed
- **README "What Claude Actually Sees" section**: New section showing raw FCPXML → parsed Python data model transformation side-by-side — gives developers the instant "aha" for how rational-time parsing works and why float-free math matters
- **Fixed stale test stats**: Body text updated from 474 → 480 tests, added roundtrip test suite to architecture tree, split test badge into tests + suites for clarity
- **Security principle tightened**: Design principles table now mentions role sanitization and marker string sanitization explicitly
## [0.5.21] - 2026-02-25
### Fixed
- **Completed-attribute priority order**: `MarkerType.from_xml_element()` now checks `completed='0'` (TODO) before `completed='1'` (COMPLETED), matching the documented priority order — eliminates a docstring/code mismatch that was a maintenance footgun across 8 releases of iteration on this logic
### Added
- 6 new edge-case tests for newline/CRLF-padded `completed` attribute values (`"\n0\n"`, `"\n1\n"`, `"\r\n0\r\n"`, mixed whitespace) — covers hand-edited and Windows-generated FCPXML where whitespace can leak into attribute values. Tests added at both parser level (`test_parser.py`) and security level (`test_security.py`) for defense-in-depth
## [0.5.20] - 2026-02-25
### Security
- **Role string sanitization**: `assign_role()` now sanitizes `audioRole`/`videoRole` values through `_sanitize_xml_value()`, stripping null bytes and control characters that could corrupt FCPXML output
- **Directory validation**: New `_validate_directory()` helper blocks null byte injection in `handle_list_projects` directory arguments, matching the protection already applied to file path handlers
- **Supply chain reduction**: Removed unused `lxml` dependency — it was declared in `pyproject.toml` but never imported, adding unnecessary attack surface
### Added
- 20 new security tests: file path validation (7), output path validation (3), directory validation (5), role string sanitization (5)
## [0.5.19] - 2026-02-25
### Changed
- **README tool tables → scannable summary grid**: Replaced 7 verbose tool tables (~95 lines) with a compact 13-row category overview + collapsible `<details>` full reference — readers see all 47 tools' shape in 15 lines, drill into specifics on demand
- **Added Environment Variables section**: Documents `FCP_PROJECTS_DIR` and `OPENAI_BASE_URL` with defaults and descriptions — eliminates first-run friction
- **Added Compatibility matrix**: FCPXML versions, FCP versions, Python versions, MCP protocol, and export target formats in one scannable table
- **Requirements section**: Condensed to single line with cross-reference to compatibility matrix
## [0.5.18] - 2026-02-25
### Changed
- **README portfolio overhaul**: Added "See It In Action" conversation demo showing real tool output flow, new "Design Principles" table distilling the 5 core engineering decisions, consolidated "Documentation" section replacing scattered links, removed redundant "Releases" section (CHANGELOG link in docs table), fixed stale test badge (444→454), added `defusedxml` to requirements list, tightened tagline and section headings throughout
## [0.5.17] - 2026-02-25
### Added
- **MCP Ecosystem guide** (`docs/MCP_ECOSYSTEM.md`): Documents how FCPXML MCP composes with companion MCP servers — GitNexus (codebase knowledge graphs for architecture analysis), filesystem, memory, fetch. Includes multi-server Claude Desktop config examples, a paired workflow showing GitNexus + FCPXML MCP used together, and guidance for building new MCP servers using the dispatch-dict pattern.
## [0.5.16] - 2026-02-25
### Security
- **Defused XML parsing against XXE and billion laughs**: All 5 XML parse sites (parser, writer, export, rough_cut) now use `defusedxml` via centralized `fcpxml/safe_xml.py` module — blocks external entity injection, entity expansion bombs, and remote DTD parameter entities. Added `defusedxml>=0.7.1` as a dependency. 10 new security tests covering billion laughs, XXE file read, and DTD entity attacks across both `safe_parse` and `safe_fromstring` entry points (52 total security tests, 454 total).
## [0.5.15] - 2026-02-25
### Changed
- **Extracted clip-tag constants**: Replaced 13 inline tag-tuple literals across `writer.py` with three named module-level constants (`CLIP_TAGS`, `CLIP_AND_AUDIO_TAGS`, `SPINE_ELEMENT_TAGS`) — eliminates inconsistent tag sets and makes adding new clip types a single-line change
- **Extracted parser clip-tag constant**: Deduplicated `clip_tags` local variable in `_parse_connected_clips` and `_parse_gap_connected_clips` into `_CONNECTED_CLIP_TAGS` module constant
- **Fixed silence marker bypass**: `remove_silence_candidates(mode="mark")` now uses `build_marker_element()` instead of raw `ET.SubElement` — gains input sanitization, fps-aware duration, and correct FCPXML attribute contract
- **Removed dead code**: Deleted unused `_time_to_fcpxml()` wrapper method from `FCPXMLModifier`
## [0.5.14] - 2026-02-25
### Fixed
- **Completed-attribute strict matching**: Added 7 adversarial edge-case tests for `MarkerType.from_xml_element` — whitespace-only `completed`, tab-padded values, leading-zero `'00'`, and Unicode fullwidth digit lookalikes (`0`, `1`). Confirms strict exact-match rejects all non-canonical inputs as STANDARD.
## [0.5.13] - 2026-02-24
### Changed
- **README overhaul**: Fixed stale stats (414→438 tests, 9→10 suites), expanded architecture tree with per-file descriptions for all test suites, added LOC badge, documented unified marker pipeline and security-first validation as key design decisions, added "Recent Highlights" section showcasing marker hardening and cross-NLE export work, updated latest release pointer to v0.5.13
## [0.5.12] - 2026-02-24
### Added
- **17 marker pipeline tests** (`test_marker_pipeline.py`): Direct unit tests for `build_marker_element` shared builder (8 tests), `batch_add_markers` auto_at_cuts and auto_at_intervals modes (4 tests), `_build_clip_index` duplicate-name last-one-wins behavior (2 tests), `write_fcpxml` output format validation (3 tests)
- **Documented `auto_at_cuts` bug**: Test proves `batch_add_markers(auto_at_cuts=True)` fails with `ValueError` when spine contains duplicate clip names — the name-indexed clip dict loses earlier occurrences
## [0.5.11] - 2026-02-24
### Changed
- **Unified marker element construction**: Extracted `build_marker_element()` as a single source of truth for creating marker/chapter-marker XML elements — eliminates duplicated tag selection, attribute setting, and note-guard logic between `FCPXMLModifier.add_marker()` and `FCPXMLWriter._add_marker()`
- **Single-pass marker collection**: `_collect_markers` in the parser now iterates element children once using `MARKER_XML_TAGS` constant instead of calling `findall()` twice (once per tag)
- **New `MARKER_XML_TAGS` constant**: Tuple of recognised marker element tags (`'marker'`, `'chapter-marker'`) exported from models for use across parser and writer modules
## [0.5.10] - 2026-02-24
### Added
- **3 strict whitespace parser tests**: `test_whitespace_padded_completed_zero_is_standard`, `test_whitespace_padded_completed_one_is_standard`, `test_empty_completed_attribute_is_standard` — parser-level defense-in-depth for `from_xml_element` strict matching
- **3 `from_string` whitespace strip tests**: Verifies `from_string(" completed ")`, `from_string(" todo ")`, and `from_string(" chapter ")` all strip correctly before enum lookup
- **2 writer strict attribute tests**: `test_marker_completed_attr_no_whitespace` confirms written `completed` attributes are exact `'0'`/`'1'` with no padding; `test_from_string_whitespace_roundtrip` confirms padded `from_string` input survives write→parse roundtrip
## [0.5.9] - 2026-02-23
### Fixed
- **Strict whitespace matching documented and unit-tested at contract level**: `from_xml_element` now has explicit docstring documenting priority order and strict matching behavior — whitespace-padded completed attributes like `' 0 '` are correctly rejected as STANDARD
- **Chapter-marker tag priority over completed attribute**: Added unit test confirming `<chapter-marker completed="0">` resolves to CHAPTER, not TODO — tag check takes priority
- **Writer docstring listed only 3 of 4 marker types**: `add_marker()` docstring now lists STANDARD, TODO, COMPLETED, and CHAPTER
### Added
- **4 new `from_xml_element` unit tests**: whitespace-padded '0', whitespace-padded '1', empty completed attribute, and chapter-marker-with-completed edge case — closes the gap between integration tests (test_security.py) and unit contract tests (test_models.py)
## [0.5.8] - 2026-02-23
### Changed
- **Consolidated marker serialization contract into `MarkerType`**: New `from_xml_element()` classmethod and `xml_attrs` property centralise the completed-attribute and posterOffset logic that was previously duplicated across parser and both writer classes
- **Unified parser marker methods**: Replaced separate `_parse_marker()` and `_parse_chapter_marker()` with a single `_parse_marker_element()` that delegates type detection to `MarkerType.from_xml_element()`
- **Extracted `_collect_markers()` helper**: Eliminated 4 duplicated `findall('marker') + findall('chapter-marker')` loops in `_parse_clip`, `_parse_one_connected_clip`, and `_parse_project`
- **Both writer paths use `xml_attrs`**: `FCPXMLModifier.add_marker()` and `FCPXMLWriter._add_marker()` now loop over `marker_type.xml_attrs` instead of manual if/elif chains
### Added
- **11 new tests** for `MarkerType.from_xml_element()`, `xml_attrs`, and round-trip symmetry (`TestMarkerTypeXmlContract`)
## [0.5.7] - 2026-02-23
### Fixed
- **Chapter markers on connected clips silently dropped**: `_parse_one_connected_clip` only parsed `<marker>` children, missing `<chapter-marker>` elements entirely — chapter markers placed on B-roll, lower-thirds, or any lane clip were lost during parse. Now parses both marker types, matching `_parse_clip` behavior.
## [0.5.6] - 2026-02-23
### Fixed
- **Marker completed-attribute edge cases**: Added 5 security tests for whitespace-padded (`" 0 "`, `" 1 "`), negative (`"-1"`), and case-variant (`"TRUE"`, `"false"`) completed attribute values — all correctly rejected as STANDARD by the strict parser
- **`from_string` → write → parse round-trip test**: New integration test proving `MarkerType.from_string('todo')` and legacy alias `'todo-marker'` both survive the full write/re-parse cycle as TODO markers
## [0.5.5] - 2026-02-23
### Added
- **TODO/COMPLETED marker tests for FCPXMLWriter**: 4 new tests covering the object-model-to-XML path (`_add_marker`) that was previously untested for task markers — catches regressions in rough cut and export generation
- **Mixed-case `from_string` tests**: 6 parametrized cases ("Todo", "tOdO", "Completed", "cOMPLETED") proving case insensitivity
- **Whitespace + legacy alias combo tests**: 3 cases ensuring " todo-marker " and similar inputs resolve correctly
- **Enum value contract test**: Asserts `.value` properties stay lowercase — they're used as dict keys across the codebase
- **Multi-marker-type parser test**: Verifies all four marker types coexist on one clip without cross-contamination
- **STANDARD marker negative test**: Confirms plain `<marker>` without `completed` attr never becomes TODO
## [0.5.4] - 2026-02-23
### Security
- **Input validation hardening:** `MarkerType.from_string()` now rejects null bytes, control characters, empty strings, and inputs exceeding 64 characters — prevents injection and memory abuse via crafted marker type strings
- **XML value sanitization:** New `_sanitize_xml_value()` helper strips null bytes and control characters from marker names and notes before writing to XML, with configurable length limits (1024 chars for names, 4096 for notes)
- **Parser file size limit:** `FCPXMLParser.parse_file()` enforces a 50 MB file size ceiling before parsing, preventing memory exhaustion from maliciously large XML files
- **Strict completed-attribute validation:** Parser now only accepts `'0'` and `'1'` for the marker `completed` attribute — any other value (e.g. `"true"`, `"yes"`, `"1 OR 1=1"`) falls through to `MarkerType.STANDARD` instead of being misinterpreted
- Added 25 security tests covering all hardening vectors
## [0.5.3] - 2026-02-22
### Added
- **Workflow recipes guide** (`docs/WORKFLOWS.md`): 8 real-world multi-step workflow recipes — delivery QC pipeline, YouTube chapter export, beat-synced music video assembly, cross-NLE handoff, documentary A/B roll, social media reformat, timeline version comparison, silence cleanup
- Each recipe documents the scenario, natural-language prompt, tool chain, and practical notes
- Section on composing tools in AI agent workflows — how to describe multi-tool pipelines in a single prompt
- README now links to workflows guide from Usage Examples section
## [0.5.2] - 2026-02-22
### Fixed
- **Spec drift:** Updated `docs/specs/03_WRITER_PSEUDOCODE.py` and `docs/specs/07_MODELS.py` MarkerType enums to match implementation — old values (`"todo-marker"`, `"completed-marker"`) replaced with correct values (`"todo"`, `"completed"`)
- **Legacy alias support:** `MarkerType.from_string()` now accepts legacy spec values (`"todo-marker"`, `"completed-marker"`, `"chapter-marker"`) and maps them to current enum values, preventing hard failures from stale references
- Added 10 new MarkerType tests covering `from_string` current values, legacy aliases, invalid input, and `xml_tag` mapping (348 total tests)
## [0.5.1] - 2026-02-22
### Changed
- **README rewrite:** Portfolio-grade overhaul — stronger narrative hook (personal story leads), architecture diagram with data flow, consolidated release history (points to CHANGELOG instead of duplicating it), tighter tool tables, updated stats (337 tests, ~7k LOC)
- Roadmap condensed from 27 line items to 12 grouped milestones for scannability
- "Why This Exists" moved from bottom of page to top — emotional hook before technical proof
- Added test badge to header badges
## [0.5.0] - 2026-02-21
### Added
- **Connected Clips:** Full multi-track support — parser extracts B-roll, titles, and audio from secondary lanes (`lane` attribute), secondary storylines (`<storyline>` elements), and gap-attached clips
- **Compound Clips:** Parse and inspect `ref-clip` compound clips with nested timelines
- **Roles Management:** 4 new tools — `list_roles`, `assign_role`, `filter_by_role`, `export_role_stems` for audio/video role workflows
- **Timeline Diff:** `compare_timelines()` engine detects added/removed/moved/trimmed clips, marker changes, transition changes, and format changes between two FCPXMLs
- **Social Media Reformat:** `reformat_timeline` with preset aspect ratios (9:16, 1:1, 4:5, 4:3, 16:9) and custom resolution support
- **Silence Detection:** Heuristic-based `detect_silence_candidates` (gaps, ultra-short clips, name patterns, duration anomalies) and `remove_silence_candidates` (mark or delete modes)
- **DaVinci Resolve Export:** `export_resolve_xml` generates simplified FCPXML v1.9 with compound clip flattening and unsupported attribute stripping
- **XMEML Export:** `export_fcp7_xml` converts spine-based FCPXML to track-based FCP7 XML (XMEML v5) for Premiere Pro / Resolve / Avid
- New dataclasses: `ConnectedClip`, `CompoundClip`, `SilenceCandidate`
- `audio_role` and `video_role` fields on `Clip` dataclass
- `connected_clips` and `compound_clips` lists on `Timeline` dataclass
- 52 new tests (337 total) covering all 6 features
- 13 new tools → 47 total
## [0.4.3] - 2026-02-20
### Changed
- Consolidated three separate marker type lookup patterns (manual dict, `MarkerType[str.upper()]`, enum value match) into single `MarkerType.from_string()` classmethod
- Added `MarkerType.xml_tag` property — eliminates `tag_map` dicts and inline ternaries for XML element name resolution
- `add_marker` and `list_markers` tool schemas now expose `"completed"` as a valid marker type
- Parser `_parse_clip()` now finds `<chapter-marker>` elements on clips (previously only `<marker>` was parsed at clip level)
- Tightened TODO marker detection: `completed` attribute must be exactly `"0"` (was `is not None`, which matched any value)
- `FCPXMLWriter._add_marker` now emits `posterOffset="0s"` on chapter markers (matches `FCPXMLModifier` behavior)
- `FCPXMLModifier.add_marker` no longer adds `note` attribute to chapter markers (invalid FCPXML, FCP ignores them)
## [0.4.2] - 2026-02-18
### Fixed
- TODO markers now set `completed="0"` attribute so they survive round-trip (save → re-parse) without degrading to STANDARD markers
- COMPLETED markers (`completed="1"`) are now correctly distinguished from TODO markers (`completed="0"`) during parsing — previously both were mapped to TODO
- FCPXMLWriter generator now emits `completed` attribute for TODO and COMPLETED markers
- `list_markers` tool now supports filtering by "completed" marker type
## [Unreleased]
### Added
- 285 unit tests across 7 test files covering models, parser, writer, server handlers, and rough cut generation
- GitHub Actions CI pipeline with linting (ruff) and test execution
- MCP registry metadata files for discoverability
### Fixed
- Import sort order in test files for ruff I001 compliance
### Security
- Add path validation to all 34 tool handlers — blocks path traversal, null bytes, symlink attacks, and oversized files (100 MB limit)
- Enforce file extension whitelists: `.fcpxml`/`.fcpxmld` for projects, `.json` for beats, `.srt`/`.vtt` for subtitles
- Validate output paths to prevent writing to arbitrary filesystem locations
- Harden error handler to avoid leaking internal paths and stack traces in unexpected errors
## [0.4.0] - 2026-02-05
### Added
- 5 pre-built MCP prompt workflows: QC check, YouTube chapters, rough cut, timeline summary, cleanup
- MCP resources for automatic FCPXML file discovery in project directories
- SRT/VTT subtitle import as timeline markers (`import_srt_markers`)
- YouTube chapter/transcript import as markers (`import_transcript_markers`)
- Server architecture refactored to dispatch-dict pattern (`TOOL_HANDLERS`)
## [0.3.0] - 2026-01-20
### Added
- AI-powered rough cut generation from source clips (`auto_rough_cut`)
- Montage generator with pacing curves: accelerating, decelerating, pyramid (`generate_montage`)
- A/B roll generator for documentary-style edits (`generate_ab_roll`)
- Beat sync tools: `import_beat_markers`, `snap_to_beats`
- Flash frame detection and auto-fix (`detect_flash_frames`, `fix_flash_frames`)
- Duplicate clip detection (`detect_duplicates`)
- Gap detection (`detect_gaps`, `fill_gaps`)
- Timeline validation with health score (`validate_timeline`)
- Batch rapid trim (`rapid_trim`)
- Speed change tool (`change_speed`)
- Clip splitting at timecodes (`split_clip`)
- Clip deletion with ripple support (`delete_clips`)
- Transition insertion: cross-dissolve, fade, wipe (`add_transition`)
## [0.2.0] - 2026-01-20
### Added
- Library clip listing (`list_library_clips`)
- Timeline clip insertion from library (`insert_clip`)
- Clip trimming with ripple (`trim_clip`)
- Clip reordering with ripple support (`reorder_clips`)
- Batch marker operations (`batch_add_markers`)
- Pacing analysis with suggestions (`analyze_pacing`)
- Keyword listing and selection (`list_keywords`, `select_by_keyword`)
- EDL export (`export_edl`)
- CSV export (`export_csv`)
## [0.1.0] - 2026-01-18
### Added
- Initial release — first MCP server for Final Cut Pro
- FCPXML parser supporting versions 1.8–1.11
- Timeline analysis (`analyze_timeline`)
- Clip listing with timecodes (`list_clips`)
- Marker listing (`list_markers`)
- Short cut and long clip detection (`find_short_cuts`, `find_long_clips`)
- Single marker insertion (`add_marker`)
- Project file discovery (`list_projects`)
- Python data models: TimeValue (rational time arithmetic), Timecode, Clip, Timeline, Project