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 <noreply@anthropic.com>
|
|
@ -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 `<svg>` root, the metadata comment, the y-flip `<g>`, a per-layer `<g>`). A
|
||||
glyph with no bundled outline is **surfaced as a diagnostic** and drawn as a
|
||||
visible bounding-box fallback `<rect>` — 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 `<svg>` root, the metadata comment, the
|
||||
y-flip `<g>`, a per-layer `<g>`). 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 `<rect>` — 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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `<svg>` root, a metadata comment, the y-flip group, a
|
||||
//! per-layer `<g>`). The renderer makes *SVG-encoding* choices only — grouping,
|
||||
//! per-layer `<g>`). 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,
|
||||
//! `<path>` vs `<rect>` 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,
|
||||
" <!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; \
|
||||
geometry is the resolved layout verbatim, no engraving performed here -->\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 {
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\" viewBox=\"0 0 1 1\">\n\
|
||||
\x20\x20<!-- epiphany-render-svg: empty resolved layout (no glyphs) -->\n\
|
||||
</svg>\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!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\" viewBox=\"0 0 1 1\">\n\
|
||||
\x20\x20<!-- epiphany-render-svg: empty resolved layout (no glyphs); {} -->\n\
|
||||
</svg>\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]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="824.5643" height="110.24" viewBox="0 0 82.4564 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.0599 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="2ef8d2dba955aee38147023b72bdabb7" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="888.7701" height="110.24" viewBox="0 0 88.877 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.065 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="2ef8d2dba955aee38147023b72bdabb7" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="279.5357" height="110.24" viewBox="0 0 27.9536 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.0599 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="bd56ad529a36a7bbf2b4c343886b55e5" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="393.3" height="110.24" viewBox="0 0 39.33 11.024">
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here -->
|
||||
<!-- epiphany-render-svg: glyphs are genuine Bravura SMuFL outlines; geometry is the resolved layout verbatim, no engraving performed here; every glyph and stroke carries a data-prov trace to its score-graph source -->
|
||||
<g transform="translate(3.065 7.392) scale(1 -1)">
|
||||
<g data-layer="0">
|
||||
<line x1="0" y1="0" x2="0" y2="0" stroke="#000000" stroke-width="0" data-prov="bd56ad529a36a7bbf2b4c343886b55e5" data-source-kind="6" data-kind="stroke"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |