Problem
Sources/FCPKitDSL/Title.swift:64-76 hardcodes the id "ts1" in both the
TextStyle(ref:) and the TextStyleDef(id:) it emits — for every title:
let style = FCPKit.TextStyle(ref: "ts1", content: text)
let definition = FCPKit.TextStyleDef(id: "ts1", textStyle: ...)
Final Cut's own DTD (FCPXMLv1_14.dtd:581, shipped inside
Final Cut Pro.app/Contents/Frameworks/Interchange.framework) declares:
<!ATTLIST text-style-def id ID #REQUIRED>
XML requires values of type ID to be unique within a document. Any document
containing two titles therefore emits invalid XML today. This is latent only
because no shipped document has more than one title; the presentation deck has seven.
Reproduced
This four-line document is enough:
Project(name: "Two Titles") {
Sequence {
Title("First", duration: FCPTime(numerator: 5))
Title("Second", duration: FCPTime(numerator: 5))
}
}
Exporting it emits text-style-def id="ts1" twice, and validating the output against
Final Cut's 1.14 DTD fails:
$ xmllint --noout --valid twotitles.xml
twotitles.xml:29: element text-style-def: validity error : ID ts1 already defined
<text-style-def id="ts1">
Real Final Cut output shows the correct behavior: in
Tests/FCPKitTests/TestData/UntitledXML.fcpxml, two titles produce ids
ts1, ts2, ts3, ts4 — numbered globally across separate titles, not restarted
per title. Match that.
Files
- Modify
Sources/FCPKitDSL/ResourceStore.swift — add a text-style id allocator
- Modify
Sources/FCPKitDSL/Title.swift — consume it in build
API
Internal only; no public surface change. ResourceStore already has a nextNumber
counter for resource ids (ResourceStore.swift:41) — model this on it:
private var nextTextStyleNumber = 1
internal mutating func textStyleID() -> String {
defer { nextTextStyleNumber += 1 }
return "ts\(nextTextStyleNumber)"
}
Title.build already receives inout ResourceStore, so no signature change is
needed — take one let styleID = resources.textStyleID() and use it in both places.
Tests
New Tests/FCPKitDSLTests/TextStyleIDTests.swift:
- A document with three titles yields
textStyleDef ids ["ts1", "ts2", "ts3"], all
distinct.
- Each title's
text[0].textStyle[0].ref equals that same title's
textStyleDef[0].id — the styles must stay correctly paired, not merely unique.
- The multi-title document passes
assertDTDValidates. This is the assertion that
fails on main today and is the regression guard.
Acceptance criteria
- A two-title document DTD-validates.
- Single-title documents still emit exactly
ts1, because the counter starts at 1 —
so FeaturePairAcceptanceTests.titlesFeaturePairMatchesAfterNormalize passes
unchanged, with no fixture edits. This structural diff against a real Final Cut
export is the backward-compatibility proof.
Conventions
- MIT header block on every new file (see
Scripts/header.sh).
- Swift Testing (
import Testing, @Test) for new tests, per .claude/agent-notes.md. Include "Tests" in either the parent enum or the child struct, never both.
- Doc comments on every public declaration.
- Keep files under 225 lines (SwiftLint
file_length); prefer Type+Modifiers.swift splits.
- Run
swift test and swift run fcpxml-diff schema-completeness Tests/FCPKitTests/TestData before each PR. Prefer opening a PR over merging.
Full spec with context and rationale: docs/planning/demo-presentation-video.md
Problem
Sources/FCPKitDSL/Title.swift:64-76hardcodes the id"ts1"in both theTextStyle(ref:)and theTextStyleDef(id:)it emits — for every title:Final Cut's own DTD (
FCPXMLv1_14.dtd:581, shipped insideFinal Cut Pro.app/Contents/Frameworks/Interchange.framework) declares:XML requires values of type
IDto be unique within a document. Any documentcontaining two titles therefore emits invalid XML today. This is latent only
because no shipped document has more than one title; the presentation deck has seven.
Reproduced
This four-line document is enough:
Exporting it emits
text-style-def id="ts1"twice, and validating the output againstFinal Cut's 1.14 DTD fails:
Real Final Cut output shows the correct behavior: in
Tests/FCPKitTests/TestData/UntitledXML.fcpxml, two titles produce idsts1, ts2, ts3, ts4— numbered globally across separate titles, not restartedper title. Match that.
Files
Sources/FCPKitDSL/ResourceStore.swift— add a text-style id allocatorSources/FCPKitDSL/Title.swift— consume it inbuildAPI
Internal only; no public surface change.
ResourceStorealready has anextNumbercounter for resource ids (
ResourceStore.swift:41) — model this on it:Title.buildalready receivesinout ResourceStore, so no signature change isneeded — take one
let styleID = resources.textStyleID()and use it in both places.Tests
New
Tests/FCPKitDSLTests/TextStyleIDTests.swift:textStyleDefids["ts1", "ts2", "ts3"], alldistinct.
text[0].textStyle[0].refequals that same title'stextStyleDef[0].id— the styles must stay correctly paired, not merely unique.assertDTDValidates. This is the assertion thatfails on
maintoday and is the regression guard.Acceptance criteria
ts1, because the counter starts at 1 —so
FeaturePairAcceptanceTests.titlesFeaturePairMatchesAfterNormalizepassesunchanged, with no fixture edits. This structural diff against a real Final Cut
export is the backward-compatibility proof.
Conventions
Scripts/header.sh).import Testing,@Test) for new tests, per.claude/agent-notes.md. Include "Tests" in either the parent enum or the child struct, never both.file_length); preferType+Modifiers.swiftsplits.swift testandswift run fcpxml-diff schema-completeness Tests/FCPKitTests/TestDatabefore each PR. Prefer opening a PR over merging.Full spec with context and rationale: docs/planning/demo-presentation-video.md