Define frozen routing corpus and isolated timing protocol

This commit is contained in:
2026-09-24 18:33:37 -07:00
parent f72aa2ca5c
commit a879341cfc
6 changed files with 897 additions and 0 deletions
@@ -384,6 +384,10 @@ fn metrics(project: &Project, diagram: Id) -> Value {
"drawing_area":routed.bounds.size.width as f64*routed.bounds.size.height as f64,
"route_bends":bends,"route_length":total_length,
"projected_nodes":view.nodes,"projected_ports":view.ports,
"projected_edges":view.edges,
"routing_constraints":edges.iter().map(|e|json!({"id":e.id,"effective_pins":e.pins,
"pin_fractions":e.pin_fractions,"custom_suspended":e.custom_suspended,
"actual_endpoint_owners":projection::edge_endpoint_placements(project,&project.diagrams[&diagram],e.id)})).collect::<Vec<_>>(),
"route_endpoints":edges.iter().map(|e|json!({"id":e.id,
"source_owner":definitions[&e.id].source,"target_owner":definitions[&e.id].target,
"source":e.points.first(),"target":e.points.last()})).collect::<Vec<_>>(),
@@ -735,6 +739,50 @@ fn observe(name: &str, f: Builder) -> Value {
"second_layout_exact":second.as_ref().is_ok_and(|s|s.project==after),"second_layout_error":second.err().map(|e|e.to_string()),
"before_roundtrip":roundtrip(&before),"after_roundtrip":roundtrip(&after)}})
}
fn route_corpus(path: &std::path::Path) {
let text = std::fs::read_to_string(path).expect("Read frozen routing corpus manifest");
let manifest: Value = serde_json::from_str(&text).expect("Parse corpus manifest");
assert_eq!(manifest["schema"], 1);
let directory = path.parent().expect("Corpus manifest directory");
let mut names = BTreeSet::new();
let mut prepared = Vec::new();
for case in manifest["cases"].as_array().expect("Corpus cases") {
let name = case["name"].as_str().expect("Case name");
assert!(names.insert(name.to_owned()), "Duplicate case name");
let file = std::path::Path::new(case["project_file"].as_str().expect("Project file"));
assert!(
file.components()
.all(|part| matches!(part, std::path::Component::Normal(_))),
"Corpus project files must use relative normal components"
);
let project: Project = serde_json::from_str(
&std::fs::read_to_string(directory.join(file)).expect("Read frozen Project"),
)
.expect("Decode frozen Project");
validate_candidate(&project).expect("Validate frozen Project");
let diagram: Id = serde_json::from_value(case["diagram"].clone()).expect("Diagram UUID");
assert!(
project.diagrams.contains_key(&diagram),
"Missing corpus diagram"
);
prepared.push((name.to_owned(), project, diagram));
}
assert!(!prepared.is_empty(), "Empty corpus");
println!(
"{}",
json!({"probe":"routing_corpus_geometry_v1","metric_selfchecks":"passed",
"mode":"Route-only observation of frozen authored Projects; no Layout, reset or timing.",
"manifest":manifest,"quality_thresholds":null,
"limitations":"Existing raw geometry metrics; estimated main-label rectangles, no glyph or port-label acceptance. Folded promotion is observed, not passed to the unsupported layered prototype. Automatic non-port attachment coordinates may legally change across router variants."})
);
for (name, project, diagram) in prepared {
println!(
"{}",
json!({"fixture":name,"diagram":diagram,"before_project":project,
"before":metrics(&project,diagram),"roundtrip":roundtrip(&project)})
);
}
}
fn main() {
// Analytic checks validate the observer, not any layout's quality.
let horizontal = [(0., 0.), (10., 0.)];
@@ -773,6 +821,16 @@ fn main() {
assert_eq!(overlap((0., 0., 10., 10.), (10., 0., 20., 10.)), 0.);
assert!((inside_length((0., 5.), (10., 5.), (2., 2., 8., 8.)) - 6.).abs() < 3. * EPS);
assert_eq!(inside_length((0., 2.), (10., 2.), (2., 2., 8., 8.)), 0.);
let args: Vec<_> = std::env::args_os().collect();
if args.len() == 3 && args[1] == "--route-corpus" {
route_corpus(std::path::Path::new(&args[2]));
return;
}
assert_eq!(
args.len(),
1,
"Usage: auto_layout_probe [--route-corpus MANIFEST]"
);
println!(
"{}",
json!({"probe":"auto_layout_probe_v2","reference_source":"715036e132677dd2bc2fb37ed316d93dc9987aec","metric_selfchecks":"passed", "quality_thresholds":null,
@@ -0,0 +1,192 @@
//! Frozen-corpus route-only timing. Protocol: harness/experiments/routing-independent-20260925/timing-protocol.md.
use oa_core::{Id, Project, projection, routing, validate_candidate};
use serde::Deserialize;
use serde_json::json;
use std::hint::black_box;
use std::path::{Component, Path, PathBuf};
use std::time::{Duration, Instant};
const ROUNDS: usize = 7;
const SLOW_CALL: Duration = Duration::from_secs(5);
#[derive(Deserialize)]
struct Manifest {
schema: u32,
cases: Vec<Case>,
#[serde(flatten)]
metadata: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
struct Case {
name: String,
project_file: PathBuf,
diagram: Id,
description: String,
#[serde(flatten)]
metadata: std::collections::BTreeMap<String, serde_json::Value>,
}
fn timed_route(
project: &Project,
diagram: &oa_core::Diagram,
view: &projection::ProjectedView,
) -> (routing::RoutedView, Duration) {
// Input black_box is outside the clock. Result consumption is inside it;
// allocation is part of routing, result destruction is outside the clock.
let project = black_box(project);
let diagram = black_box(diagram);
let view = black_box(view);
let started = Instant::now();
let result = black_box(routing::route_diagram(project, diagram, view));
let elapsed = started.elapsed();
(result, elapsed)
}
fn point_digest(route: &routing::RoutedView) -> String {
// Stable FNV-1a64 over ID bytes, point count and IEEE754 coordinate bits,
// little endian. Diagnostic only, not a cryptographic integrity guarantee.
let mut hash = 0xcbf29ce484222325_u64;
let mut feed = |bytes: &[u8]| {
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
};
for (id, edge) in &route.edges {
feed(id.as_bytes());
feed(&(edge.points.len() as u64).to_le_bytes());
for point in &edge.points {
feed(&point.x.to_bits().to_le_bytes());
feed(&point.y.to_bits().to_le_bytes());
}
}
format!("{hash:016x}")
}
fn relative_project(root: &Path, file: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
if file.as_os_str().is_empty()
|| !file
.components()
.all(|part| matches!(part, Component::Normal(_)))
{
return Err(
"project_file must be a nonempty relative path without parent traversal".into(),
);
}
let path = root.join(file).canonicalize()?;
if !path.starts_with(root) {
return Err("project_file resolves outside the manifest directory".into());
}
Ok(path)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args_os().skip(1);
let manifest_path = PathBuf::from(
args.next()
.ok_or("usage: routing_cost_probe MANIFEST.json")?,
)
.canonicalize()?;
if args.next().is_some() {
return Err(
"protocol is fixed: one warmup and seven measured calls; no extra arguments".into(),
);
}
let manifest: Manifest = serde_json::from_slice(&std::fs::read(&manifest_path)?)?;
if manifest.schema != 1 || manifest.cases.is_empty() {
return Err("expected schema1 with at least one case".into());
}
let root = manifest_path
.parent()
.ok_or("manifest has no parent directory")?;
println!(
"{}",
json!({
"protocol": "routing_cost_probe_v1", "manifest_schema": manifest.schema,
"warmup_calls": 1, "measured_calls": ROUNDS,
"slow_call_limit_ns": SLOW_CALL.as_nanos(),
"debug_assertions": cfg!(debug_assertions),
"os": std::env::consts::OS, "arch": std::env::consts::ARCH,
"case_count": manifest.cases.len(),
"corpus_metadata": manifest.metadata,
"clock_scope": "route_diagram plus result black_box; excludes projection, JSON, equality, digest, output and result destruction",
"digest": "FNV-1a64 IDs/counts/f32 point bits; diagnostic, not cryptographic",
"acceptance": "none; external receipts must bind source, optimized build, binary and corpus"
})
);
let mut names = std::collections::BTreeSet::new();
for case in manifest.cases {
if !names.insert(case.name.clone()) {
return Err("duplicate case name".into());
}
let bytes = std::fs::read(relative_project(root, &case.project_file)?)?;
let project: Project = serde_json::from_slice(&bytes)?;
validate_candidate(&project)?;
let original = project.clone();
let diagram = project
.diagrams
.get(&case.diagram)
.ok_or("case diagram is absent")?;
let view = projection::project_diagram(&project, diagram);
let (first, warmup) = timed_route(&project, diagram, &view);
let mut samples = Vec::with_capacity(ROUNDS);
let mut equality = Vec::with_capacity(ROUNDS);
let mut endpoints = Vec::with_capacity(ROUNDS);
let mut censored = warmup > SLOW_CALL;
if !censored {
for _ in 0..ROUNDS {
let (route, elapsed) = timed_route(&project, diagram, &view);
samples.push(elapsed.as_nanos());
equality.push(route == first);
endpoints.push(
route.edges.len() == first.edges.len()
&& route.edges.iter().all(|(id, edge)| {
first.edges.get(id).is_some_and(|other| {
edge.points.first() == other.points.first()
&& edge.points.last() == other.points.last()
})
}),
);
if elapsed > SLOW_CALL {
censored = true;
break;
}
}
}
let mut ordered = samples.clone();
ordered.sort_unstable();
let complete = !censored && samples.len() == ROUNDS;
let median = complete.then(|| ordered[ROUNDS / 2]);
let warnings: usize = first.edges.values().map(|edge| edge.warnings.len()).sum();
let project_unchanged = project == original;
let stable = equality.iter().all(|same| *same);
println!(
"{}",
json!({
"case": case.name, "description": case.description,
"corpus_case_metadata": case.metadata,
"project_file": case.project_file, "diagram": case.diagram,
"project_bytes": bytes.len(), "nodes": view.nodes.len(),
"edges": view.edges.len(), "ports": view.ports.len(),
"routed_edges": first.edges.len(), "warnings": warnings,
"route_points": first.edges.values().map(|edge| edge.points.len()).sum::<usize>(),
"route_point_digest": point_digest(&first),
"warmup_ns": warmup.as_nanos(), "samples_ns": samples,
"median_ns": median, "max_observed_measured_ns": ordered.last(),
"complete": complete, "censored": censored,
"censor_reason": if censored { Some("single call exceeded five seconds; remaining repeats skipped") } else { None },
"repeat_full_routed_view_equal_warmup": equality,
"repeat_endpoints_equal_warmup": endpoints,
"stability_observed": !samples.is_empty(),
"project_unchanged": project_unchanged
})
);
if !stable || !project_unchanged {
return Err(
"routing stability or Project preservation failed; receipt above retained".into(),
);
}
}
Ok(())
}
@@ -0,0 +1,547 @@
//! Source-first inputs for independent routing experiments. Never invokes layout/routing.
use oa_core::*;
use serde_json::{Value, json};
use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
struct Case {
name: &'static str,
description: &'static str,
project: Project,
diagram: Id,
trace_edges: Vec<Id>,
metadata: Value,
}
struct Builder {
project: Project,
diagram: Id,
base: u128,
next: u128,
edges: Vec<Id>,
}
impl Builder {
fn new(name: &str, base: u128) -> Self {
let mut project = Project::empty();
project.id = Id::from_u128(base);
project.alias = name.into();
project.name = name.into();
let old = project.ensure_diagram(Layer::Sa);
let diagram = Id::from_u128(base + 1);
reidentify(&mut project, old, diagram).unwrap();
let d = project.diagrams.get_mut(&diagram).unwrap();
d.alias = format!("{name}_main");
d.name = format!("{name} main view");
d.projection = ProjectionKind::FunctionalDataflow;
Self {
project,
diagram,
base,
next: base + 100,
edges: Vec::new(),
}
}
fn stable(&mut self, old: Id) -> Id {
let id = Id::from_u128(self.next);
self.next += 1;
reidentify(&mut self.project, old, id).unwrap();
id
}
fn function(&mut self, name: &str, x: f32, y: f32) -> Id {
let old = self
.project
.add_element(Kind::Function, Layer::Sa, name, None)
.unwrap();
let id = self.stable(old);
let old = self.project.place(self.diagram, id, Position { x, y });
self.stable(old);
id
}
fn exchange(
&mut self,
name: &str,
source: Id,
target: Id,
source_port: PortChoice,
target_port: PortChoice,
) -> Id {
let id = Id::from_u128(self.base + 10_000 + self.edges.len() as u128);
let request = FunctionalExchangeRequest {
id,
alias: format!("flow_{:02}", self.edges.len()),
name: name.into(),
description: String::new(),
source,
target,
source_port,
target_port,
items: vec![],
};
let commands = ports::plan_functional_exchange(&self.project, &request).unwrap();
self.project = execute_batch(&self.project, &commands).unwrap();
self.edges.push(id);
id
}
fn port_pair(&self, edge: Id) -> (Id, Id) {
let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = self.project.relations[&edge].body
else {
unreachable!()
};
(source_port, target_port)
}
fn anchor(&mut self, owner: Id, port: Id, side: PortAnchorSide, offset: f32) {
let node = self.project.diagrams[&self.diagram]
.nodes
.values()
.find(|n| n.element == owner)
.unwrap()
.id;
self.project = prepare_command(
&self.project,
&Command::SetPortAnchor {
diagram: self.diagram,
node,
anchor: PortAnchor { port, side, offset },
},
)
.unwrap()
.project;
}
}
fn sentinel(edge: Id, x: f32, y: f32) -> EdgeRoute {
EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints: vec![Position { x, y }],
label: Some(EdgeLabel {
fraction: 0.4,
offset: Position { x: -30., y: -70. },
}),
}
}
fn workshop() -> Vec<Case> {
let original =
persistence::decode(include_bytes!("../../../examples/routing-workshop.json")).unwrap();
let diagram = original
.diagrams
.values()
.find(|d| d.alias == "sa_system_boundary_and_functions")
.unwrap()
.id;
assert_eq!(
original.diagrams[&diagram].projection,
ProjectionKind::Allocation
);
assert_eq!(original.diagrams[&diagram].nodes.len(), 12);
let container = original
.elements
.values()
.find(|e| e.alias == "rescue_coordination_system")
.unwrap()
.id;
let placement = original.diagrams[&diagram]
.nodes
.values()
.find(|n| n.element == container)
.unwrap()
.id;
let traces: Vec<_> = [
"sa_report_to_relay",
"sa_direct_advice",
"sa_incident_decision",
]
.iter()
.map(|alias| {
original
.relations
.values()
.find(|r| r.alias == *alias)
.unwrap()
.id
})
.collect();
let mut folded = original.clone();
assert!(!folded.diagrams[&diagram].nodes[&placement].collapsed);
folded
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&placement)
.unwrap()
.collapsed = true;
let mut restored = folded.clone();
restored
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&placement)
.unwrap()
.collapsed = false;
assert_eq!(restored, original);
vec![
Case {
name: "workshop_sa_original",
description: "Saved coastal rescue Allocation view, all source records retained.",
project: original,
diagram,
trace_edges: traces.clone(),
metadata: json!({"source":"examples/routing-workshop.json","view_alias":"sa_system_boundary_and_functions"}),
},
Case {
name: "workshop_sa_folded",
description: "Same saved Project with only rescue_coordination_system placement folded.",
project: folded,
diagram,
trace_edges: traces,
metadata: json!({"source":"examples/routing-workshop.json","folded_placement":placement,
"folded_element":container,"unfold_restores_original_exact":true}),
},
]
}
fn mixed() -> Vec<Case> {
let mut b = Builder::new("mixed_dispatch", 100_000);
let nodes: Vec<_> = [
("Sensor A", 40., 90.),
("Sensor B", 40., 510.),
("Ingest", 380., 250.),
("Validate", 720., 80.),
("Archive", 720., 510.),
("Correlate", 1060., 270.),
("Plan", 1400., 80.),
("Dispatch", 1740., 270.),
("Actuator", 1400., 680.),
("Operator", 1740., 680.),
]
.into_iter()
.map(|(name, x, y)| b.function(name, x, y))
.collect();
let e0 = b.exchange(
"Sensor A observations",
nodes[0],
nodes[2],
PortChoice::Create,
PortChoice::Create,
);
let ingest_input = b.port_pair(e0).1;
let e1 = b.exchange(
"Sensor B observations",
nodes[1],
nodes[2],
PortChoice::Create,
PortChoice::Reuse(ingest_input),
);
let e2 = b.exchange(
"Validate incoming observations",
nodes[2],
nodes[3],
PortChoice::Create,
PortChoice::Create,
);
let ingest_output = b.port_pair(e2).0;
let e3 = b.exchange(
"Archive incoming observations",
nodes[2],
nodes[4],
PortChoice::Reuse(ingest_output),
PortChoice::Create,
);
b.exchange(
"Validated observations",
nodes[3],
nodes[5],
PortChoice::Create,
PortChoice::Create,
);
b.exchange(
"Archived context",
nodes[4],
nodes[5],
PortChoice::Create,
PortChoice::Create,
);
b.exchange(
"Correlated situation",
nodes[5],
nodes[6],
PortChoice::Create,
PortChoice::Create,
);
b.exchange(
"Planned actions",
nodes[6],
nodes[7],
PortChoice::Create,
PortChoice::Create,
);
let e8 = b.exchange(
"Actuator dispatch",
nodes[7],
nodes[8],
PortChoice::Create,
PortChoice::Create,
);
let dispatch_output = b.port_pair(e8).0;
let e9 = b.exchange(
"Operator dispatch copy",
nodes[7],
nodes[9],
PortChoice::Reuse(dispatch_output),
PortChoice::Create,
);
b.exchange(
"Operator acknowledgement",
nodes[9],
nodes[8],
PortChoice::Create,
PortChoice::Create,
);
b.exchange(
"Archived actuator policy",
nodes[4],
nodes[8],
PortChoice::Create,
PortChoice::Create,
);
b.exchange(
"Sensor B archive stream",
nodes[1],
nodes[4],
PortChoice::Create,
PortChoice::Create,
);
let feedback = b.exchange(
"Sensor A sampling feedback",
nodes[6],
nodes[0],
PortChoice::Create,
PortChoice::Create,
);
let (feedback_output, feedback_input) = b.port_pair(feedback);
b.anchor(nodes[0], b.port_pair(e0).0, PortAnchorSide::Right, 0.25);
b.anchor(nodes[2], ingest_input, PortAnchorSide::Left, 0.3);
b.anchor(nodes[2], ingest_output, PortAnchorSide::Bottom, 0.6);
b.anchor(nodes[6], feedback_output, PortAnchorSide::Top, 0.5);
b.anchor(nodes[0], feedback_input, PortAnchorSide::Left, 0.6);
b.anchor(nodes[7], dispatch_output, PortAnchorSide::Right, 0.5);
b.project
.diagrams
.get_mut(&b.diagram)
.unwrap()
.routes
.push(sentinel(feedback, 300., -100.));
let secondary = Id::from_u128(b.base + 2);
let mut other = b.project.diagrams[&b.diagram].clone();
other.id = secondary;
other.alias = "mixed_dispatch_secondary".into();
other.name = "Feedback review sentinel".into();
other.description = "Unaffected other-view preservation sentinel".into();
other.nodes.clear();
other.routes = vec![sentinel(feedback, 460., 480.)];
b.project.diagrams.insert(secondary, other);
for (i, element) in [nodes[0], nodes[2], nodes[6]].into_iter().enumerate() {
let old = b.project.place(
secondary,
element,
Position {
x: 100. + i as f32 * 340.,
y: 100.,
},
);
b.stable(old);
}
assert_eq!(b.edges.len(), 14);
assert_eq!(b.project.diagrams[&b.diagram].nodes.len(), 10);
let originals = b.edges.clone();
let replacements: Vec<_> = originals.iter().rev().copied().collect();
let mut permuted = b.project.clone();
for (i, id) in originals.iter().enumerate() {
reidentify(&mut permuted, *id, Id::from_u128(1_000_000 + i as u128)).unwrap();
}
for (i, id) in replacements.iter().enumerate() {
reidentify(&mut permuted, Id::from_u128(1_000_000 + i as u128), *id).unwrap();
}
let mut restored = permuted.clone();
for (i, id) in replacements.iter().enumerate() {
reidentify(&mut restored, *id, Id::from_u128(1_000_000 + i as u128)).unwrap();
}
for (i, id) in originals.iter().enumerate() {
reidentify(&mut restored, Id::from_u128(1_000_000 + i as u128), *id).unwrap();
}
assert_eq!(restored, b.project);
let correspondence: BTreeMap<_, _> = originals.iter().copied().zip(replacements).collect();
let traces = vec![e1, e3, e9, feedback];
let permuted_traces = traces.iter().map(|id| correspondence[id]).collect();
let metadata = json!({"shared_input":ingest_input,"shared_outputs":[ingest_output,dispatch_output],
"sentinel_edge":feedback,"secondary_diagram":secondary,"fixed_anchor_records":6,
"authored_waypoint_count_main":1,"authored_label_count_main":1,
"declared_feedback_cycle":[nodes[0],nodes[2],nodes[3],nodes[5],nodes[6],nodes[0]]});
vec![
Case {
name: "mixed_dispatch",
description: "Ten-function sensing/decision/dispatch network, explicit port sharing and fixed anchors.",
project: b.project,
diagram: b.diagram,
trace_edges: traces,
metadata,
},
Case {
name: "mixed_dispatch_edge_ids_reversed",
description: "Same mixed Project with fixed reversed relation-ID permutation, remapped references.",
project: permuted,
diagram: b.diagram,
trace_edges: permuted_traces,
metadata: json!({"paired_case":"mixed_dispatch","edge_id_correspondence":correspondence,
"inverse_recovers_original_exact":true,"secondary_diagram":secondary}),
},
]
}
fn sparse() -> Case {
let mut b = Builder::new("subsystems_40_sparse", 200_000);
let names = [
"Sensing",
"Communications",
"Assessment",
"Planning",
"Delivery",
];
let stages = [
"Acquire",
"Decode",
"Validate",
"Enrich",
"Aggregate",
"Decide",
"Package",
"Publish",
];
let mut nodes = Vec::new();
for (group, name) in names.iter().enumerate() {
let row: Vec<_> = stages
.iter()
.enumerate()
.map(|(stage, label)| {
b.function(
&format!("{name} {label}"),
80. + stage as f32 * 300.,
80. + group as f32 * 240.,
)
})
.collect();
nodes.push(row);
}
for group in 0..5 {
for stage in 0..7 {
b.exchange(
&format!(
"{} {} to {}",
names[group],
stages[stage],
stages[stage + 1]
),
nodes[group][stage],
nodes[group][stage + 1],
PortChoice::Create,
PortChoice::Create,
);
}
}
let mut trace_edges = Vec::new();
for group in 0..5 {
for stage in [1, 3, 5, 7] {
let next = (group + 1) % 5;
let id = b.exchange(
&format!(
"{} {} handoff to {} {}",
names[group],
stages[stage],
names[next],
stages[stage - 1]
),
nodes[group][stage],
nodes[next][stage - 1],
PortChoice::Create,
PortChoice::Create,
);
if (group == 0 && stage == 1) || (group == 4 && stage == 7) {
trace_edges.push(id);
}
}
}
assert_eq!(b.edges.len(), 55);
assert_eq!(b.project.diagrams[&b.diagram].nodes.len(), 40);
Case {
name: "subsystems_40_sparse",
description: "Five eight-stage subsystem chains with twenty directed cross-subsystem handoffs; all ports distinct.",
project: b.project,
diagram: b.diagram,
trace_edges,
metadata: json!({"groups":names,"stages":stages,"chain_edges":35,"cross_edges":20,
"all_exchange_ports_distinct":true,"grid_columns":8,"grid_rows":5}),
}
}
fn main() -> Result<(), Box<dyn Error>> {
let mut args = std::env::args_os().skip(1);
let output = PathBuf::from(
args.next()
.ok_or("Usage: routing_eval_corpus NEW_DIRECTORY")?,
);
if args.next().is_some() {
return Err("Expected exactly one new output directory".into());
}
if output.exists() {
return Err("Output directory already exists; refusing overwrite".into());
}
let mut cases = workshop();
cases.extend(mixed());
cases.push(sparse());
let mut files = Vec::new();
let mut entries = Vec::new();
for case in cases {
validate_candidate(&case.project)?;
let file = format!("{}.json", case.name);
let traces: Vec<_> = case
.trace_edges
.iter()
.map(|id| {
let relation = &case.project.relations[id];
json!({"edge":id,"alias":relation.alias,"name":relation.name,
"source":relation.source(),"target":relation.target()})
})
.collect();
entries.push(json!({"name":case.name,"project_file":file,"diagram":case.diagram,
"description":case.description,"metadata":case.metadata,"trace_tasks":traces,
"source_counts":{"elements":case.project.elements.len(),"relations":case.project.relations.len(),
"views":case.project.diagrams.len(),"selected_view_placements":case.project.diagrams[&case.diagram].nodes.len(),
"selected_view_authored_routes":case.project.diagrams[&case.diagram].routes.len()}}));
files.push((file, serde_json::to_vec_pretty(&case.project)?));
}
let manifest = json!({"schema":1,"cases":entries,"definition_baseline":"f72aa2ca",
"construction":"Source-authored positions/topology, no Layout/projection/routing calls; validate all before writing.",
"invariants":"Routing is read-only. Preserve all semantic records, port identities, fixed anchors, authored routes/labels, fold flags, and other views. ID permutation correspondence is explicit. No quality or automatic endpoint-side expectation.",
"generation_in_routing_timing":false});
let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
fs::create_dir(&output)?;
for (name, bytes) in files {
fs::write(output.join(name), bytes)?;
}
fs::write(output.join("manifest.json"), manifest_bytes)?;
println!("{}", output.join("manifest.json").display());
Ok(())
}