Prepare complete-scene diagram readability fixtures and probe

This commit is contained in:
2026-10-01 17:42:24 -07:00
parent 715036e132
commit 2aac3e8353
11 changed files with 2560 additions and 0 deletions
@@ -0,0 +1,548 @@
//! Experimental geometry evidence, not a native selection or usability oracle.
//! See harness/experiments/diagram-readability-20260929/README.md.
use oa_core::{Diagram, Id, Position, Project, persistence, projection, routing, validation};
use serde_json::{Value, json};
use std::{
error::Error,
fs::{self, OpenOptions},
io::Write,
path::Path,
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
const MIXED: &[u8] = include_bytes!(
"../../../harness/experiments/diagram-readability-20260929/mixed-dispatch-original.json"
);
const WARNINGS: &[u8] = include_bytes!(
"../../../harness/experiments/diagram-readability-20260929/mixed-dispatch-expected-diagnostics.json"
);
const RELEASE: &[u8] = include_bytes!("../../../examples/release1/model.json");
const RELEASE_WARNINGS: &[u8] =
include_bytes!("../../../examples/release1/expected-diagnostics.json");
const EIGHT_VIEW: &[u8] = include_bytes!(
"../../../harness/experiments/diagram-readability-20260929/eight-card-view.json"
);
const MAIN: &str = "00000000-0000-0000-0000-0000000186a1";
const EIGHT: &str = "00000000-0038-4000-8000-000000000001";
fn canonical_diagnostics(value: Value) -> Result<Vec<String>> {
let mut rows = value
.as_array()
.ok_or("Expected a diagnostics array")?
.iter()
.map(serde_json::to_string)
.collect::<std::result::Result<Vec<_>, _>>()?;
rows.sort();
Ok(rows)
}
fn admit(bytes: &[u8], baseline: &[u8]) -> Result<Project> {
let project = persistence::decode(bytes)?;
let issues = validation::validate(&project);
if issues
.iter()
.any(|i| i.severity == validation::Severity::Error)
{
return Err(format!("Invalid project: {}", serde_json::to_string(&issues)?).into());
}
if canonical_diagnostics(serde_json::to_value(&issues)?)?
!= canonical_diagnostics(serde_json::from_slice(baseline)?)?
{
return Err(format!(
"Unexpected diagnostics; review without automatically accepting: {}",
serde_json::to_string(&issues)?
)
.into());
}
Ok(project)
}
fn representative() -> Result<Project> {
let mut project = admit(RELEASE, RELEASE_WARNINGS)?;
let view: Diagram = serde_json::from_slice(EIGHT_VIEW)?;
if project.diagrams.insert(view.id, view).is_some() {
return Err("Representative view ID collision".into());
}
admit(&serde_json::to_vec(&project)?, RELEASE_WARNINGS)
}
fn rect(point: Position, origin: Position, size: oa_core::Size) -> bool {
point.x >= origin.x
&& point.x <= origin.x + size.width
&& point.y >= origin.y
&& point.y <= origin.y + size.height
}
fn distance(point: Position, a: Position, b: Position) -> f32 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let length = dx * dx + dy * dy;
let t = if length == 0. {
0.
} else {
(((point.x - a.x) * dx + (point.y - a.y) * dy) / length).clamp(0., 1.)
};
(point.x - a.x - t * dx).hypot(point.y - a.y - t * dy)
}
// Intentionally returns every distinct candidate, with no selected-edge bias,
// UUID winner, click dispatch, viewport clipping, or gesture simulation.
fn candidates(
project: &Project,
view: &projection::ProjectedView,
routed: &routing::RoutedView,
point: Position,
zoom: f32,
) -> Value {
let edges: Vec<_> = routed
.edges
.values()
.filter_map(|edge| {
let segments: Vec<_> = edge
.points
.windows(2)
.enumerate()
.filter_map(|(i, pair)| {
let d = distance(point, pair[0], pair[1]);
(d <= 7. / zoom).then(|| json!({"index":i,"distance":d}))
})
.collect();
(!segments.is_empty()).then(|| json!({"edge":edge.id,"segments":segments}))
})
.collect();
let nodes: Vec<_> = view
.nodes
.iter()
.filter(|n| rect(point, n.position, n.size))
.map(|n| n.id)
.collect();
let labels: Vec<_> = routed
.edges
.values()
.filter(|e| {
!e.label.text.is_empty() && rect(point, e.label.bounds.position, e.label.bounds.size)
})
.map(|e| e.id)
.collect();
let association_labels: Vec<_> = routed
.edges
.values()
.flat_map(|e| {
project
.relations
.get(&e.id)
.into_iter()
.flat_map(move |relation| {
routing::association_end_geometry(relation, e)
.into_iter()
.filter(move |end| {
rect(point, end.label.bounds.position, end.label.bounds.size)
})
.map(move |end| json!({"edge":e.id,"source_end":end.source_end}))
})
})
.collect();
let radius = (10. / zoom).max(10.);
let ports: Vec<_> = view
.ports
.iter()
.filter(|p| {
(p.position.x - point.x).abs() <= radius && (p.position.y - point.y).abs() <= radius
})
.map(|p| json!({"element":p.element,"owner":p.owner}))
.collect();
json!({"world_point":point,"zoom":zoom,"edge_candidates":edges,
"node_occluders":nodes,"label_occluders":labels,"association_label_occluders":association_labels,"port_occluders":ports})
}
fn unobscured(value: &Value) -> bool {
[
"node_occluders",
"label_occluders",
"association_label_occluders",
"port_occluders",
]
.iter()
.all(|key| value[key].as_array().is_some_and(Vec::is_empty))
}
fn snapshot(project: &Project, diagram: Id, point: Position, zoom: f32) -> Result<Value> {
if !zoom.is_finite() || zoom <= 0. || !point.x.is_finite() || !point.y.is_finite() {
return Err("Point must be finite; zoom must be finite and positive".into());
}
let original = project.clone();
let diagram = project.diagrams.get(&diagram).ok_or("Unknown diagram")?;
let view = projection::project_view(project, diagram.id);
let routed = routing::route_diagram(project, diagram, &view);
let mut inventory = Vec::new();
for edge in &view.edges {
let (kind, source, target) = if let Some(r) = project.relations.get(&edge.id) {
(format!("{:?}", r.kind()), r.source(), r.target())
} else if let Some(t) = project.traces.get(&edge.id) {
(format!("Trace:{:?}", t.kind), t.oa, t.sa)
} else {
return Err("Projected edge has no semantic identity".into());
};
inventory.push(
json!({"id":edge.id,"kind":kind,"source":source,"target":target,
"source_port":edge.source_port,"target_port":edge.target_port,
"projected_source":edge.source,"projected_target":edge.target,"trace":edge.trace}),
);
}
inventory.sort_by_key(|row| row["id"].to_string());
let expected: Option<Value> = if diagram.id.to_string() == MAIN {
Some(serde_json::from_slice(include_bytes!(
"../../../harness/experiments/diagram-readability-20260929/mixed-answer-key.json"
))?)
} else if diagram.id.to_string() == EIGHT {
Some(serde_json::from_slice(include_bytes!(
"../../../harness/experiments/diagram-readability-20260929/eight-answer-key.json"
))?)
} else {
None
};
if expected
.as_ref()
.is_some_and(|key| *key != json!(inventory))
{
return Err(format!(
"Projected semantic inventory differs from independently declared answer key: {}",
json!(inventory)
)
.into());
}
let mut edges = Vec::new();
let mut alternate = Vec::new();
for edge in routed.edges.values() {
let projected = view
.edges
.iter()
.find(|p| p.id == edge.id)
.ok_or("Routed edge missing projected identity")?;
let semantic = if let Some(relation) = project.relations.get(&edge.id) {
json!({"relation":relation,"source_element":project.elements.get(&relation.source()),
"target_element":project.elements.get(&relation.target())})
} else {
json!({"trace":project.traces.get(&edge.id)})
};
edges.push(
json!({"id":edge.id,"projected":projected,"semantic":semantic,
"points":edge.points,"label":{"text":edge.label.text,"fraction":edge.label.fraction,
"anchor":edge.label.anchor,"center":edge.label.center,
"position":edge.label.bounds.position,"size":edge.label.bounds.size},
"effective_pins":edge.pins,"pin_fractions":edge.pin_fractions,
"authored_route":diagram.routes.iter().find(|r| r.edge == edge.id),
"custom_suspended":edge.custom_suspended,
"route_warnings":edge.warnings.iter().map(|w| format!("{w:?}")).collect::<Vec<_>>()}),
);
// A positive sample proves a geometric alternate under this query policy.
// Failure to find one does NOT prove global inaccessibility.
let mut line_witness = None;
for pair in edge.points.windows(2) {
for t in [0.25, 0.5, 0.75] {
let p = Position {
x: pair[0].x + (pair[1].x - pair[0].x) * t,
y: pair[0].y + (pair[1].y - pair[0].y) * t,
};
let hits = candidates(project, &view, &routed, p, zoom);
if unobscured(&hits)
&& hits["edge_candidates"]
.as_array()
.is_some_and(|a| a.len() == 1)
{
line_witness = Some(p);
break;
}
}
if line_witness.is_some() {
break;
}
}
let label_hits = candidates(project, &view, &routed, edge.label.center, zoom);
let label_witness = !edge.label.text.is_empty()
&& label_hits["node_occluders"]
.as_array()
.is_some_and(Vec::is_empty)
&& label_hits["port_occluders"]
.as_array()
.is_some_and(Vec::is_empty)
&& label_hits["association_label_occluders"]
.as_array()
.is_some_and(Vec::is_empty)
&& label_hits["label_occluders"]
.as_array()
.is_some_and(|a| a.len() == 1);
alternate.push(json!({"edge":edge.id,"unoccluded_unique_segment_sample":line_witness,
"unoccluded_unique_label_center":label_witness.then_some(edge.label.center),
"negative_result":"No sampled witness is not proof of global inaccessibility",
"native_access":"Not evaluated; Inspector, viewport, selected handles and gesture state excluded"}));
}
if *project != original {
return Err("Projection/routing changed the Project".into());
}
let query = candidates(project, &view, &routed, point, zoom);
let historical_point = Position { x: 1982., y: 375. };
let historical_query = candidates(project, &view, &routed, historical_point, 1.);
let historical_ids = [
"00000000-0000-0000-0000-00000001adb8",
"00000000-0000-0000-0000-00000001adb9",
];
let historical_reproduced = diagram.id.to_string() == MAIN
&& unobscured(&historical_query)
&& historical_query["edge_candidates"]
.as_array()
.is_some_and(|rows| {
rows.len() == 2
&& historical_ids.iter().all(|id| {
rows.iter().any(|r| {
r["edge"] == *id
&& r["segments"]
.as_array()
.is_some_and(|s| s.iter().any(|s| s["distance"] == 0.))
})
})
})
&& historical_ids.iter().all(|id| {
routed
.edges
.get(&id.parse::<Id>().unwrap())
.is_some_and(|e| {
e.points.windows(2).any(|p| {
p[0].x == 1982.
&& p[1].x == 1982.
&& p[0].y.min(p[1].y) <= 336.
&& p[0].y.max(p[1].y) >= 414.
})
})
});
Ok(
json!({"experiment":"issue38 geometry probe; not feature acceptance",
"experiment_declared_base":"715036e132677dd2bc2fb37ed316d93dc9987aec",
"build_provenance":"Require external manifest/source hash verification; this field does not identify the executable build",
"diagram":diagram.id,"project":project,
"project_conserved":true,"projected_view":view,"routed_edges":edges,
"bounds":{"position":routed.bounds.position,"size":routed.bounds.size},
"query":query,"alternate_access_samples":alternate,
"projected_semantic_inventory":inventory,"declared_answer_key":expected,
"historical_witness_reproduced":if diagram.id.to_string()==MAIN {Some(historical_reproduced)} else {None},
"historical_witness_policy":"Exact IDs, zero distance at (1982,375), no occluders, common x1982 span336..414 beyond shared port lead; zoom1. False is a result, not failure.",
"diagnostics":validation::validate(project)}),
)
}
fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
file.write_all(bytes)?;
file.sync_all()?;
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
if args.len() == 2 && args[0] == "generate" {
// Refuse an existing directory so no previous experiment output is replaced.
let out = Path::new(&args[1]);
let mixed = admit(MIXED, WARNINGS)?;
let eight = representative()?;
let scenes = [
(
"mixed-dispatch",
mixed,
MAIN,
Position { x: 1982., y: 375. },
),
(
"eight-card-representative",
eight,
EIGHT,
Position { x: 600., y: 450. },
),
];
let mut outputs = Vec::new();
for (name, project, id, point) in scenes {
outputs.push((format!("{name}.json"), serde_json::to_vec_pretty(&project)?));
outputs.push((
format!("{name}-geometry.json"),
serde_json::to_vec_pretty(&snapshot(&project, id.parse()?, point, 1.)?)?,
));
}
fs::create_dir(out)?;
write_new(&out.join("mixed-dispatch-original.json"), MIXED)?;
write_new(&out.join("release1-original.json"), RELEASE)?;
for (name, bytes) in outputs {
write_new(&out.join(name), &bytes)?;
}
return Ok(());
}
if args.len() != 8 || args[0] != "probe" {
return Err("Usage: generate NEW_DIRECTORY | probe INPUT_JSON_OR_SINGLE_SOURCE EXPECTED_DIAGNOSTICS DIAGRAM_UUID WORLD_X WORLD_Y ZOOM NEW_OUTPUT_JSON".into());
}
let input = fs::read(&args[1])?;
let project = admit(&input, &fs::read(&args[2])?)?;
let mut result = snapshot(
&project,
args[3].parse()?,
Position {
x: args[4].parse()?,
y: args[5].parse()?,
},
args[6].parse()?,
)?;
result["original_input_utf8"] = json!(std::str::from_utf8(&input)?);
if fs::read(&args[1])? != input {
return Err("Input changed during experiment".into());
}
write_new(Path::new(&args[7]), &serde_json::to_vec_pretty(&result)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn representative_preserves_every_original_record_and_view() {
let baseline = admit(RELEASE, RELEASE_WARNINGS).unwrap();
let mut generated = representative().unwrap();
let added = generated.diagrams.remove(&EIGHT.parse().unwrap()).unwrap();
assert_eq!(added.nodes.len(), 8);
assert_eq!(generated, baseline);
let project = representative().unwrap();
let result = snapshot(
&project,
EIGHT.parse().unwrap(),
Position { x: 600., y: 450. },
1.,
)
.unwrap();
assert_eq!(
result["projected_semantic_inventory"]
.as_array()
.unwrap()
.len(),
12
);
assert_eq!(
result["projected_semantic_inventory"],
result["declared_answer_key"]
);
}
#[test]
fn complete_scene_preserves_semantics_sentinel_and_authored_route() {
let project = admit(MIXED, WARNINGS).unwrap();
let before = project.clone();
let id = MAIN.parse().unwrap();
let result = snapshot(&project, id, Position { x: 1982., y: 375. }, 1.).unwrap();
assert_eq!(project, before);
assert_eq!(result["project"], serde_json::to_value(&before).unwrap());
assert_eq!(project.diagrams.len(), 2);
assert_eq!(
result["projected_view"]["nodes"].as_array().unwrap().len(),
10
);
assert_eq!(result["routed_edges"].as_array().unwrap().len(), 14);
for edge in result["routed_edges"].as_array().unwrap() {
let edge_id: Id = serde_json::from_value(edge["id"].clone()).unwrap();
assert_eq!(
edge["semantic"]["relation"],
serde_json::to_value(&before.relations[&edge_id]).unwrap()
);
}
let route = &before.diagrams[&id].routes[0];
assert_eq!(
route.edge.to_string(),
"00000000-0000-0000-0000-00000001adbd"
);
assert_eq!(route.waypoints, vec![Position { x: 300., y: -100. }]);
assert_eq!(route.label.as_ref().unwrap().fraction, 0.4);
assert_eq!(
snapshot(&project, id, Position { x: 1982., y: 375. }, 1.).unwrap(),
result
);
}
#[test]
fn unexpected_diagnostics_and_invalid_source_fail_closed() {
assert!(admit(MIXED, b"[]").is_err());
assert!(admit(b"{}", b"[]").is_err());
let project = admit(MIXED, WARNINGS).unwrap();
assert!(
snapshot(
&project,
MAIN.parse().unwrap(),
Position { x: 0., y: 0. },
0.
)
.is_err()
);
}
#[test]
fn square_port_corner_is_occluded_even_outside_ten_unit_circle() {
let project = admit(MIXED, WARNINGS).unwrap();
let diagram = &project.diagrams[&MAIN.parse().unwrap()];
let view = projection::project_view(&project, diagram.id);
let routed = routing::route_diagram(&project, diagram, &view);
let port = &view.ports[0];
let point = Position {
x: port.position.x + 9.,
y: port.position.y + 9.,
};
assert!(9_f32.hypot(9.) > 10.);
for zoom in [1., 2.] {
let hits = candidates(&project, &view, &routed, point, zoom);
assert!(hits["port_occluders"].as_array().unwrap().iter()
.any(|p| p["element"]==json!(port.element) && p["owner"]==json!(port.owner)));
}
}
#[test]
fn analytical_query_controls_distinguish_edges_segments_and_labels() {
// Synthetic geometry controls using genuine identities, NOT witness evidence.
let project = admit(MIXED, WARNINGS).unwrap();
let diagram = &project.diagrams[&MAIN.parse().unwrap()];
let mut view = projection::project_view(&project, diagram.id);
let mut routed = routing::route_diagram(&project, diagram, &view);
view.nodes.clear();
view.ports.clear();
let first: Id = "00000000-0000-0000-0000-00000001adb8".parse().unwrap();
let second: Id = "00000000-0000-0000-0000-00000001adb9".parse().unwrap();
routed.edges.retain(|id, _| *id == first || *id == second);
for (id, edge) in &mut routed.edges {
edge.points = if *id == first {
vec![
Position { x: -20., y: 0. },
Position { x: 0., y: 0. },
Position { x: 0., y: 20. },
]
} else {
vec![Position { x: -20., y: 50. }, Position { x: 20., y: 50. }]
};
edge.label.text.clear();
}
let point = Position { x: 0., y: 0. };
let hits = candidates(&project, &view, &routed, point, 1.);
assert_eq!(hits["edge_candidates"].as_array().unwrap().len(), 1);
assert_eq!(
hits["edge_candidates"][0]["segments"]
.as_array()
.unwrap()
.len(),
2
);
assert!(unobscured(&hits));
let edge = routed.edges.get_mut(&second).unwrap();
edge.points = vec![Position { x: -20., y: 0. }, Position { x: 20., y: 0. }];
edge.label.bounds = routing::RoutingBounds {
position: Position { x: -5., y: -5. },
size: oa_core::Size {
width: 10.,
height: 10.,
},
};
edge.label.text = "Control label".into();
let hits = candidates(&project, &view, &routed, point, 1.);
assert_eq!(hits["edge_candidates"].as_array().unwrap().len(), 2);
assert_eq!(hits["label_occluders"], json!([second]));
assert!(!unobscured(&hits));
routed.edges.get_mut(&second).unwrap().label.text.clear();
let hits = candidates(&project, &view, &routed, point, 1.);
assert!(hits["label_occluders"].as_array().unwrap().is_empty());
assert!(unobscured(&hits));
}
}