Skip to content

v0.1.0: create-first ordered typed model, FCPKitDSL, FCPKitScripting - #29

Open
leogdion wants to merge 23 commits into
mainfrom
v0.1.x
Open

v0.1.0: create-first ordered typed model, FCPKitDSL, FCPKitScripting#29
leogdion wants to merge 23 commits into
mainfrom
v0.1.x

Conversation

@leogdion

Copy link
Copy Markdown
Member

Merges the v0.1.0 release branch into main: Steps 0–8 of the accepted plan (ADR 0002) plus scaffolding, gates, and docs sync.

Highlights

  • Ordered, typed model: FCPTime and strong value types, DTD-ordered CodingKeys, single ordered Spine.items (fixes the transition mis-ordering Final Cut rejected), AnchoredItem choice arrays, resources and document shells.
  • FCPKitDSL: create-first authoring (Document + result builders) with transition overlap packing, anchored lanes, and DTD-valid export (media-rep, not pre-1.6 src); fcpxml-dsl CLI exports smoke-test cuts and drives verify-import.
  • FCPKitScripting: macOS-only read-only ScriptingBridge inspector.
  • Gates: multicam typed-generation gate and the DSL create-path Final Cut import gate both Accepted (docs/manual/typed-generation-gate.md) — transitions and titles cuts imported, dissolve confirmed between clips.
  • Swift 6.4 toolchain, BrightDigit CI, cross-platform gating.

Before merging

Known issue shipped with v0.1.0

After merging

  • Suggest a merge commit (not squash) to keep the step history on main, then tag v0.1.0.

🤖 Generated with Claude Code

leogdion and others added 14 commits July 28, 2026 16:13
EncoderOrderingTests pins that XMLCoder emits child elements in
CodingKeys declaration order and sorts only under the opt-in
.sortedKeys, so a future XMLCoder bump that changes default ordering
fails loudly.

ChoiceEncodingSpike answers the Step 3 (#8) design question: the
synthesized decode of an items = "" choice array silently yields an
empty array, so Spine must hand-write init(from:) (attributes via the
keyed container, items via singleValueContainer choice-array decode);
the synthesized encode with items = "" + DynamicNodeEncoding composes
and emits children in array order with attributes intact.

SpineOrderTests asserts the transitions FeaturePair spine is
asset-clip, transition, asset-clip as a raw tree (permanent), and marks
the round-trip order assertion XCTExpectFailure until #8 lands ordered
Spine.items (XCTSkip on corelibs-xctest platforms).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Step 3: Ordered Spine.items (#8)

* Replace choice if/switch ladders with XMLChoiceField tables

Avoid cyclomatic_complexity disables on SpineItem/Spine by walking
field-handler and batch-append tables. Document the anti-pattern in
agent notes and a Cursor rule.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore XMLChoiceField.encode after helper split

The encode method was dropped when OrderedChoiceItems moved to its
own file; choice encoding needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add XMLChoiceCodable and OrderedChoiceItems payloads/replace

Share choice decode/encode via protocol extension and fold typed
slice get/set into OrderedChoiceItems so Spine drops isX accessors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move Spine typed item accessors into Spine+Items

Keep Spine.swift under the file_length warning limit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix strict lint violations

- Expand one-line conditional returns in SpineItem+Accessors.swift
- Split XMLChoiceCodable protocol and extension out of XMLChoiceField.swift
  into XMLChoiceCodable.swift (one declaration per file, file types order)
- Move ChoiceKey typealias above CodingKeys enum in SpineItem.swift
- Apply swift-format/swiftlint autofixes (line breaks, whitespace)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace OrderedChoiceItems with OrderedChoiceContainer protocol.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ngKeys (#23)

* feat: implement AnchoredItem choice enum and DTD-ordered CodingKeys for story elements (Issue #9)

* Replace choice if/switch ladders with XMLChoiceField tables

Avoid cyclomatic_complexity disables on SpineItem/Spine by walking
field-handler and batch-append tables. Document the anti-pattern in
agent notes and a Cursor rule.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply XMLChoiceField tables to AnchoredItem and seed inits

Convert AnchoredItem choice Codable and AssetClip/Storyline/SyncClip/
RefClip/MCClip convenience seed chains to table-driven helpers so
large if/switch ladders are gone on Step 4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add XMLChoiceCodable and OrderedChoiceItems payloads/replace

Share choice decode/encode via protocol extension and fold typed
slice get/set into OrderedChoiceItems so Spine drops isX accessors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clear file_length/line_length via choice protocols and splits

Adopt XMLChoiceCodable and AnchoredChoiceContainer, wrap DTD comments,
and move decode inits into +Coding files so clip types need no lint
disables.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix step4 lint violations: decoder docs, AnchoredItem ordering, auto-format

- Document public init(from decoder:) in the five clip +Coding files
- Put conditional returns on their own lines in AnchoredItem+Accessors
- Move ChoiceKey typealias ahead of CodingKeys in AnchoredItem
- Apply swift-format/swiftlint auto-fixes (trailing comma, whitespace)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove stray doc comments duplicated from +Coding inits

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace OrderedChoiceItems with OrderedChoiceContainer protocol.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Step 5: ResourceID/ResourceRef and typed resource shells (#10).

Adopt phantom-typed refs across resources, document shells, and story refs;
keep Asset.audioRate as String Hz while Sequence/Multicam use AudioRate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Replace choice if/switch ladders with XMLChoiceField tables

Avoid cyclomatic_complexity disables on SpineItem/Spine by walking
field-handler and batch-append tables. Document the anti-pattern in
agent notes and a Cursor rule.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore XMLChoiceField.encode after helper split

The encode method was dropped when OrderedChoiceItems moved to its
own file; choice encoding needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply XMLChoiceField tables to AnchoredItem and seed inits

Convert AnchoredItem choice Codable and AssetClip/Storyline/SyncClip/
RefClip/MCClip convenience seed chains to table-driven helpers so
large if/switch ladders are gone on Step 4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add XMLChoiceCodable and OrderedChoiceItems payloads/replace

Share choice decode/encode via protocol extension and fold typed
slice get/set into OrderedChoiceItems so Spine drops isX accessors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move Spine typed item accessors into Spine+Items

Keep Spine.swift under the file_length warning limit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clear file_length/line_length via choice protocols and splits

Adopt AnchoredChoiceContainer on clip hosts, wrap DTD comments, and move
decode inits into +Coding extensions while keeping ResourceRef typing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply lint auto-fixes from swift-format and swiftlint

No step5-specific violations found; remaining lint findings are
inherited from step3/step4 and resolve when those branches merge up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace OrderedChoiceItems with OrderedChoiceContainer protocol.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#14).

- .gitignore: /transitions.fcpxml and /titles.fcpxml are reproducible via
  `swift run fcpxml-dsl export`.
- typed-generation-gate.md: accept the DSL import gate — transitions and
  titles imported into Final Cut Pro, dissolve confirmed between clips,
  anchored title lane confirmed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 457 files, which is 357 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cd4e54c8-5860-4247-a5a7-8b99e0ef9c67

📥 Commits

Reviewing files that changed from the base of the PR and between 1f24cb2 and 829eb38.

📒 Files selected for processing (457)
  • .agents/skills/batch-grill-me/SKILL.md
  • .agents/skills/batch-grill-me/agents/openai.yaml
  • .agents/skills/claude-handoff/SKILL.md
  • .agents/skills/claude-handoff/agents/openai.yaml
  • .agents/skills/code-review/SKILL.md
  • .agents/skills/code-review/agents/openai.yaml
  • .agents/skills/codebase-design/DEEPENING.md
  • .agents/skills/codebase-design/DESIGN-IT-TWICE.md
  • .agents/skills/codebase-design/SKILL.md
  • .agents/skills/codebase-design/agents/openai.yaml
  • .agents/skills/domain-modeling/ADR-FORMAT.md
  • .agents/skills/domain-modeling/CONTEXT-FORMAT.md
  • .agents/skills/domain-modeling/SKILL.md
  • .agents/skills/domain-modeling/agents/openai.yaml
  • .agents/skills/grill-me/SKILL.md
  • .agents/skills/grill-me/agents/openai.yaml
  • .agents/skills/grill-with-docs/SKILL.md
  • .agents/skills/grill-with-docs/agents/openai.yaml
  • .agents/skills/grilling/SKILL.md
  • .agents/skills/grilling/agents/openai.yaml
  • .agents/skills/handoff/SKILL.md
  • .agents/skills/handoff/agents/openai.yaml
  • .agents/skills/implement/SKILL.md
  • .agents/skills/implement/agents/openai.yaml
  • .agents/skills/loop-me/SKILL.md
  • .agents/skills/loop-me/agents/openai.yaml
  • .agents/skills/prototype/LOGIC.md
  • .agents/skills/prototype/SKILL.md
  • .agents/skills/prototype/UI.md
  • .agents/skills/prototype/agents/openai.yaml
  • .agents/skills/qa/SKILL.md
  • .agents/skills/qa/agents/openai.yaml
  • .agents/skills/research/SKILL.md
  • .agents/skills/research/agents/openai.yaml
  • .agents/skills/to-spec/SKILL.md
  • .agents/skills/to-spec/agents/openai.yaml
  • .agents/skills/to-tickets/SKILL.md
  • .agents/skills/to-tickets/agents/openai.yaml
  • .agents/skills/triage/AGENT-BRIEF.md
  • .agents/skills/triage/OUT-OF-SCOPE.md
  • .agents/skills/triage/SKILL.md
  • .agents/skills/triage/agents/openai.yaml
  • .agents/skills/wayfinder/SKILL.md
  • .agents/skills/wayfinder/agents/openai.yaml
  • .claude/agent-notes.md
  • .claude/memory/MEMORY.md
  • .claude/memory/corrections-log-directive.md
  • .claude/memory/fcp-scripting-live-facts.md
  • .claude/memory/v0.1.0-plan-and-dsl.md
  • .claude/settings.json
  • .claude/skills/batch-grill-me
  • .claude/skills/claude-handoff
  • .claude/skills/code-review
  • .claude/skills/codebase-design
  • .claude/skills/domain-modeling
  • .claude/skills/grill-me
  • .claude/skills/grill-with-docs
  • .claude/skills/grilling
  • .claude/skills/handoff
  • .claude/skills/implement
  • .claude/skills/loop-me
  • .claude/skills/prototype
  • .claude/skills/qa
  • .claude/skills/research
  • .claude/skills/to-spec
  • .claude/skills/to-tickets
  • .claude/skills/triage
  • .claude/skills/wayfinder
  • .cursor/rules/dsl-effect-presets.mdc
  • .cursor/rules/xml-choice-codable.mdc
  • .github/actions/setup-tools/action.yml
  • .github/dependabot.yml
  • .github/matrices/macos-full.json
  • .github/matrices/macos-spm.json
  • .github/workflows/FCPKit.yml
  • .github/workflows/check-unsafe-flags.yml
  • .github/workflows/claude-code-review.yml
  • .github/workflows/claude.yml
  • .github/workflows/cleanup-caches.yml
  • .github/workflows/codeql.yml
  • .github/workflows/swift-source-compat.yml
  • .gitignore
  • .mise.toml
  • .spi.yml
  • .swift-format
  • .swift-version
  • .swiftlint.yml
  • .vscode/launch.json
  • AGENTS.md
  • Package.swift
  • README.md
  • Scripts/generate-test-media.sh
  • Scripts/header.sh
  • Scripts/lint.sh
  • Sources/FCPKit/Adjustments/AdjustBlend.swift
  • Sources/FCPKit/Adjustments/AdjustColorConform.swift
  • Sources/FCPKit/Adjustments/AdjustCrop.swift
  • Sources/FCPKit/Adjustments/AdjustLoudness.swift
  • Sources/FCPKit/Adjustments/AdjustTransform.swift
  • Sources/FCPKit/Adjustments/AdjustVolume.swift
  • Sources/FCPKit/Adjustments/AudioChannelSource.swift
  • Sources/FCPKit/Adjustments/ColorCorrection.swift
  • Sources/FCPKit/Adjustments/ConformRate.swift
  • Sources/FCPKit/Adjustments/Fade.swift
  • Sources/FCPKit/Adjustments/FilterAudio.swift
  • Sources/FCPKit/Adjustments/FilterVideo.swift
  • Sources/FCPKit/Adjustments/Motion.swift
  • Sources/FCPKit/Adjustments/RetimeClip.swift
  • Sources/FCPKit/Adjustments/TimeMap.swift
  • Sources/FCPKit/Adjustments/Timept.swift
  • Sources/FCPKit/Adjustments/TrimRect.swift
  • Sources/FCPKit/AssetClip+Editing.swift
  • Sources/FCPKit/AssetClipEditingError.swift
  • Sources/FCPKit/Document/Event.swift
  • Sources/FCPKit/Document/FCPXML.swift
  • Sources/FCPKit/Document/Library.swift
  • Sources/FCPKit/Document/Project.swift
  • Sources/FCPKit/Document/Resources.swift
  • Sources/FCPKit/Elements/AudioRole.swift
  • Sources/FCPKit/Elements/Caption.swift
  • Sources/FCPKit/Elements/CaptionRole.swift
  • Sources/FCPKit/Elements/ChapterMarker.swift
  • Sources/FCPKit/Elements/DataElement.swift
  • Sources/FCPKit/Elements/Generator.swift
  • Sources/FCPKit/Elements/Keyframe.swift
  • Sources/FCPKit/Elements/KeyframeAnimation.swift
  • Sources/FCPKit/Elements/Marker.swift
  • Sources/FCPKit/Elements/ParamElement.swift
  • Sources/FCPKit/Elements/TextElement.swift
  • Sources/FCPKit/Elements/TextStyle.swift
  • Sources/FCPKit/Elements/TextStyleDef.swift
  • Sources/FCPKit/Elements/Title.swift
  • Sources/FCPKit/Elements/VideoRole.swift
  • Sources/FCPKit/FCPNodeEncodable.swift
  • Sources/FCPKit/FCPXML.swift
  • Sources/FCPKit/FCPXMLError.swift
  • Sources/FCPKit/FCPXMLExtended.swift
  • Sources/FCPKit/FCPXMLMissingElements.swift
  • Sources/FCPKit/FCPXMLParser.swift
  • Sources/FCPKit/FCPXMLVersion.swift
  • Sources/FCPKit/FCPXMLVersionCompatibility.swift
  • Sources/FCPKit/Metadata/AssetMetadata.swift
  • Sources/FCPKit/Metadata/Keyword.swift
  • Sources/FCPKit/Metadata/MatchAnalysisType.swift
  • Sources/FCPKit/Metadata/MatchClip.swift
  • Sources/FCPKit/Metadata/MatchMedia.swift
  • Sources/FCPKit/Metadata/MatchRatings.swift
  • Sources/FCPKit/Metadata/MetadataArray.swift
  • Sources/FCPKit/Metadata/MetadataEntry.swift
  • Sources/FCPKit/Metadata/MetadataString.swift
  • Sources/FCPKit/Metadata/Rating.swift
  • Sources/FCPKit/Metadata/SmartCollection.swift
  • Sources/FCPKit/Resources/Asset.swift
  • Sources/FCPKit/Resources/Effect.swift
  • Sources/FCPKit/Resources/Format.swift
  • Sources/FCPKit/Resources/Media.swift
  • Sources/FCPKit/Resources/MediaRep.swift
  • Sources/FCPKit/Resources/Multicam.swift
  • Sources/FCPKit/Timeline/AnchoredChoiceContainer.swift
  • Sources/FCPKit/Timeline/AnchoredItem+Accessors.swift
  • Sources/FCPKit/Timeline/AnchoredItem.swift
  • Sources/FCPKit/Timeline/AssetClip+Anchored.swift
  • Sources/FCPKit/Timeline/AssetClip+Coding.swift
  • Sources/FCPKit/Timeline/AssetClip.swift
  • Sources/FCPKit/Timeline/Clip+Coding.swift
  • Sources/FCPKit/Timeline/Clip.swift
  • Sources/FCPKit/Timeline/CompoundClip.swift
  • Sources/FCPKit/Timeline/ConnectedClip.swift
  • Sources/FCPKit/Timeline/Gap.swift
  • Sources/FCPKit/Timeline/MCAngle.swift
  • Sources/FCPKit/Timeline/MCClip+Coding.swift
  • Sources/FCPKit/Timeline/MCClip.swift
  • Sources/FCPKit/Timeline/MCSource.swift
  • Sources/FCPKit/Timeline/OrderedChoiceContainer.swift
  • Sources/FCPKit/Timeline/RefClip+Coding.swift
  • Sources/FCPKit/Timeline/RefClip.swift
  • Sources/FCPKit/Timeline/Sequence.swift
  • Sources/FCPKit/Timeline/Spine+Items.swift
  • Sources/FCPKit/Timeline/Spine.swift
  • Sources/FCPKit/Timeline/SpineItem+Accessors.swift
  • Sources/FCPKit/Timeline/SpineItem.swift
  • Sources/FCPKit/Timeline/Storyline.swift
  • Sources/FCPKit/Timeline/SyncClip+Coding.swift
  • Sources/FCPKit/Timeline/SyncClip.swift
  • Sources/FCPKit/Timeline/Transition.swift
  • Sources/FCPKit/Timeline/Video.swift
  • Sources/FCPKit/Timeline/XMLChoiceCodable.swift
  • Sources/FCPKit/Timeline/XMLChoiceField.swift
  • Sources/FCPKit/Values/AssetKind.swift
  • Sources/FCPKit/Values/AudioLayout.swift
  • Sources/FCPKit/Values/AudioRate.swift
  • Sources/FCPKit/Values/Color+AppKit.swift
  • Sources/FCPKit/Values/Color+CoreGraphics.swift
  • Sources/FCPKit/Values/Color+SwiftUI.swift
  • Sources/FCPKit/Values/Color+UIKit.swift
  • Sources/FCPKit/Values/Color.swift
  • Sources/FCPKit/Values/ColorProcessing.swift
  • Sources/FCPKit/Values/EffectKind.swift
  • Sources/FCPKit/Values/FCPBool.swift
  • Sources/FCPKit/Values/FCPTime+Arithmetic.swift
  • Sources/FCPKit/Values/FCPTime+Interval.swift
  • Sources/FCPKit/Values/FCPTime.swift
  • Sources/FCPKit/Values/FCPTimeError.swift
  • Sources/FCPKit/Values/FormatKind.swift
  • Sources/FCPKit/Values/MediaKind.swift
  • Sources/FCPKit/Values/MediaRepKind.swift
  • Sources/FCPKit/Values/ResourceID.swift
  • Sources/FCPKit/Values/ResourceKind.swift
  • Sources/FCPKit/Values/ResourceRef.swift
  • Sources/FCPKit/Values/SrcEnable.swift
  • Sources/FCPKit/Values/TCFormat.swift
  • Sources/FCPKit/Values/TimeptInterp.swift
  • Sources/FCPKit/Values/XMLAttributeCase.swift
  • Sources/FCPKit/Values/XMLAttributeValue.swift
  • Sources/FCPKitDSL/Anchor.swift
  • Sources/FCPKitDSL/AnchorableItem.swift
  • Sources/FCPKitDSL/Array+Lowering.swift
  • Sources/FCPKitDSL/AssetClip+Modifiers.swift
  • Sources/FCPKitDSL/AssetClip.swift
  • Sources/FCPKitDSL/AssetSource.swift
  • Sources/FCPKitDSL/BuildError.swift
  • Sources/FCPKitDSL/Built+Anchoring.swift
  • Sources/FCPKitDSL/Built+Promotion.swift
  • Sources/FCPKitDSL/Built.swift
  • Sources/FCPKitDSL/Color+DSL.swift
  • Sources/FCPKitDSL/CrossDissolveFilters.swift
  • Sources/FCPKitDSL/DSLNode.swift
  • Sources/FCPKitDSL/Document+Export.swift
  • Sources/FCPKitDSL/Document.swift
  • Sources/FCPKitDSL/DocumentBuilder.swift
  • Sources/FCPKitDSL/DocumentContent.swift
  • Sources/FCPKitDSL/DocumentGroup.swift
  • Sources/FCPKitDSL/Event.swift
  • Sources/FCPKitDSL/FCPXMLVersion+Defaults.swift
  • Sources/FCPKitDSL/FormatPreset.swift
  • Sources/FCPKitDSL/FramePosition.swift
  • Sources/FCPKitDSL/Gap.swift
  • Sources/FCPKitDSL/Generator+Modifiers.swift
  • Sources/FCPKitDSL/Generator.swift
  • Sources/FCPKitDSL/GeneratorPreset.swift
  • Sources/FCPKitDSL/Layout+Packing.swift
  • Sources/FCPKitDSL/Layout.swift
  • Sources/FCPKitDSL/Library.swift
  • Sources/FCPKitDSL/ModelSequence.swift
  • Sources/FCPKitDSL/Project.swift
  • Sources/FCPKitDSL/RGBDocument.swift
  • Sources/FCPKitDSL/ResourceStore.swift
  • Sources/FCPKitDSL/Sequence.swift
  • Sources/FCPKitDSL/Spine.swift
  • Sources/FCPKitDSL/StoryItem.swift
  • Sources/FCPKitDSL/String+FCPXMLValue.swift
  • Sources/FCPKitDSL/Title+Modifiers.swift
  • Sources/FCPKitDSL/Title.swift
  • Sources/FCPKitDSL/TitlePreset.swift
  • Sources/FCPKitDSL/TitleStyle.swift
  • Sources/FCPKitDSL/Transition.swift
  • Sources/FCPKitDSL/TransitionPreset.swift
  • Sources/FCPKitMediaTools/FCPXMLUtilities.swift
  • Sources/FCPKitMediaTools/MulticamXMLBuilder+Library.swift
  • Sources/FCPKitMediaTools/MulticamXMLBuilder+Media.swift
  • Sources/FCPKitMediaTools/MulticamXMLBuilder+Multicam.swift
  • Sources/FCPKitMediaTools/MulticamXMLBuilder+Resources.swift
  • Sources/FCPKitMediaTools/MulticamXMLBuilder.swift
  • Sources/FCPKitMediaTools/VideoMetadata.swift
  • Sources/FCPKitMediaTools/VideoMetadataError.swift
  • Sources/FCPKitMediaTools/VideoMetadataExtractor.swift
  • Sources/FCPKitScripting/FCPApplication.swift
  • Sources/FCPKitScripting/FCPLibraryInspector.swift
  • Sources/FCPKitScripting/FCPScriptedEvent.swift
  • Sources/FCPKitScripting/FCPScriptedLibrary+ProjectCounts.swift
  • Sources/FCPKitScripting/FCPScriptedLibrary.swift
  • Sources/FCPKitScripting/FCPScriptedProject.swift
  • Sources/FCPKitScripting/FCPScriptedSequence.swift
  • Sources/FCPKitScripting/FCPScriptedTimecodeFormat.swift
  • Sources/FCPKitScripting/FCPScriptingError.swift
  • Sources/FCPKitScripting/FCPScriptingMediaTime.swift
  • Sources/FCPKitScripting/FCPScriptingObject.swift
  • Sources/FCPKitScripting/FCPScriptingObjectMock.swift
  • Sources/FCPKitScripting/FCPScriptingTimecodeFormatParser.swift
  • Sources/FCPKitScripting/SBObject+FCPScriptingObject.swift
  • Sources/FCPXMLDiff/FCPXMLDTDLocator.swift
  • Sources/FCPXMLDiff/FCPXMLDTDValidator.swift
  • Sources/FCPXMLDiff/FCPXMLDiffEngine.swift
  • Sources/FCPXMLDiff/FCPXMLDiffMode.swift
  • Sources/FCPXMLDiff/FCPXMLDifference.swift
  • Sources/FCPXMLDiff/FCPXMLDifferenceKind.swift
  • Sources/FCPXMLDiff/FCPXMLNormalizer.swift
  • Sources/FCPXMLDiff/FCPXMLRoundTripAnalyzer.swift
  • Sources/FCPXMLDiff/FCPXMLRoundTripReport.swift
  • Sources/FCPXMLDiff/FCPXMLValidationError.swift
  • Sources/FCPXMLDiff/FCPXMLValidationIssue.swift
  • Sources/FCPXMLDiff/FCPXMLValidationReport.swift
  • Sources/FCPXMLDiff/FCPXMLValidationReportRenderer.swift
  • Sources/FCPXMLDiff/RawPairAnalyzer.swift
  • Sources/FCPXMLDiff/RawPairReport.swift
  • Sources/FCPXMLDiff/RawPairReportRenderer.swift
  • Sources/FCPXMLDiff/SchemaCompletenessAcceptance.swift
  • Sources/FCPXMLDiff/SchemaCompletenessAnalyzer.swift
  • Sources/FCPXMLDiff/SchemaCompletenessFileReport.swift
  • Sources/FCPXMLDiff/SchemaCompletenessReport.swift
  • Sources/FCPXMLDiff/SchemaCompletenessReportRenderer.swift
  • Sources/FCPXMLDiff/SchemaCompletenessSummary.swift
  • Sources/FCPXMLDiff/TreeParserDelegate.swift
  • Sources/FCPXMLDiff/XMLTreeInventory.swift
  • Sources/FCPXMLDiff/XMLTreeNode.swift
  • Sources/FCPXMLDiff/XMLTreeParser.swift
  • Sources/FCPXMLDiff/XMLTreeParserError.swift
  • Sources/FCPXMLDiffCLI/CommandError.swift
  • Sources/FCPXMLDiffCLI/Data+WriteAtomically.swift
  • Sources/FCPXMLDiffCLI/FCPXMLDiffCommand+Commands.swift
  • Sources/FCPXMLDiffCLI/FCPXMLDiffCommand+Parsing.swift
  • Sources/FCPXMLDiffCLI/FCPXMLDiffCommand.swift
  • Sources/fcpxml-dsl/FCPXMLDSLCommand+Export.swift
  • Sources/fcpxml-dsl/FCPXMLDSLCommand+VerifyImport.swift
  • Sources/fcpxml-dsl/FCPXMLDSLCommand.swift
  • Sources/fcpxml-dsl/FCPXMLDSLCommandError.swift
  • Sources/fcpxml-dsl/FCPXMLDSLMediaProbe.swift
  • Sources/fcpxml-dsl/RGBDocument.swift
  • Sources/fcpxml-dsl/TitlesCutDocument.swift
  • Sources/fcpxml-dsl/TransitionsCutDocument.swift
  • Sources/fcpxml-generator/FCPXMLGeneratorTool.swift
  • Tests/FCPKitDSLTests/DTDValidationSupport.swift
  • Tests/FCPKitDSLTests/FCPKitDSLTests.swift
  • Tests/FCPKitDSLTests/FCPTimeIntervalTests.swift
  • Tests/FCPKitDSLTests/FeaturePairAcceptanceTests.swift
  • Tests/FCPKitDSLTests/FeaturePairDocuments.swift
  • Tests/FCPKitDSLTests/FramePositionTests.swift
  • Tests/FCPKitDSLTests/GeneratorDSLTests.swift
  • Tests/FCPKitDSLTests/PositionedOrderingDoc.swift
  • Tests/FCPKitDSLTests/PositionedSpineOrderingTests.swift
  • Tests/FCPKitDSLTests/SmartCollectionVersionTests.swift
  • Tests/FCPKitDSLTests/StoryItemAnchorErrorTests.swift
  • Tests/FCPKitDSLTests/StoryItemDoc.swift
  • Tests/FCPKitDSLTests/StoryItemSupport.swift
  • Tests/FCPKitDSLTests/StoryItemTests.swift
  • Tests/FCPKitDSLTests/TextStyleIDTests.swift
  • Tests/FCPKitDSLTests/TitleStyleSupport.swift
  • Tests/FCPKitDSLTests/TitleStyleSupportError.swift
  • Tests/FCPKitDSLTests/TitleStyleTests.swift
  • Tests/FCPKitScriptingTests/FCPLibraryInspectorLiveTests.swift
  • Tests/FCPKitScriptingTests/FCPLibraryInspectorMockTests.swift
  • Tests/FCPKitScriptingTests/FCPScriptedLibraryProjectCountsTests.swift
  • Tests/FCPKitScriptingTests/FCPScriptedTimecodeFormatTests.swift
  • Tests/FCPKitScriptingTests/FCPScriptingMediaTimeTests.swift
  • Tests/FCPKitScriptingTests/FCPScriptingTimecodeFormatParserTests.swift
  • Tests/FCPKitTests/AssetClipEditingTests.swift
  • Tests/FCPKitTests/ChoiceEncodingSpike.swift
  • Tests/FCPKitTests/ColorTests.swift
  • Tests/FCPKitTests/DTDValidationTests.swift
  • Tests/FCPKitTests/EncoderOrderingTests.swift
  • Tests/FCPKitTests/FCPBoolTests.swift
  • Tests/FCPKitTests/FCPKitTests.swift
  • Tests/FCPKitTests/FCPTimeArithmeticTests.swift
  • Tests/FCPKitTests/FCPTimeEqualityTests.swift
  • Tests/FCPKitTests/FCPTimeParsingTests.swift
  • Tests/FCPKitTests/FCPXMLDiffTests+Fixtures.swift
  • Tests/FCPKitTests/FCPXMLDiffTests+Reports.swift
  • Tests/FCPKitTests/FCPXMLDiffTests+RoundTrip.swift
  • Tests/FCPKitTests/FCPXMLDiffTests.swift
  • Tests/FCPKitTests/FCPXMLVersionTests.swift
  • Tests/FCPKitTests/FeaturePairTests.swift
  • Tests/FCPKitTests/FeaturePairs/markers/after.fcpxml
  • Tests/FCPKitTests/FeaturePairs/markers/before.fcpxml
  • Tests/FCPKitTests/FeaturePairs/markers/diff.json
  • Tests/FCPKitTests/FeaturePairs/markers/diff.md
  • Tests/FCPKitTests/FeaturePairs/markers/metadata.json
  • Tests/FCPKitTests/FeaturePairs/retiming/after.fcpxml
  • Tests/FCPKitTests/FeaturePairs/retiming/before.fcpxml
  • Tests/FCPKitTests/FeaturePairs/retiming/diff.json
  • Tests/FCPKitTests/FeaturePairs/retiming/diff.md
  • Tests/FCPKitTests/FeaturePairs/retiming/metadata.json
  • Tests/FCPKitTests/FeaturePairs/roles/after.fcpxml
  • Tests/FCPKitTests/FeaturePairs/roles/before.fcpxml
  • Tests/FCPKitTests/FeaturePairs/roles/diff.json
  • Tests/FCPKitTests/FeaturePairs/roles/diff.md
  • Tests/FCPKitTests/FeaturePairs/roles/metadata.json
  • Tests/FCPKitTests/FeaturePairs/titles/after.fcpxml
  • Tests/FCPKitTests/FeaturePairs/titles/before.fcpxml
  • Tests/FCPKitTests/FeaturePairs/titles/diff.json
  • Tests/FCPKitTests/FeaturePairs/titles/diff.md
  • Tests/FCPKitTests/FeaturePairs/titles/metadata.json
  • Tests/FCPKitTests/FeaturePairs/transitions/after.fcpxml
  • Tests/FCPKitTests/FeaturePairs/transitions/before.fcpxml
  • Tests/FCPKitTests/FeaturePairs/transitions/diff.json
  • Tests/FCPKitTests/FeaturePairs/transitions/diff.md
  • Tests/FCPKitTests/FeaturePairs/transitions/metadata.json
  • Tests/FCPKitTests/Fixtures/ChoiceSpikeInterleavedSpineXml.fcpxml
  • Tests/FCPKitTests/Fixtures/CompleteSyntheticModelHasNoRoundTripLossXml.fcpxml
  • Tests/FCPKitTests/Fixtures/DataElementTextContentRoundTripsXml.fcpxml
  • Tests/FCPKitTests/Fixtures/DeletedAndReorderedResourcesDoNotChangeSurvivingReferencesLeft.fcpxml
  • Tests/FCPKitTests/Fixtures/DeletedAndReorderedResourcesDoNotChangeSurvivingReferencesRight.fcpxml
  • Tests/FCPKitTests/Fixtures/InsertedResourceDoesNotChangeLaterReferencesLeft.fcpxml
  • Tests/FCPKitTests/Fixtures/InsertedResourceDoesNotChangeLaterReferencesRight.fcpxml
  • Tests/FCPKitTests/Fixtures/NormalizerSuppressesChurnAndSurfacesStructuralDeltaLeft.fcpxml
  • Tests/FCPKitTests/Fixtures/NormalizerSuppressesChurnAndSurfacesStructuralDeltaRight.fcpxml
  • Tests/FCPKitTests/Fixtures/OpaqueMaskingKeepsElementPathsAndAttributesSignificantChangedAttribute.fcpxml
  • Tests/FCPKitTests/Fixtures/OpaqueMaskingKeepsElementPathsAndAttributesSignificantLeft.fcpxml
  • Tests/FCPKitTests/Fixtures/OpaqueMaskingKeepsElementPathsAndAttributesSignificantPayloadOnly.fcpxml
  • Tests/FCPKitTests/Fixtures/ParseRequiringWellFormedVersionRejectsMalformedNewer.fcpxml
  • Tests/FCPKitTests/Fixtures/ParseRequiringWellFormedVersionRejectsMalformedXml.fcpxml
  • Tests/FCPKitTests/Fixtures/ParseWithCompatibilityReportsDocumentVersionXml.fcpxml
  • Tests/FCPKitTests/Fixtures/RawPairNormalizationIdentityFilteringAndRenderingAreDeterministicAfter.fcpxml
  • Tests/FCPKitTests/Fixtures/RawPairNormalizationIdentityFilteringAndRenderingAreDeterministicBefore.fcpxml
  • Tests/FCPKitTests/Fixtures/RawPairReportsAddedRemovedAndChangedStructuresAfter.fcpxml
  • Tests/FCPKitTests/Fixtures/RawPairReportsAddedRemovedAndChangedStructuresBefore.fcpxml
  • Tests/FCPKitTests/Fixtures/RoundTripAnalyzerCanCheckAnEditedEncodedDocumentEdited.fcpxml
  • Tests/FCPKitTests/Fixtures/RoundTripAnalyzerCanCheckAnEditedEncodedDocumentOriginal.fcpxml
  • Tests/FCPKitTests/Fixtures/RoundTripAnalyzerReportsUnsupportedContentWithoutRejectingInputXml.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleAudioXML.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleFCPXML.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleGeneratorXML.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleMarkerXML.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleTitleXML.fcpxml
  • Tests/FCPKitTests/Fixtures/SampleTransitionXML.fcpxml
  • Tests/FCPKitTests/Fixtures/ValidateThrowsWhenDTDMissingXml.fcpxml
  • Tests/FCPKitTests/MulticamXMLBuilderTests.swift
  • Tests/FCPKitTests/RealFCPXMLTests+Helpers.swift
  • Tests/FCPKitTests/RealFCPXMLTests+Mutation.swift
  • Tests/FCPKitTests/RealFCPXMLTests.swift
  • Tests/FCPKitTests/ResourceRefTests.swift
  • Tests/FCPKitTests/SchemaCompletenessTests.swift
  • Tests/FCPKitTests/SpineOrderTests.swift
  • Tests/FCPKitTests/StoryElementOrderingTests.swift
  • Tests/FCPKitTests/TestCodingKey.swift
  • Tests/FCPKitTests/TestData/Both-Multicam.fcpxml
  • Tests/FCPKitTests/TestData/FCPKit-Sample-1.14.fcpxml
  • Tests/FCPKitTests/TestData/FCPKit-Sample-1.14.metadata.json
  • Tests/FCPKitTests/TypedGenerationTests+Fixtures.swift
  • Tests/FCPKitTests/TypedGenerationTests.swift
  • Tests/FCPKitTests/XMLAttributeCaseTests.swift
  • Tests/FCPKitTests/XMLAttributeValueCodingTests.swift
  • Tests/FCPKitTests/XMLFixture.swift
  • codecov.yml
  • docs/NEXT_STEPS.md
  • docs/adr/0001-supported-schema-and-best-effort-editing.md
  • docs/adr/0002-create-first-ordered-typed-model.md
  • docs/agents/domain.md
  • docs/agents/issue-tracker.md
  • docs/agents/triage-labels.md
  • docs/archive/ROADMAP_HANDOFF.md
  • docs/manual/easy-export-recipes.md
  • docs/manual/export-recommendations.md
  • docs/manual/final-cut-artifacts.md
  • docs/manual/typed-generation-gate.md
  • docs/planning/apple-platform-frameworks.md
  • docs/planning/demo-parallel-worktree-plan.md
  • docs/planning/demo-presentation-video.md
  • docs/planning/ffmpeg-in-library-viability.md
  • docs/planning/non-apple-media-alternatives.md
  • docs/planning/v0.1.0-execution-handoff.md
  • docs/planning/v0.1.0-first-working-version.md
  • docs/planning/v0.1.0-investigation-findings.md
  • docs/planning/v0.1.0-issues.md
  • docs/planning/v0.1.0-worktree-plan.md
  • docs/reports/SCHEMA_COMPLETENESS_REPORT.json
  • docs/reports/SCHEMA_COMPLETENESS_REPORT.md
  • skills-lock.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

CI and others added 2 commits July 31, 2026 15:29
SBObject proxies resolve sdef term names (name, id, duration, file, ...)
through value(forKey:), not the sdef cocoa keys the inspector was passing
(displayName, uniqueIdentifier, durationDict, URL). The cocoa keys raised
NSUnknownKeyException against a running Final Cut Pro, and ObjC exceptions
are uncatchable from Swift, so the whole test process died.

Every key was probed individually against a live FCP in isolated child
processes before adoption:

- name/id/file/duration/frameDuration/startTime/timecodeFormat and the
  children keys all resolve live.
- persistent ID is declared in the sdef but unimplemented by FCP (errors
  -1728 even in AppleScript); persistentID is now optional on the scripted
  models and read tolerantly.
- media time records arrive as value/timescale/epoch/flags dictionaries,
  which the existing parser already handles.
- timecode format arrives as an OSType NSNumber (drop/ndrp/unsp); the
  parser now decodes four-char codes, with a regression test.

Mock tests re-pin the term-name contract so they can no longer pass while
the live path is broken, and the live test now asserts non-empty library
content when Final Cut is running (verified live: 12 tests, 6 suites pass
with FCP open). Docs known-issue callouts removed accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apple-platform legs were using the library-only FCPKit scheme (no test
action); omit scheme so swift-build derives FCPKit-Package. Stub xmllint
Process usage on iOS/tvOS/watchOS/visionOS. Point CodeQL at xcode-27 so
autobuild can parse swift-tools-version 6.4.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

leogdion and others added 5 commits August 2, 2026 16:26
…42)

* Fix #41: gate match-analysis-type smart collection on FCPXML version

`Defaults.smartCollections()` unconditionally emitted a "Missing Analysis"
smart collection containing `<match-analysis-type>`. That element does not
exist in the FCPXML 1.13 DTD (0 occurrences in FCPXMLv1_13.dtd, 5 in
FCPXMLv1_14.dtd), so every DSL document exported at the library default
version — `FCPXMLVersion.supportedGeneration`, which is 1.13 — was invalid
against Final Cut's own DTD. The `fcpxml-dsl` CLI defaults to 1.14, which is
why the common path validated and this went unnoticed.

Thread the target version through `ResourceStore`, which is already passed
`inout` to every `build` call, and gate the collection on
`compatibility(relativeTo:)` rather than string-matching a version.

The collection is gated, not dropped: the feature-pair acceptance tests
export at 1.14 and their real Final Cut fixtures do contain "Missing
Analysis", so removing it outright would fail the symmetric structural diff.

Also extracts the previously private `assertDTDValidates` helper into a
shared `DTDValidationSupport.swift` for reuse by other test suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Namespace soft-promotion behind an enum instead of global functions

Addresses review feedback on #42: "no global properties or functions".

`softPromote` and its four private helpers become statics on a `SoftPromote`
enum. The entry point is renamed to `promote(_:resources:)` so it does not
overload the private `library(for:version:)` helpers it calls.

These globals predate this branch, but the rule is a general one, so they are
converted here rather than left as the only exceptions in the module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Replace Defaults and SoftPromote with type extensions; adopt typed throws

Addresses review feedback on #42: Defaults belongs on the types it concerns,
and SoftPromote reads as an anti-pattern rather than a namespace.

`Defaults` held two unrelated concerns and is split along that seam:

- The version-gated vocabulary moves onto the type that decides it —
  `FCPXMLVersion.admitsAnalysisMatching` and `.defaultSmartCollections`. Both
  depended on nothing but the version, so this is a clean move. The documented
  `.malformed`-admits-the-element behaviour is preserved.
- The sequence factory becomes `ModelSequence.init(packing:format:)`, an
  extension on the FCPXML model type it constructs. The typealias exists
  because FCPKitDSL declares its own `Sequence`, so the file cannot be named
  for a qualified `FCPKit.Sequence`.

`SoftPromote` becomes `Built.promotedLibrary(resources:)`. The switch is a fold
up a fixed ladder — item to spine to sequence to project to event to library —
where each case enters at its own rung, which is an operation on `Built` rather
than a bag of functions. No protocol here: one implementation, closed case set.

Two incidental fixes fall out of the rewrite: the `.event` arm no longer
inlines `FCPKit.Library(...)` instead of calling its own helper, and the
`.item` arm no longer packs twice (once here, once inside the old
`Defaults.sequence`).

Adopts typed throws across FCPKitDSL. Every throw site in the module already
raised `BuildError` and nothing else, and the module calls no throwing FCPKit
API, so `throws(BuildError)` now runs end to end from `DSLNode.build` through
`Document.export` — callers get a concrete catch type. Closures need explicit
`throws(BuildError)` annotations where they propagate.

Note this narrows `DSLNode.build`: an external conformer declaring plain
`throws` no longer satisfies the protocol. Pre-1.0 and in-policy for a protocol
already documented as evolving, but it is an API narrowing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix #36: lift .anchor onto StoryItem and make DSLNode public

`.anchor(lane:offset:content:)` was declared only on `AssetClip`, so nothing
could be anchored onto a generator or a color. A media-free slide deck needs
exactly that: text over a solid color background.

Introduce `StoryItem` — "content that can sit in a spine", matching the DTD's
`%clip_item;` entity — and hang the shared `.anchor` modifier off it.
`AssetClip`, `Generator`, `Title`, `Gap`, and `Transition` conform directly.
`Color` promotes to `Generator`, since it is a model type in another module
and cannot gain stored properties; `Color.build` already desugared that way,
so anchoring just performs the desugaring one step earlier.

A public protocol requirement cannot mention internal types, so `DSLNode`,
`Built`, and `ResourceStore` become public. Every `ResourceStore` member stays
internal, so only the type name is exposed and no usable extension point ships.

Anchoring onto a `Transition` is accepted but never emitted: the DTD does not
admit anchored items there. Pinned by a test so it stays documented behavior
rather than something a later change "fixes" into a throw.

Also fixes a latent bug: `.anchor` replaced rather than appended, so chained
calls silently dropped earlier lanes. The shared modifier now appends, and
`replacingAnchors` is documented as the pure setter it is. This was latent
because nothing chained; `StoryItem` makes chaining the natural idiom.

Adds the missing `.video` and `.generator` cases to `anchoredExtent`, so an
anchored generator contributes to its parent's extent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cover the new anchoredExtent case, negative lanes, and Title/Gap hosts

Follow-up from an adversarial review of e4325f7.

The `.video` case added to `anchoredExtent` had no test: the existing
"anchored item longer than its background" test used a Title, which the
pre-existing `.title` case already handled. The new branch could have been
deleted with the suite staying green. Now covered by an anchored *generator*
outrunning its background.

Also adds the negative-lane case (legal FCPXML — content below the primary
storyline; only lane 0 is reserved) and Title/Gap as anchor hosts, both of
which gained StoryItem conformance in e4325f7 without direct coverage.

Documents why the `.generator` packing branch passes `anchoredExtent: 0`:
`FCPKit.Generator` has no `anchoredItems` property at all, because
`<generator>` is absent from the DTD's `%anchor_item;` list. Zero is the
correct value there, not a stub — the previous commit message implied that
call site was also updated, which overstated the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Replace global functions with namespaced statics; StoryItem refines DSLNode

Addresses review feedback on #43.

"As a rule never use global functions or properties": the anchored-item
lowering helpers become statics on a new `AnchoredItemBuilder` enum, and the
pre-existing `storyItems` global becomes `StoryItemLowering.items`. The latter
predates this branch, but the rule is a general one, so leaving it behind would
undercut it. `StoryItems.swift` is renamed to match its type.

"Are all StoryItem a DSLNode?" — yes. Every conformer is a `DSLNode`, and the
two extension-based conformances (`Color`, `Generator`) extend types already
declared as such. `StoryItem` now refines `DSLNode` rather than
`DocumentContent`, which makes the relationship explicit and lets the four
`DSLNode, StoryItem` declarations drop the redundant conformance.

This also tightens the protocol: the free `.anchor` extension calls
`replacingAnchors` and relies on the conformer's `build` lowering those anchors,
which only a `DSLNode` can do. The old declaration did not require that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Replace lowering namespaces with a protocol and array extensions

Addresses review feedback on #43: AnchoredItemBuilder and StoryItemLowering are
namespaces holding functions rather than types that mean anything.

The anchoring case had a real duplication behind it. `Anchor.applyLaneOffset`
and `AnchoredItemBuilder.item` each switched over the same four shapes, and both
already omitted `.gap` despite the schema admitting it — two switches, one
concept, already drifted.

`AnchorableItem` states that set once. Each conformer describes how to re-wrap
itself into both ordered-choice containers, so `Built.anchorable` is the only
switch and no downcasts are needed. `Anchor.build` drops from a body with its
own parallel switch to three lines. `.gap` stays unsupported, so behaviour is
unchanged; widening it is a capability change and gets its own issue.

The rest become extensions on what they operate on:

- `Built.anchoredItem()` and `Built.placed(lane:offset:)` — the mapping is
  genuinely `Built` to an anchored item, so it lives on `Built`.
- `[any DSLNode].anchoredItems(resources:)` and
  `[any DocumentContent].spineItems(resources:)` — both always took a
  collection, so they read as collection operations:
  `content.contents.spineItems(resources: &resources)`.

Adopts typed throws across the module, matching #42. Every throw site already
raised `BuildError`, so `throws(BuildError)` runs end to end and callers get a
concrete catch type. Note this narrows `DSLNode.build`: an external conformer
declaring plain `throws` no longer satisfies the protocol. Pre-1.0 and
in-policy for a protocol documented as evolving, but it is an API narrowing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Carry the version property through the v0.1.x merge

`ResourceStore` needs both this branch's public cascade and #42's `version`
property. The merge resolution restored this branch's copy, which predates
#42, so `ResourceStore(version:)` went missing and `Document+Export` failed to
compile against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…yle ids (#44)

* Fix #35: allocate unique text-style-def ids per document

`Title.build` hardcoded the id "ts1" in both the `TextStyle(ref:)` and the
`TextStyleDef(id:)` it emitted, for every title. The DTD declares
`text-style-def/@id` as `ID`, and XML requires `ID` values to be unique within
a document, so any document containing two titles emitted invalid XML.

This was latent only because no shipped document had more than one title. The
presentation deck (#38) has seven.

Add a `textStyleID()` allocator to `ResourceStore`, modeled on the existing
`nextNumber` resource counter but in its own `ts1`, `ts2`, … namespace. Ids are
numbered globally across titles, matching what Final Cut itself writes:
`Tests/FCPKitTests/TestData/UntitledXML.fcpxml` shows two titles producing
ts1..ts4 rather than restarting per title.

The counter starts at 1, so single-title documents still emit exactly "ts1" and
the feature-pair fixtures need no edits.

The DTD regression test exports at 1.14 on purpose: at the 1.13 default the
document is also invalid because the default smart collections emit
`match-analysis-type`, which 1.13 does not declare (#41, fixed separately).
That unrelated failure would otherwise mask the ID regression this guards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix #37: add Title styling and frame positioning modifiers

`Title.build` hardcoded Helvetica 63pt Regular white centered for every title.
A slide deck needs to distinguish a heading from body text and to place text
somewhere other than dead centre.

Adds `TitleStyle` plus `.font`, `.fontSize`, `.fontFace`, `.fontColor`,
`.alignment`, `.bold`, and `.name` modifiers, and `FramePosition` with an
alignment-first `.position(_:inset:)` and an absolute `.position(x:y:)`
escape hatch.

Positioning emits a sibling `adjust-transform`, which composes over whatever
the title preset does internally. The unit is percent of frame HEIGHT on both
axes, measured from the centre, Y-up — verified against real Final Cut output
(TestData/UntitledXML.fcpxml:448, position="-17.8241 7.77778"), which the
tests reproduce exactly rather than asserting a hand-derived number.

The frame size rides on `ResourceStore`, which is already threaded through
every `build` call, so `DSLNode.build`'s signature is untouched. `Sequence`
publishes its format's dimensions before building children and restores the
outer value afterwards, so nested sequences do not leak frame sizes.

Nothing is emitted unless a modifier is applied: an unstyled, unpositioned
title serializes byte-identically to today, `bold` is written only when true,
and `fontSize` collapses whole numbers (63, not 63.0). The feature-pair
structural diff against a real Final Cut export passes unchanged.

Also fills in `AdjustTransform`'s missing DTD attributes — `enabled`,
`rotation`, and `anchor` — which were silently dropped. CodingKeys follow the
DTD's declaration order, in which `enabled` comes first.

Adds post-resolution spine ordering tests: resolution rewrites title elements
on the way out, and the diff engine's inventory is order-blind, so ordering
needed a guard that runs after resolution rather than only after build.

Allows `x` and `y` as identifier names, alongside the existing `id`/`no`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix a crash and two silent-wrong-output bugs in title positioning

Follow-up from an adversarial review of c6b74c5. All three share a root cause:
unvalidated Double -> String conversion, and an Optional treated as "use a
default" rather than "cannot resolve".

1. CRASH. `fontSize` and the position formatter both did an unguarded
   `Int(value)` after a whole-number check. `1e21` is a whole number, so
   `Title("Hi").fontSize(1e21)` trapped with "Double value cannot be converted
   to Int" and took the host process down (signal 5). Infinity and NaN hit the
   same path. Replaced both with a shared `decimalString(_:)` that falls back
   to the plain Double description outside Int's range or when non-finite.
   A library must not crash on user input.

2. `.position(.top, inset: 80)` with no enclosing format silently DROPPED the
   inset and emitted "0 50". An inset is in points, and converting points to
   Final Cut's percent-of-height unit requires the frame height. The absolute
   `.position(x:y:)` path already threw `missingFrameSize` in exactly this
   situation, so the design already agreed it is unresolvable — the alignment
   path just failed silently instead. It now throws too. A zero inset still
   never throws, since plain alignments need no frame size.

3. `.position(.center, inset: 100)` emitted a pointless `adjust-transform`
   with "0 0", breaking the "centred titles emit nothing" invariant while
   ignoring the inset. `.center` is the frame centre on both axes, so an inset
   has no direction to move along; it now always resolves to no transform.

Also corrects a misleading test comment: the fixture whose position value the
formula test reproduces uses FFVideoFormat3840x2160p24, not 1080p. The math is
scale-invariant so the test passed for the right reason, but the stated
provenance would have misled the next person deriving from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Namespace the decimal formatter; document why not Decimal.FormatStyle

Addresses review feedback on #44.

"Make it a rule never to have global functions": `decimalString` becomes
`AttributeValue.decimal(_:)`, and the file is renamed to match. The trivial
`FramePosition.format` wrapper is inlined rather than left as an alias.

"Is there something here to use Decimal.FormatStyle?" — I tested it, and no.
Three reasons, all verified rather than assumed:

- It is locale-aware. `63.5` renders as "63,5" under de_DE and fr_FR, which
  would emit invalid FCPXML on any non-English machine. This is the decisive
  one, and it is now pinned by a test so nobody swaps a locale-aware formatter
  back in.
- It rounds to six fractional digits, so a position component like
  7.777777777 would be silently truncated.
- `Decimal(Double.infinity)` traps, so it does not even solve the crash that
  motivated this helper.

It does collapse whole numbers natively, which is the one thing it would have
bought us — not worth the three costs above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Express the FCPXML decimal formatter as String.init(fcpxmlValue:)

Addresses review feedback on #44: make it an extension on Double or an init
extension on String rather than a namespaced static.

Went with `String.init(fcpxmlValue:)`. Both call sites produce a String from a
Double, which is exactly what an init is for, and it keeps the conversion with
the type being produced instead of hanging a domain-specific property off
`Double`.

The argument label matters: a bare `String(_: Double)` already exists via
`LosslessStringConvertible`, so an unlabelled init would quietly overload it.
Verified `String(63.5)` still routes to the stdlib and returns "63.5" while
`String(fcpxmlValue:)` applies the whole-number collapse.

Behaviour is unchanged — same guards, same fallbacks, same tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Adopt typed throws across FCPKitDSL

Matches the typed-throws work on #42 and #43 so the three branches agree.

Every throw site in FCPKitDSL already raised `BuildError` and nothing else, and
the module calls no throwing FCPKit API, so `throws(BuildError)` runs end to end
from `DSLNode.build` through `Document.export`. Callers get a concrete catch
type instead of `any Error`.

Closures that propagate need explicit `throws(BuildError)` annotations. In
`Title.build` the `flatMap`/`map` chain resolving a deferred position is
rewritten as a plain `if let`, which types cleanly and reads better than an
annotated chain.

Note this narrows `DSLNode.build`: an external conformer declaring plain
`throws` no longer satisfies the protocol. Pre-1.0 and in-policy for a protocol
documented as evolving, but it is an API narrowing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Step 8: Docs sync after code lands

2 participants