Compare commits

..
37 changed files with 5284 additions and 110 deletions
@@ -0,0 +1,14 @@
//! Focused test harness for the production Allocation planner and Document.
//! This does not exercise App input, painting, or native gestures.
#[cfg(test)]
#[allow(dead_code)] // App-facing entry points are intentionally absent here.
#[path = "../src/allocation_drag.rs"]
mod allocation_drag;
#[cfg(test)]
#[allow(dead_code, unused_imports)] // App-facing API and reexports are absent here.
#[path = "../src/document.rs"]
mod document;
fn main() {}
+122 -110
View File
@@ -13,6 +13,19 @@ use oa_core::{
};
use std::collections::{BTreeMap, BTreeSet};
// The pure planner is verified before its App gesture adapter is connected.
#[allow(dead_code)]
#[path = "allocation_drag/arrange.rs"]
pub mod arrange;
#[path = "allocation_drag/geometry.rs"]
mod geometry;
// The combined semantic planner is verified before connecting its App adapter.
#[allow(dead_code)]
#[path = "allocation_drag/assignment.rs"]
pub mod assignment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Destination {
LayoutOnly,
@@ -76,6 +89,11 @@ pub enum Refusal {
pub fn capture(p: &Project, diagram: Id, work: Id, placement: Id) -> Result<Scope, Refusal> {
validate_candidate(p).map_err(|e| Refusal::InvalidCandidate(e.to_string()))?;
capture_validated(p, diagram, work, placement)
}
/// Collect work facts after the caller's single complete Project validation.
fn capture_validated(p: &Project, diagram: Id, work: Id, placement: Id) -> Result<Scope, Refusal> {
let d = p.diagrams.get(&diagram).ok_or(Refusal::StaleScope)?;
let root = p.elements.get(&work).ok_or(Refusal::StaleScope)?;
if !root.kind().is_work()
@@ -232,8 +250,93 @@ pub fn plan_semantics(
let same_performer =
matches!(destination, Destination::Performer(id) if scope.uniform == Uniform::Assigned(id));
let mut expected = p.clone();
let mut commands = Vec::new();
let mut deleted = BTreeSet::new();
let mut edits = IntendedEdits::default();
append_semantic_edits(
p,
scope,
destination,
allocation_ids,
&mut expected,
&mut edits,
)?;
// A route cannot survive the intentional deletion of its own edge, even in
// another diagram. No other saved-view cleanup is authorized by this drag.
for diagram in expected.diagrams.values_mut() {
diagram.routes.retain(|r| !edits.deleted.contains(&r.edge));
}
let actual = prepare_command(p, &Command::Batch(edits.commands.clone()))
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
let retained_roles = scope
.leaves
.values()
.any(|l| !l.role_assignments.is_empty());
let label = match destination {
Destination::LayoutOnly => "Arrange only".into(),
Destination::Performer(id) if same_performer => format!("Arrange within {}", p.named(id)),
Destination::Performer(id) => format!(
"Assign {} leaf {} to {}{}",
scope.leaves.len(),
match (scope.diagram.layer, scope.leaves.len()) {
(Layer::Oa, 1) => "activity",
(Layer::Oa, _) => "activities",
(Layer::Sa, 1) => "function",
(Layer::Sa, _) => "functions",
},
p.named(id),
if retained_roles {
" — explicit assignment; role responsibility retained"
} else {
""
}
),
Destination::OutsidePerformers if edits.commands.is_empty() => {
"Arrange unassigned work — decomposition retained".into()
}
Destination::OutsidePerformers => "Remove assignment — decomposition retained".into(),
};
let warnings = validation::validate(&actual)
.into_iter()
.filter(|i| i.severity == Severity::Warning)
.collect();
let index = ViewIndex::new(&actual);
Ok(SemanticPlan {
scope: scope.clone(),
destination,
label,
warnings,
accepted: p.clone(),
candidate: actual,
index,
commands: edits.commands,
before_visible,
before_expanded,
})
}
#[derive(Default)]
struct IntendedEdits {
commands: Vec<Command>,
deleted: BTreeSet<Id>,
changed: BTreeSet<Id>,
}
/// Read each root's accepted responsibility while accumulating one intended
/// Project, reserving relation identities and aliases across every root.
fn append_semantic_edits(
p: &Project,
scope: &Scope,
destination: Destination,
allocation_ids: &BTreeMap<Id, Id>,
expected: &mut Project,
edits: &mut IntendedEdits,
) -> Result<(), Refusal> {
let same_performer =
matches!(destination, Destination::Performer(id) if scope.uniform == Uniform::Assigned(id));
let commands = &mut edits.commands;
let deleted = &mut edits.deleted;
let changed = &mut edits.changed;
if destination != Destination::LayoutOnly && !same_performer {
for (work, leaf) in &scope.leaves {
match destination {
@@ -246,6 +349,7 @@ pub fn plan_semantics(
target: *work,
};
expected.relations.insert(edited.id, edited.clone());
changed.insert(*work);
commands.push(Command::EditRelation(edited.id, edited));
}
} else {
@@ -269,6 +373,7 @@ pub fn plan_semantics(
},
};
expected.relations.insert(id, relation.clone());
changed.insert(*work);
commands.push(Command::CreateRelation(relation));
}
}
@@ -327,6 +432,7 @@ pub fn plan_semantics(
if let Some(existing) = &leaf.direct {
expected.relations.remove(&existing.id);
deleted.insert(existing.id);
changed.insert(*work);
commands.push(Command::Delete(existing.id));
}
}
@@ -334,60 +440,7 @@ pub fn plan_semantics(
}
}
}
// A route cannot survive the intentional deletion of its own edge, even in
// another diagram. No other saved-view cleanup is authorized by this drag.
for diagram in expected.diagrams.values_mut() {
diagram.routes.retain(|r| !deleted.contains(&r.edge));
}
let actual = prepare_command(p, &Command::Batch(commands.clone()))
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
let retained_roles = scope
.leaves
.values()
.any(|l| !l.role_assignments.is_empty());
let label = match destination {
Destination::LayoutOnly => "Arrange only".into(),
Destination::Performer(id) if same_performer => format!("Arrange within {}", p.named(id)),
Destination::Performer(id) => format!(
"Assign {} leaf {} to {}{}",
scope.leaves.len(),
match (scope.diagram.layer, scope.leaves.len()) {
(Layer::Oa, 1) => "activity",
(Layer::Oa, _) => "activities",
(Layer::Sa, 1) => "function",
(Layer::Sa, _) => "functions",
},
p.named(id),
if retained_roles {
" — explicit assignment; role responsibility retained"
} else {
""
}
),
Destination::OutsidePerformers if commands.is_empty() => {
"Arrange unassigned work — decomposition retained".into()
}
Destination::OutsidePerformers => "Remove assignment — decomposition retained".into(),
};
let warnings = validation::validate(&actual)
.into_iter()
.filter(|i| i.severity == Severity::Warning)
.collect();
let index = ViewIndex::new(&actual);
Ok(SemanticPlan {
scope: scope.clone(),
destination,
label,
warnings,
accepted: p.clone(),
candidate: actual,
index,
commands,
before_visible,
before_expanded,
})
Ok(())
}
/// World-space rectangle in the gesture's frozen transform. The UI resolver
@@ -462,60 +515,18 @@ pub fn place(
"Make room or adjust the view, then drag again.".into(),
));
}
let root = &plan.scope.placements[&plan.scope.placement];
let delta = Position {
x: (requested.x / 10.).round() * 10. - root.position.x,
y: (requested.y / 10.).round() * 10. - root.position.y,
};
let mut diagram = plan.candidate.diagrams[&plan.scope.diagram.id].clone();
let mut commands = Vec::new();
let mut deltas = BTreeMap::new();
for (id, original) in &plan.scope.placements {
let node = diagram.nodes.get_mut(id).ok_or(Refusal::StaleScope)?;
node.position = Position {
x: original.position.x + delta.x,
y: original.position.y + delta.y,
};
if !(Rect {
position: node.position,
size: node.size,
})
.valid()
{
return Err(Refusal::InvalidCandidate(
"Moved coordinates exceed the model bounds.".into(),
));
}
deltas.insert(*id, delta);
if node != original {
commands.push(Command::EditNode {
diagram: diagram.id,
original: *id,
node: node.clone(),
});
}
}
// Resolve endpoints using the captured accepted semantics, not a newly
// regrouped descendant walk. Own-edge deletions were removed above already.
for route in projection::translated_routes(&plan.accepted, &plan.scope.diagram, &deltas) {
if let Some(existing) = diagram.routes.iter_mut().find(|r| r.edge == route.edge) {
if route.waypoints.iter().any(|p| {
!p.x.is_finite()
|| !p.y.is_finite()
|| p.x.abs() > MAX_COORDINATE
|| p.y.abs() > MAX_COORDINATE
}) {
return Err(Refusal::InvalidCandidate(
"Moved route exceeds the model bounds.".into(),
));
}
*existing = route.clone();
commands.push(Command::SetEdgeRoute {
diagram: diagram.id,
route,
});
}
}
let geometry::Translation {
mut diagram,
mut commands,
delta,
} = geometry::translate(
&plan.accepted,
&plan.scope.diagram,
&plan.candidate.diagrams[&plan.scope.diagram.id],
&plan.scope.placements,
plan.scope.placement,
requested,
)?;
let mut unfolded = None;
if let Destination::Performer(performer) = plan.destination {
let target = presentation.target.ok_or_else(|| {
@@ -833,4 +844,5 @@ fn exact_project(expected: &Project, actual: &Project) -> Result<(), Refusal> {
}
#[cfg(test)]
#[path = "allocation_drag/tests.rs"]
mod tests;
@@ -0,0 +1,232 @@
//! Explicit, semantics-preserving arrangement of one or more Allocation cards.
//!
//! The App must admit the entire original typed selection before converting it
//! to placement/element identities. These values do not grant input authority.
use super::*;
#[derive(Debug)]
pub struct Plan {
accepted: Project,
diagram: Id,
selected: BTreeMap<Id, Id>,
anchor: Id,
placements: BTreeMap<Id, NodePlacement>,
expanded_ids: BTreeSet<Id>,
index: ViewIndex,
before_visible: ProjectedView,
before_expanded: ProjectedView,
}
impl Plan {
pub fn project(&self) -> &Project {
&self.accepted
}
pub fn index(&self) -> &ViewIndex {
&self.index
}
pub fn affected(&self) -> &BTreeMap<Id, NodePlacement> {
&self.placements
}
pub fn selected(&self) -> &BTreeMap<Id, Id> {
&self.selected
}
pub fn anchor(&self) -> Id {
self.anchor
}
pub fn diagram(&self) -> &Diagram {
&self.accepted.diagrams[&self.diagram]
}
}
/// Capture every original placement identity before normalizing semantic roots.
/// Document/revision/selection-generation and clean-owner guards belong to App.
pub fn capture(
p: &Project,
diagram: Id,
selected: &BTreeMap<Id, Id>,
anchor: Id,
) -> Result<Plan, Refusal> {
validate_candidate(p).map_err(|e| Refusal::InvalidCandidate(e.to_string()))?;
let d = p.diagrams.get(&diagram).ok_or(Refusal::StaleScope)?;
if d.projection != ProjectionKind::Allocation
|| selected.is_empty()
|| !selected.contains_key(&anchor)
{
return Err(Refusal::Ineligible(
"Arrange requires selected placements and a selected anchor in an Allocation view."
.into(),
));
}
let index = ViewIndex::new(p);
let before_visible = projection::project_diagram_with_index(p, d, &index);
let visible: BTreeMap<_, _> = before_visible
.nodes
.iter()
.map(|n| (n.id, n.element))
.collect();
let mut work = BTreeMap::new();
let mut performers = Vec::new();
for (placement, element) in selected {
let node = d.nodes.get(placement).ok_or(Refusal::StaleScope)?;
let record = p.elements.get(element).ok_or(Refusal::StaleScope)?;
if node.element != *element || visible.get(placement) != Some(element) {
return Err(Refusal::StaleScope);
}
if record.layer() != d.layer || !(record.kind().is_work() || record.kind().is_performer()) {
return Err(Refusal::Ineligible(
"Arrange requires only activities, functions or performers in this layer.".into(),
));
}
if record.kind().is_work() {
work.insert(*element, *placement);
} else {
performers.push(*placement);
}
}
let mut placements = BTreeMap::new();
for (element, placement) in &work {
let root = &p.elements[element];
let mut ancestor = root.parent;
let mut covered = false;
let mut seen = BTreeSet::new();
while let Some(id) = ancestor {
if !seen.insert(id) {
return Err(Refusal::StaleScope);
}
let parent = p.elements.get(&id).ok_or(Refusal::StaleScope)?;
if parent.kind() != root.kind() {
break;
}
if work.contains_key(&id) {
covered = true;
break;
}
ancestor = parent.parent;
}
if !covered {
placements.extend(capture_validated(p, diagram, *element, *placement)?.placements);
}
}
for performer in performers {
for id in
std::iter::once(performer).chain(projection::descendant_nodes(p, diagram, performer))
{
placements.insert(id, d.nodes.get(&id).ok_or(Refusal::StaleScope)?.clone());
}
}
// Arrange preserves ancestry, so accepted diagnostic identities also apply
// to every translated candidate.
let expanded_ids = geometry::expansion_ids(p, d, &index, &placements)?;
let before_expanded = geometry::expanded_view(p, d, &index, &expanded_ids);
Ok(Plan {
accepted: p.clone(),
diagram,
selected: selected.clone(),
anchor,
placements,
expanded_ids,
index,
before_visible,
before_expanded,
})
}
/// Snap the captured anchor once; preview the complete union without cloning or
/// validating a Project, rebuilding its semantic index, or expanding scope.
pub fn place(
plan: &Plan,
requested: Position,
presentation: PresentationContext,
) -> Result<PlacementPlan, Refusal> {
if presentation.target.is_some() {
return Err(Refusal::Ineligible(
"Arrange only has no performer destination.".into(),
));
}
if !requested.x.is_finite()
|| !requested.y.is_finite()
|| !presentation.viewport.valid()
|| !presentation.zoom.is_finite()
|| presentation.zoom <= 0.
{
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
let geometry::Translation {
diagram,
commands,
delta,
} = geometry::translate(
&plan.accepted,
plan.diagram(),
plan.diagram(),
&plan.placements,
plan.anchor,
requested,
)?;
let view = projection::project_diagram_with_index(&plan.accepted, &diagram, &plan.index);
let expanded =
geometry::expanded_view(&plan.accepted, &diagram, &plan.index, &plan.expanded_ids);
// Preserve Arrange's refusal precedence: projected bounds, then rigidity,
// then overlap growth. The shared scene check combines bounds and overlaps.
let scenes = match geometry::check_scenes(
&plan.before_visible,
&plan.before_expanded,
&view,
&expanded,
) {
Err(error @ Refusal::InvalidCandidate(_)) => return Err(error),
result => result,
};
geometry::check_rigid(&plan.before_expanded, &expanded, &plan.placements, delta)?;
scenes?;
geometry::check_headers(&view, &plan.selected, presentation.viewport)?;
Ok(PlacementPlan {
diagram,
view,
delta,
unfolded: None,
requested,
presentation,
commands,
})
}
/// Recompute the candidate and compare the complete expected Project before the
/// caller performs one source-admitted transaction. None means no history entry.
pub fn finish(
plan: &Plan,
live: &Project,
placement: &PlacementPlan,
) -> Result<Option<Command>, Refusal> {
if live != &plan.accepted {
return Err(Refusal::StaleScope);
}
let checked = place(plan, placement.requested, placement.presentation)?;
if checked.diagram != placement.diagram
|| checked.view != placement.view
|| checked.delta != placement.delta
|| checked.unfolded != placement.unfolded
|| checked.commands != placement.commands
{
return Err(Refusal::StaleScope);
}
let mut expected = plan.accepted.clone();
expected
.diagrams
.insert(checked.diagram.id, checked.diagram);
let command = Command::Batch(checked.commands);
let actual = prepare_command(live, &command)
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
Ok((actual != *live).then_some(command))
}
@@ -0,0 +1,443 @@
use super::*;
struct Distributed {
f: Fixture,
a: Id,
b: Id,
x: Id,
y: Id,
internal: Id,
}
fn distributed(layer: Layer, folded: bool) -> Distributed {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let x = f.p.add_element(kind, layer, "X", Some(f.w)).unwrap();
let y = f.p.add_element(kind, layer, "Y", Some(f.w)).unwrap();
let outside = f.p.add_element(kind, layer, "Outside", None).unwrap();
let xn = f.p.place(f.d, x, xy(140., 200.));
let yn = f.p.place(f.d, y, xy(940., 200.));
f.p.place(f.d, outside, xy(1800., 800.));
f.p.relate(RelationKind::Allocation, "A owns X", f.a, x);
f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y);
let exchange = match layer {
Layer::Oa => RelationKind::Interaction,
Layer::Sa => RelationKind::FunctionalExchange,
};
let internal = f.p.relate(exchange, "X to Y", x, y);
let external = f.p.relate(exchange, "Y to Outside", y, outside);
let d = f.p.diagrams.get_mut(&f.d).unwrap();
let a = d.nodes.values().find(|n| n.element == f.a).unwrap().id;
let b = d.nodes.values().find(|n| n.element == f.b).unwrap().id;
d.nodes.get_mut(&a).unwrap().collapsed = folded;
d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.);
d.routes = vec![route(internal), route(external)];
validate_candidate(&f.p).unwrap();
Distributed {
f,
a,
b,
x: xn,
y: yn,
internal,
}
}
fn identities(m: &Distributed, ids: &[Id]) -> BTreeMap<Id, Id> {
ids.iter()
.map(|id| (*id, m.f.p.diagrams[&m.f.d].nodes[id].element))
.collect()
}
fn expected(m: &Distributed, include_a: bool) -> Project {
let mut p = m.f.p.clone();
let d = p.diagrams.get_mut(&m.f.d).unwrap();
for (id, position) in [
(m.f.n, xy(500., 800.)),
(m.x, xy(540., 200.)),
(m.y, xy(1340., 200.)),
] {
d.nodes.get_mut(&id).unwrap().position = position;
}
if include_a {
d.nodes.get_mut(&m.a).unwrap().position = xy(500., 100.);
}
d.routes
.iter_mut()
.find(|r| r.edge == m.internal)
.unwrap()
.waypoints = vec![xy(820., 400.), xy(820., 550.)];
p
}
#[test]
fn explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor() {
for layer in [Layer::Oa, Layer::Sa] {
for folded in [false, true] {
for include_a in [false, true] {
let m = distributed(layer, folded);
let members = if include_a {
vec![m.f.n, m.a]
} else {
vec![m.f.n]
};
let selected = identities(&m, &members);
for anchor in selected.keys().copied() {
let plan = arrange::capture(&m.f.p, m.f.d, &selected, anchor).unwrap();
assert_eq!(plan.selected(), &selected);
let mut affected = BTreeSet::from([m.f.n, m.x, m.y]);
if include_a {
affected.insert(m.a);
}
assert_eq!(
plan.affected().keys().copied().collect::<BTreeSet<_>>(),
affected
);
let requested = if anchor == m.a {
xy(503., 102.)
} else {
xy(503., 802.)
};
let placed =
arrange::place(&plan, requested, presentation(&m.f.p, m.f.d, None))
.unwrap();
assert_eq!(placed.delta, xy(400., 0.));
assert_eq!(placed.unfolded, None);
let command = arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap();
assert_eq!(roundtrip(&m.f.p, command), expected(&m, include_a));
// Explicit Arrange does not widen ordinary distributed assignment.
assert!(matches!(
plan_semantics(
&m.f.p,
&scope(&m.f),
Destination::Performer(m.f.a),
&BTreeMap::new()
),
Err(Refusal::MixedLayoutRequired)
));
}
}
}
}
}
#[test]
fn explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root() {
for layer in [Layer::Oa, Layer::Sa] {
let m = distributed(layer, true);
let selected = identities(&m, &[m.a]);
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.a).unwrap();
assert_eq!(
plan.affected().keys().copied().collect::<BTreeSet<_>>(),
BTreeSet::from([m.a, m.x])
);
let placed =
arrange::place(&plan, xy(500., 100.), presentation(&m.f.p, m.f.d, None)).unwrap();
let mut expected = m.f.p.clone();
let d = expected.diagrams.get_mut(&m.f.d).unwrap();
d.nodes.get_mut(&m.a).unwrap().position = xy(500., 100.);
d.nodes.get_mut(&m.x).unwrap().position = xy(540., 200.);
// X-to-Y is now one-ended; all saved pins remain byte-for-byte unchanged.
assert_eq!(
roundtrip(
&m.f.p,
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
#[test]
fn explicit_arrange_validates_all_original_members_before_normalizing() {
let mut m = distributed(Layer::Oa, false);
let selected = identities(&m, &[m.f.n, m.x]);
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.x).unwrap();
assert_eq!(plan.selected(), &selected);
assert_eq!(plan.affected().len(), 3);
let placed = arrange::place(&plan, xy(540., 200.), presentation(&m.f.p, m.f.d, None)).unwrap();
assert_eq!(
roundtrip(
&m.f.p,
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
),
expected(&m, false)
);
let mut stale = selected.clone();
stale.insert(m.x, m.f.w);
assert!(arrange::capture(&m.f.p, m.f.d, &stale, m.f.n).is_err());
m.f.p
.diagrams
.get_mut(&m.f.d)
.unwrap()
.nodes
.get_mut(&m.a)
.unwrap()
.collapsed = true;
assert!(
arrange::capture(&m.f.p, m.f.d, &selected, m.f.n).is_err(),
"covered selected X is hidden and cannot disappear in normalization"
);
assert!(arrange::capture(&m.f.p, m.f.d, &BTreeMap::new(), m.f.n).is_err());
assert!(arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.a).is_err());
assert!(arrange::capture(&m.f.p, m.f.second, &identities(&m, &[m.f.n]), m.f.n).is_err());
let foreign = Id::new_v4();
assert!(
arrange::capture(
&m.f.p,
m.f.d,
&BTreeMap::from([(m.f.n, m.f.w), (foreign, m.f.w)]),
m.f.n
)
.is_err()
);
m.f.p.diagrams.get_mut(&m.f.d).unwrap().projection = ProjectionKind::Overview;
assert!(arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.f.n).is_err());
}
#[test]
fn explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let y = f.p.add_element(kind, layer, "Second root", None).unwrap();
let yn = f.p.place(f.d, y, xy(400., 500.));
let relation = f.p.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
"Between roots",
f.w,
y,
);
f.p.diagrams.get_mut(&f.d).unwrap().routes = vec![route(relation)];
let context = presentation(&f.p, f.d, None);
let single = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
assert!(
arrange::place(&single, xy(400., 500.), context).is_err(),
"singleton would collide with stationary Y"
);
let plan =
arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap();
let placed = arrange::place(&plan, xy(400., 500.), context).unwrap();
let mut expected = f.p.clone();
let d = expected.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.);
d.nodes.get_mut(&yn).unwrap().position = xy(700., 500.);
d.routes[0].waypoints = vec![xy(720., 400.), xy(720., 550.)];
assert_eq!(
roundtrip(
&f.p,
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
#[test]
fn explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants() {
let mut f = fixture(Layer::Oa);
let a = f.a;
role(&mut f, Some(a));
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(150., 200.);
let plan = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
let placed = arrange::place(&plan, xy(160., 210.), presentation(&f.p, f.d, None)).unwrap();
let mut role_expected = f.p.clone();
role_expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(160., 210.);
assert_eq!(
roundtrip(
&f.p,
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
role_expected
);
for layer in [Layer::Oa, Layer::Sa] {
let mut m = distributed(layer, true);
let kind = m.f.p.elements[&m.f.w].kind();
m.f.p
.add_element(kind, layer, "Unplaced leaf", Some(m.f.w))
.unwrap();
let plan = arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.f.n]), m.f.n).unwrap();
assert_eq!(plan.affected().len(), 3);
let placed =
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)).unwrap();
assert_eq!(
roundtrip(
&m.f.p,
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
),
expected(&m, false)
);
}
}
#[test]
fn explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth() {
for layer in [Layer::Oa, Layer::Sa] {
for hidden in [false, true] {
let mut m = distributed(layer, hidden);
let kind = m.f.p.elements[&m.f.w].kind();
let z = m.f.p.add_element(kind, layer, "Obstacle", None).unwrap();
let zn = m.f.p.place(
m.f.d,
z,
if hidden {
xy(550., 230.)
} else {
xy(1200., 100.)
},
);
// The independent candidate differs only by literal admitted edits.
// Establish which geometric case the real planner must reject.
let candidate = expected(&m, true);
let before = projection::project_view(&m.f.p, m.f.d);
let after = projection::project_view(&candidate, m.f.d);
let find =
|view: &ProjectedView, id| view.nodes.iter().find(|n| n.id == id).unwrap().clone();
if hidden {
assert!(!overlaps(&before, &after));
assert!(!after.nodes.iter().any(|n| n.id == m.x));
let mut diagnostic = candidate.diagrams[&m.f.d].clone();
diagnostic.nodes.get_mut(&m.a).unwrap().collapsed = false;
let expanded = projection::project_diagram(&candidate, &diagnostic);
assert_eq!(
overlap(&find(&expanded, m.x), &find(&expanded, zn)),
if layer == Layer::Oa { 13696. } else { 20116. }
);
} else {
assert_eq!(overlap(&find(&before, m.b), &find(&before, zn)), 0.);
assert_eq!(overlap(&find(&after, m.b), &find(&after, zn)), 21056.);
for id in [m.a, m.f.n, m.x, m.y] {
assert_eq!(overlap(&find(&after, id), &find(&after, zn)), 0.);
}
assert_eq!(
candidate.diagrams[&m.f.d].nodes[&m.b],
m.f.p.diagrams[&m.f.d].nodes[&m.b]
);
}
let plan =
arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.a, m.f.n]), m.f.n).unwrap();
assert!(matches!(
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)),
Err(Refusal::LayoutRequired(_))
));
}
}
}
#[test]
fn explicit_arrange_preserves_existing_overlap_but_refuses_its_growth() {
let mut f = fixture(Layer::Oa);
let y =
f.p.add_element(Kind::Activity, Layer::Oa, "Existing overlap", None)
.unwrap();
let yn = f.p.place(f.d, y, xy(250., 500.));
let context = presentation(&f.p, f.d, None);
let single = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
assert!(arrange::place(&single, xy(110., 500.), context).is_err());
let plan = arrange::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap();
let placed = arrange::place(&plan, xy(400., 500.), context).unwrap();
let mut expected = f.p.clone();
let d = expected.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.);
d.nodes.get_mut(&yn).unwrap().position = xy(550., 500.);
assert_eq!(
roundtrip(
&f.p,
arrange::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
expected
);
}
#[test]
fn explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary() {
for layer in [Layer::Oa, Layer::Sa] {
let mut m = distributed(layer, true);
let performer = if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
};
let d = m.f.p.add_element(performer, layer, "D", None).unwrap();
let z =
m.f.p
.add_element(m.f.p.elements[&m.f.w].kind(), layer, "Hidden Z", None)
.unwrap();
m.f.p.relate(RelationKind::Allocation, "D owns Z", d, z);
let dn = m.f.p.place(m.f.d, d, xy(2500., 100.));
m.f.p.place(m.f.d, z, xy(550., 230.));
m.f.p
.diagrams
.get_mut(&m.f.d)
.unwrap()
.nodes
.get_mut(&dn)
.unwrap()
.collapsed = true;
let plan = arrange::capture(&m.f.p, m.f.d, &identities(&m, &[m.a, m.f.n]), m.f.n).unwrap();
let placed =
arrange::place(&plan, xy(500., 800.), presentation(&m.f.p, m.f.d, None)).unwrap();
assert_eq!(
roundtrip(
&m.f.p,
arrange::finish(&plan, &m.f.p, &placed).unwrap().unwrap()
),
expected(&m, true)
);
}
}
#[test]
fn explicit_arrange_noop_stale_tamper_target_and_all_selected_headers() {
let m = distributed(Layer::Sa, false);
let selected = identities(&m, &[m.a, m.f.n]);
let plan = arrange::capture(&m.f.p, m.f.d, &selected, m.f.n).unwrap();
let context = presentation(&m.f.p, m.f.d, None);
let noop = arrange::place(&plan, xy(103., 802.), context).unwrap();
assert!(arrange::finish(&plan, &m.f.p, &noop).unwrap().is_none());
let mut placed = arrange::place(&plan, xy(500., 800.), context).unwrap();
let mut stale = m.f.p.clone();
stale.name.push_str(" changed");
assert!(matches!(
arrange::finish(&plan, &stale, &placed),
Err(Refusal::StaleScope)
));
placed.diagram.nodes.get_mut(&m.b).unwrap().position.x += 1.;
assert!(matches!(
arrange::finish(&plan, &m.f.p, &placed),
Err(Refusal::StaleScope)
));
assert!(
arrange::place(
&plan,
xy(500., 800.),
presentation(&m.f.p, m.f.d, Some(m.f.b))
)
.is_err()
);
let mut clipped = context;
clipped.viewport.position = xy(400., 700.);
assert!(
arrange::place(&plan, xy(500., 800.), clipped).is_err(),
"anchor C visible, other selected A header clipped"
);
assert!(arrange::place(&plan, xy(f32::NAN, 800.), context).is_err());
assert!(arrange::place(&plan, xy(MAX_COORDINATE, 800.), context).is_err());
}
@@ -0,0 +1,444 @@
//! Ordinary work-only group assignment, with one combined semantic/geometry plan.
//! App retains authority over the complete typed selection and gesture lifetime.
use super::*;
use std::sync::Arc;
#[derive(Debug)]
struct Captured {
accepted: Project,
diagram: Id,
selected: BTreeMap<Id, Id>,
anchor: Id,
roots: Vec<super::Scope>,
placements: BTreeMap<Id, NodePlacement>,
leaves: BTreeMap<Id, Leaf>,
index: ViewIndex,
before_visible: ProjectedView,
before_expanded: ProjectedView,
}
/// Destination plans share one immutable accepted capture.
#[derive(Debug, Clone)]
pub struct Scope(Arc<Captured>);
impl Scope {
pub fn project(&self) -> &Project {
&self.0.accepted
}
pub fn index(&self) -> &ViewIndex {
&self.0.index
}
pub fn diagram(&self) -> &Diagram {
&self.0.accepted.diagrams[&self.0.diagram]
}
pub fn selected(&self) -> &BTreeMap<Id, Id> {
&self.0.selected
}
pub fn affected(&self) -> &BTreeMap<Id, NodePlacement> {
&self.0.placements
}
pub fn anchor(&self) -> Id {
self.0.anchor
}
pub fn leaves(&self) -> &BTreeMap<Id, Leaf> {
&self.0.leaves
}
}
/// App must reject non-placement members before constructing this complete map.
/// Every original identity is checked before redundant work roots are removed.
pub fn capture(
p: &Project,
diagram: Id,
selected: &BTreeMap<Id, Id>,
anchor: Id,
) -> Result<Scope, Refusal> {
validate_candidate(p).map_err(|e| Refusal::InvalidCandidate(e.to_string()))?;
let d = p.diagrams.get(&diagram).ok_or(Refusal::StaleScope)?;
if d.projection != ProjectionKind::Allocation
|| selected.is_empty()
|| !selected.contains_key(&anchor)
{
return Err(Refusal::Ineligible(
"Assignment requires selected work placements and a selected anchor in an Allocation view."
.into(),
));
}
let index = ViewIndex::new(p);
let before_visible = projection::project_diagram_with_index(p, d, &index);
let visible: BTreeMap<_, _> = before_visible
.nodes
.iter()
.map(|node| (node.id, node.element))
.collect();
let mut work = BTreeMap::new();
for (placement, element) in selected {
let node = d.nodes.get(placement).ok_or(Refusal::StaleScope)?;
let record = p.elements.get(element).ok_or(Refusal::StaleScope)?;
if node.element != *element || visible.get(placement) != Some(element) {
return Err(Refusal::StaleScope);
}
if !record.kind().is_work() || record.layer() != d.layer {
return Err(Refusal::Ineligible(
"Assignment requires only activities or functions in this layer. Use Arrange only for a supported mixed selection."
.into(),
));
}
work.insert(*element, *placement);
}
let mut roots = Vec::new();
let mut placements = BTreeMap::new();
let mut leaves = BTreeMap::new();
for (element, placement) in &work {
let record = &p.elements[element];
let mut ancestor = record.parent;
let mut seen = BTreeSet::new();
let mut covered = false;
while let Some(id) = ancestor {
if !seen.insert(id) {
return Err(Refusal::StaleScope);
}
let parent = p.elements.get(&id).ok_or(Refusal::StaleScope)?;
if parent.kind() != record.kind() {
break;
}
if work.contains_key(&id) {
covered = true;
break;
}
ancestor = parent.parent;
}
if covered {
continue;
}
let root = capture_validated(p, diagram, *element, *placement)?;
if root.work_records.len() > 1 && root.uniform == Uniform::MixedOrUnresolved {
return Err(Refusal::MixedLayoutRequired);
}
for (id, leaf) in &root.leaves {
if leaves.insert(*id, leaf.clone()).is_some() {
return Err(Refusal::StaleScope);
}
}
placements.extend(root.placements.clone());
roots.push(root);
}
let expansion = geometry::expansion_ids(p, d, &index, &placements)?;
let before_expanded = geometry::expanded_view(p, d, &index, &expansion);
if roots
.iter()
.any(|root| root.work_records.len() > 1 && !coherent(root, &before_expanded))
{
return Err(Refusal::MixedLayoutRequired);
}
Ok(Scope(Arc::new(Captured {
accepted: p.clone(),
diagram,
selected: selected.clone(),
anchor,
roots,
placements,
leaves,
index,
before_visible,
before_expanded,
})))
}
#[derive(Debug)]
pub struct SemanticPlan {
scope: Scope,
destination: Destination,
candidate: Project,
index: ViewIndex,
commands: Vec<Command>,
changed: BTreeSet<Id>,
expanded_ids: BTreeSet<Id>,
label: String,
warnings: Vec<Issue>,
}
impl SemanticPlan {
pub fn scope(&self) -> &Scope {
&self.scope
}
pub fn project(&self) -> &Project {
&self.candidate
}
pub fn index(&self) -> &ViewIndex {
&self.index
}
pub fn destination(&self) -> Destination {
self.destination
}
pub fn label(&self) -> &str {
&self.label
}
pub fn warnings(&self) -> &[Issue] {
&self.warnings
}
pub fn changed_count(&self) -> usize {
self.changed.len()
}
/// Already effective at the performer, or already unassigned for Outside.
pub fn already_correct_count(&self) -> usize {
self.scope.leaves().len() - self.changed.len()
}
}
/// Build one destination candidate. Per-root decisions read accepted facts;
/// aliases and new identities are reserved in the combined expected Project.
pub fn plan_semantics(
scope: &Scope,
destination: Destination,
allocation_ids: &BTreeMap<Id, Id>,
) -> Result<SemanticPlan, Refusal> {
let accepted = scope.project();
match destination {
Destination::LayoutOnly => {
return Err(Refusal::Ineligible(
"Use the explicit Arrange only planner for presentation-only movement.".into(),
));
}
Destination::Performer(id) => {
if !accepted.elements.get(&id).is_some_and(|element| {
element.kind().is_performer() && element.layer() == scope.diagram().layer
}) {
return Err(Refusal::Ineligible(
"Choose a performer in this layer.".into(),
));
}
}
Destination::OutsidePerformers => {}
}
let mut expected = accepted.clone();
let mut edits = IntendedEdits::default();
for root in &scope.0.roots {
append_semantic_edits(
accepted,
root,
destination,
allocation_ids,
&mut expected,
&mut edits,
)?;
}
for diagram in expected.diagrams.values_mut() {
diagram
.routes
.retain(|route| !edits.deleted.contains(&route.edge));
}
let candidate = prepare_command(accepted, &Command::Batch(edits.commands.clone()))
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &candidate)?;
let warnings = validation::validate(&candidate)
.into_iter()
.filter(|issue| issue.severity == Severity::Warning)
.collect();
let index = ViewIndex::new(&candidate);
// Assignment may change visual ancestry: never reuse accepted expansion IDs.
let expanded_ids = geometry::expansion_ids(
&candidate,
&candidate.diagrams[&scope.0.diagram],
&index,
scope.affected(),
)?;
let total = scope.leaves().len();
let changed = edits.changed.len();
let already_correct = total - changed;
let noun = match (scope.diagram().layer, total) {
(Layer::Oa, 1) => "activity",
(Layer::Oa, _) => "activities",
(Layer::Sa, 1) => "function",
(Layer::Sa, _) => "functions",
};
let retained_roles = scope
.leaves()
.values()
.any(|leaf| !leaf.role_assignments.is_empty());
let label = match destination {
Destination::Performer(id) if changed == 0 => {
format!(
"Arrange within {} — all {total} leaf {noun} already assigned",
accepted.named(id)
)
}
Destination::Performer(id) => format!(
"Assign {changed} of {total} leaf {noun} to {}{}{}",
accepted.named(id),
if already_correct > 0 {
format!(" · {already_correct} already assigned")
} else {
String::new()
},
if retained_roles {
" — role responsibility retained"
} else {
""
},
),
Destination::OutsidePerformers if changed == 0 => {
"Arrange unassigned work — decomposition retained".into()
}
Destination::OutsidePerformers => format!(
"Remove assignments from {changed} of {total} leaf {noun} — decomposition retained"
),
Destination::LayoutOnly => unreachable!(),
};
Ok(SemanticPlan {
scope: scope.clone(),
destination,
candidate,
index,
commands: edits.commands,
changed: edits.changed,
expanded_ids,
label,
warnings,
})
}
/// Translate the entire captured Q once, then evaluate the combined candidate.
/// This path does not clone/validate a Project or rebuild semantic indexes.
pub fn place(
plan: &SemanticPlan,
requested: Position,
presentation: PresentationContext,
) -> Result<PlacementPlan, Refusal> {
if !requested.x.is_finite()
|| !requested.y.is_finite()
|| !presentation.viewport.valid()
|| !presentation.zoom.is_finite()
|| presentation.zoom <= 0.
{
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
let scope = &plan.scope;
let geometry::Translation {
mut diagram,
mut commands,
delta,
} = geometry::translate(
scope.project(),
scope.diagram(),
&plan.candidate.diagrams[&scope.0.diagram],
scope.affected(),
scope.anchor(),
requested,
)?;
let mut unfolded = None;
if let Destination::Performer(performer) = plan.destination {
let target = presentation.target.ok_or_else(|| {
Refusal::LayoutRequired("A visible accepted performer target is required.".into())
})?;
if !scope
.0
.before_visible
.nodes
.iter()
.any(|node| node.id == target.placement && node.element == performer)
{
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
let target_node = diagram
.nodes
.get_mut(&target.placement)
.ok_or(Refusal::StaleScope)?;
if target_node.element != performer {
return Err(Refusal::StaleScope);
}
if !plan.changed.is_empty() && target_node.collapsed {
target_node.collapsed = false;
unfolded = Some(target_node.id);
commands.push(Command::EditNode {
diagram: diagram.id,
original: target_node.id,
node: target_node.clone(),
});
}
} else if presentation.target.is_some() {
return Err(Refusal::Ineligible(
"An outside destination has no performer target.".into(),
));
}
let view = projection::project_diagram_with_index(&plan.candidate, &diagram, &plan.index);
let expanded =
geometry::expanded_view(&plan.candidate, &diagram, &plan.index, &plan.expanded_ids);
if scope
.0
.roots
.iter()
.any(|root| root.work_records.len() > 1 && !coherent(root, &expanded))
{
return Err(Refusal::MixedLayoutRequired);
}
geometry::check_scenes(
&scope.0.before_visible,
&scope.0.before_expanded,
&view,
&expanded,
)?;
geometry::check_rigid(&scope.0.before_expanded, &expanded, scope.affected(), delta)?;
geometry::check_headers(&view, scope.selected(), presentation.viewport)?;
Ok(PlacementPlan {
diagram,
view,
delta,
unfolded,
requested,
presentation,
commands,
})
}
/// Recheck immutable facts and recompute geometry before one final source batch.
/// Zero delta remains a transaction when assignment semantics changed.
pub fn finish(
plan: &SemanticPlan,
live: &Project,
placement: &PlacementPlan,
) -> Result<Option<Command>, Refusal> {
if live != plan.scope.project() {
return Err(Refusal::StaleScope);
}
let checked = place(plan, placement.requested, placement.presentation)?;
if checked.diagram != placement.diagram
|| checked.view != placement.view
|| checked.delta != placement.delta
|| checked.unfolded != placement.unfolded
|| checked.commands != placement.commands
{
return Err(Refusal::StaleScope);
}
let mut expected = plan.candidate.clone();
expected
.diagrams
.insert(checked.diagram.id, checked.diagram);
let mut commands = plan.commands.clone();
commands.extend(checked.commands);
let command = Command::Batch(commands);
let actual = prepare_command(live, &command)
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
Ok((actual != *live).then_some(command))
}
@@ -0,0 +1,497 @@
use super::*;
struct Composite {
f: Fixture,
child: Id,
child_node: Id,
unplaced: Id,
}
fn composite(layer: Layer) -> Composite {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let child =
f.p.add_element(kind, layer, "Placed child", Some(f.w))
.unwrap();
let unplaced =
f.p.add_element(kind, layer, "Unplaced leaf", Some(f.w))
.unwrap();
let child_node = f.p.place(f.d, child, xy(140., 600.));
validate_candidate(&f.p).unwrap();
Composite {
f,
child,
child_node,
unplaced,
}
}
fn expected_allocation(id: Id, alias: &str, name: &str, performer: Id, work: Id) -> Relation {
Relation {
id,
alias: alias.into(),
name: name.into(),
description: String::new(),
body: RelationBody::Allocation {
source: performer,
target: work,
},
}
}
#[test]
fn group_assignment_checks_original_identity_and_visibility_before_normalization() {
for layer in [Layer::Oa, Layer::Sa] {
let c = composite(layer);
let valid = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]);
let admitted = assignment::capture(&c.f.p, c.f.d, &valid, c.f.n).unwrap();
assert_eq!(admitted.selected(), &valid);
let foreign = c.f.p.diagrams[&c.f.second]
.nodes
.values()
.find(|node| node.element == c.f.w)
.unwrap()
.id;
// Each extra member is semantically covered by the valid selected parent.
// Normalizing first would incorrectly hide its invalid original identity.
for invalid in [
BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.unplaced)]),
BTreeMap::from([(c.f.n, c.f.w), (Id::new_v4(), c.child)]),
BTreeMap::from([(c.f.n, c.f.w), (foreign, c.f.w)]),
] {
assert!(matches!(
assignment::capture(&c.f.p, c.f.d, &invalid, c.f.n),
Err(Refusal::StaleScope)
));
}
let mut folded = c.f.p.clone();
folded
.diagrams
.get_mut(&c.f.d)
.unwrap()
.nodes
.get_mut(&c.f.n)
.unwrap()
.collapsed = true;
validate_candidate(&folded).unwrap();
assert!(
!projection::project_view(&folded, c.f.d)
.nodes
.iter()
.any(|node| node.id == c.child_node)
);
// Hidden descendants remain in Q when only the visible parent is selected.
let parent_only =
assignment::capture(&folded, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n).unwrap();
assert!(parent_only.affected().contains_key(&c.child_node));
assert!(matches!(
assignment::capture(&folded, c.f.d, &valid, c.f.n),
Err(Refusal::StaleScope)
));
}
}
#[test]
fn group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once() {
for layer in [Layer::Oa, Layer::Sa] {
let c = composite(layer);
let selected = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]);
let scope = assignment::capture(&c.f.p, c.f.d, &selected, c.f.n).unwrap();
assert_eq!(scope.selected(), &selected);
assert_eq!(
scope.affected().keys().copied().collect::<BTreeSet<_>>(),
BTreeSet::from([c.f.n, c.child_node])
);
assert_eq!(
scope.leaves().keys().copied().collect::<BTreeSet<_>>(),
BTreeSet::from([c.child, c.unplaced])
);
let child_allocation = Id::new_v4();
let unplaced_allocation = Id::new_v4();
let ids = BTreeMap::from([
(c.child, child_allocation),
(c.unplaced, unplaced_allocation),
]);
let plan = assignment::plan_semantics(&scope, Destination::Performer(c.f.a), &ids).unwrap();
assert_eq!(plan.changed_count(), 2);
let mut expected = c.f.p.clone();
expected.relations.insert(
child_allocation,
expected_allocation(
child_allocation,
"a_performs_placed_child",
"A performs Placed child",
c.f.a,
c.child,
),
);
expected.relations.insert(
unplaced_allocation,
expected_allocation(
unplaced_allocation,
"a_performs_unplaced_leaf",
"A performs Unplaced leaf",
c.f.a,
c.unplaced,
),
);
assert_eq!(plan.project(), &expected);
let placed = assignment::place(
&plan,
xy(140., 200.),
presentation(&c.f.p, c.f.d, Some(c.f.a)),
)
.unwrap();
let mut clipped = presentation(&c.f.p, c.f.d, Some(c.f.a));
clipped.viewport.size.height = 450.; // bottom350: parent header fits; selected child ends362.
for (id, should_fit) in [(c.f.n, true), (c.child_node, false)] {
let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap();
let header = Rect {
position: node.position,
size: Size {
width: node.size.width,
height: 62.,
},
};
assert_eq!(clipped.viewport.contains(header), should_fit);
}
assert!(
matches!(
assignment::place(&plan, xy(140., 200.), clipped),
Err(Refusal::LayoutRequired(_))
),
"normalized-away selected child still requires its header"
);
let d = expected.diagrams.get_mut(&c.f.d).unwrap();
d.nodes.get_mut(&c.f.n).unwrap().position = xy(140., 200.);
d.nodes.get_mut(&c.child_node).unwrap().position = xy(180., 300.);
assert_eq!(placed.delta, xy(40., -300.));
assert_eq!(
roundtrip(
&c.f.p,
assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap()
),
expected
);
assert_eq!(
expected.diagrams[&c.f.d].nodes.len(),
c.f.p.diagrams[&c.f.d].nodes.len()
);
assert!(
!expected.diagrams[&c.f.d]
.nodes
.values()
.any(|node| node.element == c.unplaced)
);
assert!(
!expected
.relations
.values()
.any(|relation| relation.kind() == RelationKind::Allocation
&& relation.target() == c.f.w)
);
}
}
#[test]
fn group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign() {
for layer in [Layer::Oa, Layer::Sa] {
let mut c = composite(layer);
let second_node = c.f.p.place(c.f.d, c.unplaced, xy(940., 200.));
let performer =
c.f.p
.add_element(
if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
},
layer,
"C",
None,
)
.unwrap();
c.f.p.place(c.f.d, performer, xy(1700., 100.));
let d = c.f.p.diagrams.get_mut(&c.f.d).unwrap();
d.nodes.get_mut(&c.f.n).unwrap().position = xy(1700., 800.);
d.nodes.get_mut(&c.child_node).unwrap().position = xy(140., 200.);
let ax =
c.f.p
.relate(RelationKind::Allocation, "A child", c.f.a, c.child);
let by =
c.f.p
.relate(RelationKind::Allocation, "B child", c.f.b, c.unplaced);
validate_candidate(&c.f.p).unwrap();
let visible = projection::project_view(&c.f.p, c.f.d);
for id in [c.f.n, c.child_node, second_node] {
assert!(visible.nodes.iter().any(|node| node.id == id));
}
assert_eq!(scope(&c.f).uniform, Uniform::MixedOrUnresolved);
assert!(matches!(
assignment::capture(&c.f.p, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n,),
Err(Refusal::MixedLayoutRequired)
));
let selected = BTreeMap::from([(c.child_node, c.child), (second_node, c.unplaced)]);
let captured = assignment::capture(&c.f.p, c.f.d, &selected, c.child_node).unwrap();
let plan = assignment::plan_semantics(
&captured,
Destination::Performer(performer),
&BTreeMap::new(),
)
.unwrap();
let mut expected = c.f.p.clone();
expected.relations.get_mut(&ax).unwrap().body = RelationBody::Allocation {
source: performer,
target: c.child,
};
expected.relations.get_mut(&by).unwrap().body = RelationBody::Allocation {
source: performer,
target: c.unplaced,
};
assert_eq!(plan.project(), &expected);
// One delta moves both leaves. The unselected semantic parent remains
// authored at (1700,800), while its derived enclosure may change.
let placed = assignment::place(
&plan,
xy(1740., 120.),
presentation(&c.f.p, c.f.d, Some(performer)),
)
.unwrap();
let d = expected.diagrams.get_mut(&c.f.d).unwrap();
d.nodes.get_mut(&c.child_node).unwrap().position = xy(1740., 120.);
d.nodes.get_mut(&second_node).unwrap().position = xy(2540., 120.);
assert_eq!(
roundtrip(
&c.f.p,
assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
fn separate_unassigned(layer: Layer) -> (Fixture, Id, Id) {
let mut f = fixture(layer);
let companion =
f.p.add_element(f.p.elements[&f.w].kind(), layer, "Companion", None)
.unwrap();
let companion_node = f.p.place(f.d, companion, xy(100., 1000.));
validate_candidate(&f.p).unwrap();
(f, companion, companion_node)
}
#[test]
fn group_assignment_finish_rejects_stale_project_and_each_tampered_result() {
let (f, companion, companion_node) = separate_unassigned(Layer::Oa);
let selected = BTreeMap::from([(f.n, f.w), (companion_node, companion)]);
let captured = assignment::capture(&f.p, f.d, &selected, f.n).unwrap();
let plan =
assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
.unwrap();
let context = presentation(&f.p, f.d, None);
let valid = assignment::place(&plan, xy(500., 500.), context).unwrap();
let mut expected = f.p.clone();
expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(500., 500.);
expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&companion_node)
.unwrap()
.position = xy(500., 1000.);
assert_eq!(
roundtrip(
&f.p,
assignment::finish(&plan, &f.p, &valid).unwrap().unwrap()
),
expected
);
let mut stale = f.p.clone();
stale.elements.get_mut(&f.b).unwrap().description = "Changed unrelated accepted record".into();
validate_candidate(&stale).unwrap();
assert!(matches!(
assignment::finish(&plan, &stale, &valid),
Err(Refusal::StaleScope)
));
for field in 0..6 {
let mut altered = assignment::place(&plan, xy(500., 500.), context).unwrap();
match field {
0 => altered.diagram.nodes.get_mut(&f.n).unwrap().position.x += 20.,
1 => {
altered
.view
.nodes
.iter_mut()
.find(|node| node.id == f.n)
.unwrap()
.position
.x += 20.
}
2 => altered.delta.x += 20.,
3 => altered.unfolded = Some(f.n),
4 => altered.commands.clear(),
5 => altered.requested.x += 10.,
_ => unreachable!(),
}
assert!(
matches!(
assignment::finish(&plan, &f.p, &altered),
Err(Refusal::StaleScope)
),
"tampered result field {field}"
);
}
}
#[test]
fn group_assignment_requires_non_anchor_selected_header_inside_viewport() {
let (f, companion, companion_node) = separate_unassigned(Layer::Oa);
let captured = assignment::capture(
&f.p,
f.d,
&BTreeMap::from([(f.n, f.w), (companion_node, companion)]),
f.n,
)
.unwrap();
let plan =
assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
.unwrap();
let wide = presentation(&f.p, f.d, None);
let placed = assignment::place(&plan, xy(500., 500.), wide).unwrap();
let mut narrow = wide;
narrow.viewport.size.height = 800.; // world bottom700: anchor header fits, companion does not.
let header = |id| {
let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap();
Rect {
position: node.position,
size: Size {
width: node.size.width,
height: 62.,
},
}
};
assert!(narrow.viewport.contains(header(f.n)));
assert!(!narrow.viewport.contains(header(companion_node)));
// Only the viewport differs from the already admitted identical geometry.
assert!(matches!(
assignment::place(&plan, xy(500., 500.), narrow),
Err(Refusal::LayoutRequired(_))
));
}
#[test]
fn group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside() {
for ambiguous in [false, true] {
let (mut f, companion, companion_node) = separate_unassigned(Layer::Oa);
let (role_id, role_assignment) = role(&mut f, None);
if ambiguous {
f.p.relate(
RelationKind::RoleRealization,
"A realizes responsibility",
f.a,
role_id,
);
f.p.relate(
RelationKind::RoleRealization,
"B realizes responsibility",
f.b,
role_id,
);
}
validate_candidate(&f.p).unwrap();
assert_eq!(validation::effective_performer(&f.p, f.w), None);
let captured = assignment::capture(
&f.p,
f.d,
&BTreeMap::from([(f.n, f.w), (companion_node, companion)]),
f.n,
)
.unwrap();
let Refusal::RoleExitRequired {
work,
roles,
performers,
..
} = assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
.unwrap_err()
else {
panic!("unresolved role must produce its specific outward refusal");
};
assert_eq!(work, f.w);
assert_eq!(roles, vec![role_id]);
assert_eq!(
performers.into_iter().collect::<BTreeSet<_>>(),
if ambiguous {
BTreeSet::from([f.a, f.b])
} else {
BTreeSet::new()
}
);
let work_allocation = Id::new_v4();
let companion_allocation = Id::new_v4();
let ids = BTreeMap::from([(f.w, work_allocation), (companion, companion_allocation)]);
let plan =
assignment::plan_semantics(&captured, Destination::Performer(f.b), &ids).unwrap();
assert_eq!(plan.changed_count(), 2);
assert!(plan.label().contains("role responsibility retained"));
let mut expected = f.p.clone();
expected.relations.insert(
work_allocation,
expected_allocation(
work_allocation,
"b_performs_work",
"B performs Work",
f.b,
f.w,
),
);
expected.relations.insert(
companion_allocation,
expected_allocation(
companion_allocation,
"b_performs_companion",
"B performs Companion",
f.b,
companion,
),
);
assert_eq!(plan.project(), &expected);
assert_eq!(
plan.project().relations[&role_assignment],
f.p.relations[&role_assignment]
);
let placed =
assignment::place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap();
expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(940., 200.);
expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&companion_node)
.unwrap()
.position = xy(940., 700.);
assert_eq!(
roundtrip(
&f.p,
assignment::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
@@ -0,0 +1,606 @@
use super::*;
struct Roots {
f: Fixture,
c: Id,
cn: Id,
y: Id,
yn: Id,
z: Id,
zn: Id,
ax: Id,
by: Id,
}
fn roots(layer: Layer) -> Roots {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let c =
f.p.add_element(
if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
},
layer,
"C",
None,
)
.unwrap();
let y = f.p.add_element(kind, layer, "Y", None).unwrap();
let z = f.p.add_element(kind, layer, "Z", None).unwrap();
let cn = f.p.place(f.d, c, xy(1500., 100.));
let yn = f.p.place(f.d, y, xy(140., 800.));
let zn = f.p.place(f.d, z, xy(1540., 1400.));
let d = f.p.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(140., 200.);
d.nodes
.values_mut()
.find(|n| n.element == f.b)
.unwrap()
.position = xy(100., 700.);
let ax = f.p.relate(
RelationKind::Allocation,
"Original X responsibility",
f.a,
f.w,
);
let by = f.p.relate(
RelationKind::Allocation,
"Original Y responsibility",
f.b,
y,
);
f.p.relations.get_mut(&ax).unwrap().description = "Reviewed X identity must survive".into();
f.p.relations.get_mut(&by).unwrap().description = "Reviewed Y identity must survive".into();
if layer == Layer::Oa {
let r =
f.p.add_element(Kind::Role, layer, "Responsibility", None)
.unwrap();
f.p.relate(RelationKind::RoleAssignment, "Z role", r, z);
f.p.relate(RelationKind::RoleRealization, "C role", c, r);
} else {
f.p.relate(RelationKind::Allocation, "Z already assigned", c, z);
}
validate_candidate(&f.p).unwrap();
Roots {
f,
c,
cn,
y,
yn,
z,
zn,
ax,
by,
}
}
fn wide(p: &Project, d: Id, target: Option<Id>) -> PresentationContext {
let mut context = presentation(p, d, target);
context.viewport.size = Size {
width: 5000.,
height: 3000.,
};
context
}
fn selected(r: &Roots, with_z: bool) -> BTreeMap<Id, Id> {
let mut ids = BTreeMap::from([(r.f.n, r.f.w), (r.yn, r.y)]);
if with_z {
ids.insert(r.zn, r.z);
}
ids
}
fn retarget(p: &mut Project, relation: Id, target: Id) {
let r = p.relations.get_mut(&relation).unwrap();
r.body = RelationBody::Allocation {
source: target,
target: r.target(),
};
}
#[test]
fn group_assignment_independent_performers_preserves_already_correct_provenance() {
for layer in [Layer::Oa, Layer::Sa] {
let r = roots(layer);
let selected = selected(&r, true);
let scope = assignment::capture(&r.f.p, r.f.d, &selected, r.f.n).unwrap();
let plan =
assignment::plan_semantics(&scope, Destination::Performer(r.c), &BTreeMap::new())
.unwrap();
assert_eq!(plan.changed_count(), 2);
assert_eq!(plan.already_correct_count(), 1);
let placed =
assignment::place(&plan, xy(1540., 200.), wide(&r.f.p, r.f.d, Some(r.c))).unwrap();
let mut expected = r.f.p.clone();
retarget(&mut expected, r.ax, r.c);
retarget(&mut expected, r.by, r.c);
let d = expected.diagrams.get_mut(&r.f.d).unwrap();
d.nodes.get_mut(&r.f.n).unwrap().position = xy(1540., 200.);
d.nodes.get_mut(&r.yn).unwrap().position = xy(1540., 800.);
d.nodes.get_mut(&r.zn).unwrap().position = xy(2940., 1400.);
assert_eq!(
roundtrip(
&r.f.p,
assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap()
),
expected
);
if layer == Layer::Oa {
assert!(
!plan
.project()
.relations
.values()
.any(|relation| relation.kind() == RelationKind::Allocation
&& relation.target() == r.z)
);
assert!(matches!(
assignment::plan_semantics(
&scope,
Destination::OutsidePerformers,
&BTreeMap::new()
),
Err(Refusal::RoleExitRequired { .. })
));
}
assert!(
assignment::plan_semantics(&scope, Destination::LayoutOnly, &BTreeMap::new()).is_err()
);
}
}
#[test]
fn group_assignment_zero_delta_changes_semantics_once_and_then_is_noop() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let d = f.p.diagrams.get_mut(&f.d).unwrap();
d.nodes
.values_mut()
.find(|n| n.element == f.a)
.unwrap()
.size = Size {
width: 600.,
height: 400.,
};
d.nodes.get_mut(&f.n).unwrap().position = xy(140., 200.);
let scope = assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
let id = Id::new_v4();
let plan = assignment::plan_semantics(
&scope,
Destination::Performer(f.a),
&BTreeMap::from([(f.w, id)]),
)
.unwrap();
let placed = assignment::place(&plan, xy(140., 200.), wide(&f.p, f.d, Some(f.a))).unwrap();
assert_eq!(placed.delta, xy(0., 0.));
let mut expected = f.p.clone();
expected.relations.insert(
id,
Relation {
id,
alias: "a_performs_work".into(),
name: "A performs Work".into(),
description: String::new(),
body: RelationBody::Allocation {
source: f.a,
target: f.w,
},
},
);
assert_eq!(
roundtrip(
&f.p,
assignment::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
expected
);
let scope =
assignment::capture(&expected, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
let noop =
assignment::plan_semantics(&scope, Destination::Performer(f.a), &BTreeMap::new())
.unwrap();
let placed =
assignment::place(&noop, xy(140., 200.), wide(&expected, f.d, Some(f.a))).unwrap();
assert!(
assignment::finish(&noop, &expected, &placed)
.unwrap()
.is_none()
);
}
}
#[test]
fn group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap() {
for layer in [Layer::Oa, Layer::Sa] {
for obstacle in [false, true] {
let mut r = roots(layer);
let d = r.f.p.diagrams.get_mut(&r.f.d).unwrap();
d.nodes
.values_mut()
.find(|n| n.element == r.f.b)
.unwrap()
.position = xy(100., 400.);
d.nodes.get_mut(&r.yn).unwrap().position = xy(140., 500.);
d.nodes.get_mut(&r.zn).unwrap().position = xy(1540., 800.);
d.nodes.get_mut(&r.cn).unwrap().collapsed = true;
if obstacle {
let k =
r.f.p
.add_element(r.f.p.elements[&r.f.w].kind(), layer, "Hidden overlap", None)
.unwrap();
r.f.p
.relate(RelationKind::Allocation, "C also owns K", r.c, k);
r.f.p.place(r.f.d, k, xy(1540., 800.));
}
assert!(
!projection::project_view(&r.f.p, r.f.d)
.nodes
.iter()
.any(|n| n.id == r.zn)
);
assert!(
assignment::capture(&r.f.p, r.f.d, &selected(&r, true), r.f.n).is_err(),
"hidden selected member is not normalized away"
);
let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap();
let plan =
assignment::plan_semantics(&scope, Destination::Performer(r.c), &BTreeMap::new())
.unwrap();
let placed = assignment::place(&plan, xy(1540., 120.), wide(&r.f.p, r.f.d, Some(r.c)));
if obstacle {
assert!(matches!(placed, Err(Refusal::LayoutRequired(_))));
continue;
}
let placed = placed.unwrap();
assert_eq!(placed.unfolded, Some(r.cn));
let mut expected = r.f.p.clone();
retarget(&mut expected, r.ax, r.c);
retarget(&mut expected, r.by, r.c);
let d = expected.diagrams.get_mut(&r.f.d).unwrap();
d.nodes.get_mut(&r.cn).unwrap().collapsed = false;
d.nodes.get_mut(&r.f.n).unwrap().position = xy(1540., 120.);
d.nodes.get_mut(&r.yn).unwrap().position = xy(1540., 420.);
assert_eq!(
roundtrip(
&r.f.p,
assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
}
#[test]
fn group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse() {
let mut f = fixture(Layer::Oa);
let y =
f.p.add_element(Kind::Activity, Layer::Oa, "Work", None)
.unwrap();
let yn = f.p.place(f.d, y, xy(400., 500.));
let selected = BTreeMap::from([(f.n, f.w), (yn, y)]);
let scope = assignment::capture(&f.p, f.d, &selected, f.n).unwrap();
let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]);
let first = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap();
let other = assignment::plan_semantics(&scope, Destination::Performer(f.b), &ids).unwrap();
let repeated = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap();
assert_eq!(first.project(), repeated.project());
assert_eq!(first.changed_count(), 2);
let mut expected = f.p.clone();
let aliases: BTreeSet<_> = ids
.values()
.map(|id| first.project().relations[id].alias.clone())
.collect();
assert_eq!(aliases.len(), 2);
assert!(aliases.contains("a_performs_work"));
for (ordinal, (work, id)) in ids.iter().enumerate() {
let relation = &first.project().relations[id];
let expected_relation = Relation {
id: *id,
alias: if ordinal == 0 {
"a_performs_work".into()
} else {
format!("a_performs_work_{}", &id.simple().to_string()[..8])
},
name: "A performs Work".into(),
description: String::new(),
body: RelationBody::Allocation {
source: f.a,
target: *work,
},
};
assert_eq!(relation, &expected_relation);
assert_eq!(
other.project().relations[id].body,
RelationBody::Allocation {
source: f.b,
target: *work
}
);
expected.relations.insert(*id, expected_relation);
}
assert_eq!(first.project(), &expected);
let duplicate = Id::new_v4();
for bad in [
BTreeMap::from([(f.w, duplicate), (y, duplicate)]),
BTreeMap::from([(f.w, Id::nil()), (y, ids[&y])]),
BTreeMap::from([(f.w, f.a), (y, ids[&y])]),
] {
assert!(assignment::plan_semantics(&scope, Destination::Performer(f.a), &bad).is_err());
}
}
#[test]
fn group_assignment_source_failure_preserves_existing_history_and_retry_ids() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let y =
f.p.add_element(f.p.elements[&f.w].kind(), layer, "Y", None)
.unwrap();
let yn = f.p.place(f.d, y, xy(400., 800.));
let mut bundle = format_bundle(&f.p);
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(
0,
"// Keep reviewed source through a refused group assignment.\n",
);
let mut limits = ProjectLimits::default();
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 128;
let mut document = Document::from_bundle(bundle, limits).unwrap();
document
.execute(Command::Describe(f.w, "Reviewed".into()))
.unwrap();
document
.execute(Command::Rename(y, "Future Y".into()))
.unwrap();
let redo_project = document.project().clone();
let redo_source = document.bundle().clone();
assert!(document.undo());
assert!(document.can_undo() && document.can_redo());
let accepted = document.project().clone();
let source = document.bundle().clone();
let revision = document.revision();
let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]);
let scope =
assignment::capture(&accepted, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n)
.unwrap();
let plan = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap();
let placed =
assignment::place(&plan, xy(150., 200.), wide(&accepted, f.d, Some(f.a))).unwrap();
let command = assignment::finish(&plan, &accepted, &placed)
.unwrap()
.unwrap();
assert!(matches!(
document.execute(command.clone()),
Err(crate::document::DocumentError::Source(_))
));
assert_eq!(document.project(), &accepted);
assert_eq!(document.bundle(), &source);
assert_eq!(document.revision(), revision);
assert!(document.can_undo() && document.can_redo());
assert!(document.redo());
assert_eq!(document.project(), &redo_project);
assert_eq!(document.bundle(), &redo_source);
assert!(document.undo());
assert_eq!(document.project(), &accepted);
assert_eq!(document.bundle(), &source);
let mut expected = accepted.clone();
for (work, name, alias) in [
(f.w, "A performs Work", "a_performs_work"),
(y, "A performs Y", "a_performs_y"),
] {
expected.relations.insert(
ids[&work],
Relation {
id: ids[&work],
alias: alias.into(),
name: name.into(),
description: String::new(),
body: RelationBody::Allocation {
source: f.a,
target: work,
},
},
);
}
let d = expected.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(150., 200.);
d.nodes.get_mut(&yn).unwrap().position = xy(450., 500.);
assert_eq!(roundtrip(&accepted, command), expected);
}
}
#[test]
fn group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate() {
for layer in [Layer::Oa, Layer::Sa] {
let mut r = roots(layer);
let k =
r.f.p
.add_element(Kind::Constraint, layer, "Keep reviewed assignment", None)
.unwrap();
*r.f.p.elements.get_mut(&k).unwrap().shared_mut().unwrap() = SharedBody::Constraint {
expression: "reviewed assignment".into(),
constrained: vec![r.ax, r.f.w],
};
let core = prepare_command(&r.f.p, &Command::Delete(r.ax))
.unwrap()
.project;
validate_candidate(&core).unwrap();
assert!(core.elements.contains_key(&k));
assert_ne!(core.elements[&k], r.f.p.elements[&k]);
let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap();
let Err(Refusal::CollateralChangeRequired(changed)) =
assignment::plan_semantics(&scope, Destination::OutsidePerformers, &BTreeMap::new())
else {
panic!("surviving constraint edit must refuse the entire plan");
};
assert!(changed.iter().any(|record| record.id == k));
}
}
#[test]
fn group_assignment_uses_vacated_space_and_one_complete_route_owner_map() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let y = f.p.add_element(kind, layer, "Y", None).unwrap();
let outside = f.p.add_element(kind, layer, "Outside", None).unwrap();
let yn = f.p.place(f.d, y, xy(400., 500.));
f.p.place(f.d, outside, xy(1600., 500.));
let d = f.p.diagrams.get_mut(&f.d).unwrap();
let a = d.nodes.values_mut().find(|n| n.element == f.a).unwrap();
a.position = xy(350., 350.);
a.size = Size {
width: 1000.,
height: 600.,
};
let edge_kind = if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
};
let internal = f.p.relate(edge_kind, "Internal", f.w, y);
let external = f.p.relate(edge_kind, "External", y, outside);
let mut internal_route = route(internal);
internal_route.waypoints = vec![xy(370., 550.), xy(370., 650.)];
internal_route.source_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Bottom,
offset: 0.3,
});
internal_route.target_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.7,
});
let mut external_route = internal_route.clone();
external_route.edge = external;
external_route.waypoints = vec![xy(1200., 550.), xy(1300., 650.)];
for diagram in [f.d, f.second] {
f.p.diagrams.get_mut(&diagram).unwrap().routes =
vec![internal_route.clone(), external_route.clone()];
}
if let RelationBody::FunctionalExchange { source_port, .. } = f.p.relations[&internal].body
{
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.port_anchors
.push(PortAnchor {
port: source_port,
side: PortAnchorSide::Bottom,
offset: 0.4,
});
}
validate_candidate(&f.p).unwrap();
let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]);
// The singleton candidate really occupies stationary Y's card. The
// combined candidate must not inherit that sequential-planning veto.
let mut singleton = f.p.clone();
singleton
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(400., 500.);
let projected = projection::project_view(&singleton, f.d);
let x_card = projected.nodes.iter().find(|n| n.id == f.n).unwrap();
let y_card = projected.nodes.iter().find(|n| n.id == yn).unwrap();
assert_eq!(
overlap(x_card, y_card),
if layer == Layer::Oa { 21056. } else { 29568. }
);
let single = assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap();
let single =
assignment::plan_semantics(&single, Destination::Performer(f.a), &ids).unwrap();
assert!(matches!(
assignment::place(&single, xy(400., 500.), wide(&f.p, f.d, Some(f.a))),
Err(Refusal::LayoutRequired(_))
));
let scope =
assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap();
let plan = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap();
let placed = assignment::place(&plan, xy(400., 500.), wide(&f.p, f.d, Some(f.a))).unwrap();
let mut expected = f.p.clone();
for (work, name, alias) in [
(f.w, "A performs Work", "a_performs_work"),
(y, "A performs Y", "a_performs_y"),
] {
expected.relations.insert(
ids[&work],
Relation {
id: ids[&work],
alias: alias.into(),
name: name.into(),
description: String::new(),
body: RelationBody::Allocation {
source: f.a,
target: work,
},
},
);
}
let d = expected.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.);
d.nodes.get_mut(&yn).unwrap().position = xy(700., 500.);
d.routes[0].waypoints = vec![xy(670., 550.), xy(670., 650.)];
assert_eq!(placed.diagram.routes[1], external_route);
assert_eq!(
roundtrip(
&f.p,
assignment::finish(&plan, &f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
#[test]
fn group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow() {
for layer in [Layer::Oa, Layer::Sa] {
let mut r = roots(layer);
let kind = if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
};
let internal = r.f.p.relate(kind, "Keep internal flow", r.f.w, r.y);
for diagram in [r.f.d, r.f.second] {
r.f.p.diagrams.get_mut(&diagram).unwrap().routes =
vec![route(r.ax), route(internal), route(r.by)];
}
validate_candidate(&r.f.p).unwrap();
let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap();
let plan =
assignment::plan_semantics(&scope, Destination::OutsidePerformers, &BTreeMap::new())
.unwrap();
assert_eq!(plan.changed_count(), 2);
let placed = assignment::place(&plan, xy(640., 200.), wide(&r.f.p, r.f.d, None)).unwrap();
let mut expected = r.f.p.clone();
expected.relations.remove(&r.ax);
expected.relations.remove(&r.by);
for diagram in [r.f.d, r.f.second] {
expected.diagrams.get_mut(&diagram).unwrap().routes = vec![route(internal)];
}
let d = expected.diagrams.get_mut(&r.f.d).unwrap();
d.nodes.get_mut(&r.f.n).unwrap().position = xy(640., 200.);
d.nodes.get_mut(&r.yn).unwrap().position = xy(640., 800.);
d.routes[0].waypoints = vec![xy(920., 400.), xy(920., 550.)];
assert_eq!(
roundtrip(
&r.f.p,
assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap()
),
expected
);
}
}
@@ -0,0 +1,195 @@
//! Geometry operations over an explicitly captured placement set.
use super::*;
pub(super) struct Translation {
pub diagram: Diagram,
pub commands: Vec<Command>,
pub delta: Position,
}
/// Translate exactly `placements` using one snapped anchor delta. `base` must
/// come from an exact, validated semantic candidate; absent routes are permitted
/// only because that candidate has already admitted their semantic deletion.
pub(super) fn translate(
accepted: &Project,
original: &Diagram,
base: &Diagram,
placements: &BTreeMap<Id, NodePlacement>,
anchor: Id,
requested: Position,
) -> Result<Translation, Refusal> {
let anchor = placements.get(&anchor).ok_or(Refusal::StaleScope)?;
let delta = Position {
x: (requested.x / 10.).round() * 10. - anchor.position.x,
y: (requested.y / 10.).round() * 10. - anchor.position.y,
};
let mut diagram = base.clone();
let mut commands = Vec::new();
let mut deltas = BTreeMap::new();
for (id, captured) in placements {
let node = diagram.nodes.get_mut(id).ok_or(Refusal::StaleScope)?;
node.position = Position {
x: captured.position.x + delta.x,
y: captured.position.y + delta.y,
};
if !(Rect {
position: node.position,
size: node.size,
})
.valid()
{
return Err(Refusal::InvalidCandidate(
"Moved coordinates exceed the model bounds.".into(),
));
}
deltas.insert(*id, delta);
if node != captured {
commands.push(Command::EditNode {
diagram: diagram.id,
original: *id,
node: node.clone(),
});
}
}
// Resolve actual endpoint owners once against accepted semantics and the
// complete delta map. Never reinsert a semantically deleted saved route.
for route in projection::translated_routes(accepted, original, &deltas) {
if let Some(stored) = diagram.routes.iter_mut().find(|r| r.edge == route.edge) {
if route.waypoints.iter().any(|point| {
!point.x.is_finite()
|| !point.y.is_finite()
|| point.x.abs() > MAX_COORDINATE
|| point.y.abs() > MAX_COORDINATE
}) {
return Err(Refusal::InvalidCandidate(
"Moved route exceeds the model bounds.".into(),
));
}
*stored = route.clone();
commands.push(Command::SetEdgeRoute {
diagram: diagram.id,
route,
});
}
}
Ok(Translation {
diagram,
commands,
delta,
})
}
/// Recover hidden ancestry for identity lookup only. The all-unfolded scene is
/// never used for collision admission; only affected nodes and their ancestors
/// enter the required diagnostic expansion.
pub(super) fn expansion_ids(
p: &Project,
d: &Diagram,
index: &ViewIndex,
placements: &BTreeMap<Id, NodePlacement>,
) -> Result<BTreeSet<Id>, Refusal> {
let mut all_unfolded = d.clone();
for node in all_unfolded.nodes.values_mut() {
node.collapsed = false;
}
let all_view = projection::project_diagram_with_index(p, &all_unfolded, index);
let ancestry = ancestors(&all_view);
let mut ids: BTreeSet<_> = placements.keys().copied().collect();
for id in placements.keys() {
ids.extend(ancestry.get(id).ok_or(Refusal::StaleScope)?.iter().copied());
}
Ok(ids)
}
pub(super) fn expanded_view(
p: &Project,
d: &Diagram,
index: &ViewIndex,
ids: &BTreeSet<Id>,
) -> ProjectedView {
let mut expanded = d.clone();
for id in ids {
if let Some(node) = expanded.nodes.get_mut(id) {
node.collapsed = false;
}
}
projection::project_diagram_with_index(p, &expanded, index)
}
pub(super) fn check_scenes(
before_visible: &ProjectedView,
before_expanded: &ProjectedView,
visible: &ProjectedView,
expanded: &ProjectedView,
) -> Result<(), Refusal> {
if visible.nodes.iter().chain(&expanded.nodes).any(|node| {
!(Rect {
position: node.position,
size: node.size,
})
.valid()
}) {
return Err(Refusal::InvalidCandidate(
"The projected enclosure exceeds the model bounds.".into(),
));
}
if overlaps(before_visible, visible) || overlaps(before_expanded, expanded) {
return Err(Refusal::LayoutRequired(
"Make room before arranging: the candidate would introduce or enlarge overlapping cards."
.into(),
));
}
Ok(())
}
pub(super) fn check_rigid(
before: &ProjectedView,
after: &ProjectedView,
placements: &BTreeMap<Id, NodePlacement>,
delta: Position,
) -> Result<(), Refusal> {
let before: BTreeMap<_, _> = before.nodes.iter().map(|n| (n.id, n)).collect();
let after: BTreeMap<_, _> = after.nodes.iter().map(|n| (n.id, n)).collect();
for id in placements.keys() {
let old = before.get(id).ok_or(Refusal::StaleScope)?;
let new = after.get(id).ok_or(Refusal::StaleScope)?;
if (new.position.x - old.position.x - delta.x).abs() > 0.01
|| (new.position.y - old.position.y - delta.y).abs() > 0.01
|| (new.size.width - old.size.width).abs() > 0.01
|| (new.size.height - old.size.height).abs() > 0.01
{
return Err(Refusal::LayoutRequired(
"This selection requires an explicit layout; its footprint cannot be translated unchanged."
.into(),
));
}
}
Ok(())
}
pub(super) fn check_headers(
view: &ProjectedView,
selected: &BTreeMap<Id, Id>,
viewport: Rect,
) -> Result<(), Refusal> {
for (id, element) in selected {
let node = view
.nodes
.iter()
.find(|node| node.id == *id && node.element == *element)
.ok_or_else(|| Refusal::LayoutRequired("A selected header would be hidden.".into()))?;
let header = Rect {
position: node.position,
size: Size {
width: node.size.width,
height: node.size.height.min(62.),
},
};
if !viewport.contains(header) {
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
}
Ok(())
}
@@ -0,0 +1,450 @@
//! Reviewed policy experiment using the production projection and translator.
//! No production admission or atomic-refusal behavior is tested here.
use super::*;
struct Mixed {
f: Fixture,
an: Id,
xn: Id,
yn: Id,
internal: Id,
include_performer: bool,
}
fn mixed(layer: Layer, collapsed: bool, obstacle: bool, include_performer: bool) -> Mixed {
let mut f = fixture(layer);
let work = f.p.elements[&f.w].kind();
let exchange = match layer {
Layer::Oa => RelationKind::Interaction,
Layer::Sa => RelationKind::FunctionalExchange,
};
let x = f.p.add_element(work, layer, "X", Some(f.w)).unwrap();
let y = f.p.add_element(work, layer, "Y", Some(f.w)).unwrap();
let outside = f.p.add_element(work, layer, "Outside", None).unwrap();
let xn = f.p.place(f.d, x, xy(140., 200.));
let yn = f.p.place(f.d, y, xy(940., 200.));
f.p.place(f.d, outside, xy(1800., 800.));
f.p.relate(RelationKind::Allocation, "A owns X", f.a, x);
f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y);
// FunctionalExchange's actual builder creates valid function input/output
// ports; do not manufacture Interaction records between SA Functions.
let internal = f.p.relate(exchange, "X to Y", x, y);
let external = f.p.relate(exchange, "Y to Outside", y, outside);
let an = f.p.diagrams[&f.d]
.nodes
.values()
.find(|n| n.element == f.a)
.unwrap()
.id;
let d = f.p.diagrams.get_mut(&f.d).unwrap();
d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.);
d.nodes.get_mut(&an).unwrap().collapsed = collapsed;
d.routes = vec![route(internal), route(external)];
if obstacle {
let id =
f.p.add_element(work, layer, "Stationary obstacle", None)
.unwrap();
f.p.place(f.d, id, xy(550., 230.));
}
validate_candidate(&f.p).unwrap();
let captured = scope(&f);
assert_eq!(captured.uniform, Uniform::MixedOrUnresolved);
assert_eq!(
captured.placements.keys().copied().collect::<BTreeSet<_>>(),
BTreeSet::from([f.n, xn, yn])
);
// Explicit control: the real current API still refuses Arrange for this root.
assert!(matches!(
plan_semantics(&f.p, &captured, Destination::LayoutOnly, &BTreeMap::new()),
Err(Refusal::MixedLayoutRequired)
));
assert!(matches!(
plan_semantics(
&f.p,
&captured,
Destination::Performer(f.a),
&BTreeMap::new()
),
Err(Refusal::MixedLayoutRequired)
));
Mixed {
f,
an,
xn,
yn,
internal,
include_performer,
}
}
fn affected(m: &Mixed) -> BTreeSet<Id> {
let mut ids: BTreeSet<_> = scope(&m.f).placements.keys().copied().collect();
let mut expected = BTreeSet::from([m.f.n, m.xn, m.yn]);
if m.include_performer {
ids.insert(m.an);
ids.extend(projection::descendant_nodes(&m.f.p, m.f.d, m.an));
expected.insert(m.an);
}
assert_eq!(ids, expected);
ids
}
// Diagnostic expansion changes only a temporary Diagram. Include affected
// descendants and ancestors even when the normal projection hides them.
// Unrelated folded interiors are intentionally OUTSIDE this diagnostic domain.
fn diagnostic(p: &Project, diagram: Id, ids: &BTreeSet<Id>) -> ProjectedView {
let mut d = p.diagrams[&diagram].clone();
for (id, node) in &mut d.nodes {
if ids.contains(id)
|| projection::descendant_nodes(p, diagram, *id)
.iter()
.any(|n| ids.contains(n))
{
node.collapsed = false;
}
}
projection::project_diagram(p, &d)
}
fn rigid(before: &ProjectedView, after: &ProjectedView, ids: &BTreeSet<Id>) {
for id in ids {
let old = before.nodes.iter().find(|n| n.id == *id).unwrap();
let new = after.nodes.iter().find(|n| n.id == *id).unwrap();
assert_eq!(new.position, xy(old.position.x + 400., old.position.y));
assert_eq!(new.size, old.size, "no reflow of an affected footprint");
}
}
// The only experimental bypass: use the pure general geometry command instead
// of plan_semantics(LayoutOnly), whose mixed-root pre-veto is asserted above.
// This is not a fabricated Scope or a call to singleton check_geometry with a
// falsely enlarged Scope. The combined scene uses the real overlap predicate.
fn candidate(m: &Mixed) -> (Command, Project) {
let ids = affected(m);
let command =
oa_core::diagram_edit::translate_placements(&m.f.p, m.f.d, &ids, xy(400., 0.)).unwrap();
let actual = prepare_command(&m.f.p, &command).unwrap().project;
validate_candidate(&actual).unwrap();
// Independent literal expected Project: no planner or route helper oracle.
let mut expected = m.f.p.clone();
let d = expected.diagrams.get_mut(&m.f.d).unwrap();
for (id, position) in [
(m.f.n, xy(500., 800.)),
(m.xn, xy(540., 200.)),
(m.yn, xy(1340., 200.)),
] {
d.nodes.get_mut(&id).unwrap().position = position;
}
if m.include_performer {
d.nodes.get_mut(&m.an).unwrap().position = xy(500., 100.);
}
let internal = d.routes.iter_mut().find(|r| r.edge == m.internal).unwrap();
internal.waypoints = vec![xy(820., 400.), xy(820., 550.)];
assert_eq!(
actual, expected,
"complete records, including folds, routes, other view and relations"
);
(command, actual)
}
#[test]
fn mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly() {
for layer in [Layer::Oa, Layer::Sa] {
let enclosure_height = match layer {
Layer::Oa => 218.,
Layer::Sa => 256.,
};
for include_performer in [false, true] {
for collapsed in [false, true] {
let m = mixed(layer, collapsed, false, include_performer);
let accepted = m.f.p.clone();
let (command, actual) = candidate(&m);
let ids = affected(&m);
let before = projection::project_view(&accepted, m.f.d);
let after = projection::project_view(&actual, m.f.d);
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
let after_expanded = diagnostic(&actual, m.f.d, &ids);
assert!(
!overlaps(&before, &after),
"visible scene must admit the combined move"
);
assert!(
!overlaps(&before_expanded, &after_expanded),
"expanded scene must admit the combined move"
);
rigid(&before_expanded, &after_expanded, &ids);
for node in after.nodes.iter().chain(&after_expanded.nodes) {
assert!(
(Rect {
position: node.position,
size: node.size
})
.valid()
);
}
// Explicit selected headers remain visible. Native clipping/readability
// and complete group gesture admission are intentionally not claimed.
assert!(after.nodes.iter().any(|n| n.id == m.f.n));
if include_performer {
assert!(after.nodes.iter().any(|n| n.id == m.an));
} else {
assert_eq!(
actual.diagrams[&m.f.d].nodes[&m.an],
accepted.diagrams[&m.f.d].nodes[&m.an]
);
assert_eq!(node(&before_expanded, m.an).position, xy(100., 100.));
assert_eq!(
node(&before_expanded, m.an).size,
Size {
width: 288.,
height: enclosure_height
}
);
assert_eq!(node(&after_expanded, m.an).position, xy(100., 100.));
assert_eq!(
node(&after_expanded, m.an).size,
Size {
width: 688.,
height: enclosure_height
}
);
}
assert_eq!(roundtrip(&accepted, command), actual);
assert_eq!(m.f.p, accepted);
}
}
}
}
#[test]
fn mixed_arrange_experiment_detects_moved_folded_child_collision() {
for layer in [Layer::Oa, Layer::Sa] {
let expected_overlap = match layer {
Layer::Oa => 13696.,
Layer::Sa => 20116.,
};
let m = mixed(layer, true, true, true);
let accepted = m.f.p.clone();
let (_, actual) = candidate(&m);
let ids = affected(&m);
let before = projection::project_view(&accepted, m.f.d);
let after = projection::project_view(&actual, m.f.d);
assert!(!before.nodes.iter().any(|n| n.id == m.xn));
assert!(!after.nodes.iter().any(|n| n.id == m.xn));
assert!(
!overlaps(&before, &after),
"must distinguish hidden collision from ordinary visible refusal"
);
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
let after_expanded = diagnostic(&actual, m.f.d, &ids);
rigid(&before_expanded, &after_expanded, &ids);
let obstacle = before_expanded
.nodes
.iter()
.find(|n| n.position == xy(550., 230.))
.unwrap()
.id;
assert_eq!(
overlap(
node(&before_expanded, m.xn),
node(&before_expanded, obstacle)
),
0.
);
assert_eq!(
overlap(node(&after_expanded, m.xn), node(&after_expanded, obstacle)),
expected_overlap
);
assert!(
overlaps(&before_expanded, &after_expanded),
"combined diagnostic scene must refuse"
);
// Detection only: there is no production group admission/commit API here.
assert_eq!(m.f.p, accepted);
}
}
fn node(view: &ProjectedView, id: Id) -> &ProjectedNode {
view.nodes.iter().find(|n| n.id == id).unwrap()
}
#[test]
fn mixed_arrange_experiment_detects_stationary_enclosure_growth_collision() {
for layer in [Layer::Oa, Layer::Sa] {
let enclosure_height = match layer {
Layer::Oa => 218.,
Layer::Sa => 256.,
};
let mut m = mixed(layer, false, false, true);
let kind = m.f.p.elements[&m.f.w].kind();
let z =
m.f.p
.add_element(kind, layer, "Stationary Z", None)
.unwrap();
let zn = m.f.p.place(m.f.d, z, xy(1200., 100.));
validate_candidate(&m.f.p).unwrap();
let accepted = m.f.p.clone();
let (_, actual) = candidate(&m);
let bn = accepted.diagrams[&m.f.d]
.nodes
.values()
.find(|n| n.element == m.f.b)
.unwrap()
.id;
let before = projection::project_view(&accepted, m.f.d);
let after = projection::project_view(&actual, m.f.d);
assert_eq!(node(&before, bn).position, xy(900., 100.));
assert_eq!(
node(&before, bn).size,
Size {
width: 288.,
height: enclosure_height
}
);
assert_eq!(node(&after, bn).position, xy(900., 100.));
assert_eq!(
node(&after, bn).size,
Size {
width: 688.,
height: enclosure_height
}
);
assert_eq!(node(&after, zn).position, xy(1200., 100.));
assert_eq!(
node(&after, zn).size,
Size {
width: 224.,
height: 94.
}
);
assert_eq!(overlap(node(&before, bn), node(&before, zn)), 0.);
assert_eq!(overlap(node(&after, bn), node(&after, zn)), 21056.);
assert_eq!(node(&after, m.yn).position, xy(1340., 200.));
assert_eq!(
node(&after, m.yn).position.y
- (node(&after, zn).position.y + node(&after, zn).size.height),
6.
);
for id in affected(&m) {
assert_eq!(overlap(node(&before, id), node(&before, zn)), 0.);
assert_eq!(overlap(node(&after, id), node(&after, zn)), 0.);
}
assert_eq!(
actual.diagrams[&m.f.d].nodes[&bn],
accepted.diagrams[&m.f.d].nodes[&bn]
);
assert_eq!(
actual.diagrams[&m.f.d].nodes[&zn],
accepted.diagrams[&m.f.d].nodes[&zn]
);
assert!(
overlaps(&before, &after),
"complete scene must detect stationary enclosure growth"
);
let ids = affected(&m);
assert!(overlaps(
&diagnostic(&accepted, m.f.d, &ids),
&diagnostic(&actual, m.f.d, &ids)
));
assert_eq!(m.f.p, accepted);
}
}
#[test]
fn mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain() {
for layer in [Layer::Oa, Layer::Sa] {
// Independent source-derived literals: SA's auto-created function ports
// raise projected X/Y height to 76 + 28 * (1 + 1), while Z is port-free.
let (work_height, expected_overlap) = match layer {
Layer::Oa => (94., 13696.),
Layer::Sa => (132., 20116.),
};
let mut m = mixed(layer, true, false, true);
let performer = match layer {
Layer::Oa => Kind::Actor,
Layer::Sa => Kind::SystemActor,
};
let kind = m.f.p.elements[&m.f.w].kind();
let d =
m.f.p
.add_element(performer, layer, "Unrelated D", None)
.unwrap();
let z =
m.f.p
.add_element(kind, layer, "Unrelated hidden Z", None)
.unwrap();
m.f.p.relate(RelationKind::Allocation, "D owns Z", d, z);
let dn = m.f.p.place(m.f.d, d, xy(2500., 100.));
let zn = m.f.p.place(m.f.d, z, xy(550., 230.));
m.f.p
.diagrams
.get_mut(&m.f.d)
.unwrap()
.nodes
.get_mut(&dn)
.unwrap()
.collapsed = true;
validate_candidate(&m.f.p).unwrap();
let accepted = m.f.p.clone();
let (_, actual) = candidate(&m);
let ids = affected(&m);
let before = diagnostic(&accepted, m.f.d, &ids);
let after = diagnostic(&actual, m.f.d, &ids);
assert!(!before.nodes.iter().any(|n| n.id == zn));
assert!(!after.nodes.iter().any(|n| n.id == zn));
assert!(
!overlaps(&before, &after),
"chosen diagnostic domain excludes unrelated folded contents"
);
assert!(!overlaps(
&projection::project_view(&accepted, m.f.d),
&projection::project_view(&actual, m.f.d)
));
let all_unfolded = |p: &Project| {
let mut diagram = p.diagrams[&m.f.d].clone();
for n in diagram.nodes.values_mut() {
n.collapsed = false;
}
projection::project_diagram(p, &diagram)
};
let broad_before = all_unfolded(&accepted);
let broad_after = all_unfolded(&actual);
assert_eq!(node(&broad_before, m.xn).position, xy(140., 200.));
assert_eq!(node(&broad_after, m.xn).position, xy(540., 200.));
assert_eq!(
node(&broad_after, m.xn).size,
Size {
width: 224.,
height: work_height
}
);
assert_eq!(node(&broad_before, zn).position, xy(550., 230.));
assert_eq!(node(&broad_after, zn).position, xy(550., 230.));
assert_eq!(
node(&broad_after, zn).size,
Size {
width: 224.,
height: 94.
}
);
assert_eq!(
overlap(node(&broad_before, m.xn), node(&broad_before, zn)),
0.
);
assert_eq!(
overlap(node(&broad_after, m.xn), node(&broad_after, zn)),
expected_overlap
);
// Other newly exposed enclosure pairs may also overlap. X/Z is a
// concrete discriminator, not a claim that it is the only cause.
assert!(overlaps(&broad_before, &broad_after));
for id in [dn, zn] {
assert_eq!(
actual.diagrams[&m.f.d].nodes[&id],
accepted.diagrams[&m.f.d].nodes[&id]
);
}
assert!(actual.diagrams[&m.f.d].nodes[&dn].collapsed);
assert_eq!(m.f.p, accepted);
}
}
@@ -2,6 +2,18 @@ use super::*;
use crate::document::Document;
use oa_core::source_project::{ProjectLimits, compile_bundle, format_bundle};
#[path = "group_arrange_experiment.rs"]
mod group_arrange_experiment;
#[path = "arrange_tests.rs"]
mod arrange_tests;
#[path = "assignment_tests.rs"]
mod assignment_tests;
#[path = "assignment_boundary_tests.rs"]
mod assignment_boundary_tests;
struct Fixture {
p: Project,
d: Id,
@@ -0,0 +1,69 @@
# Explicit Allocation Arrange amendment — 2026-09-23
**Status: accepted design; pure planner verified, App integration and native acceptance pending.** Codex-9e8r6t /root adopts this bounded policy after reviewing the actual OA/SA experiment, its independent critique and corrected geometry oracles. No user-owned application specification is changed.
User-derived basis: [PROJECT_GOAL](../PROJECT_GOAL.md), [multiselect contract](multiselect-contract-20260923.md). Existing authority: [Allocation drag v2](release-activity-allocation-drag-contract-v2-20260911.md) and its linked destination-feedback amendment. [Experiment and planner evidence](allocation-arrange-planner-20260923.md) records the decision history, exact source, results and limits.
## Scope of the change
Explicit **Arrange only** supports one or more selected work/performer placements through one declared footprint and one combined geometry check. A distributed work root does not fail Arrange solely because its allocation summary is MixedOrUnresolved or its semantic descendants occupy different visual performer groups. Its affected projected footprint must still translate rigidly and satisfy all geometry/readability checks below.
This narrowly supersedes v2's mixed-composite/coherence pre-veto **only for explicit Arrange**. Ordinary work-body assignment is unchanged: work-only groups use one frozen common destination, normalize work ancestry, and admit each root under existing role/coherence rules. A mixed/distributed composite still refuses ordinary assignment; different valid initial performers on independent roots do not alone require refusal. Same-effective-performer role-only roots gain no direct Allocation, unsupported role exits refuse wholly, and no drag changes semantic work ownership. Ordinary mixed work/performer selection cannot partially assign its eligible members. Offer Arrange only when its own full-set admission is available.
Arrange admission cannot depend on having two selected items. C alone and {A,C} use the same rule; adding A changes the actual movement footprint rather than unlocking an otherwise identical operation. Existing ordinary performer arrangement remains governed by its existing behavior; this amendment defines the explicit mode consistently across cardinalities.
## Exact target and footprint definitions
1. Validate every original typed target before normalization: exact placement/lifetime in the same active Allocation diagram and layer, currently visible, eligible work or performer kind, and core-valid accepted Project. Any stale, missing, foreign, port, edge or grouping-relation member refuses the whole action. Same-element duplicate placement in one diagram is malformed input. Preserve exact selected membership and primary separately from planning roots.
2. Normalize explicitly selected work roots by same-kind semantic ancestry. For each root, S contains its recursive same-kind work closure. Q is all existing active-diagram placements of the union S, including folded descendants. Unplaced work contributes no fabricated placement.
3. V contains each explicitly selected performer placement and its accepted visual descendants, including folded descendants. R = Q ∪ V. Capture this union once; do not acquire semantic ancestors/siblings or new semantic roots merely because a work placement is an incidental visual descendant. Each R placement moves once. Authored placements outside R and every other diagram remain exact.
4. The press anchor must be an exact selected placement. Snap its requested position once on the existing ten-world-unit grid and apply that one delta to all R. Another selected anchor may change the snapped delta but never the mode or R. Preserve authored sizes, fold states and port anchors. Arrange authorizes no destination unfold, reparenting, packing, Fit or automatic topology change.
5. Preserve every engineering record, including Allocations, RoleAssignments, RoleRealizations and semantic parents. Resolve routes using accepted actual endpoint owners and the complete R delta map: internal pins with both owners moving by the same nonzero delta translate once; one-ended pins remain fixed. Relative labels and attachments remain exact.
## Combined geometry and its deliberate diagnostic boundary
Project the complete accepted and candidate diagrams using the appropriate semantic index. Check the entire resulting visible scene, not only moved cards. An unselected performer enclosure may change its derived bounds even though its authored record is untouched. Such growth is allowed only if the complete candidate meets model bounds and the existing unrelated-overlap rule: no new or enlarged positive-area overlap beyond the current 0.01 area tolerance; visual ancestor/descendant containment is exempt.
Also compare accepted/candidate diagnostic scenes. In each scene, temporarily unfold R and the visual ancestors necessary to reveal R; use the corresponding accepted/candidate visual ancestry. Include stationary geometry revealed by that required expansion. These temporary Diagram changes are never committed. A moved hidden child cannot evade collision checking through a selected performer's fold.
**Unrelated stationary folded interiors are outside this diagnostic domain.** Do not unfold every container in the diagram. Their visible collapsed headers still participate in the ordinary scene check. This limit prevents invisible distant contents from becoming an unexplained blocker; it does not promise that future expansion will be free of overlap. A stricter all-unfolded policy is a different candidate, not an implicit interpretation of this amendment.
Every R node's affected diagnostic footprint must preserve size and translate by the common delta. No automatic convergence/reflow is licensed by removing the semantic pre-veto. Check the required originally selected headers for visibility and containment in the captured viewport; a normalized-away selected descendant cannot silently disappear. Any unsupported footprint, unreadable selection, invalid bounds or new/enlarged unrelated overlap refuses the complete action. Show the actual combined candidate and affected counts; do not present independently valid singleton ghosts as evidence of group feasibility.
## Valid and refused examples
| Example | Required result under this amendment |
| --- | --- |
| Composite C contains x on A and y on B; select C; enough room for one rigid delta | Arrange may move Q={C,x,y}, preserving A/B authored positions and every allocation. Their derived enclosures may grow if the complete scenes remain admissible. Ordinary assignment still refuses the distributed root. |
| Same model, select {A,C} | R additionally includes A and its accepted visual descendants. Move each union member once. The operation is not equivalent to C alone. |
| Select parent C and its already covered child x | Validate both selected targets, deduplicate planning scope, move each placement once, retain the two exact selection identities/primary. |
| X's translated bounds enter Y's old bounds, while both selected roots move clear together | Check simultaneous candidate geometry. Do not refuse because an independent singleton-X check treated Y as stationary. |
| Unselected B's projected enclosure grows into stationary Z although no R card intersects Z | Refuse. Unchanged authored B geometry is not evidence that its derived enclosure is harmless. |
| Folded selected A hides moved x, which newly intersects a stationary visible obstacle | Required diagnostic expansion detects the collision and refuses. |
| Unrelated folded D hides Z that intersects moved x only in an all-unfolded alternative | Z alone does not block under this diagnostic policy. Required visible/R-plus-ancestor checks must still pass; retain D/Z authored records and D's fold. Future expansion may reveal overlap. |
| Selection includes a work card plus port, edge or grouping-relation target | Whole-set refusal; no eligible-subset fallback. |
| Snapped zero delta, unchanged semantics/folds/routes | No command and no history entry. Arrange never changes assignment merely because a pointer crosses a performer. |
## Lifecycle, source and history
Retain v2's clean-owner, dirty-pointer shielding, modal/tool priority and frozen camera/input lifetime. ArrangeNext is explicit and consumed once at press, whether the anchor is work or performer. Preserve the selected set and primary on selected-member press while distinguishing stationary release from group drag; a drag cannot become a singleton click after crossing the three-screen-pixel threshold and returning near its origin. Mode and footprint cannot change midway through a token.
Capture exact selection generation with document/revision, active diagram/layer, epoch, bounds/pan/zoom and accepted scope. Dirty source/property/behavior input, owner/tool/lifetime change, Fit/resize, Escape, focus loss or outside/hidden release cancels without partial movement. Retiring a token is irreversible even when numeric values later return. Unsupported mixtures never fall through to legacy MoveNode, assignment, reconnection or deletion.
Preview is immutable. On release, revalidate the same capability/snapshot, recompute combined geometry and compare the complete prepared Project with an independently constructed expected Project containing only the permitted R/route edits. Execute one Batch through normal Document source admission. Source failure/refusal leaves accepted source, selection/primary and history unchanged; successful movement is one Undo/Redo step with exact source roundtrip. Geometry detector assertions alone do not establish any of these transaction/lifecycle properties.
## Choice rationale and rejected alternatives
Retaining strict v2 is the smallest implementation but rejects a semantics-preserving explicit layout solely for distribution; until this amendment is implemented, its refusal remains truthful. Visual-selected-only translation is rejected because it omits required semantic/folded descendants. Sequential singleton planning is rejected because geometry, routes, deduplication and final expected Projects are not compositional. Universal unfolding is deliberately not chosen for this bounded mode; it introduces invisible unrelated geometry into admission. Packing/reflow and arbitrary convergence remain separate layout work.
## Evidence and acceptance still required
The [experiment/planner report](allocation-arrange-planner-20260923.md) is authoritative for exact runs, source identities, independent review and retained failures. It distinguishes actual projection/source/history evidence from policy predictions and App acceptance. Its independent expected geometry accounts for SA function ports; collision detection is not presented as atomic UI refusal.
Production integration still requires:
- Whole original typed-set admission, token/selection-generation, dirty/stale/source-failure, one-shot mode, irreversible threshold, cancellation and minimum-readable-text-scale checks.
- OA/SA native workflows anchored on performer and work members, truthful counts/preview/refusal, one Undo/Redo, Save/reopen and exact unrelated-record preservation.
- Separate ordinary work-only group assignment with per-root responsibility rules and one combined candidate/geometry gate.
This amendment defines a bounded Arrange capability. It does not establish all Allocation group interactions, general convergence, grouping removal, routing usability or completion of the multiselect issue.
@@ -0,0 +1 @@
** -text whitespace=cr-at-eol
@@ -0,0 +1,24 @@
# Allocation captured-gate evidence tools
Adapted from the reviewed Process prepare/verify/selfcheck tools. Read-only preparation consumes explicitly selected **closed** runs in CHECKOUT/.local/allocation-group-gates. No Cargo, source execution, checkout, GUI, network or archived helper execution occurs.
```powershell
python -B prepare.py --root CHECKOUT --runs experiment-1 experiment-2 experiment-3 planner-document-1 probe-lint-1 format-1 --check-only
```
Keep failed experiment-2: verification preserves its recorded compile failure rather than converting archive consistency into test success. The example selects the first six closed gates. Later closed planner runs can be explicitly listed with the same interface; the final package manifest is authoritative for its exact inventory. No implicit discovery includes active directories.
To create a portable package, replace --check-only with `--output NEW_DIRECTORY`. Existing output refuses. The exact seven raw files are required, with only `source-proof.json` accepted as an optional eighth file. No raw byte normalization or receipt rewriting occurs. Maps and nested input ZIPs deduplicate by SHA-256; other logical raw files keep their names. All original helper bytes, failed-run streams and optional proof bytes remain archived. The Allocation-specific manifest kind distinguishes this extension from the seven-file Process schema.
The gate.py and source-proof.py helper catalog is preserved. Every gate's recorded helper must match available bytes; use repeatable `--helper PATH` for older captures. If a proof exists, its exact comparator bytes must be available at the default `.local/process-source-evidence-tools/verify.py` or a repeatable `--comparator PATH`. They are archived under their content hash and never executed.
An optional source proof is independently pinned in the manifest with its full named commit. Verification binds it to the enclosing run, exact result receipt, captured map/count, completed gate time, status and comparator bytes/hash. Captured HEAD is preserved as historical metadata and is not substituted for the named source commit. This packager checks historical proof consistency; it does **not** repeat Git blob comparison. A source proof on a failed gate is source-identity evidence only, never a passing-test claim.
```powershell
python -B PACKAGE/verify.py
python -B selfcheck.py --root CHECKOUT --runs experiment-1 experiment-2 experiment-3 planner-document-1 probe-lint-1 format-1 --output TOOLS/NEW_SELFCHECK_DIRECTORY
```
The verifier checks pins, exact ZIP inventory, process/start/result relationships, streams, command/exit identities, source capsules, vendor inventory and Cargo artifact relationships. Selfchecks compare packaged raw bytes to originals, reject existing output, and mutate only in-memory copies to test stream/helper/exit/capsule/proof corruption. The package contains tools, manifest, files.json and evidence.zip; selfcheck is a preparation utility, not packaged runtime input.
Limits: internal consistency is not external authentication, hermetic build proof, native usability, a new source comparison or production acceptance. Review exact run results and source identity separately. Run with `python -B` to avoid incidental bytecode writes.
@@ -0,0 +1,22 @@
{
"prepare.py": {
"bytes": 6316,
"sha256": "aa52592fb8e373c2a90007e187734d29129d38d9fce9dc45461482c7f6376bff"
},
"verify.py": {
"bytes": 11440,
"sha256": "584187370a1076e13190914158fc72a9ab65f07e72cc6a0d3b5c319f3dc7f05f"
},
"README.md": {
"bytes": 3284,
"sha256": "3ff0a61fb15ca26e54b8903962c99ba7802a87f0204e7b71f2d04c812b9588b7"
},
"manifest.json": {
"bytes": 31065,
"sha256": "a993618118369f293569ffdf10c6c05ded4fe8f105df50838c56dca25e0b9eab"
},
"evidence.zip": {
"bytes": 20799621,
"sha256": "a3fdde41e8e8cb4c2c2381a28628b5c9687461c18a17008bb1294a33ab962bfd"
}
}
@@ -0,0 +1,719 @@
{
"schema": 1,
"kind": "allocation-multiselect-cargo-gates",
"runs": {
"experiment-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"--",
"mixed_arrange_experiment",
"--nocapture",
"--test-threads=1"
],
"head": "6d99aacab8ca1a91c857a2179baa14d80a2901e9",
"exitCode": 0,
"inputCount": 743,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"pin": {
"bytes": 591,
"sha256": "ca8e9265a34d465c365a5ba1472d80fef8720a4fb56d82dc7cfaa0a7fa9e391b"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"experiment-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"mixed_arrange_experiment",
"--nocapture",
"--test-threads=1"
],
"head": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"exitCode": 101,
"inputCount": 744,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"experiment-3": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"mixed_arrange_experiment",
"--nocapture",
"--test-threads=1"
],
"head": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"exitCode": 0,
"inputCount": 744,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"pin": {
"bytes": 591,
"sha256": "e1e330d2dc4fee475991d0ba1a1bb75fae77c13d5874689af26987be12db0aae"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"planner-document-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"exitCode": 0,
"inputCount": 744,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"pin": {
"bytes": 597,
"sha256": "c64e4c556e27e6a6fbc8be463738783bcd5afa40959d4cec87801a53c8b08773"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"probe-lint-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--tests",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"exitCode": 0,
"inputCount": 744,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"pin": {
"bytes": 591,
"sha256": "d49e047e0bfe3591836e702141be0f3935b94ecff11d5eb4bcd26658e153ccb8"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "3a7923b96d671041cbc747a614cbd0fad6b6b3d3",
"exitCode": 0,
"inputCount": 744,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"pin": {
"bytes": 587,
"sha256": "26071a3c7f146022eebe2f22c50debf0c5f3f554cd561516d1a473be31a88f0d"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"arrange-planner-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"exitCode": 101,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"arrange-planner-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"exitCode": 101,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"arrange-planner-3": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"exitCode": 0,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"arrange-planner-4": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"exitCode": 0,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "9b83f7ca0dcd34056765b9399863a21da72c3a9f",
"pin": {
"bytes": 596,
"sha256": "cbf8c9171bb6debd47397e48a62231f8295f86c7b79bfd3c36ad885689c742e6"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"arrange-lint-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--tests",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0",
"exitCode": 0,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "9b83f7ca0dcd34056765b9399863a21da72c3a9f",
"pin": {
"bytes": 593,
"sha256": "d476324a632b56df88343b7bc90c89fc14ee70b30fe91c0be55de01c0de5c846"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"arrange-format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "9b83f7ca0dcd34056765b9399863a21da72c3a9f",
"exitCode": 0,
"inputCount": 746,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "9b83f7ca0dcd34056765b9399863a21da72c3a9f",
"pin": {
"bytes": 595,
"sha256": "03ae19dca92db8cdde62c5a0f63c6c5789d75a977079bbd1d84c76c91d775f08"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"source-proof.py": "helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py"
},
"logicalFiles": {
"cargo/arrange-format-1/inputs.json": "capsules/c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686.json",
"cargo/arrange-format-1/inputs.zip": "capsules/29ab4dce454ec8dd92208620e6f8eb0749b6fd72c1687bf1d19264c77159e4f1.zip",
"cargo/arrange-format-1/process.json": "cargo/arrange-format-1/process.json",
"cargo/arrange-format-1/result.json": "cargo/arrange-format-1/result.json",
"cargo/arrange-format-1/source-proof.json": "cargo/arrange-format-1/source-proof.json",
"cargo/arrange-format-1/started.json": "cargo/arrange-format-1/started.json",
"cargo/arrange-format-1/stderr.log": "cargo/arrange-format-1/stderr.log",
"cargo/arrange-format-1/stdout.log": "cargo/arrange-format-1/stdout.log",
"cargo/arrange-lint-1/inputs.json": "capsules/c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686.json",
"cargo/arrange-lint-1/inputs.zip": "capsules/29ab4dce454ec8dd92208620e6f8eb0749b6fd72c1687bf1d19264c77159e4f1.zip",
"cargo/arrange-lint-1/process.json": "cargo/arrange-lint-1/process.json",
"cargo/arrange-lint-1/result.json": "cargo/arrange-lint-1/result.json",
"cargo/arrange-lint-1/source-proof.json": "cargo/arrange-lint-1/source-proof.json",
"cargo/arrange-lint-1/started.json": "cargo/arrange-lint-1/started.json",
"cargo/arrange-lint-1/stderr.log": "cargo/arrange-lint-1/stderr.log",
"cargo/arrange-lint-1/stdout.log": "cargo/arrange-lint-1/stdout.log",
"cargo/arrange-planner-1/inputs.json": "capsules/084e4bd0288b35761193f3e28cdc580fa545de80f9147ef0ba997d090f0ba2a1.json",
"cargo/arrange-planner-1/inputs.zip": "capsules/3c0175ff95e0639f8e400e41f6014f8b0223672494682b4f39a8ca8d45ff898f.zip",
"cargo/arrange-planner-1/process.json": "cargo/arrange-planner-1/process.json",
"cargo/arrange-planner-1/result.json": "cargo/arrange-planner-1/result.json",
"cargo/arrange-planner-1/started.json": "cargo/arrange-planner-1/started.json",
"cargo/arrange-planner-1/stderr.log": "cargo/arrange-planner-1/stderr.log",
"cargo/arrange-planner-1/stdout.log": "cargo/arrange-planner-1/stdout.log",
"cargo/arrange-planner-2/inputs.json": "capsules/f2ffd9ca8ee08a31769e1a5f460f5edd1a78e539463e25c1dcec8c7065829619.json",
"cargo/arrange-planner-2/inputs.zip": "capsules/5c2134898d477eca46ed26eff9a9833de4005660eb7ee89a124eacba21a9b18e.zip",
"cargo/arrange-planner-2/process.json": "cargo/arrange-planner-2/process.json",
"cargo/arrange-planner-2/result.json": "cargo/arrange-planner-2/result.json",
"cargo/arrange-planner-2/started.json": "cargo/arrange-planner-2/started.json",
"cargo/arrange-planner-2/stderr.log": "cargo/arrange-planner-2/stderr.log",
"cargo/arrange-planner-2/stdout.log": "cargo/arrange-planner-2/stdout.log",
"cargo/arrange-planner-3/inputs.json": "capsules/d918a5797a645d1483139139b1a56a558f5c54fe5f797433cbaa7ed5c34c07a0.json",
"cargo/arrange-planner-3/inputs.zip": "capsules/0b08fc45ecc1d38421d9c5b42e65d2b577d4641d0addb0e04043815e76b4b50f.zip",
"cargo/arrange-planner-3/process.json": "cargo/arrange-planner-3/process.json",
"cargo/arrange-planner-3/result.json": "cargo/arrange-planner-3/result.json",
"cargo/arrange-planner-3/started.json": "cargo/arrange-planner-3/started.json",
"cargo/arrange-planner-3/stderr.log": "cargo/arrange-planner-3/stderr.log",
"cargo/arrange-planner-3/stdout.log": "cargo/arrange-planner-3/stdout.log",
"cargo/arrange-planner-4/inputs.json": "capsules/c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686.json",
"cargo/arrange-planner-4/inputs.zip": "capsules/29ab4dce454ec8dd92208620e6f8eb0749b6fd72c1687bf1d19264c77159e4f1.zip",
"cargo/arrange-planner-4/process.json": "cargo/arrange-planner-4/process.json",
"cargo/arrange-planner-4/result.json": "cargo/arrange-planner-4/result.json",
"cargo/arrange-planner-4/source-proof.json": "cargo/arrange-planner-4/source-proof.json",
"cargo/arrange-planner-4/started.json": "cargo/arrange-planner-4/started.json",
"cargo/arrange-planner-4/stderr.log": "cargo/arrange-planner-4/stderr.log",
"cargo/arrange-planner-4/stdout.log": "cargo/arrange-planner-4/stdout.log",
"cargo/experiment-1/inputs.json": "capsules/fc74b619bba31bed7a075cf519456d32f7ccffd1e06624bdb92ad309d55cac45.json",
"cargo/experiment-1/inputs.zip": "capsules/f2e0d3cfeff569cc7794d56f1c439ae1fe1eac15269ee0c8edd58bc80aacaa2d.zip",
"cargo/experiment-1/process.json": "cargo/experiment-1/process.json",
"cargo/experiment-1/result.json": "cargo/experiment-1/result.json",
"cargo/experiment-1/source-proof.json": "cargo/experiment-1/source-proof.json",
"cargo/experiment-1/started.json": "cargo/experiment-1/started.json",
"cargo/experiment-1/stderr.log": "cargo/experiment-1/stderr.log",
"cargo/experiment-1/stdout.log": "cargo/experiment-1/stdout.log",
"cargo/experiment-2/inputs.json": "capsules/4e26f58e200b8c7b825f3c96e2f1df0bdc56f9f02aa8e1d58866b00a81275cdc.json",
"cargo/experiment-2/inputs.zip": "capsules/c2e6e9747e345dba2f35c79b00dd1302c59072a848f18534016acf2e81454acd.zip",
"cargo/experiment-2/process.json": "cargo/experiment-2/process.json",
"cargo/experiment-2/result.json": "cargo/experiment-2/result.json",
"cargo/experiment-2/started.json": "cargo/experiment-2/started.json",
"cargo/experiment-2/stderr.log": "cargo/experiment-2/stderr.log",
"cargo/experiment-2/stdout.log": "cargo/experiment-2/stdout.log",
"cargo/experiment-3/inputs.json": "capsules/acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb.json",
"cargo/experiment-3/inputs.zip": "capsules/8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e.zip",
"cargo/experiment-3/process.json": "cargo/experiment-3/process.json",
"cargo/experiment-3/result.json": "cargo/experiment-3/result.json",
"cargo/experiment-3/source-proof.json": "cargo/experiment-3/source-proof.json",
"cargo/experiment-3/started.json": "cargo/experiment-3/started.json",
"cargo/experiment-3/stderr.log": "cargo/experiment-3/stderr.log",
"cargo/experiment-3/stdout.log": "cargo/experiment-3/stdout.log",
"cargo/format-1/inputs.json": "capsules/acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb.json",
"cargo/format-1/inputs.zip": "capsules/8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e.zip",
"cargo/format-1/process.json": "cargo/format-1/process.json",
"cargo/format-1/result.json": "cargo/format-1/result.json",
"cargo/format-1/source-proof.json": "cargo/format-1/source-proof.json",
"cargo/format-1/started.json": "cargo/format-1/started.json",
"cargo/format-1/stderr.log": "cargo/format-1/stderr.log",
"cargo/format-1/stdout.log": "cargo/format-1/stdout.log",
"cargo/planner-document-1/inputs.json": "capsules/acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb.json",
"cargo/planner-document-1/inputs.zip": "capsules/8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e.zip",
"cargo/planner-document-1/process.json": "cargo/planner-document-1/process.json",
"cargo/planner-document-1/result.json": "cargo/planner-document-1/result.json",
"cargo/planner-document-1/source-proof.json": "cargo/planner-document-1/source-proof.json",
"cargo/planner-document-1/started.json": "cargo/planner-document-1/started.json",
"cargo/planner-document-1/stderr.log": "cargo/planner-document-1/stderr.log",
"cargo/planner-document-1/stdout.log": "cargo/planner-document-1/stdout.log",
"cargo/probe-lint-1/inputs.json": "capsules/acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb.json",
"cargo/probe-lint-1/inputs.zip": "capsules/8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e.zip",
"cargo/probe-lint-1/process.json": "cargo/probe-lint-1/process.json",
"cargo/probe-lint-1/result.json": "cargo/probe-lint-1/result.json",
"cargo/probe-lint-1/source-proof.json": "cargo/probe-lint-1/source-proof.json",
"cargo/probe-lint-1/started.json": "cargo/probe-lint-1/started.json",
"cargo/probe-lint-1/stderr.log": "cargo/probe-lint-1/stderr.log",
"cargo/probe-lint-1/stdout.log": "cargo/probe-lint-1/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py": "helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py"
},
"members": {
"capsules/084e4bd0288b35761193f3e28cdc580fa545de80f9147ef0ba997d090f0ba2a1.json": {
"bytes": 124966,
"sha256": "084e4bd0288b35761193f3e28cdc580fa545de80f9147ef0ba997d090f0ba2a1"
},
"capsules/0b08fc45ecc1d38421d9c5b42e65d2b577d4641d0addb0e04043815e76b4b50f.zip": {
"bytes": 2998637,
"sha256": "0b08fc45ecc1d38421d9c5b42e65d2b577d4641d0addb0e04043815e76b4b50f"
},
"capsules/29ab4dce454ec8dd92208620e6f8eb0749b6fd72c1687bf1d19264c77159e4f1.zip": {
"bytes": 2999625,
"sha256": "29ab4dce454ec8dd92208620e6f8eb0749b6fd72c1687bf1d19264c77159e4f1"
},
"capsules/3c0175ff95e0639f8e400e41f6014f8b0223672494682b4f39a8ca8d45ff898f.zip": {
"bytes": 2998617,
"sha256": "3c0175ff95e0639f8e400e41f6014f8b0223672494682b4f39a8ca8d45ff898f"
},
"capsules/4e26f58e200b8c7b825f3c96e2f1df0bdc56f9f02aa8e1d58866b00a81275cdc.json": {
"bytes": 124628,
"sha256": "4e26f58e200b8c7b825f3c96e2f1df0bdc56f9f02aa8e1d58866b00a81275cdc"
},
"capsules/5c2134898d477eca46ed26eff9a9833de4005660eb7ee89a124eacba21a9b18e.zip": {
"bytes": 2998623,
"sha256": "5c2134898d477eca46ed26eff9a9833de4005660eb7ee89a124eacba21a9b18e"
},
"capsules/8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e.zip": {
"bytes": 2992280,
"sha256": "8fd3ea41c3f5278bbb3986de804cc93229084b00096c131acd09ba8705db502e"
},
"capsules/acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb.json": {
"bytes": 124628,
"sha256": "acd1c8c1f4f2f529455431bbe4193acb25ab8d69bf72ff543265007847b808eb"
},
"capsules/c2e6e9747e345dba2f35c79b00dd1302c59072a848f18534016acf2e81454acd.zip": {
"bytes": 2992231,
"sha256": "c2e6e9747e345dba2f35c79b00dd1302c59072a848f18534016acf2e81454acd"
},
"capsules/c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686.json": {
"bytes": 124966,
"sha256": "c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686"
},
"capsules/d918a5797a645d1483139139b1a56a558f5c54fe5f797433cbaa7ed5c34c07a0.json": {
"bytes": 124966,
"sha256": "d918a5797a645d1483139139b1a56a558f5c54fe5f797433cbaa7ed5c34c07a0"
},
"capsules/f2e0d3cfeff569cc7794d56f1c439ae1fe1eac15269ee0c8edd58bc80aacaa2d.zip": {
"bytes": 2990212,
"sha256": "f2e0d3cfeff569cc7794d56f1c439ae1fe1eac15269ee0c8edd58bc80aacaa2d"
},
"capsules/f2ffd9ca8ee08a31769e1a5f460f5edd1a78e539463e25c1dcec8c7065829619.json": {
"bytes": 124966,
"sha256": "f2ffd9ca8ee08a31769e1a5f460f5edd1a78e539463e25c1dcec8c7065829619"
},
"capsules/fc74b619bba31bed7a075cf519456d32f7ccffd1e06624bdb92ad309d55cac45.json": {
"bytes": 124458,
"sha256": "fc74b619bba31bed7a075cf519456d32f7ccffd1e06624bdb92ad309d55cac45"
},
"cargo/arrange-format-1/process.json": {
"bytes": 116,
"sha256": "2eb27eb4ad4b1d6cdb493ece9df3247de50b4f494522a7fc0cd75cf286847507"
},
"cargo/arrange-format-1/result.json": {
"bytes": 1871,
"sha256": "a5f6e31729519aa3fc064d675731a79c9d93297f713a39afad04de9c8fedeaa8"
},
"cargo/arrange-format-1/source-proof.json": {
"bytes": 595,
"sha256": "03ae19dca92db8cdde62c5a0f63c6c5789d75a977079bbd1d84c76c91d775f08"
},
"cargo/arrange-format-1/started.json": {
"bytes": 1483,
"sha256": "098fd74217e751dc4aa4767f6d59b24ca65bc834dfe7a9dee8330f557baef8ce"
},
"cargo/arrange-format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/arrange-format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/arrange-lint-1/process.json": {
"bytes": 213,
"sha256": "930d2a3b839ac22ae66dbf283634878a90bff77cd3f21c3efbefed87d2561f1b"
},
"cargo/arrange-lint-1/result.json": {
"bytes": 2005,
"sha256": "278a880fc03cd922c0055b84cf364c0f17ee6217f0cfa1d75ca3f52399e96aad"
},
"cargo/arrange-lint-1/source-proof.json": {
"bytes": 593,
"sha256": "d476324a632b56df88343b7bc90c89fc14ee70b30fe91c0be55de01c0de5c846"
},
"cargo/arrange-lint-1/started.json": {
"bytes": 1610,
"sha256": "8171889b3bebe97b49b4f2214bfe732f2651a1c827c3777ea91fa57235e73fc2"
},
"cargo/arrange-lint-1/stderr.log": {
"bytes": 137,
"sha256": "412177405c27599386afb28d603f0895c31becfc6b6de4ebb5a4063734ba94a7"
},
"cargo/arrange-lint-1/stdout.log": {
"bytes": 268845,
"sha256": "f6b71a7a22bd4af2205cde4becc7e1e017ad4d78013944cb2b5218fc32d2e4b8"
},
"cargo/arrange-planner-1/process.json": {
"bytes": 202,
"sha256": "ffeb3bed75938dc4335bd6253783601f21c72999cad952dd191ca7243bf4c506"
},
"cargo/arrange-planner-1/result.json": {
"bytes": 1987,
"sha256": "6e71d9a4f75ce5aeb7714aeffb23f622d4b3a1b267601e45368fcc3597742f47"
},
"cargo/arrange-planner-1/started.json": {
"bytes": 1589,
"sha256": "be88ce011f96d06e07ba02b5e485fe365940a8fb8aa19158ba82e46f5abbb94c"
},
"cargo/arrange-planner-1/stderr.log": {
"bytes": 179,
"sha256": "fff43766ea766c831cd1963c57fd67791b065badb93ec96b62e8546233f3109c"
},
"cargo/arrange-planner-1/stdout.log": {
"bytes": 295296,
"sha256": "427033f5308ecb85e754d6a5fc21ea56d0276edaa0d0d0bf41bf234d9cb452e1"
},
"cargo/arrange-planner-2/process.json": {
"bytes": 202,
"sha256": "d1e6c39dcdf512da2c2ad457200cd14d3f989048d0244c7eb784c7547f2db63f"
},
"cargo/arrange-planner-2/result.json": {
"bytes": 1987,
"sha256": "ccea0caeaf49f4bb0db9ac790cac9fef2138cff072c4d7104fea5597412260b3"
},
"cargo/arrange-planner-2/started.json": {
"bytes": 1589,
"sha256": "22c4c2579b3d16b61929323cb7f8c2be55f4402d8091dcba20b3dc539cb65b87"
},
"cargo/arrange-planner-2/stderr.log": {
"bytes": 180,
"sha256": "e96d24ab1396169886e66c47bfc8ad423c47eb1ba04f23cb0b993a42fcd00672"
},
"cargo/arrange-planner-2/stdout.log": {
"bytes": 304852,
"sha256": "b1de0cb57b6c9d8a1163635bb9428c9058989f3a3e04f771c6d52ec3e98bb38d"
},
"cargo/arrange-planner-3/process.json": {
"bytes": 202,
"sha256": "3d40643702d3e4cd5f1c77665d6a6fe7489a8962ad4831a4bf36176716fc3806"
},
"cargo/arrange-planner-3/result.json": {
"bytes": 3388,
"sha256": "281bd88cc0e8908b12e11ca8c32c87b904e9aa511022d5737aa208f6d09d5516"
},
"cargo/arrange-planner-3/started.json": {
"bytes": 1589,
"sha256": "97f7f2fb041de0bcfb4c55fc998c33181d50a15aad3f1ef9d98e9f8ae81c91af"
},
"cargo/arrange-planner-3/stderr.log": {
"bytes": 323,
"sha256": "c02d99f2243e3b34e2ca3512d4b18ecd60cd27eb1066e2336df3f88908ef26af"
},
"cargo/arrange-planner-3/stdout.log": {
"bytes": 300602,
"sha256": "b681a64a77fb83078b2aef1035c3ce5a7a7fc4c0cc439795eb5ad476216a8bea"
},
"cargo/arrange-planner-4/process.json": {
"bytes": 202,
"sha256": "ce53d413817d6ba48cd361c9d97989c65f53b9ced8fa772870c0162a0725c44b"
},
"cargo/arrange-planner-4/result.json": {
"bytes": 3388,
"sha256": "71ff6ed0ff007f15dc0c9d574981c7fb765771baa0370b69f2868822c3b0cd76"
},
"cargo/arrange-planner-4/source-proof.json": {
"bytes": 596,
"sha256": "cbf8c9171bb6debd47397e48a62231f8295f86c7b79bfd3c36ad885689c742e6"
},
"cargo/arrange-planner-4/started.json": {
"bytes": 1589,
"sha256": "9686ab1af2b7131de0656d131eadb217b869eeeeeef860c94025e42c4a102ae7"
},
"cargo/arrange-planner-4/stderr.log": {
"bytes": 324,
"sha256": "8ba7a613f49ce57efa9a35742c9bfe8acf9c1536ebb9a9f1873f5d0ca9a3301b"
},
"cargo/arrange-planner-4/stdout.log": {
"bytes": 300969,
"sha256": "e0cd9d35beeb9f48ee7e97fee96d7bda700c96786a1c7199c4c38170d9b71bc2"
},
"cargo/experiment-1/process.json": {
"bytes": 232,
"sha256": "021352da56436e72c00da14992f8a94d26efd0893af5d7230b97d1f0af97927a"
},
"cargo/experiment-1/result.json": {
"bytes": 3364,
"sha256": "a82c43d59954773907d5268395e40c9829793fb2d4db5387cb306fd5b39edd01"
},
"cargo/experiment-1/source-proof.json": {
"bytes": 591,
"sha256": "ca8e9265a34d465c365a5ba1472d80fef8720a4fb56d82dc7cfaa0a7fa9e391b"
},
"cargo/experiment-1/started.json": {
"bytes": 1629,
"sha256": "b2bd9e31d8242956809a0d3b6a3cf506f910539d776d9cc0c8131102b1846081"
},
"cargo/experiment-1/stderr.log": {
"bytes": 288,
"sha256": "c8e5c4c82f33c431eca2fdec4dee37250fcaadd4871cb896520ee779a3523924"
},
"cargo/experiment-1/stdout.log": {
"bytes": 293505,
"sha256": "a5fb7e62505efc44ff3d17f11437ede717eedd05e639d9c19db8e80d52699c70"
},
"cargo/experiment-2/process.json": {
"bytes": 245,
"sha256": "2b60185d235cf572b2a1ed79795dbf0868fbc6cfc532edd895d51dd09eb889cd"
},
"cargo/experiment-2/result.json": {
"bytes": 2040,
"sha256": "746b1a53511248914d647ac51db2c222d7378cedc7e59e91ef8beea8eb043315"
},
"cargo/experiment-2/started.json": {
"bytes": 1642,
"sha256": "4cd8ca5c534c16594c52e4fe46f641652e88146afbf025c63a140670cd3e6174"
},
"cargo/experiment-2/stderr.log": {
"bytes": 200,
"sha256": "75cc2e0e245d1c80b04a4a2c2d822a925df836958c4b5c887c1a8f11f99f1f63"
},
"cargo/experiment-2/stdout.log": {
"bytes": 355372,
"sha256": "c513f481adcaedc4461b2d37ea2a30a72b1b49d20cf66fce587bdbead9cdc3fa"
},
"cargo/experiment-3/process.json": {
"bytes": 245,
"sha256": "159f9bea3bea574f63fd1f2a505e245c1839ded6943abf33f0228a08b55f7b9a"
},
"cargo/experiment-3/result.json": {
"bytes": 3439,
"sha256": "dffdc1b4b417888f83c87118e981be7a9aa5b1a1dfddb9c3a166ac149013f937"
},
"cargo/experiment-3/source-proof.json": {
"bytes": 591,
"sha256": "e1e330d2dc4fee475991d0ba1a1bb75fae77c13d5874689af26987be12db0aae"
},
"cargo/experiment-3/started.json": {
"bytes": 1642,
"sha256": "26a7b07180d4a82b3e73ab2bd3435b5dc88f5bf4b3d9657a1e3e4a87e9502415"
},
"cargo/experiment-3/stderr.log": {
"bytes": 323,
"sha256": "21871e9ad66301bf27eecf78b22ea4ff2171bf37ed32d5fdc3bd15f1148f6791"
},
"cargo/experiment-3/stdout.log": {
"bytes": 293837,
"sha256": "f618b8bff61a3d7dd50d3a427c9a42b4650cedb308752343b3878a997c146e5c"
},
"cargo/format-1/process.json": {
"bytes": 116,
"sha256": "e513f657fb6f90a0774e327591f0e5c4e500a36fc09dbbd632758412c5191e31"
},
"cargo/format-1/result.json": {
"bytes": 1872,
"sha256": "26947da874f10973bb5f7f00692b354cf1e9680d1a8743d7185ddfbab8ba690a"
},
"cargo/format-1/source-proof.json": {
"bytes": 587,
"sha256": "26071a3c7f146022eebe2f22c50debf0c5f3f554cd561516d1a473be31a88f0d"
},
"cargo/format-1/started.json": {
"bytes": 1483,
"sha256": "a6cd6be75c5aa87e5dbdebd5e91f1dea44272d5a8bc359133f0bba88093c99d1"
},
"cargo/format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/planner-document-1/process.json": {
"bytes": 202,
"sha256": "62b9efe10f28e06ae1889aec50c64d08122e7c59498bea461a0d246fd8191f86"
},
"cargo/planner-document-1/result.json": {
"bytes": 3387,
"sha256": "c1fcf4c5ba394dd060a7480c6528d3003fbbe28d0393f31f6b29af97ab6e0f96"
},
"cargo/planner-document-1/source-proof.json": {
"bytes": 597,
"sha256": "c64e4c556e27e6a6fbc8be463738783bcd5afa40959d4cec87801a53c8b08773"
},
"cargo/planner-document-1/started.json": {
"bytes": 1589,
"sha256": "46e446a7aef0bcf53433f6da250773ec1b1180eeebc5423659240d2d9f57993d"
},
"cargo/planner-document-1/stderr.log": {
"bytes": 247,
"sha256": "a9712b2a81ccf9c8b139170c92ed4d0477596d634c30fd0b0d71c362fafdff93"
},
"cargo/planner-document-1/stdout.log": {
"bytes": 299855,
"sha256": "ce239ba7364a035c08c0bf4c46174d0c0eb63f0bf2e9233bddac2b74e2f31de7"
},
"cargo/probe-lint-1/process.json": {
"bytes": 212,
"sha256": "a2bfa1d2452c0ca13feebd54840af0425817d114cd864c04e26c87a5229538a1"
},
"cargo/probe-lint-1/result.json": {
"bytes": 2004,
"sha256": "2bdac177fc3ce3c4f6536a3348ec516e0a08fe1a478dd5382b70dc64a41d1385"
},
"cargo/probe-lint-1/source-proof.json": {
"bytes": 591,
"sha256": "d49e047e0bfe3591836e702141be0f3935b94ecff11d5eb4bcd26658e153ccb8"
},
"cargo/probe-lint-1/started.json": {
"bytes": 1610,
"sha256": "682ae85b82b0551aec98da64192baa124e9dc9b7b2efa302b09cfeec24e395a4"
},
"cargo/probe-lint-1/stderr.log": {
"bytes": 137,
"sha256": "6cef38e57b28fdf01ceda8d4115372f4b9957505bbe28f9b6062e6c93c8feebc"
},
"cargo/probe-lint-1/stdout.log": {
"bytes": 268846,
"sha256": "38245fd5fc2ba8b6aac057f12438497e67a77a4fe96b2ca6f5aa6e1db36084f5"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": {
"bytes": 3827,
"sha256": "1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20"
},
"helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py": {
"bytes": 1399,
"sha256": "58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119"
}
}
}
@@ -0,0 +1,110 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/allocation-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "allocation-multiselect-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/allocation-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -0,0 +1,194 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "allocation-multiselect-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
@@ -0,0 +1,26 @@
# Preserved independent review records
[reviews.zip](reviews.zip) contains six exact historical Markdown records,
pinned by [members.json](members.json). Their original context was
`.local/allocation-group-design/` in the reviewed checkout. Relative links and
line references inside this historical archive retain that context and reviewed
source version; they are not links to an installed peer workspace.
| Record | Scope |
| --- | --- |
| critique.md | Initial combined assignment/Arrange proposal criticism |
| fresh-policy-critique.md | Independent policy alternatives and falsifiers |
| experiment/v2-review.md | Four-family experiment; SA port sizing oracle defect |
| selection-boundary.md | Exact typed App selection versus pure placement API |
| input-boundaries.md | Pending click, irreversible threshold and input integration obligations |
| arrange-planner-review.md | Fresh pure-planner review plus R1/R2 test-disposition addendum |
The [accepted contract](../../allocation-arrange-amendment-20260923.md) and
[result report](../../allocation-arrange-planner-20260923.md) are authoritative for
current decisions and acceptance. Historical recommendations or proposed APIs
inside the archive do not supersede them. Root verified reported execution
separately; review authors did not independently run Cargo or native workflows.
The report records remaining P3 coverage suggestions and App/native gaps.
The archive preserves earlier findings even when the dated addendum addresses
them. [files.json](files.json) pins this index, the archive and its member map.
@@ -0,0 +1,14 @@
{
"README.md": {
"bytes": 1576,
"sha256": "b25dc1db4a8a46f5f41381426a237f1a21f21f8b18c0bbfc8590b6f91804d03b"
},
"reviews.zip": {
"bytes": 28248,
"sha256": "d9202f0c6b82ad383de3b4e9e84bb0457f62ae53f1511c458294b0593753e2f1"
},
"members.json": {
"bytes": 831,
"sha256": "2edee904da60ca409192a6152952b556d61bcb36cddcfcfce29fd65476a4fba7"
}
}
@@ -0,0 +1,26 @@
{
"critique.md": {
"bytes": 8955,
"sha256": "a69c27b472f27e06567a216836b42c1593956f23616a6d12ef6ec61c1cf29bdb"
},
"fresh-policy-critique.md": {
"bytes": 15227,
"sha256": "d9ce5c5e6b3f61c8f5933424e178a476f65e262a8990380021c461fd7bcbe38b"
},
"experiment/v2-review.md": {
"bytes": 5901,
"sha256": "46439cdf44cef6d95b8b45df1ba93fd27ca9b78c9470e4c5b9e166ce3082a1f2"
},
"selection-boundary.md": {
"bytes": 6420,
"sha256": "f7971131bac65cde48810abb629695a5a3d0a0aa5b8d115b5fa141b8c3fe5988"
},
"input-boundaries.md": {
"bytes": 16200,
"sha256": "81a2b55d3dde1f658bf10811d06fa0bf1759afc90aa616a376a754cec5af6d22"
},
"arrange-planner-review.md": {
"bytes": 13461,
"sha256": "6171cd269a261ebf255fd75b04a155b78cdb359940b164557a3bc2409b7df3b8"
}
}
@@ -0,0 +1,136 @@
# Allocation Arrange policy and pure planner — 2026-09-23
Codex-9e8r6t contribution to [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29), above Process delivery `6d99aacab8ca1a91c857a2179baa14d80a2901e9`.
**Pure planner verified; App integration and native acceptance remain open.**
This is not a release or full Allocation/multiselect completion claim.
## Result and decision
The [accepted amendment](allocation-arrange-amendment-20260923.md) defines explicit
Arrange for one or more work/performer placements. It preserves engineering
records and moves one union: semantic descendants of explicitly selected work
plus accepted visual descendants of explicitly selected performers. It does not
acquire incidental semantic parents or siblings. Ordinary assignment retains its
existing per-root responsibility and coherence rules.
The experiment compared strict refusal, visible-selected-only movement and this
combined footprint. Strict refusal discarded valid semantics-preserving layouts;
visible-only movement omitted hidden/semantic descendants. Actual OA/SA examples
support the bounded union policy, subject to complete geometry checks. There is
no automatic packing, convergence, semantic reparenting or target unfolding.
Both visible and required diagnostic scenes check **all** unrelated card pairs.
An unselected performer's derived enclosure can grow into an obstacle even when
none of the moved cards touches it. Diagnostics unfold affected placements and
their required visual ancestors; unrelated stationary folded interiors remain
outside that domain. The experiment explicitly distinguishes this policy from
universal unfolding. Future expansion can reveal overlap.
At source **9b83f7ca0dcd34056765b9399863a21da72c3a9f**,
[`arrange.rs`](../crates/oa-desktop/src/allocation_drag/arrange.rs) implements
immutable capture/place/finish, exact original placement identities, one snapped
anchor delta, route translation using actual accepted endpoint owners, combined
projection, every selected header, stale/tamper rejection and an exact final
Project comparison. Motion reuses the semantic index and captured footprint;
it does not clone/validate a Project per pointer position. Pairwise collision
cost remains real; no large-scene latency benchmark is claimed.
The planner's ID map is deliberately not an input capability. App must validate
the entire original typed selection before conversion, retaining its exact
snapshot/generation and clean-owner lifetime. The new module is not yet connected
to App; its temporary dead-code annotation states that boundary. The only change
to singleton capture extracts its already-validated fact collector; ordinary
singleton policy is unchanged.
## Exact verification
All twelve closed runs are preserved in the [portable evidence](allocation-arrange-evidence-20260923/checks/README.md).
Every run reports unchanged captured inputs, including all 71 patched Iced files.
Recorded compile failures remain failures. Historical HEAD alone does not identify
uncommitted captured inputs; named source proofs compare the complete relevant
Git inventory and every blob byte with the captured map.
| Gates | Observed result | Exact source |
| --- | --- | --- |
| experiment-1 | 2 OA experiment tests pass | 743 blobs at `3a7923b96d671041cbc747a614cbd0fad6b6b3d3` |
| experiment-2 | Compile failure: imported module resolved desktop-wide tests | Captured capsule retained; no named-commit proof |
| experiment-3 | Four OA/SA experiment families pass, 57 filtered | 744 blobs at `8dbbaf262e3b8d26a32d6c30f0cc5f963fab47a0` |
| planner-document-1 | All 61 planner/Document module tests pass | Same `8dbbaf2` bytes |
| probe-lint-1 / format-1 | Strict scoped Clippy and workspace format pass | Same `8dbbaf2` bytes |
| arrange-planner-1 / -2 | Compile failures: missing explicit module path, then test temporary-array borrow | Both exact capsules retained |
| arrange-planner-3 | 67 tests pass, 2.44s | Earlier captured planner/tests; not labelled as `8dbbaf2` |
| arrange-planner-4 | **70 tests pass**, 2.77s | 746 blobs at `9b83f7ca0dcd34056765b9399863a21da72c3a9f` |
| arrange-lint-1 / arrange-format-1 | Strict scoped Clippy and workspace format pass | Same `9b83f7c` bytes |
The final input-map SHA-256 is
`c8192fd84fba63fa576975e9fd44f0fa2d6c8ed2e6b5ada3fd4bee076d36f686`.
The 70 include nine new Arrange test families and the existing 61 module tests.
Literal full-Project oracles check engineering records, sizes/folds, other views,
internal and one-ended saved routes. Actual Document execution checks compiled
source, reopen and one exact Undo/Redo. Further cases exercise either selected
anchor, parent/child deduplication, role-only work, unplaced leaves, simultaneous
vacated-space movement, preexisting-overlap preservation versus growth, hidden
collision versus unselected enclosure growth, no-op, stale/tampered results and
all selected headers. These are pure/source checks, not native gesture evidence.
The first experiment's untouched fresh-Document assertion was not atomic refusal
evidence; the successor removed that overstatement. Independent review also
corrected SA oracle literals before execution: FunctionalExchange auto-ports
raise projected work height from OA 94 to SA 132, enclosure height 218 to 256,
and the hidden overlap area 13696 to 20116. Production sizing was not changed to
fit an oracle.
## Independent review and remaining checks
[Preserved reviews](allocation-arrange-evidence-20260923/reviews/README.md) record
policy criticism, the SA oracle finding, the selection boundary and fresh planner
review. No concrete planner defect was found. Review R1/R2 led to independent
collision-cause discriminators, a singleton-refuses/combined-passes vacated-space
case, and unchanged-overlap versus enlargement controls. The final planner
production bytes are unchanged from the reviewed 67-test capture; only
`arrange_tests.rs` differs in the 70-test capture.
Deeper nested folded ancestry, explicit endpoint attachment fixtures and broader
tamper/non-grid cases remain protective P3 coverage suggestions, not reproduced
defects or completed checks. App must retain minimum readable text scale; the
pure planner validates world-space header containment and finite positive zoom.
App integration must still prove exact whole typed-set admission, deferred
selected-member click collapse, an irreversible drag threshold, one-shot Arrange
on both performer/work anchors, dirty/source-failure preservation, stale selection
generation, cancellation, frozen geometry and truthful preview/counts. Ordinary
work-only group assignment needs its own combined semantic candidate. OA/SA native
workflows, actual Save/reopen, Scenario group operations and aggregate deletion
remain open. Existing Process routing overlap is not solved here. Linux/WSL
verification remains user-paused.
## Reproduction and portable records
Verified on Windows x86_64 gnullvm, Rust/Cargo 1.98.1, LLVM-MinGW20260826,
one build job and incremental compilation disabled. Initialize the documented
project toolchain/DLL environment before invoking Cargo. From the named source:
```powershell
$env:CARGO_BUILD_JOBS='1'
$env:CARGO_INCREMENTAL='0'
cargo test -p oa-desktop --example allocation_policy_probe --message-format=json -- --test-threads=1
cargo clippy -p oa-desktop --example allocation_policy_probe --tests --message-format=json -- -D warnings
cargo fmt --all -- --check
python -B harness/allocation-arrange-evidence-20260923/checks/verify.py
```
The focused example imports the actual planner and Document modules. It does not
replace desktop/App/native acceptance. The initial full desktop experiment
reported 30m42s compile time; the focused successor reported 7.11s. This useful
iteration improvement is not a controlled benchmark: target graphs, warm artifacts
and machine state differ.
The package retains twelve runs, three compile failures, eight named source
proofs, 95 logical files in 85 stored members and seven deduplicated source
capsule pairs. Offline verification checks exact inventories, pins, streams,
receipts, source capsule contents and proof relationships; it does not execute
archived code, independently authenticate logs or repeat Git comparisons.
The packager's corruption checks rejected eleven changed relationships and an
attempt to overwrite an existing output. Root verified all eleven exact staged
evidence files in a fresh export: the portable checks verifier and every review
archive/member pin passed without consulting working-copy evidence bytes.
@@ -0,0 +1 @@
** -text whitespace=cr-at-eol
@@ -0,0 +1,24 @@
# Allocation captured-gate evidence tools
Adapted from the reviewed Process prepare/verify/selfcheck tools. Read-only preparation consumes explicitly selected **closed** runs in CHECKOUT/.local/allocation-group-gates. No Cargo, source execution, checkout, GUI, network or archived helper execution occurs.
```powershell
python -B prepare.py --root CHECKOUT --runs experiment-1 experiment-2 experiment-3 planner-document-1 probe-lint-1 format-1 --check-only
```
Keep failed experiment-2: verification preserves its recorded compile failure rather than converting archive consistency into test success. The example selects the first six closed gates. Later closed planner runs can be explicitly listed with the same interface; the final package manifest is authoritative for its exact inventory. No implicit discovery includes active directories.
To create a portable package, replace --check-only with `--output NEW_DIRECTORY`. Existing output refuses. The exact seven raw files are required, with only `source-proof.json` accepted as an optional eighth file. No raw byte normalization or receipt rewriting occurs. Maps and nested input ZIPs deduplicate by SHA-256; other logical raw files keep their names. All original helper bytes, failed-run streams and optional proof bytes remain archived. The Allocation-specific manifest kind distinguishes this extension from the seven-file Process schema.
The gate.py and source-proof.py helper catalog is preserved. Every gate's recorded helper must match available bytes; use repeatable `--helper PATH` for older captures. If a proof exists, its exact comparator bytes must be available at the default `.local/process-source-evidence-tools/verify.py` or a repeatable `--comparator PATH`. They are archived under their content hash and never executed.
An optional source proof is independently pinned in the manifest with its full named commit. Verification binds it to the enclosing run, exact result receipt, captured map/count, completed gate time, status and comparator bytes/hash. Captured HEAD is preserved as historical metadata and is not substituted for the named source commit. This packager checks historical proof consistency; it does **not** repeat Git blob comparison. A source proof on a failed gate is source-identity evidence only, never a passing-test claim.
```powershell
python -B PACKAGE/verify.py
python -B selfcheck.py --root CHECKOUT --runs experiment-1 experiment-2 experiment-3 planner-document-1 probe-lint-1 format-1 --output TOOLS/NEW_SELFCHECK_DIRECTORY
```
The verifier checks pins, exact ZIP inventory, process/start/result relationships, streams, command/exit identities, source capsules, vendor inventory and Cargo artifact relationships. Selfchecks compare packaged raw bytes to originals, reject existing output, and mutate only in-memory copies to test stream/helper/exit/capsule/proof corruption. The package contains tools, manifest, files.json and evidence.zip; selfcheck is a preparation utility, not packaged runtime input.
Limits: internal consistency is not external authentication, hermetic build proof, native usability, a new source comparison or production acceptance. Review exact run results and source identity separately. Run with `python -B` to avoid incidental bytecode writes.
@@ -0,0 +1,22 @@
{
"prepare.py": {
"bytes": 6316,
"sha256": "aa52592fb8e373c2a90007e187734d29129d38d9fce9dc45461482c7f6376bff"
},
"verify.py": {
"bytes": 11440,
"sha256": "584187370a1076e13190914158fc72a9ab65f07e72cc6a0d3b5c319f3dc7f05f"
},
"README.md": {
"bytes": 3284,
"sha256": "3ff0a61fb15ca26e54b8903962c99ba7802a87f0204e7b71f2d04c812b9588b7"
},
"manifest.json": {
"bytes": 17748,
"sha256": "f053e2f1492ee4fdfa2d4dfd7e48c5768d098cf96b629471699810d06cb61537"
},
"evidence.zip": {
"bytes": 8984108,
"sha256": "51b898205ee7a2907ce746389882f0607f7ad7290a1759b392cd307ef9e9ef5a"
}
}
@@ -0,0 +1,396 @@
{
"schema": 1,
"kind": "allocation-multiselect-cargo-gates",
"runs": {
"assignment-planner-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "eed80ffd6e94c7127491de1fd45c0415685f6328",
"exitCode": 0,
"inputCount": 749,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"assignment-planner-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "eed80ffd6e94c7127491de1fd45c0415685f6328",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"assignment-lint-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--tests",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "eed80ffd6e94c7127491de1fd45c0415685f6328",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"assignment-format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "eed80ffd6e94c7127491de1fd45c0415685f6328",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py"
},
"assignment-planner-3": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--message-format=json",
"--",
"--test-threads=1"
],
"head": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"pin": {
"bytes": 599,
"sha256": "3b249398b0ade218ec9737677e1021e21eae7aa03deeb6bc071e0e7e7fe6182b"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"assignment-lint-2": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--example",
"allocation_policy_probe",
"--tests",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"pin": {
"bytes": 596,
"sha256": "3a8f4d17bc6e696f8c0a1373f739650da5aa5b84683478c697e71b280d07b430"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"assignment-format-2": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"exitCode": 0,
"inputCount": 750,
"helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"sourceProof": {
"commit": "33e880103c5fa935a4431ec3dc089e2c4c92f680",
"pin": {
"bytes": 598,
"sha256": "41553cf5d5e5f6414930ad088fa82515630d3478da53a5ac961c8bf4191d1bc6"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"source-proof.py": "helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py"
},
"logicalFiles": {
"cargo/assignment-format-1/inputs.json": "capsules/e41e7d1a2b9519b78730821f96c1bf7ca8431935fe67efa203daa80d3f15c437.json",
"cargo/assignment-format-1/inputs.zip": "capsules/b471d434d8eac790ef7cc9073be0d395c9c9ad1472ba06a05b5818269ef03dfe.zip",
"cargo/assignment-format-1/process.json": "cargo/assignment-format-1/process.json",
"cargo/assignment-format-1/result.json": "cargo/assignment-format-1/result.json",
"cargo/assignment-format-1/started.json": "cargo/assignment-format-1/started.json",
"cargo/assignment-format-1/stderr.log": "cargo/assignment-format-1/stderr.log",
"cargo/assignment-format-1/stdout.log": "cargo/assignment-format-1/stdout.log",
"cargo/assignment-format-2/inputs.json": "capsules/ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27.json",
"cargo/assignment-format-2/inputs.zip": "capsules/ae03a0a847a5c2000ba46e356e3dcd13a219e5fb699be8ad71a619b593e13c23.zip",
"cargo/assignment-format-2/process.json": "cargo/assignment-format-2/process.json",
"cargo/assignment-format-2/result.json": "cargo/assignment-format-2/result.json",
"cargo/assignment-format-2/source-proof.json": "cargo/assignment-format-2/source-proof.json",
"cargo/assignment-format-2/started.json": "cargo/assignment-format-2/started.json",
"cargo/assignment-format-2/stderr.log": "cargo/assignment-format-2/stderr.log",
"cargo/assignment-format-2/stdout.log": "cargo/assignment-format-2/stdout.log",
"cargo/assignment-lint-1/inputs.json": "capsules/e41e7d1a2b9519b78730821f96c1bf7ca8431935fe67efa203daa80d3f15c437.json",
"cargo/assignment-lint-1/inputs.zip": "capsules/b471d434d8eac790ef7cc9073be0d395c9c9ad1472ba06a05b5818269ef03dfe.zip",
"cargo/assignment-lint-1/process.json": "cargo/assignment-lint-1/process.json",
"cargo/assignment-lint-1/result.json": "cargo/assignment-lint-1/result.json",
"cargo/assignment-lint-1/started.json": "cargo/assignment-lint-1/started.json",
"cargo/assignment-lint-1/stderr.log": "cargo/assignment-lint-1/stderr.log",
"cargo/assignment-lint-1/stdout.log": "cargo/assignment-lint-1/stdout.log",
"cargo/assignment-lint-2/inputs.json": "capsules/ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27.json",
"cargo/assignment-lint-2/inputs.zip": "capsules/ae03a0a847a5c2000ba46e356e3dcd13a219e5fb699be8ad71a619b593e13c23.zip",
"cargo/assignment-lint-2/process.json": "cargo/assignment-lint-2/process.json",
"cargo/assignment-lint-2/result.json": "cargo/assignment-lint-2/result.json",
"cargo/assignment-lint-2/source-proof.json": "cargo/assignment-lint-2/source-proof.json",
"cargo/assignment-lint-2/started.json": "cargo/assignment-lint-2/started.json",
"cargo/assignment-lint-2/stderr.log": "cargo/assignment-lint-2/stderr.log",
"cargo/assignment-lint-2/stdout.log": "cargo/assignment-lint-2/stdout.log",
"cargo/assignment-planner-1/inputs.json": "capsules/4e237aa6ba6359490d65efd40e15a697991ce53350af2080503c0635d2d9de3e.json",
"cargo/assignment-planner-1/inputs.zip": "capsules/1bb6e8b18022977f43e29bd44d25238acbf8c424bd4bf736474fce74bd8de8bc.zip",
"cargo/assignment-planner-1/process.json": "cargo/assignment-planner-1/process.json",
"cargo/assignment-planner-1/result.json": "cargo/assignment-planner-1/result.json",
"cargo/assignment-planner-1/started.json": "cargo/assignment-planner-1/started.json",
"cargo/assignment-planner-1/stderr.log": "cargo/assignment-planner-1/stderr.log",
"cargo/assignment-planner-1/stdout.log": "cargo/assignment-planner-1/stdout.log",
"cargo/assignment-planner-2/inputs.json": "capsules/e41e7d1a2b9519b78730821f96c1bf7ca8431935fe67efa203daa80d3f15c437.json",
"cargo/assignment-planner-2/inputs.zip": "capsules/b471d434d8eac790ef7cc9073be0d395c9c9ad1472ba06a05b5818269ef03dfe.zip",
"cargo/assignment-planner-2/process.json": "cargo/assignment-planner-2/process.json",
"cargo/assignment-planner-2/result.json": "cargo/assignment-planner-2/result.json",
"cargo/assignment-planner-2/started.json": "cargo/assignment-planner-2/started.json",
"cargo/assignment-planner-2/stderr.log": "cargo/assignment-planner-2/stderr.log",
"cargo/assignment-planner-2/stdout.log": "cargo/assignment-planner-2/stdout.log",
"cargo/assignment-planner-3/inputs.json": "capsules/ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27.json",
"cargo/assignment-planner-3/inputs.zip": "capsules/ae03a0a847a5c2000ba46e356e3dcd13a219e5fb699be8ad71a619b593e13c23.zip",
"cargo/assignment-planner-3/process.json": "cargo/assignment-planner-3/process.json",
"cargo/assignment-planner-3/result.json": "cargo/assignment-planner-3/result.json",
"cargo/assignment-planner-3/source-proof.json": "cargo/assignment-planner-3/source-proof.json",
"cargo/assignment-planner-3/started.json": "cargo/assignment-planner-3/started.json",
"cargo/assignment-planner-3/stderr.log": "cargo/assignment-planner-3/stderr.log",
"cargo/assignment-planner-3/stdout.log": "cargo/assignment-planner-3/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py",
"helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py": "helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py"
},
"members": {
"capsules/1bb6e8b18022977f43e29bd44d25238acbf8c424bd4bf736474fce74bd8de8bc.zip": {
"bytes": 3008043,
"sha256": "1bb6e8b18022977f43e29bd44d25238acbf8c424bd4bf736474fce74bd8de8bc"
},
"capsules/4e237aa6ba6359490d65efd40e15a697991ce53350af2080503c0635d2d9de3e.json": {
"bytes": 125475,
"sha256": "4e237aa6ba6359490d65efd40e15a697991ce53350af2080503c0635d2d9de3e"
},
"capsules/ae03a0a847a5c2000ba46e356e3dcd13a219e5fb699be8ad71a619b593e13c23.zip": {
"bytes": 3013577,
"sha256": "ae03a0a847a5c2000ba46e356e3dcd13a219e5fb699be8ad71a619b593e13c23"
},
"capsules/b471d434d8eac790ef7cc9073be0d395c9c9ad1472ba06a05b5818269ef03dfe.zip": {
"bytes": 3013606,
"sha256": "b471d434d8eac790ef7cc9073be0d395c9c9ad1472ba06a05b5818269ef03dfe"
},
"capsules/ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27.json": {
"bytes": 125659,
"sha256": "ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27"
},
"capsules/e41e7d1a2b9519b78730821f96c1bf7ca8431935fe67efa203daa80d3f15c437.json": {
"bytes": 125659,
"sha256": "e41e7d1a2b9519b78730821f96c1bf7ca8431935fe67efa203daa80d3f15c437"
},
"cargo/assignment-format-1/process.json": {
"bytes": 116,
"sha256": "ea332ddf4129442f2a8f19b150a6c2fc21a41299eadc32287ba6ba67769e2b58"
},
"cargo/assignment-format-1/result.json": {
"bytes": 1872,
"sha256": "46047bb39986083c89d12751153b678eedd660963b9dafb02c1738986f0dd47a"
},
"cargo/assignment-format-1/started.json": {
"bytes": 1484,
"sha256": "3657a2a7ac270bca405c128328557fb5e1725f913ae0c9c5baa1ba08269bb501"
},
"cargo/assignment-format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/assignment-format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/assignment-format-2/process.json": {
"bytes": 116,
"sha256": "8371d4a455f0dd7678c5b8fb91f679355c05f9f0594985460aee9796bd6adc65"
},
"cargo/assignment-format-2/result.json": {
"bytes": 1872,
"sha256": "319d67031a3f30f70b21f869d755b91196ce0be68d8132775288b3d86b223d06"
},
"cargo/assignment-format-2/source-proof.json": {
"bytes": 598,
"sha256": "41553cf5d5e5f6414930ad088fa82515630d3478da53a5ac961c8bf4191d1bc6"
},
"cargo/assignment-format-2/started.json": {
"bytes": 1483,
"sha256": "a59985096b89da3518c367131b3ec257596725d18f62307bfc174ccb7969c362"
},
"cargo/assignment-format-2/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/assignment-format-2/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/assignment-lint-1/process.json": {
"bytes": 213,
"sha256": "b14d908c775b772f6993b40965a21ae894a406665fa5c2794ebfdb7ffe082769"
},
"cargo/assignment-lint-1/result.json": {
"bytes": 2006,
"sha256": "a8c6cc8a726c9bee821b2c12a0768d860bb1c40261783bcc23da2ea19a677959"
},
"cargo/assignment-lint-1/started.json": {
"bytes": 1610,
"sha256": "3f42270e22476b1698f19cfdc8cd6c78b0754480b613963d582d98d89bf541f6"
},
"cargo/assignment-lint-1/stderr.log": {
"bytes": 137,
"sha256": "d64f9789f0e3e22b0c98af77d19231a9026083ad317f6b2f989ebebd556f1269"
},
"cargo/assignment-lint-1/stdout.log": {
"bytes": 268845,
"sha256": "98a24be8306e586a1acb6a72c3e62005afc0db01fa4ec0b242c420aafadcf779"
},
"cargo/assignment-lint-2/process.json": {
"bytes": 212,
"sha256": "17b1f10f739cdf456da58ef2dcd9c15a8569132904876b8e2289ea543f1217d7"
},
"cargo/assignment-lint-2/result.json": {
"bytes": 2005,
"sha256": "ec3bc72c846cc08188ff95d38aa743c52ac9ec2c616fb8acb7566a6619490b68"
},
"cargo/assignment-lint-2/source-proof.json": {
"bytes": 596,
"sha256": "3a8f4d17bc6e696f8c0a1373f739650da5aa5b84683478c697e71b280d07b430"
},
"cargo/assignment-lint-2/started.json": {
"bytes": 1610,
"sha256": "afdad6e675e3351c749dccea6759c01f8b303a0d598f41592f62beecbc94fdf7"
},
"cargo/assignment-lint-2/stderr.log": {
"bytes": 137,
"sha256": "6ceb01ea1a2f7759c0f7e4fcc98504fae7f6f9cb758b16a2701f804c88ec401d"
},
"cargo/assignment-lint-2/stdout.log": {
"bytes": 268845,
"sha256": "8b993ec00e2a4a58b15af1e57051906cbee8d3d51f4314b71f71af28f582a675"
},
"cargo/assignment-planner-1/process.json": {
"bytes": 202,
"sha256": "1aec12d9a133576033cb736bdec098873005e8b248e09157a3e7608c2d551fdc"
},
"cargo/assignment-planner-1/result.json": {
"bytes": 3388,
"sha256": "b1bc1f3f3013c9df43f7af3cef801e0b977ec484457cfc314e4aa2f1c8d6db98"
},
"cargo/assignment-planner-1/started.json": {
"bytes": 1589,
"sha256": "75d0eeca99ff5f2819a0302cbdc739d2377a2f524890f448f040585288680d2e"
},
"cargo/assignment-planner-1/stderr.log": {
"bytes": 324,
"sha256": "8ee7484db45f8ddf1bf0057317feb689990758c35dc0946c12e0eef0de08e808"
},
"cargo/assignment-planner-1/stdout.log": {
"bytes": 301624,
"sha256": "7be201e6c1a812d8a32d73e75999e5e50a07af2f11eebc77f442aee44f64e5af"
},
"cargo/assignment-planner-2/process.json": {
"bytes": 202,
"sha256": "5837e6b91cbd5c6c95bbeaf08ac89a47149e84c4c3f20004457e80c316e388c9"
},
"cargo/assignment-planner-2/result.json": {
"bytes": 3387,
"sha256": "2dd1c6036eb805cd8e95050661e3dce9d132f9071186165097d4212f2b8253c0"
},
"cargo/assignment-planner-2/started.json": {
"bytes": 1589,
"sha256": "a4384901a0d3a29631a29d5e91b6d3dc944cefca1bb7653901d351a7bd906ec1"
},
"cargo/assignment-planner-2/stderr.log": {
"bytes": 324,
"sha256": "a046d75d45956402707b7c1089ec5005b0ba640857473e1f284a8f377f896f2c"
},
"cargo/assignment-planner-2/stdout.log": {
"bytes": 302839,
"sha256": "1905a2726b6e9c388566af75d7705b3916ade3a7d2c9dab772bffbb5b115762b"
},
"cargo/assignment-planner-3/process.json": {
"bytes": 202,
"sha256": "ec191e2d953d52f6da8c54bc6e9dfa5638273eb6c351823fc4871628320b3bab"
},
"cargo/assignment-planner-3/result.json": {
"bytes": 3388,
"sha256": "117bcfa31629682df708dfebb892464f01060888388ea680a231b12cd447db67"
},
"cargo/assignment-planner-3/source-proof.json": {
"bytes": 599,
"sha256": "3b249398b0ade218ec9737677e1021e21eae7aa03deeb6bc071e0e7e7fe6182b"
},
"cargo/assignment-planner-3/started.json": {
"bytes": 1589,
"sha256": "30cf725d9475a24b177c2dbddb3f59b7d298343556598bc0e760d51ad7e111e5"
},
"cargo/assignment-planner-3/stderr.log": {
"bytes": 324,
"sha256": "d3a0110134db80cd8715e6081027595056203feada2278053fdc62ae75c6df31"
},
"cargo/assignment-planner-3/stdout.log": {
"bytes": 302839,
"sha256": "aa832fe75c46be8990d0c679e6e321494c0ea6638657634010bae7022b7a2ee9"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": {
"bytes": 3827,
"sha256": "1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20"
},
"helpers/58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119.py": {
"bytes": 1399,
"sha256": "58597566ae1cb5c4021ddc7e86635b76d127bdb63b5d3026c427771aae7e2119"
}
}
}
@@ -0,0 +1,110 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/allocation-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "allocation-multiselect-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/allocation-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -0,0 +1,194 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "allocation-multiselect-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
@@ -0,0 +1,16 @@
# Assignment review archive
Exact historical design, fixtures and independent review notes, with original
`.local` paths preserved inside the archive. Their relative links refer to that
original checkout context; extract only into a separate scratch directory if
needed. Do not interpret historical plans or source-derived predictions as
executed results. The review addendum inspects the recorded 84-test gate without
claiming an independent rerun. The App adapter plan is design only.
The newline diagnosis records why three initial exact source comparisons
refused. Their captures were retained and canonical-byte gates rerun; historical
receipts were not rewritten. Current accepted facts and limitations are in the
[planner report](../../allocation-assignment-planner-20260923.md).
`files.json` pins this archive/index/readme; `members.json` pins every exact ZIP
member. Neither internal hashes nor independent review replace runtime evidence.
@@ -0,0 +1,14 @@
{
"reviews.zip": {
"bytes": 26141,
"sha256": "35a5faaf9f60eaa88d8c97f8a34dceac7a2cebf356ac6f69ec0e794cec258101"
},
"members.json": {
"bytes": 840,
"sha256": "3b1ce35c9b65b241b80367c90adc0ffbc65155d9cea8872f98e58901b2d52513"
},
"README.md": {
"bytes": 952,
"sha256": "a518a056c78b982c45f8d9a2098729f27df8f1702132c0e4cc6c555c603dbd22"
}
}
@@ -0,0 +1,22 @@
{
".local/allocation-group-design/assignment-plan.md": {
"bytes": 16268,
"sha256": "000b80e13845a1532e083281ee94486964306af0ce7fb27fee3a9eb2074f2405"
},
".local/allocation-group-design/assignment-fixtures.md": {
"bytes": 11563,
"sha256": "507e5d33745ffb65f096a8c5640f6e22d61ecb8701eb13769bfe538069e71ecc"
},
".local/allocation-group-design/assignment-review.md": {
"bytes": 11510,
"sha256": "f1c36cdba4a6f74770d541d9529d7f116473b2bc78bc2b2902c80f387eba82d1"
},
".local/allocation-group-design/app-adapter-plan.md": {
"bytes": 21648,
"sha256": "316633ae45d8f9836212d37e64639251cdb2fd96cda522d0a979d9bc81782cf7"
},
".local/allocation-evidence-tools/assignment-newline-diagnosis.json": {
"bytes": 622,
"sha256": "60631de3a45d0cb6c4108e7aaaf170b80e43e00e076674aabf379d05ee77a62a"
}
}
@@ -0,0 +1,115 @@
# Ordinary Allocation group assignment: pure planner checkpoint
Status: implemented and verified as a pure planner; App wiring and native
acceptance remain pending. Source commit
`33e880103c5fa935a4431ec3dc089e2c4c92f680`, on the Allocation contribution branch
above the [Arrange foundation](allocation-arrange-planner-20260923.md).
This is a branch-specific checkpoint, not an integrated application release.
Required behavior comes from [multiselect](multiselect-contract-20260923.md),
[Allocation v2](release-activity-allocation-drag-contract-v2-20260911.md) and the
[explicit Arrange amendment](allocation-arrange-amendment-20260923.md).
No core model, source schema or user-owned specification changed.
## Result and design
Ordinary assignment captures every original visible work placement before
normalizing selected semantic ancestors. It collects the union of leaf work,
including unplaced leaves, and existing active placements, including folded
descendants. Each normalized root retains its own accepted responsibility:
independent roots may start under different performers; a distributed composite
still refuses ordinary assignment. Already-correct role-only work does not gain
a direct Allocation merely because another selected root changes performer.
One destination plan accumulates stable IDs, unique aliases and intended edits,
prepares one semantic Batch, and compares the complete candidate Project with
the intended result. Existing Allocation IDs and header metadata survive
retargeting. Outside removes only intended direct Allocations and their own
saved routes across views. Role exits and collateral changes to surviving
records refuse the whole plan. Warning collection and changed/already-correct
leaf counts are retained for future App feedback.
One shared geometry helper now serves singleton assignment, group assignment
and explicit Arrange. It uses accepted endpoint ownership and the complete
movement map: internal pins translate once, one-ended pins stay fixed, labels
and attachments remain exact. Geometry checks compare whole visible scenes and
the affected/ancestor diagnostic expansion, require rigid affected footprints
and every original selected header, and authorize target unfolding only when
assignment actually changes. Accepted and candidate expansion use their own
ancestry. Per-pointer placement does not clone or validate a Project or rebuild
semantic indexes. Finish rechecks the entire live Project and recomputes the
placement before one exact source Batch. Zero movement can still change
assignment; complete Project equality alone yields no command.
## Verification
Windows gnullvm, cargo/rustc 1.98.1, LLVM-MinGW 20260826, one Cargo build job,
incremental disabled. Linux remains user-deferred. The focused example imports
the actual production planner and Document modules; it is not a substitute
implementation. Commands:
```powershell
cargo test -p oa-desktop --example allocation_policy_probe --message-format=json -- --test-threads=1
cargo clippy -p oa-desktop --example allocation_policy_probe --tests --message-format=json -- -D warnings
cargo fmt --all -- --check
```
Final canonical-byte gates: `assignment-planner-3` **84 passed, 0 failed**,
`assignment-lint-2` strict lint passed, `assignment-format-2` passed. Each captured
750 unchanged inputs, including 71 vendored dependency files and both explicit
compiled fixtures; all captured blobs compare exactly with source commit
`33e880103c5fa935a4431ec3dc089e2c4c92f680`.
Final input-map SHA256:
`ce988ecaeaca395d0f120844db7e3da90d039633beef10d2d4a8b8f19e92ee27`.
The 14 added test families cover OA/SA independent roots and original identity,
normalization/unplaced work, distributed composite versus independent leaves,
role provenance and unresolved OA roles, stable IDs/aliases, zero-delta semantic
change, target unfold and newly exposed overlap, simultaneous vacated-space
movement, internal/external routes with explicit attachments and SA port anchor,
own-route deletion across views, surviving Constraint collateral refusal,
stale/tampered results and all selected headers. Existing singleton/Arrange and
Document tests remain in the same passing suite.
Positive tests use independently authored complete Projects and actual Document
execute, source compilation/reopen and exact one-step Undo/Redo. The source-budget
case executes the actual group command against an existing Document with Undo
and Redo available, preserves Project/bundle/revision, and exercises both history
paths after refusal. The same command/IDs then succeed in a fresh normal-budget
Document; this is not evidence of same-session gesture retry.
Independent review found no concrete planner defect. A second pass reviewed
the added evaluators and found no blocking invalid fixture or circular oracle.
It inspected the captured 84-test result without claiming its own execution.
Exact historical notes and newline diagnosis are in the
[review archive](allocation-assignment-evidence-20260923/reviews/README.md).
## Evidence identity and limits
The [portable gate package](allocation-assignment-evidence-20260923/checks/README.md)
retains seven runs: the first 75-test capture, expanded 84-test capture, initial
lint/format and three final canonical-byte gates. Initial expanded checks
passed, but exact Git comparison refused because `arrange.rs` was CRLF in the
working tree and LF in the committed blob. The independently checked difference
was newline-only. Those historical captures are not attributed to the pristine
commit and have no source-proof receipt. The file was restored to the committed
bytes and all three gates rerun. No receipt or historical log was rewritten.
Run the package verifier with `python -B checks/verify.py` from the evidence
directory. It checks archive/receipt consistency and pinned historical source
proofs, not external authenticity or a new Git comparison. The report's named
source comparisons were executed separately against actual Git blobs.
Remaining focused protective coverage includes deeper folded-grandchild group
reassignment and a changed-assignment enclosure-growth blocker. Existing
singleton and shared geometry coverage reduces but does not remove those gaps.
Role warning collection is reviewed; the new role tests do not independently
assert every warning code/severity.
There is no new App execution, full desktop test run, matching GUI build or native
acceptance for this checkpoint. Exact typed selection, frozen pointer target,
minimum readable zoom, pending-click behavior, sticky drag threshold, selection
generation, clean-owner admission and focus/camera/token retirement remain the
App adapter's obligations. The archived App plan is design only. Neither this
checkpoint nor the earlier Arrange foundation closes issue 29 or authorizes a
combined release; the coordinated integration/release owner is unchanged.
+7
View File
@@ -80,6 +80,13 @@ members; explain the refusal and offer the existing Arrange-only operation.
Explicit Arrange-only always preserves every allocation record. Destination
feedback and hit geometry remain stable throughout the gesture.
The [explicit Arrange amendment](allocation-arrange-amendment-20260923.md) defines
the bounded one-or-more work/performer footprint and diagnostic collision domain.
Its [pure planner evidence](allocation-arrange-planner-20260923.md) and the
[ordinary assignment foundation](allocation-assignment-planner-20260923.md)
establish bounded source/planner behavior. App/native group input acceptance
remains pending.
## Related group actions
Move/reorder and removal are the initial useful operation families. Saved-view
@@ -1,5 +1,12 @@
# Allocation drag contract v2 — 2026-09-11
**2026-09-23 explicit Arrange amendment:**
[the accepted bounded policy](allocation-arrange-amendment-20260923.md) supersedes
the mixed/coherence pre-veto only for explicit Arrange, with a declared combined
footprint and geometry gate. [Pure planner verification](allocation-arrange-planner-20260923.md)
is complete within its recorded scope; App/native integration remains pending.
Ordinary assignment and all other v2 lifetime/source rules remain binding.
Codex-956atf `/root/capella_process_gap`. Implementation-ready proposal for root ratification of [issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3). The [original design](release-activity-allocation-drag-design-20260911.md) and [independent review](release-activity-allocation-drag-review-20260911.md) remain immutable. Root accepted all five review corrections; this document resolves them without claiming implementation, issue closure or full-goal completion.
**2026-09-12 presentation amendment:** the [destination feedback decision](release-allocation-feedback-decision-20260912.md) supersedes references below requiring a duplicate target-name label at its frozen canvas position. Target hit regions and outlines remain frozen; destination identity moves to a dedicated stable toolbar slot. This prevents label occlusion without making ordinary collapsed-target drops unreachable. All semantic, exact-placement and lifetime rules remain binding.