From 41cd8bf58b91a83075b9dccd68b0c7d5abef74ec Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sat, 27 Jun 2026 09:45:56 -0400 Subject: [PATCH] Agent I-4b: a trace-free render declares itself display-only `RenderOptions::emit_provenance = false` dropped every data-prov trace silently, so the output was indistinguishable from an archival render even though it no longer satisfies the renderer's "every element traces to its source" contract. Now the SVG's metadata comment declares the provenance state, so suppression is announced rather than silent: - archival (default): "...; every glyph and stroke carries a data-prov trace to its score-graph source" - display-only (false): "...; provenance traces suppressed (display-only output, not archival)" A shared `provenance_note(emit_provenance)` helper feeds both the main render and the empty-canvas path, so an empty trace-free layout is held to the same honesty contract as a full one (neither can drift). The module doc and the render-svg DECISIONS.md non-overreach rule now frame data-prov as the default archival contract plus an explicit, declared display-only mode. Tests assert the suppressed marker (full and empty layouts) and that the default render declares traces present. Goldens regenerated: the default (archival) render's metadata comment now carries the new "every glyph and stroke carries a data-prov trace" clause, so the four `.svg` goldens change by that one line (the snapshots, which omit the comment, do not). Full gate green: build, fmt, clippy, 581 tests, conformance scale 1. Co-Authored-By: Claude Opus 4.8 --- crates/epiphany-render-svg/DECISIONS.md | 19 +++-- crates/epiphany-render-svg/src/svg.rs | 76 +++++++++++++++---- .../ten_measure_single_staff.engrave.svg | 2 +- .../golden/ten_measure_single_staff.stub.svg | 2 +- .../tests/golden/valid_score_rich.engrave.svg | 2 +- .../tests/golden/valid_score_rich.stub.svg | 2 +- 6 files changed, 79 insertions(+), 24 deletions(-) diff --git a/crates/epiphany-render-svg/DECISIONS.md b/crates/epiphany-render-svg/DECISIONS.md index c970259..a4b0a41 100644 --- a/crates/epiphany-render-svg/DECISIONS.md +++ b/crates/epiphany-render-svg/DECISIONS.md @@ -22,13 +22,18 @@ no renderer change (the demo binary's `--solver=stub|real` flag exercises both). ## The non-overreach rule (Chapter 7 / QUICKSTART, Agent I) The renderer makes **SVG-encoding choices only** and **no engraving-semantic** -choices. Every emitted element traces to a `ResolvedGlyph` (and thus a score-graph -source, via `data-prov`/`data-source-kind`) or to a declared renderer wrapper -(the `` root, the metadata comment, the y-flip ``, a per-layer ``). A -glyph with no bundled outline is **surfaced as a diagnostic** and drawn as a -visible bounding-box fallback `` — never silently dropped and never -invented. The acceptance harness asserts one drawn element per glyph and one -provenance trace per drawn element. +choices. In the default **archival** mode every emitted element traces to a +`ResolvedGlyph` (and thus a score-graph source, via `data-prov`/`data-source-kind`) +or to a declared renderer wrapper (the `` root, the metadata comment, the +y-flip ``, a per-layer ``). Traces can be turned off +(`RenderOptions::emit_provenance = false`) for a smaller **display-only** SVG; that +is an explicit mode the **metadata comment declares** (archival → "carries a +data-prov trace", display-only → "provenance traces suppressed"), so a trace-free +SVG — including the empty canvas — announces itself rather than passing as +archival. A glyph with no bundled outline is **surfaced as a diagnostic** and drawn +as a visible bounding-box fallback `` — never silently dropped and never +invented. The acceptance harness (archival mode) asserts one drawn element per +glyph and one provenance trace per drawn element. ## Implementation decisions (QUICKSTART "Decisions you'll need to make") diff --git a/crates/epiphany-render-svg/src/svg.rs b/crates/epiphany-render-svg/src/svg.rs index 57763f8..47a1b4e 100644 --- a/crates/epiphany-render-svg/src/svg.rs +++ b/crates/epiphany-render-svg/src/svg.rs @@ -6,7 +6,11 @@ //! (`spec/PHASE2_QUICKSTART.md`, Agent I), every emitted SVG element traces to a //! [`ResolvedGlyph`] (and thus to its score-graph source) or to a declared //! renderer wrapper (the `` root, a metadata comment, the y-flip group, a -//! per-layer ``). The renderer makes *SVG-encoding* choices only — grouping, +//! per-layer ``). Provenance traces are on by default; turning them off (when +//! [`RenderOptions::emit_provenance`] is `false`) is an explicit display-only mode +//! that the metadata comment *declares*, so a trace-free SVG announces itself +//! rather than passing as an archival one. The renderer makes *SVG-encoding* +//! choices only — grouping, //! `` vs `` fallback, the viewBox, the coordinate flip, colour //! representation. It makes **no engraving-semantic** choices (stem direction, //! spacing, beam slope, accidental placement, glyph selection): those are the @@ -63,6 +67,9 @@ pub struct RenderOptions { /// How glyphs are drawn. pub glyph_mode: GlyphMode, /// Emit `data-*` provenance attributes tracing each element to its source. + /// `true` (the default) is the archival/traceable mode; `false` is an explicit + /// display-only mode that the emitted SVG's metadata comment *declares*, so the + /// absence of traces is announced rather than silently produced. pub emit_provenance: bool, } @@ -202,7 +209,7 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut Some(b) => b, // Empty layout: a minimal, valid, honest empty canvas. None => { - let svg = empty_svg(); + let svg = empty_svg(options.emit_provenance); let well_formed = check_well_formed(&svg).is_ok(); debug_assert!(well_formed); return RenderOutput { @@ -255,10 +262,15 @@ pub fn render(resolved: &ResolvedLayoutIR, options: &RenderOptions) -> RenderOut num(width), num(height), ); - // Declared metadata wrapper (a comment — honest about what this is). - s.push_str( + // Declared metadata wrapper (a comment — honest about what this is, including + // whether provenance traces are present: suppressing them is an explicit + // display-only choice the output announces rather than dropping silently). + let _ = writeln!( + s, " \n", + geometry is the resolved layout verbatim, no engraving performed here; \ + {} -->", + provenance_note(options.emit_provenance), ); // The single y-flip wrapper: staff-space/y-up world -> SVG y-down. let _ = writeln!( @@ -560,13 +572,28 @@ fn colour(rgba: u32) -> (String, String) { (fill, opacity) } -/// A minimal, valid empty SVG for a layout with nothing to draw. -fn empty_svg() -> String { - "\n\ - \n\ - \x20\x20\n\ - \n" - .to_owned() +/// The provenance-state clause of the metadata comment. Archival mode declares +/// traces present; display-only mode declares them suppressed — so a trace-free +/// SVG (including the empty canvas) announces itself rather than passing as +/// archival. Shared by the main render and [`empty_svg`] so neither can drift. +fn provenance_note(emit_provenance: bool) -> &'static str { + if emit_provenance { + "every glyph and stroke carries a data-prov trace to its score-graph source" + } else { + "provenance traces suppressed (display-only output, not archival)" + } +} + +/// A minimal, valid empty SVG for a layout with nothing to draw — still declaring +/// its provenance state, so an empty trace-free render is honest like a full one. +fn empty_svg(emit_provenance: bool) -> String { + format!( + "\n\ + \n\ + \x20\x20\n\ + \n", + provenance_note(emit_provenance), + ) } /// Formats a coordinate to at most 4 decimals, trimming trailing zeros and @@ -767,10 +794,22 @@ mod tests { assert!(out.is_well_formed()); assert_eq!(out.stats.glyph_count, 0); assert_eq!(out.stats.path_count, 0); + + // Even with nothing to draw, suppressing provenance is declared display-only + // — the empty canvas is held to the same honesty contract as a full render. + let suppressed = render( + &layout, + &RenderOptions { + emit_provenance: false, + ..RenderOptions::default() + }, + ); + assert!(suppressed.is_well_formed()); + assert!(suppressed.svg.contains("provenance traces suppressed")); } #[test] - fn provenance_can_be_suppressed() { + fn provenance_can_be_suppressed_but_is_declared() { let layout = stub_layout(5); let out = render( &layout, @@ -782,6 +821,17 @@ mod tests { assert_eq!(out.stats.provenance_count, 0); assert!(!out.svg.contains("data-prov=")); assert!(out.is_well_formed()); + // Suppression is announced, not silent: the metadata comment declares the + // output display-only, so it can't be mistaken for a traceable/archival one. + assert!( + out.svg.contains("provenance traces suppressed"), + "a trace-free SVG must declare itself display-only" + ); + + // The default (archival) render instead declares that traces are present. + let archival = render(&layout, &RenderOptions::default()); + assert!(archival.svg.contains("data-prov trace")); + assert!(!archival.svg.contains("provenance traces suppressed")); } #[test] diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg index 08b0108..5872acf 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.engrave.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg index ec9bda4..7c832af 100644 --- a/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/ten_measure_single_staff.stub.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg index 1346811..e525062 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.engrave.svg @@ -1,6 +1,6 @@ - + diff --git a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg index fe573e5..114502a 100644 --- a/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg +++ b/crates/epiphany-render-svg/tests/golden/valid_score_rich.stub.svg @@ -1,6 +1,6 @@ - +