Follow-up to PR #117 and the format-interop review. Principle (the Fontra lesson, adapted): model what you model, preserve what you don't — a UFO placed in the middle of a pipeline must come back with everything Shift doesn't model intact.
Good news from the audit: the preservation architecture already exists end-to-end. LibData/LibValue in shift-font (including a Data bytes variant), reader conversion for font/glyph/layer libs and groups (ufo/reader.rs), writer conversion back to plists (ufo/writer.rs:130-153), and .shift persistence via modules/shift.libData.json (shift-source/src/package.rs:26,467). The remaining holes are bounded:
1. plist::Value::Date and Uid are destroyed (bug)
ufo/reader.rs:99 catch-all maps them to LibValue::String(String::new()) — empty string in, empty string round-tripped out. Dates are common in real-world libs (tool timestamps). Fix: add LibValue::Date (and either a Uid variant or a tagged encoding), extend the writer conversion and the .shift LibValueDoc accordingly. Also: Integer(i).as_signed().unwrap_or(0) at :85 silently zeroes u64 values above i64::MAX (edge case, same fix pass).
2. UFO data/ and images/ directories are silently dropped
Arbitrary binary payloads (source images, tool caches, foundry assets) never enter the IR. norad exposes them via DataRequest. Fix: carry them as opaque binary entries in the IR and .shift package (they're zip entries — natural fit), write back verbatim.
3. layerinfo.plist (per-layer color, lib) — verify and carry
Not obviously read today. Layer colors are widely used (background/marking workflows).
4. Unmapped fontinfo fields — the big one, needs a decision
Reader/writer copy a hand-picked fontinfo field list (names, versions, metrics). UFO fontinfo has ~100 fields — postscript blue values, OS/2 fields, font-level guidelines, WOFF metadata — all silently stripped on round-trip. Two strategies:
- (a) Expand the mapped field set to full norad
FontInfo coverage — mechanical but long, and every field becomes modeled (editable, interpolatable where relevant).
- (b) Passthrough map — carry unmapped fontinfo as an opaque preserved blob alongside the modeled fields.
(a) is more honest long-term (fontinfo is font data, not tool data); (b) ships the safety guarantee immediately. Could do (b) now and burn it down with (a) over time.
Acceptance
A pinned round-trip test: open a UFO fixture stuffed with every plist type, data/images payloads, layerinfo, and exotic fontinfo fields → save → byte-meaningful equality on everything not deliberately modeled-and-edited. This test is the preservation contract; fidelity regressions are silent by nature and only a pinned fixture makes them loud.
Items 1–3 are mechanical and agent-sized. Item 4(a) vs 4(b) is the one real design decision.
Follow-up to PR #117 and the format-interop review. Principle (the Fontra lesson, adapted): model what you model, preserve what you don't — a UFO placed in the middle of a pipeline must come back with everything Shift doesn't model intact.
Good news from the audit: the preservation architecture already exists end-to-end.
LibData/LibValuein shift-font (including aDatabytes variant), reader conversion for font/glyph/layer libs and groups (ufo/reader.rs), writer conversion back to plists (ufo/writer.rs:130-153), and.shiftpersistence viamodules/shift.libData.json(shift-source/src/package.rs:26,467). The remaining holes are bounded:1.
plist::Value::DateandUidare destroyed (bug)ufo/reader.rs:99catch-all maps them toLibValue::String(String::new())— empty string in, empty string round-tripped out. Dates are common in real-world libs (tool timestamps). Fix: addLibValue::Date(and either aUidvariant or a tagged encoding), extend the writer conversion and the.shiftLibValueDocaccordingly. Also:Integer(i).as_signed().unwrap_or(0)at:85silently zeroes u64 values above i64::MAX (edge case, same fix pass).2. UFO
data/andimages/directories are silently droppedArbitrary binary payloads (source images, tool caches, foundry assets) never enter the IR. norad exposes them via
DataRequest. Fix: carry them as opaque binary entries in the IR and.shiftpackage (they're zip entries — natural fit), write back verbatim.3.
layerinfo.plist(per-layer color, lib) — verify and carryNot obviously read today. Layer colors are widely used (background/marking workflows).
4. Unmapped
fontinfofields — the big one, needs a decisionReader/writer copy a hand-picked fontinfo field list (names, versions, metrics). UFO fontinfo has ~100 fields — postscript blue values, OS/2 fields, font-level guidelines, WOFF metadata — all silently stripped on round-trip. Two strategies:
FontInfocoverage — mechanical but long, and every field becomes modeled (editable, interpolatable where relevant).(a) is more honest long-term (fontinfo is font data, not tool data); (b) ships the safety guarantee immediately. Could do (b) now and burn it down with (a) over time.
Acceptance
A pinned round-trip test: open a UFO fixture stuffed with every plist type, data/images payloads, layerinfo, and exotic fontinfo fields → save → byte-meaningful equality on everything not deliberately modeled-and-edited. This test is the preservation contract; fidelity regressions are silent by nature and only a pinned fixture makes them loud.
Items 1–3 are mechanical and agent-sized. Item 4(a) vs 4(b) is the one real design decision.