epiphany/spikes/editor-toolkit/round1-oracle/src/main.rs

315 lines
13 KiB
Rust

//! Generates the committed Round-1 oracle: `oracle.json` (machine-readable,
//! consumed by candidate runs in a later packet) and `ORACLE_SUMMARY.md`
//! (human-readable, for the user's pin-13 review). Run with `cargo run -p
//! round1-oracle` from `spikes/editor-toolkit/round1-oracle/`.
use epiphany_glyphs::BravuraGlyphCatalog;
use epiphany_layout_ir::GlyphCatalog;
use round1_oracle::{derive_glyph_oracle, GlyphOracle, Requirement, SampleClass};
use serde::Serialize;
use std::fmt::Write as _;
use std::fs;
/// Round 1's bounded-hole check class: "≥3 must-be-ink and ≥3 must-be-
/// background points, each inside a bounded hole".
const HOLE_GLYPHS: &[(&str, usize)] = &[
("gClef", 4),
("timeSig8", 3),
("accidentalFlat", 2),
("noteheadHalf", 2),
];
/// Round 1's disjoint-component check class: "no bounded hole ... ≥1 ink
/// point inside each of its three filled subpaths".
const DISJOINT_GLYPHS: &[(&str, usize)] = &[("fClef", 3)];
#[derive(Serialize)]
struct OracleFile {
contract: &'static str,
round: &'static str,
render_transform_rule: &'static str,
flatten_tolerance_staff_space: f64,
clearance_floor_device_px: f64,
glyphs: Vec<GlyphOracle>,
}
fn main() {
let catalog = BravuraGlyphCatalog;
let mut glyphs = Vec::new();
let mut findings = Vec::new();
for (name, expected_subpaths) in HOLE_GLYPHS {
let data = catalog
.render_data(name)
.unwrap_or_else(|| panic!("{name}: BravuraGlyphCatalog has no render data"));
match derive_glyph_oracle(
name,
&data.outline,
Some(*expected_subpaths),
Requirement::BoundedHole,
) {
Ok(oracle) => {
if oracle.subpath_count != *expected_subpaths {
findings.push(format!(
"{name}: parsed {} subpaths, contract's verified-starting-point section \
records {}",
oracle.subpath_count, expected_subpaths
));
}
if !oracle.satisfied {
let d = &oracle.hole_diagnostic;
let detail = if d.raw_hole_grid_hits == 0 {
"this glyph has NO bounded hole at all under even-odd fill — every \
subpath is either the outer silhouette or a disjoint ink island, none \
nested inside another"
.to_string()
} else {
format!(
"this glyph has a bounded hole ({} grid hits) but the best \
achievable clearance in it is {:.3} device px, below the \
{}px floor",
d.raw_hole_grid_hits,
d.best_hole_clearance_device_px.unwrap_or(f64::NAN),
round1_oracle::CLEARANCE_MIN_DEVICE_PX
)
};
findings.push(format!(
"{name}: fewer than {} must-be-background sample points could be \
derived — {detail}. Per Round 1's hole-check clause ('each inside a \
bounded hole ... asserted by the derivation, not assumed'), no fallback \
point was substituted; {name}'s oracle carries ink points only and is \
NOT satisfied.",
round1_oracle::POINTS_PER_CLASS
));
}
glyphs.push(oracle);
}
Err(e) => findings.push(e),
}
}
for (name, expected_subpaths) in DISJOINT_GLYPHS {
let data = catalog
.render_data(name)
.unwrap_or_else(|| panic!("{name}: BravuraGlyphCatalog has no render data"));
match derive_glyph_oracle(
name,
&data.outline,
Some(*expected_subpaths),
Requirement::DisjointComponents,
) {
Ok(oracle) => {
if oracle.subpath_count != *expected_subpaths {
findings.push(format!(
"{name}: parsed {} subpaths, contract's verified-starting-point section \
records {}",
oracle.subpath_count, expected_subpaths
));
}
// `oracle.satisfied` is expected true here (no bounded-hole
// shortfall is possible for this class — see
// `derive_glyph_oracle`'s DisjointComponents arm, which only
// returns Ok once every subpath is covered), but this is
// reported as a finding rather than assumed, matching every
// other status check in this file.
if !oracle.satisfied {
findings.push(format!(
"{name}: disjoint-component coverage requirement not met — see \
subpath_coverage_satisfied in oracle.json"
));
}
glyphs.push(oracle);
}
Err(e) => findings.push(e),
}
}
let oracle_file = OracleFile {
contract: "spec/CONTRACT_EDITOR_T4_SPIKE.md Round 1 (criterion 1, compound-path fill \
correctness)",
round: "round1",
render_transform_rule: "device = (staff.x * scale + tx, ty - staff.y * scale); scale = \
100 device px per staff space; tx/ty center the glyph's own flattened bounding box \
in a 1920x1080 target (pin 4); staff-space is y-up, device space is y-down",
flatten_tolerance_staff_space: round1_oracle::FLATTEN_TOLERANCE,
clearance_floor_device_px: round1_oracle::CLEARANCE_MIN_DEVICE_PX,
glyphs,
};
let json = serde_json::to_string_pretty(&oracle_file).expect("oracle serializes");
fs::write("oracle.json", &json).expect("write oracle.json");
let summary = render_summary(&oracle_file, &findings);
fs::write("ORACLE_SUMMARY.md", &summary).expect("write ORACLE_SUMMARY.md");
println!("wrote oracle.json ({} bytes)", json.len());
println!("wrote ORACLE_SUMMARY.md");
if !findings.is_empty() {
println!("\nFINDINGS:");
for f in &findings {
println!(" - {f}");
}
}
}
fn render_summary(oracle: &OracleFile, findings: &[String]) -> String {
let mut s = String::new();
writeln!(
s,
"# Round 1 precommitted oracle — compound-path fill correctness\n"
)
.unwrap();
writeln!(
s,
"Generated by `round1-oracle` (`spikes/editor-toolkit/round1-oracle`). Governed by \
`spec/CONTRACT_EDITOR_T4_SPIKE.md` Revision 6, Round 1 (\"criterion 1, compound-path \
fill correctness\"). No rendering crate was used to produce this file — every point \
below is derived from the typed `PathCommand` outline alone.\n\n\
Two check classes, testing two different properties: **bounded-hole** (`gClef`, \
`timeSig8`, `accidentalFlat`, `noteheadHalf`) requires ≥3 ink and ≥3 must-be-background \
points inside a bounded hole; **disjoint-component** (`fClef`) has no bounded hole by \
design and instead requires ≥1 ink point inside each of its three filled subpaths, \
tagged with `subpath_index`.\n"
)
.unwrap();
writeln!(s, "## Render transform (pinned)\n").unwrap();
writeln!(
s,
"`{}`\n\n- Flatten tolerance: `{}` staff-space units (Bezier chord deviation).\n\
- Clearance floor: `{}` device px.\n",
oracle.render_transform_rule,
oracle.flatten_tolerance_staff_space,
oracle.clearance_floor_device_px
)
.unwrap();
writeln!(s, "## Overall status\n").unwrap();
writeln!(s, "| glyph | requirement | satisfied | ring_signed_areas |").unwrap();
writeln!(s, "|---|---|---|---|").unwrap();
for g in &oracle.glyphs {
let requirement = match g.requirement {
Requirement::BoundedHole => "BoundedHole",
Requirement::DisjointComponents => "DisjointComponents",
};
let areas = g
.ring_signed_areas
.iter()
.map(|a| format!("{a:.3}"))
.collect::<Vec<_>>()
.join(", ");
writeln!(
s,
"| `{}` | {} | **{}** | [{}] |",
g.name, requirement, g.satisfied, areas
)
.unwrap();
}
writeln!(
s,
"\n`fClef` is `satisfied = true` with zero background points — that is the correct, \
designed-for outcome for its `DisjointComponents` requirement class, not a shortfall. \
The `satisfied` column above is the field to read; `background_satisfied` alone would \
make that correct outcome indistinguishable from a bounded-hole glyph's genuine failure.\n"
)
.unwrap();
if !findings.is_empty() {
writeln!(s, "## Findings\n").unwrap();
for f in findings {
writeln!(s, "- **{f}**").unwrap();
}
writeln!(s).unwrap();
}
for g in &oracle.glyphs {
writeln!(s, "## `{}`\n", g.name).unwrap();
let requirement = match g.requirement {
Requirement::BoundedHole => "BoundedHole (≥3 ink, ≥3 background inside a bounded hole)",
Requirement::DisjointComponents => {
"DisjointComponents (no background requirement; ≥1 ink point per filled subpath)"
}
};
let subpath_note = match g.expected_subpath_count {
Some(exp) if exp == g.subpath_count => format!("matches recorded {exp}"),
Some(exp) => format!("**MISMATCH** — recorded {exp}, parsed {}", g.subpath_count),
None => "no recorded expectation".to_string(),
};
let areas = g
.ring_signed_areas
.iter()
.map(|a| format!("{a:.3}"))
.collect::<Vec<_>>()
.join(", ");
writeln!(
s,
"- Requirement class: {requirement}\n\
- Overall status: **satisfied = {}**\n\
- Subpaths: {} ({subpath_note})\n\
- Ring signed areas (staff-space², shoelace, ring order): `[{areas}]`\n\
- Bounding box (staff space): `{:?}`\n\
- Outer contour ring index: {}\n\
- Transform: scale={}, tx={:.3}, ty={:.3}, target={}x{}\n\
- background_required={}, background_satisfied={}\n\
- subpath_coverage_required={}, subpath_coverage_satisfied={}\n\
- Ink candidates found (>= {}px clearance): {}\n\
- Background/hole candidates found (>= {}px clearance): {}\n\
- Ink point spacing relaxed: {}\n\
- Background point spacing relaxed: {}\n",
g.satisfied,
g.subpath_count,
g.bbox_staff,
g.outer_contour_ring_index,
g.transform.scale,
g.transform.tx,
g.transform.ty,
g.transform.target_width as i32,
g.transform.target_height as i32,
g.background_required,
g.background_satisfied,
g.subpath_coverage_required,
g.subpath_coverage_satisfied,
oracle.clearance_floor_device_px,
g.ink_candidates_found,
oracle.clearance_floor_device_px,
g.background_candidates_found,
g.ink_spacing_relaxed,
g.background_spacing_relaxed,
)
.unwrap();
writeln!(
s,
"| class | subpath_index | staff (x, y) | device (x, y) | clearance (device px) | hole evidence |"
)
.unwrap();
writeln!(s, "|---|---|---|---|---|---|").unwrap();
for p in &g.points {
let class = match p.class {
SampleClass::Ink => "Ink",
SampleClass::Background => "Background",
};
let subpath = match p.subpath_index {
Some(i) => i.to_string(),
None => "-".to_string(),
};
let evidence = match &p.hole_evidence {
Some(ev) => format!(
"inside_outer_contour={}, even_odd_filled={}, nonzero_filled={} (outer ring {})",
ev.inside_outer_contour, ev.even_odd_filled, ev.nonzero_filled, ev.outer_contour_ring_index
),
None => "-".to_string(),
};
writeln!(
s,
"| {class} | {subpath} | ({:.4}, {:.4}) | ({:.2}, {:.2}) | {:.3} | {evidence} |",
p.staff.0, p.staff.1, p.device.0, p.device.1, p.clearance_device_px
)
.unwrap();
}
writeln!(s).unwrap();
}
s
}