Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b7c28501 | ||
|
|
fe43730bec | ||
|
|
d46854c2dc | ||
|
|
acbfb6cc92 |
@@ -750,7 +750,7 @@ pub fn plan_transition(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !compatible_existing(project, *source, target, request.direction) {
|
if !counterpart_compatible(project, *source, target, request.direction) {
|
||||||
conflict(
|
conflict(
|
||||||
&mut plan,
|
&mut plan,
|
||||||
Some(*source),
|
Some(*source),
|
||||||
@@ -1317,12 +1317,22 @@ fn compatible_kinds(source: Kind, target: Kind, direction: TransitionDirection)
|
|||||||
}
|
}
|
||||||
inferred_kind(source, direction) == Some(target)
|
inferred_kind(source, direction) == Some(target)
|
||||||
}
|
}
|
||||||
fn compatible_existing(
|
/// Whether existing records have compatible layers, categories and kinds for
|
||||||
|
/// an ordinary transition. This is candidate eligibility, not approval of a
|
||||||
|
/// complete plan: properties, dependencies and structural conflicts still need
|
||||||
|
/// the normal preview and apply validation. Manual traces are not restricted
|
||||||
|
/// by this predicate.
|
||||||
|
pub fn counterpart_compatible(
|
||||||
p: &Project,
|
p: &Project,
|
||||||
source: Id,
|
source: Id,
|
||||||
target: Id,
|
target: Id,
|
||||||
direction: TransitionDirection,
|
direction: TransitionDirection,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if p.layer_of(source) != Some(direction.source_layer())
|
||||||
|
|| p.layer_of(target) != Some(direction.target_layer())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
match (p.elements.get(&source), p.elements.get(&target)) {
|
match (p.elements.get(&source), p.elements.get(&target)) {
|
||||||
(Some(a), Some(b)) => compatible_kinds(a.kind(), b.kind(), direction),
|
(Some(a), Some(b)) => compatible_kinds(a.kind(), b.kind(), direction),
|
||||||
_ => match (p.relations.get(&source), p.relations.get(&target)) {
|
_ => match (p.relations.get(&source), p.relations.get(&target)) {
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
//! Existing counterpart eligibility is a layer/category/kind decision, not
|
||||||
|
//! validation or acceptance of an entire transition plan. The catalog fixtures
|
||||||
|
//! below intentionally contain partial records (unowned states, etc.). The
|
||||||
|
//! final regression separately starts from a structurally valid real project.
|
||||||
|
use oa_core::{transitions::*, *};
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
fn element(p: &mut Project, layer: Layer, kind: Kind) -> Id {
|
||||||
|
let id = Id::new_v4();
|
||||||
|
let body = if kind == Kind::Port {
|
||||||
|
// Legacy ports can exist in preserved OA models. This does not offer
|
||||||
|
// new typed SA implementation ports for creation in portless OA.
|
||||||
|
ElementBody::shared_in(
|
||||||
|
layer,
|
||||||
|
SharedBody::Port {
|
||||||
|
contract: PortContract::Legacy,
|
||||||
|
legacy: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ElementBody::new(kind, layer).unwrap()
|
||||||
|
};
|
||||||
|
p.elements.insert(
|
||||||
|
id,
|
||||||
|
Element {
|
||||||
|
id,
|
||||||
|
alias: format!("candidate_{}", id.simple()),
|
||||||
|
name: "Identical candidate label".into(),
|
||||||
|
description: String::new(),
|
||||||
|
parent: None,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_element_candidates_obey_explicit_engineering_families_in_both_directions() {
|
||||||
|
// Contract: language.rst §5 trace families and the explicit capability /
|
||||||
|
// mission alternative. State constructors remain eligible for explicit
|
||||||
|
// review across the state family; selecting a candidate does not accept a
|
||||||
|
// constructor change or make invalid ownership legal.
|
||||||
|
let mut allowed: BTreeSet<(Kind, Kind)> = [
|
||||||
|
(Kind::Activity, Kind::Function),
|
||||||
|
(Kind::Entity, Kind::System),
|
||||||
|
(Kind::Entity, Kind::SystemActor),
|
||||||
|
(Kind::Actor, Kind::System),
|
||||||
|
(Kind::Actor, Kind::SystemActor),
|
||||||
|
(Kind::Capability, Kind::SystemCapability),
|
||||||
|
(Kind::OperationalMission, Kind::Mission),
|
||||||
|
(Kind::Capability, Kind::Mission),
|
||||||
|
(Kind::Process, Kind::FunctionalChain),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
for kind in [
|
||||||
|
Kind::ExchangeItem,
|
||||||
|
Kind::ExchangeCategory,
|
||||||
|
Kind::DataType,
|
||||||
|
Kind::DataField,
|
||||||
|
Kind::Interface,
|
||||||
|
Kind::Port,
|
||||||
|
Kind::Scenario,
|
||||||
|
Kind::Lifeline,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Kind::StateMachine,
|
||||||
|
Kind::Region,
|
||||||
|
Kind::Event,
|
||||||
|
Kind::Constraint,
|
||||||
|
] {
|
||||||
|
allowed.insert((kind, kind));
|
||||||
|
}
|
||||||
|
let vertices = [
|
||||||
|
Kind::State,
|
||||||
|
Kind::Mode,
|
||||||
|
Kind::Initial,
|
||||||
|
Kind::Final,
|
||||||
|
Kind::Choice,
|
||||||
|
Kind::Junction,
|
||||||
|
Kind::Fork,
|
||||||
|
Kind::Join,
|
||||||
|
Kind::ShallowHistory,
|
||||||
|
Kind::DeepHistory,
|
||||||
|
Kind::EntryPoint,
|
||||||
|
Kind::ExitPoint,
|
||||||
|
Kind::Terminate,
|
||||||
|
];
|
||||||
|
for oa in vertices {
|
||||||
|
for sa in vertices {
|
||||||
|
allowed.insert((oa, sa));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let oa: BTreeMap<_, _> = Kind::ALL
|
||||||
|
.into_iter()
|
||||||
|
.filter(|kind| kind.supports(Layer::Oa))
|
||||||
|
.map(|kind| (kind, element(&mut p, Layer::Oa, kind)))
|
||||||
|
.collect();
|
||||||
|
let sa: BTreeMap<_, _> = Kind::ALL
|
||||||
|
.into_iter()
|
||||||
|
.filter(|kind| kind.supports(Layer::Sa))
|
||||||
|
.map(|kind| (kind, element(&mut p, Layer::Sa, kind)))
|
||||||
|
.collect();
|
||||||
|
let original = p.clone();
|
||||||
|
for (&oa_kind, &oa_id) in &oa {
|
||||||
|
for (&sa_kind, &sa_id) in &sa {
|
||||||
|
let expected = allowed.contains(&(oa_kind, sa_kind));
|
||||||
|
assert_eq!(
|
||||||
|
counterpart_compatible(&p, oa_id, sa_id, TransitionDirection::OaToSa),
|
||||||
|
expected,
|
||||||
|
"OA {oa_kind:?} → SA {sa_kind:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
counterpart_compatible(&p, sa_id, oa_id, TransitionDirection::SaToOa),
|
||||||
|
expected,
|
||||||
|
"SA {sa_kind:?} → OA {oa_kind:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
p, original,
|
||||||
|
"Eligibility must not establish correspondence or edit records"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relation(p: &mut Project, kind: RelationKind, anchor: Id) -> Id {
|
||||||
|
let id = Id::new_v4();
|
||||||
|
// Only the semantic layer and relation category participate in candidate
|
||||||
|
// eligibility. Endpoint compatibility belongs to full plan validation.
|
||||||
|
p.relations.insert(
|
||||||
|
id,
|
||||||
|
Relation {
|
||||||
|
id,
|
||||||
|
alias: format!("relation_{}", id.simple()),
|
||||||
|
name: "Identical relation label".into(),
|
||||||
|
description: String::new(),
|
||||||
|
body: RelationBody::new(kind, anchor, anchor),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_relation_candidates_distinguish_work_boundary_and_shared_relations() {
|
||||||
|
// Work and boundary exchanges are different families. The boundary /
|
||||||
|
// boundary pair permits preserved legacy OA boundary records; ordinary
|
||||||
|
// portless OA communication means map to SA boundary exchanges.
|
||||||
|
let mut allowed = vec![
|
||||||
|
(RelationKind::Interaction, RelationKind::FunctionalExchange),
|
||||||
|
(
|
||||||
|
RelationKind::CommunicationMean,
|
||||||
|
RelationKind::BoundaryExchange,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RelationKind::BoundaryExchange,
|
||||||
|
RelationKind::BoundaryExchange,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let shared = [
|
||||||
|
RelationKind::Allocation,
|
||||||
|
RelationKind::InterfaceItemAllocation,
|
||||||
|
RelationKind::DataAssociation,
|
||||||
|
RelationKind::ExchangeAllocation,
|
||||||
|
RelationKind::CapabilityInvolvement,
|
||||||
|
RelationKind::CapabilityInclude,
|
||||||
|
RelationKind::CapabilityExtend,
|
||||||
|
RelationKind::CapabilityGeneralization,
|
||||||
|
RelationKind::MissionInvolvement,
|
||||||
|
RelationKind::MissionCapability,
|
||||||
|
RelationKind::SequenceLink,
|
||||||
|
RelationKind::ContextualLink,
|
||||||
|
RelationKind::ChainInvolvement,
|
||||||
|
RelationKind::InterfaceProvision,
|
||||||
|
RelationKind::InterfaceRequirement,
|
||||||
|
RelationKind::StateTransition,
|
||||||
|
RelationKind::Availability,
|
||||||
|
RelationKind::StateAction,
|
||||||
|
];
|
||||||
|
for kind in shared {
|
||||||
|
allowed.push((kind, kind));
|
||||||
|
}
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let oa_anchor = element(&mut p, Layer::Oa, Kind::Activity);
|
||||||
|
let sa_anchor = element(&mut p, Layer::Sa, Kind::Function);
|
||||||
|
let oa: Vec<_> = shared
|
||||||
|
.into_iter()
|
||||||
|
.chain([
|
||||||
|
RelationKind::Interaction,
|
||||||
|
RelationKind::CommunicationMean,
|
||||||
|
RelationKind::BoundaryExchange,
|
||||||
|
RelationKind::RoleRealization,
|
||||||
|
RelationKind::RoleAssignment,
|
||||||
|
])
|
||||||
|
.map(|kind| (kind, relation(&mut p, kind, oa_anchor)))
|
||||||
|
.collect();
|
||||||
|
let sa: Vec<_> = shared
|
||||||
|
.into_iter()
|
||||||
|
.chain([
|
||||||
|
RelationKind::FunctionalExchange,
|
||||||
|
RelationKind::BoundaryExchange,
|
||||||
|
RelationKind::PortAllocation,
|
||||||
|
])
|
||||||
|
.map(|kind| (kind, relation(&mut p, kind, sa_anchor)))
|
||||||
|
.collect();
|
||||||
|
for &(oa_kind, oa_id) in &oa {
|
||||||
|
for &(sa_kind, sa_id) in &sa {
|
||||||
|
let expected = allowed.contains(&(oa_kind, sa_kind));
|
||||||
|
assert_eq!(
|
||||||
|
counterpart_compatible(&p, oa_id, sa_id, TransitionDirection::OaToSa),
|
||||||
|
expected,
|
||||||
|
"OA {oa_kind:?} → SA {sa_kind:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
counterpart_compatible(&p, sa_id, oa_id, TransitionDirection::SaToOa),
|
||||||
|
expected,
|
||||||
|
"SA {sa_kind:?} → OA {oa_kind:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn candidate_eligibility_rejects_wrong_layers_categories_and_missing_identities() {
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let oa = element(&mut p, Layer::Oa, Kind::Activity);
|
||||||
|
let sa = element(&mut p, Layer::Sa, Kind::Function);
|
||||||
|
let oa_exchange = relation(&mut p, RelationKind::Interaction, oa);
|
||||||
|
let sa_exchange = relation(&mut p, RelationKind::FunctionalExchange, sa);
|
||||||
|
let diagram = p.ensure_diagram(Layer::Sa);
|
||||||
|
let node = p.place(diagram, sa, Position { x: 1., y: 2. });
|
||||||
|
for (direction, source, target, source_relation, target_relation) in [
|
||||||
|
(
|
||||||
|
TransitionDirection::OaToSa,
|
||||||
|
oa,
|
||||||
|
sa,
|
||||||
|
oa_exchange,
|
||||||
|
sa_exchange,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TransitionDirection::SaToOa,
|
||||||
|
sa,
|
||||||
|
oa,
|
||||||
|
sa_exchange,
|
||||||
|
oa_exchange,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert!(counterpart_compatible(&p, source, target, direction));
|
||||||
|
assert!(counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
source_relation,
|
||||||
|
target_relation,
|
||||||
|
direction
|
||||||
|
));
|
||||||
|
for (a, b) in [
|
||||||
|
(target, source),
|
||||||
|
(source, source),
|
||||||
|
(target, target),
|
||||||
|
(target_relation, source_relation),
|
||||||
|
(source_relation, source_relation),
|
||||||
|
(source, target_relation),
|
||||||
|
(source_relation, target),
|
||||||
|
] {
|
||||||
|
assert!(!counterpart_compatible(&p, a, b, direction));
|
||||||
|
}
|
||||||
|
for absent_or_nonsemantic in [Id::nil(), Id::new_v4(), p.id, diagram, node] {
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
source,
|
||||||
|
absent_or_nonsemantic,
|
||||||
|
direction
|
||||||
|
));
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
absent_or_nonsemantic,
|
||||||
|
target,
|
||||||
|
direction
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.elements.remove(&sa);
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
oa,
|
||||||
|
sa,
|
||||||
|
TransitionDirection::OaToSa
|
||||||
|
));
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
sa,
|
||||||
|
oa,
|
||||||
|
TransitionDirection::SaToOa
|
||||||
|
));
|
||||||
|
// The retained relation header cannot borrow a layer from a deleted owner.
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
oa_exchange,
|
||||||
|
sa_exchange,
|
||||||
|
TransitionDirection::OaToSa
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn activity_picker_rejects_real_sa_port_and_agrees_with_explicit_plan_refusal() {
|
||||||
|
let p = Project::demo();
|
||||||
|
let original = p.clone();
|
||||||
|
assert!(validation::validate(&p).is_empty());
|
||||||
|
let source = p
|
||||||
|
.elements
|
||||||
|
.values()
|
||||||
|
.find(|e| e.kind() == Kind::Activity)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let port = p
|
||||||
|
.elements
|
||||||
|
.values()
|
||||||
|
.find(|e| e.layer() == Layer::Sa && e.kind() == Kind::Port)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let function = p
|
||||||
|
.elements
|
||||||
|
.values()
|
||||||
|
.find(|e| e.kind() == Kind::Function)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
assert!(counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
source,
|
||||||
|
function,
|
||||||
|
TransitionDirection::OaToSa
|
||||||
|
));
|
||||||
|
assert!(!counterpart_compatible(
|
||||||
|
&p,
|
||||||
|
source,
|
||||||
|
port,
|
||||||
|
TransitionDirection::OaToSa
|
||||||
|
));
|
||||||
|
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
|
||||||
|
request.mappings.push(TransitionMapping {
|
||||||
|
source,
|
||||||
|
target: Some(port),
|
||||||
|
target_kind: None,
|
||||||
|
});
|
||||||
|
let plan = plan_transition(&p, &request).unwrap();
|
||||||
|
assert!(
|
||||||
|
plan.conflicts
|
||||||
|
.iter()
|
||||||
|
.any(|conflict| conflict.source == Some(source)
|
||||||
|
&& conflict.target == Some(port)
|
||||||
|
&& conflict.field.is_none())
|
||||||
|
);
|
||||||
|
assert!(apply_transition(&p, &plan).is_err());
|
||||||
|
assert_eq!(p, original);
|
||||||
|
}
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
//! Endpoint lifecycle acceptance for the ordinary, prose-reviewed transition API.
|
||||||
|
//! OA is portless; the SA half contains typed ports. Deletion expectations below
|
||||||
|
//! are fixture-owned identity lists, never results of the deletion implementation.
|
||||||
|
use oa_core::{
|
||||||
|
language, persistence,
|
||||||
|
source_project::{ProjectLimits, SourceBundle, compile_bundle},
|
||||||
|
transitions::*,
|
||||||
|
validation::{self, Severity},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
struct Side {
|
||||||
|
root: Id,
|
||||||
|
elements: Vec<Id>,
|
||||||
|
exchange: Id,
|
||||||
|
diagram: Id,
|
||||||
|
nodes: Vec<Id>,
|
||||||
|
artifacts: Vec<Id>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
project: Project,
|
||||||
|
source: Side,
|
||||||
|
target: Side,
|
||||||
|
trace: Id,
|
||||||
|
trace_artifact: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn artifact(p: &mut Project, owner: Id, label: &str) -> Id {
|
||||||
|
let id = Id::new_v4();
|
||||||
|
p.artifact_links.insert(
|
||||||
|
id,
|
||||||
|
ArtifactLink {
|
||||||
|
id,
|
||||||
|
alias: p.fresh_alias(label, "evidence", id),
|
||||||
|
name: label.into(),
|
||||||
|
description: "Independently authored rationale".into(),
|
||||||
|
owner,
|
||||||
|
relation: ArtifactRelation::References,
|
||||||
|
uri: "git:engineering/design.md".into(),
|
||||||
|
revision: Some("review-17".into()),
|
||||||
|
fragment: Some("retained-decision".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side(p: &mut Project, layer: Layer) -> Side {
|
||||||
|
let (kind, prefix, relation) = match layer {
|
||||||
|
Layer::Oa => (Kind::Activity, "Operational", RelationKind::Interaction),
|
||||||
|
Layer::Sa => (
|
||||||
|
Kind::Function,
|
||||||
|
"Technical",
|
||||||
|
RelationKind::FunctionalExchange,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let root = p
|
||||||
|
.add_element(kind, layer, &format!("{prefix} design"), None)
|
||||||
|
.unwrap();
|
||||||
|
p.elements.get_mut(&root).unwrap().description =
|
||||||
|
format!("{prefix} independently reviewed intent");
|
||||||
|
let send = p
|
||||||
|
.add_element(kind, layer, &format!("{prefix} send"), Some(root))
|
||||||
|
.unwrap();
|
||||||
|
let receive = p
|
||||||
|
.add_element(kind, layer, &format!("{prefix} receive"), Some(root))
|
||||||
|
.unwrap();
|
||||||
|
p.elements.get_mut(&send).unwrap().description =
|
||||||
|
"Preserve the detailed sending responsibility".into();
|
||||||
|
p.elements.get_mut(&receive).unwrap().description =
|
||||||
|
"Preserve the detailed receiving responsibility".into();
|
||||||
|
// A shared, separately owned definition survives deletion of its users.
|
||||||
|
let item = p
|
||||||
|
.add_element(
|
||||||
|
Kind::ExchangeItem,
|
||||||
|
layer,
|
||||||
|
&format!("{prefix} information"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||||
|
kind: ExchangeItemKind::Information,
|
||||||
|
mechanism: ExchangeMechanism::Flow,
|
||||||
|
supertype: None,
|
||||||
|
};
|
||||||
|
p.add_element(
|
||||||
|
Kind::DataField,
|
||||||
|
layer,
|
||||||
|
&format!("{prefix} payload"),
|
||||||
|
Some(item),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let exchange = p.relate(relation, &format!("{prefix} delivery"), send, receive);
|
||||||
|
let mut elements = vec![root, send, receive];
|
||||||
|
match &mut p.relations.get_mut(&exchange).unwrap().body {
|
||||||
|
RelationBody::Interaction { items, .. } => *items = vec![item],
|
||||||
|
RelationBody::FunctionalExchange {
|
||||||
|
items,
|
||||||
|
source_port,
|
||||||
|
target_port,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
*items = vec![item];
|
||||||
|
let (output, input) = (*source_port, *target_port);
|
||||||
|
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||||
|
contract: PortContract::FunctionOutput {
|
||||||
|
outgoing_items: vec![item],
|
||||||
|
},
|
||||||
|
legacy: None,
|
||||||
|
};
|
||||||
|
*p.elements.get_mut(&input).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||||
|
contract: PortContract::FunctionInput {
|
||||||
|
incoming_items: vec![item],
|
||||||
|
},
|
||||||
|
legacy: None,
|
||||||
|
};
|
||||||
|
elements.extend([output, input]);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
p.relations.get_mut(&exchange).unwrap().description =
|
||||||
|
"Retain direction, item contract and relation identity".into();
|
||||||
|
let diagram = p.ensure_diagram(layer);
|
||||||
|
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Decomposition;
|
||||||
|
let nodes = [root, send, receive]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, element)| {
|
||||||
|
p.place(
|
||||||
|
diagram,
|
||||||
|
element,
|
||||||
|
Position {
|
||||||
|
x: 43.25 + index as f32 * 271.,
|
||||||
|
y: 97.5 + index as f32 * 83.,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let root_node = p
|
||||||
|
.diagrams
|
||||||
|
.get_mut(&diagram)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&nodes[0])
|
||||||
|
.unwrap();
|
||||||
|
root_node.size = Size {
|
||||||
|
width: 611.,
|
||||||
|
height: 319.,
|
||||||
|
};
|
||||||
|
root_node.collapsed = true;
|
||||||
|
p.diagrams
|
||||||
|
.get_mut(&diagram)
|
||||||
|
.unwrap()
|
||||||
|
.routes
|
||||||
|
.push(EdgeRoute {
|
||||||
|
edge: exchange,
|
||||||
|
waypoints: vec![Position {
|
||||||
|
x: 337.5,
|
||||||
|
y: 423.25,
|
||||||
|
}],
|
||||||
|
label: Some(EdgeLabel {
|
||||||
|
fraction: 0.7,
|
||||||
|
offset: Position { x: 11., y: -17. },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let artifacts = [root, send, exchange, nodes[0]]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, owner)| artifact(p, owner, &format!("{prefix} evidence {index}")))
|
||||||
|
.collect();
|
||||||
|
artifact(p, item, &format!("{prefix} shared item evidence"));
|
||||||
|
Side {
|
||||||
|
root,
|
||||||
|
elements,
|
||||||
|
exchange,
|
||||||
|
diagram,
|
||||||
|
nodes,
|
||||||
|
artifacts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepted(p: &Project) {
|
||||||
|
let issues = validation::validate(p);
|
||||||
|
// This fixture deliberately isolates decomposition and correspondence from
|
||||||
|
// performer allocation and process/chain/scenario inclusion. Each surviving
|
||||||
|
// leaf (including a newly transitioned root) must have exactly those two
|
||||||
|
// incomplete-work warnings; each surviving local exchange lacks a behavior
|
||||||
|
// context. Check exact record IDs and fields, including duplicate warnings.
|
||||||
|
// The system of interest and item contracts are complete fixture facts.
|
||||||
|
assert!(
|
||||||
|
issues
|
||||||
|
.iter()
|
||||||
|
.all(|issue| issue.severity == Severity::Warning),
|
||||||
|
"{issues:?}"
|
||||||
|
);
|
||||||
|
let mut expected = Vec::new();
|
||||||
|
for element in p.elements.values().filter(|element| {
|
||||||
|
matches!(element.kind(), Kind::Activity | Kind::Function)
|
||||||
|
&& !p
|
||||||
|
.elements
|
||||||
|
.values()
|
||||||
|
.any(|child| child.parent == Some(element.id) && child.kind() == element.kind())
|
||||||
|
}) {
|
||||||
|
expected.push(("unallocated_leaf", Some(element.id), Some("allocation")));
|
||||||
|
expected.push(("work_without_behavior_context", Some(element.id), None));
|
||||||
|
}
|
||||||
|
for relation in p.relations.values().filter(|relation| {
|
||||||
|
matches!(
|
||||||
|
relation.kind(),
|
||||||
|
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
expected.push(("exchange_without_behavior_context", Some(relation.id), None));
|
||||||
|
}
|
||||||
|
let mut actual = issues
|
||||||
|
.iter()
|
||||||
|
.map(|issue| (issue.code.as_str(), issue.element, issue.field.as_deref()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
expected.sort();
|
||||||
|
actual.sort();
|
||||||
|
assert_eq!(
|
||||||
|
actual, expected,
|
||||||
|
"Unexpected fixture diagnostics: {issues:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn round_trip(p: &Project) -> String {
|
||||||
|
accepted(p);
|
||||||
|
let json = serde_json::to_vec_pretty(p).unwrap();
|
||||||
|
assert_eq!(persistence::decode(&json).unwrap(), *p);
|
||||||
|
let source = language::format(p);
|
||||||
|
let compiled = compile_bundle(
|
||||||
|
&SourceBundle {
|
||||||
|
entry: "project.arcadia".into(),
|
||||||
|
files: BTreeMap::from([("project.arcadia".into(), source.clone())]),
|
||||||
|
},
|
||||||
|
ProjectLimits::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(compiled.decoded.project, *p);
|
||||||
|
assert_eq!(language::format(&compiled.decoded.project), source);
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
fn explicit(direction: TransitionDirection, source: Id, target: Id) -> TransitionRequest {
|
||||||
|
let mut request = TransitionRequest::new(direction, vec![source]);
|
||||||
|
request.mappings.push(TransitionMapping {
|
||||||
|
source,
|
||||||
|
target: Some(target),
|
||||||
|
target_kind: None,
|
||||||
|
});
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture(direction: TransitionDirection) -> Fixture {
|
||||||
|
let mut project = Project::empty();
|
||||||
|
project.name = "Endpoint lifecycle conservation".into();
|
||||||
|
project
|
||||||
|
.add_element(Kind::System, Layer::Sa, "System of interest", None)
|
||||||
|
.unwrap();
|
||||||
|
let oa = side(&mut project, Layer::Oa);
|
||||||
|
let sa = side(&mut project, Layer::Sa);
|
||||||
|
let (source, target) = match direction {
|
||||||
|
TransitionDirection::OaToSa => (oa, sa),
|
||||||
|
TransitionDirection::SaToOa => (sa, oa),
|
||||||
|
};
|
||||||
|
accepted(&project);
|
||||||
|
let before = project.clone();
|
||||||
|
let mut request = explicit(direction, source.root, target.root);
|
||||||
|
for field in [TransitionField::Name, TransitionField::Description] {
|
||||||
|
request.resolutions.push(FieldResolution {
|
||||||
|
source: source.root,
|
||||||
|
field,
|
||||||
|
decision: FieldDecision::KeepTarget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let plan = plan_transition(&project, &request).unwrap();
|
||||||
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||||
|
project = apply_transition(&project, &plan).unwrap();
|
||||||
|
let trace = assert_only_new_trace(&before, &project, direction, source.root, target.root);
|
||||||
|
let mut without_trace = project.clone();
|
||||||
|
without_trace.traces.remove(&trace);
|
||||||
|
assert_eq!(
|
||||||
|
without_trace, before,
|
||||||
|
"Initial review must only add correspondence"
|
||||||
|
);
|
||||||
|
let trace_artifact = artifact(&mut project, trace, "Correspondence rationale");
|
||||||
|
round_trip(&project);
|
||||||
|
Fixture {
|
||||||
|
project,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
trace,
|
||||||
|
trace_artifact,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_only_new_trace(
|
||||||
|
before: &Project,
|
||||||
|
after: &Project,
|
||||||
|
direction: TransitionDirection,
|
||||||
|
source: Id,
|
||||||
|
target: Id,
|
||||||
|
) -> Id {
|
||||||
|
let added = after
|
||||||
|
.traces
|
||||||
|
.values()
|
||||||
|
.filter(|trace| !before.traces.contains_key(&trace.id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(added.len(), 1);
|
||||||
|
assert_eq!(after.traces.len(), before.traces.len() + 1);
|
||||||
|
let trace = added[0];
|
||||||
|
let (oa, sa) = match direction {
|
||||||
|
TransitionDirection::OaToSa => (source, target),
|
||||||
|
TransitionDirection::SaToOa => (target, source),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(trace.oa, trace.sa, trace.kind),
|
||||||
|
(oa, sa, TraceKind::Refinement)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
trace.baseline,
|
||||||
|
Some(TraceBaseline {
|
||||||
|
oa: after.properties(oa).unwrap(),
|
||||||
|
sa: after.properties(sa).unwrap(),
|
||||||
|
semantic: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(trace_drift(after, trace), Drift::Unchanged);
|
||||||
|
trace.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_deletion(fixture: &Fixture, side: &Side) -> Project {
|
||||||
|
let mut expected = fixture.project.clone();
|
||||||
|
for id in &side.elements {
|
||||||
|
assert!(expected.elements.remove(id).is_some());
|
||||||
|
}
|
||||||
|
assert!(expected.relations.remove(&side.exchange).is_some());
|
||||||
|
assert!(expected.traces.remove(&fixture.trace).is_some());
|
||||||
|
for id in side.artifacts.iter().chain([&fixture.trace_artifact]) {
|
||||||
|
assert!(expected.artifact_links.remove(id).is_some());
|
||||||
|
}
|
||||||
|
let diagram = expected.diagrams.get_mut(&side.diagram).unwrap();
|
||||||
|
for id in &side.nodes {
|
||||||
|
assert!(diagram.nodes.remove(id).is_some());
|
||||||
|
}
|
||||||
|
// This fixture authored exactly one route, for its removed local exchange.
|
||||||
|
assert_eq!(diagram.routes.len(), 1);
|
||||||
|
assert_eq!(diagram.routes[0].edge, side.exchange);
|
||||||
|
diagram.routes.clear();
|
||||||
|
expected
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unchanged_retry(p: &Project, direction: TransitionDirection, source: Id, target: Id) {
|
||||||
|
let request = TransitionRequest::new(direction, vec![source]);
|
||||||
|
let plan = plan_transition(p, &request).unwrap();
|
||||||
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||||
|
assert_eq!(plan.changes.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
plan.changes[0].source,
|
||||||
|
plan.changes[0].target,
|
||||||
|
plan.changes[0].action
|
||||||
|
),
|
||||||
|
(source, target, TransitionAction::Reuse)
|
||||||
|
);
|
||||||
|
assert!(plan.changes[0].fields.is_empty());
|
||||||
|
assert!(plan.commands.is_empty(), "{:?}", plan.commands);
|
||||||
|
let mut session = Session::new(p.clone());
|
||||||
|
let revision = session.revision();
|
||||||
|
session
|
||||||
|
.execute(Command::Replace(apply_transition(p, &plan).unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(session.project(), p);
|
||||||
|
assert_eq!(session.revision(), revision);
|
||||||
|
assert!(!session.can_undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_deletion(direction: TransitionDirection) {
|
||||||
|
let fixture = fixture(direction);
|
||||||
|
let original_source = round_trip(&fixture.project);
|
||||||
|
let expected = expected_deletion(&fixture, &fixture.source);
|
||||||
|
let mut session = Session::new(fixture.project.clone());
|
||||||
|
session
|
||||||
|
.execute(Command::Delete(fixture.source.root))
|
||||||
|
.unwrap();
|
||||||
|
// Whole-project equality covers every retained target child, typed SA port,
|
||||||
|
// exchange, placement, route, artifact, shared item and unchanged identity.
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
let deleted_source = round_trip(session.project());
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &fixture.project);
|
||||||
|
assert_eq!(round_trip(session.project()), original_source);
|
||||||
|
assert!(!session.can_undo(), "Deletion is one history transaction");
|
||||||
|
session.redo();
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
assert_eq!(round_trip(session.project()), deleted_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn implicit_creation(
|
||||||
|
p: &Project,
|
||||||
|
direction: TransitionDirection,
|
||||||
|
source: Id,
|
||||||
|
forbidden_target: Id,
|
||||||
|
) -> (Project, Id) {
|
||||||
|
let request = TransitionRequest::new(direction, vec![source]);
|
||||||
|
let plan = plan_transition(p, &request).unwrap();
|
||||||
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||||
|
assert_eq!(plan.changes.len(), 1);
|
||||||
|
let change = &plan.changes[0];
|
||||||
|
assert_eq!(change.source, source);
|
||||||
|
assert_eq!(change.action, TransitionAction::Create);
|
||||||
|
let target = change.target;
|
||||||
|
assert_ne!(target, forbidden_target);
|
||||||
|
assert!(!p.contains(target));
|
||||||
|
let after = apply_transition(p, &plan).unwrap();
|
||||||
|
let trace = assert_only_new_trace(p, &after, direction, source, target);
|
||||||
|
let created = &after.elements[&target];
|
||||||
|
assert_eq!(created.id, target, "Apply preserves the preview identity");
|
||||||
|
assert_eq!(created.name, p.elements[&source].name);
|
||||||
|
assert_eq!(created.description, p.elements[&source].description);
|
||||||
|
assert_eq!(created.parent, None);
|
||||||
|
assert_eq!(
|
||||||
|
created.body,
|
||||||
|
match direction {
|
||||||
|
TransitionDirection::OaToSa => ElementBody::Sa(SaBody::Function),
|
||||||
|
TransitionDirection::SaToOa => ElementBody::Oa(OaBody::Activity),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(!created.alias.is_empty());
|
||||||
|
assert!(
|
||||||
|
!p.identities()
|
||||||
|
.iter()
|
||||||
|
.any(|(_, alias)| *alias == created.alias)
|
||||||
|
);
|
||||||
|
// Remove only the independently expected two additions. Any incidental
|
||||||
|
// mutation, clone of target children or extra correspondence fails here.
|
||||||
|
let mut retained = after.clone();
|
||||||
|
retained.elements.remove(&target);
|
||||||
|
retained.traces.remove(&trace);
|
||||||
|
assert_eq!(retained, *p);
|
||||||
|
round_trip(&after);
|
||||||
|
// Replaying a creation plan after acceptance violates its snapshot, while
|
||||||
|
// a fresh unchanged request reuses the accepted counterpart.
|
||||||
|
assert!(
|
||||||
|
apply_transition(&after, &plan)
|
||||||
|
.unwrap_err()
|
||||||
|
.0
|
||||||
|
.contains("stale")
|
||||||
|
);
|
||||||
|
unchanged_retry(&after, direction, source, target);
|
||||||
|
(after, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_deletion(direction: TransitionDirection) {
|
||||||
|
let fixture = fixture(direction);
|
||||||
|
let request = TransitionRequest::new(direction, vec![fixture.source.root]);
|
||||||
|
let old_plan = plan_transition(&fixture.project, &request).unwrap();
|
||||||
|
assert!(old_plan.conflicts.is_empty());
|
||||||
|
let expected = expected_deletion(&fixture, &fixture.target);
|
||||||
|
let mut session = Session::new(fixture.project.clone());
|
||||||
|
session
|
||||||
|
.execute(Command::Delete(fixture.target.root))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
round_trip(session.project());
|
||||||
|
let revision = session.revision();
|
||||||
|
assert!(
|
||||||
|
apply_transition(session.project(), &old_plan)
|
||||||
|
.unwrap_err()
|
||||||
|
.0
|
||||||
|
.contains("stale")
|
||||||
|
);
|
||||||
|
let explicit_plan = plan_transition(
|
||||||
|
session.project(),
|
||||||
|
&explicit(direction, fixture.source.root, fixture.target.root),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
explicit_plan
|
||||||
|
.conflicts
|
||||||
|
.iter()
|
||||||
|
.any(|conflict| conflict.source == Some(fixture.source.root)
|
||||||
|
&& conflict.target == Some(fixture.target.root)
|
||||||
|
&& conflict.field.is_none())
|
||||||
|
);
|
||||||
|
assert!(apply_transition(session.project(), &explicit_plan).is_err());
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
assert_eq!(session.revision(), revision);
|
||||||
|
let (created, _) = implicit_creation(
|
||||||
|
session.project(),
|
||||||
|
direction,
|
||||||
|
fixture.source.root,
|
||||||
|
fixture.target.root,
|
||||||
|
);
|
||||||
|
session.execute(Command::Replace(created.clone())).unwrap();
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &fixture.project);
|
||||||
|
assert!(!session.can_undo());
|
||||||
|
session.redo();
|
||||||
|
session.redo();
|
||||||
|
assert_eq!(session.project(), &created);
|
||||||
|
round_trip(session.project());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_recreation(direction: TransitionDirection) {
|
||||||
|
let fixture = fixture(direction);
|
||||||
|
let mut replacement = fixture.project.elements[&fixture.source.root].clone();
|
||||||
|
replacement.id = Id::new_v4();
|
||||||
|
let source = replacement.id;
|
||||||
|
// Same alias, name and prose, different UUID: name matching must never
|
||||||
|
// silently relink the retained engineering design.
|
||||||
|
assert_ne!(source, fixture.source.root);
|
||||||
|
let deleted = execute_batch(&fixture.project, &[Command::Delete(fixture.source.root)]).unwrap();
|
||||||
|
assert_eq!(deleted, expected_deletion(&fixture, &fixture.source));
|
||||||
|
let recreated =
|
||||||
|
execute_batch(&deleted, &[Command::CreateElement(replacement.clone())]).unwrap();
|
||||||
|
let mut expected = deleted.clone();
|
||||||
|
expected.elements.insert(source, replacement);
|
||||||
|
assert_eq!(recreated, expected);
|
||||||
|
round_trip(&recreated);
|
||||||
|
let (implicit, new_target) =
|
||||||
|
implicit_creation(&recreated, direction, source, fixture.target.root);
|
||||||
|
assert!(implicit.elements.contains_key(&fixture.target.root));
|
||||||
|
assert_ne!(new_target, fixture.target.root);
|
||||||
|
|
||||||
|
// The explicit branch starts from exactly the same recreated project.
|
||||||
|
let mut request = explicit(direction, source, fixture.target.root);
|
||||||
|
let unreviewed = plan_transition(&recreated, &request).unwrap();
|
||||||
|
for field in [TransitionField::Name, TransitionField::Description] {
|
||||||
|
assert!(
|
||||||
|
unreviewed
|
||||||
|
.conflicts
|
||||||
|
.iter()
|
||||||
|
.any(|conflict| conflict.source == Some(source)
|
||||||
|
&& conflict.target == Some(fixture.target.root)
|
||||||
|
&& conflict.field == Some(field))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(apply_transition(&recreated, &unreviewed).is_err());
|
||||||
|
request.resolutions = vec![
|
||||||
|
FieldResolution {
|
||||||
|
source,
|
||||||
|
field: TransitionField::Name,
|
||||||
|
decision: FieldDecision::TakeSource,
|
||||||
|
},
|
||||||
|
FieldResolution {
|
||||||
|
source,
|
||||||
|
field: TransitionField::Description,
|
||||||
|
decision: FieldDecision::KeepTarget,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let plan = plan_transition(&recreated, &request).unwrap();
|
||||||
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||||
|
assert_eq!(plan.changes.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
(plan.changes[0].target, plan.changes[0].action),
|
||||||
|
(fixture.target.root, TransitionAction::Update)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plan.changes[0].fields,
|
||||||
|
vec![FieldChange {
|
||||||
|
field: TransitionField::Name,
|
||||||
|
before: recreated.elements[&fixture.target.root].name.clone(),
|
||||||
|
after: recreated.elements[&source].name.clone(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
let relinked = apply_transition(&recreated, &plan).unwrap();
|
||||||
|
let new_trace = assert_only_new_trace(
|
||||||
|
&recreated,
|
||||||
|
&relinked,
|
||||||
|
direction,
|
||||||
|
source,
|
||||||
|
fixture.target.root,
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
new_trace, fixture.trace,
|
||||||
|
"Deleted correspondence is not silently resurrected"
|
||||||
|
);
|
||||||
|
let mut expected = recreated.clone();
|
||||||
|
expected
|
||||||
|
.elements
|
||||||
|
.get_mut(&fixture.target.root)
|
||||||
|
.unwrap()
|
||||||
|
.name = recreated.elements[&source].name.clone();
|
||||||
|
let mut without_trace = relinked.clone();
|
||||||
|
without_trace.traces.remove(&new_trace);
|
||||||
|
assert_eq!(
|
||||||
|
without_trace, expected,
|
||||||
|
"Only the reviewed target name may change; rich target design survives"
|
||||||
|
);
|
||||||
|
round_trip(&relinked);
|
||||||
|
unchanged_retry(&relinked, direction, source, fixture.target.root);
|
||||||
|
let original_source = round_trip(&recreated);
|
||||||
|
let mut session = Session::new(recreated.clone());
|
||||||
|
session.execute(Command::Replace(relinked)).unwrap();
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &recreated);
|
||||||
|
assert_eq!(round_trip(session.project()), original_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oa_source_deletion_preserves_rich_sa_design_and_exact_undo() {
|
||||||
|
source_deletion(TransitionDirection::OaToSa);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sa_source_deletion_preserves_rich_portless_oa_design_and_exact_undo() {
|
||||||
|
source_deletion(TransitionDirection::SaToOa);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deleted_sa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||||
|
target_deletion(TransitionDirection::OaToSa);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn deleted_oa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||||
|
target_deletion(TransitionDirection::SaToOa);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn recreated_oa_identity_requires_explicit_relink_to_retained_sa_design() {
|
||||||
|
source_recreation(TransitionDirection::OaToSa);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn recreated_sa_identity_requires_explicit_relink_to_retained_oa_design() {
|
||||||
|
source_recreation(TransitionDirection::SaToOa);
|
||||||
|
}
|
||||||
@@ -78,7 +78,11 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Owner<'_, Mes
|
|||||||
}
|
}
|
||||||
if state.context.is_none_or(|previous| {
|
if state.context.is_none_or(|previous| {
|
||||||
(previous.document, previous.diagram, previous.epoch)
|
(previous.document, previous.diagram, previous.epoch)
|
||||||
!= (self.context.document, self.context.diagram, self.context.epoch)
|
!= (
|
||||||
|
self.context.document,
|
||||||
|
self.context.diagram,
|
||||||
|
self.context.epoch,
|
||||||
|
)
|
||||||
}) || !self.enabled
|
}) || !self.enabled
|
||||||
{
|
{
|
||||||
state.focused = false;
|
state.focused = false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use iced::{
|
|||||||
use oa_core::{transitions::*, *};
|
use oa_core::{transitions::*, *};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
mod communication;
|
mod communication;
|
||||||
|
mod counterparts;
|
||||||
pub(crate) mod recovery;
|
pub(crate) mod recovery;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod transition_tests;
|
mod transition_tests;
|
||||||
@@ -34,6 +35,7 @@ pub enum Event {
|
|||||||
Scope(String),
|
Scope(String),
|
||||||
Destination(Choice),
|
Destination(Choice),
|
||||||
MappingTarget(Id, Choice),
|
MappingTarget(Id, Choice),
|
||||||
|
MappingQuery(Id, String),
|
||||||
MappingKind(Id, Kind),
|
MappingKind(Id, Kind),
|
||||||
CommunicationPort(Id, bool, communication::PortMenuChoice),
|
CommunicationPort(Id, bool, communication::PortMenuChoice),
|
||||||
OperationOmission(Id, bool),
|
OperationOmission(Id, bool),
|
||||||
@@ -274,6 +276,7 @@ pub struct Transition {
|
|||||||
pub request: TransitionRequest,
|
pub request: TransitionRequest,
|
||||||
pub plan: TransitionPlan,
|
pub plan: TransitionPlan,
|
||||||
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
|
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
|
||||||
|
counterpart_queries: std::collections::BTreeMap<Id, String>,
|
||||||
}
|
}
|
||||||
impl Transition {
|
impl Transition {
|
||||||
fn presentation(&self, p: &Project) -> (Project, Vec<Id>) {
|
fn presentation(&self, p: &Project) -> (Project, Vec<Id>) {
|
||||||
@@ -515,6 +518,7 @@ impl Workflow {
|
|||||||
request,
|
request,
|
||||||
plan,
|
plan,
|
||||||
communication_ports: Default::default(),
|
communication_ports: Default::default(),
|
||||||
|
counterpart_queries: Default::default(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
/// Return a complete accepted candidate only on explicit Apply. Intermediate
|
/// Return a complete accepted candidate only on explicit Apply. Intermediate
|
||||||
@@ -619,6 +623,12 @@ impl Workflow {
|
|||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Self::Transition(d) => {
|
Self::Transition(d) => {
|
||||||
|
if let Event::MappingQuery(source, query) = event {
|
||||||
|
d.counterpart_queries.insert(source, query);
|
||||||
|
// Searching is presentation only: preserve reviewed decisions,
|
||||||
|
// preview identities and the exact recovery snapshot.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
match &event {
|
match &event {
|
||||||
Event::OperationOmission(source, enabled) => {
|
Event::OperationOmission(source, enabled) => {
|
||||||
d.request
|
d.request
|
||||||
@@ -1148,48 +1158,17 @@ impl Workflow {
|
|||||||
content=content.push(text("The SA operation remains unchanged. Correlated replies derive this Call's abstraction.").size(11));
|
content=content.push(text("The SA operation remains unchanged. Correlated replies derive this Call's abstraction.").size(11));
|
||||||
}
|
}
|
||||||
let mapping = d.request.mappings.iter().find(|m| m.source == source);
|
let mapping = d.request.mappings.iter().find(|m| m.source == source);
|
||||||
let existing = p
|
let target_choice = counterparts::target(p, source, &d.request);
|
||||||
.traces
|
let target = target_choice.flatten();
|
||||||
.values()
|
let query = d
|
||||||
.filter_map(|t| {
|
.counterpart_queries
|
||||||
if d.request.direction == TransitionDirection::OaToSa && t.oa == source
|
.get(&source)
|
||||||
{
|
.map_or("", String::as_str);
|
||||||
Some(t.sa)
|
let targets = counterparts::choices(p, source, d.request.direction, query);
|
||||||
} else if d.request.direction == TransitionDirection::SaToOa
|
let no_matches = targets.len() == 1;
|
||||||
&& t.sa == source
|
let selected = target_choice.map(|target| {
|
||||||
{
|
counterparts::selected(p, source, d.request.direction, target)
|
||||||
Some(t.oa)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let target = mapping.map(|m| m.target).unwrap_or_else(|| {
|
|
||||||
if existing.len() == 1 {
|
|
||||||
Some(existing[0])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
let mut targets = vec![Choice {
|
|
||||||
id: Id::nil(),
|
|
||||||
label: "Create new counterpart".into(),
|
|
||||||
}];
|
|
||||||
targets.extend(
|
|
||||||
p.elements
|
|
||||||
.keys()
|
|
||||||
.chain(p.relations.keys())
|
|
||||||
.filter(|id| {
|
|
||||||
p.layer_of(**id) == Some(d.request.direction.target_layer())
|
|
||||||
&& p.elements.contains_key(&source)
|
|
||||||
== p.elements.contains_key(id)
|
|
||||||
})
|
|
||||||
.map(|id| choice(p, *id)),
|
|
||||||
);
|
|
||||||
let selected = targets
|
|
||||||
.iter()
|
|
||||||
.find(|c| Some(c.id) == target.or(Some(Id::nil())))
|
|
||||||
.cloned();
|
|
||||||
let mut item = column![
|
let mut item = column![
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} · {}",
|
"{} · {}",
|
||||||
@@ -1200,14 +1179,29 @@ impl Workflow {
|
|||||||
.unwrap_or("Relationship")
|
.unwrap_or("Relationship")
|
||||||
))
|
))
|
||||||
.size(14),
|
.size(14),
|
||||||
|
text_input("Find counterpart by name or @alias", query)
|
||||||
|
.on_input(move |query| Message::Workflow(Event::MappingQuery(
|
||||||
|
source, query
|
||||||
|
)))
|
||||||
|
.size(12)
|
||||||
|
.padding(8),
|
||||||
pick_list(targets, selected, move |v| Message::Workflow(
|
pick_list(targets, selected, move |v| Message::Workflow(
|
||||||
Event::MappingTarget(source, v)
|
Event::MappingTarget(source, v)
|
||||||
))
|
))
|
||||||
|
.placeholder("Choose a counterpart")
|
||||||
.text_size(12)
|
.text_size(12)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
]
|
]
|
||||||
.spacing(7);
|
.spacing(7);
|
||||||
|
if no_matches {
|
||||||
|
let message = if query.trim().is_empty() {
|
||||||
|
"No compatible existing counterparts. Create a new one."
|
||||||
|
} else {
|
||||||
|
"No matching existing counterparts. Clear the search or create a new one."
|
||||||
|
};
|
||||||
|
item = item.push(text(message).size(11));
|
||||||
|
}
|
||||||
if target.is_none()
|
if target.is_none()
|
||||||
&& let Some(e) = p.elements.get(&source)
|
&& let Some(e) = p.elements.get(&source)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
//! Search is separate from the committed counterpart mapping.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Outer None requires a choice between distinct traced targets; inner None
|
||||||
|
/// means creation (explicit, or no existing correspondence).
|
||||||
|
pub(super) fn target(p: &Project, source: Id, request: &TransitionRequest) -> Option<Option<Id>> {
|
||||||
|
if let Some(mapping) = request.mappings.iter().find(|m| m.source == source) {
|
||||||
|
return Some(mapping.target);
|
||||||
|
}
|
||||||
|
let existing = p
|
||||||
|
.traces
|
||||||
|
.values()
|
||||||
|
.filter_map(|trace| match request.direction {
|
||||||
|
TransitionDirection::OaToSa if trace.oa == source => Some(trace.sa),
|
||||||
|
TransitionDirection::SaToOa if trace.sa == source => Some(trace.oa),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
match existing.len() {
|
||||||
|
0 => Some(None),
|
||||||
|
1 => Some(existing.first().copied()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_choice() -> Choice {
|
||||||
|
Choice {
|
||||||
|
id: Id::nil(),
|
||||||
|
label: "Create new counterpart".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identity(p: &Project, id: Id) -> Option<(&str, &str, String)> {
|
||||||
|
if let Some(e) = p.elements.get(&id) {
|
||||||
|
Some((&e.name, &e.alias, e.kind().label().into()))
|
||||||
|
} else {
|
||||||
|
p.relations
|
||||||
|
.get(&id)
|
||||||
|
.map(|r| (r.name.as_str(), r.alias.as_str(), r.kind().to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(p: &Project, id: Id) -> Choice {
|
||||||
|
let label = match identity(p, id) {
|
||||||
|
Some((name, alias, kind)) => format!("{} · {kind} · @{alias}", short(name, 48)),
|
||||||
|
None => format!("Missing counterpart · {id}"),
|
||||||
|
};
|
||||||
|
Choice { id, label }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn selected(
|
||||||
|
p: &Project,
|
||||||
|
source: Id,
|
||||||
|
direction: TransitionDirection,
|
||||||
|
target: Option<Id>,
|
||||||
|
) -> Choice {
|
||||||
|
let Some(target) = target else {
|
||||||
|
return new_choice();
|
||||||
|
};
|
||||||
|
let mut choice = label(p, target);
|
||||||
|
if p.contains(target) && !counterpart_compatible(p, source, target, direction) {
|
||||||
|
choice.label.push_str(" (incompatible)");
|
||||||
|
}
|
||||||
|
choice
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn choices(
|
||||||
|
p: &Project,
|
||||||
|
source: Id,
|
||||||
|
direction: TransitionDirection,
|
||||||
|
query: &str,
|
||||||
|
) -> Vec<Choice> {
|
||||||
|
let query = query.trim().to_lowercase();
|
||||||
|
let mut candidates = p
|
||||||
|
.elements
|
||||||
|
.keys()
|
||||||
|
.chain(p.relations.keys())
|
||||||
|
.copied()
|
||||||
|
.filter(|id| counterpart_compatible(p, source, *id, direction))
|
||||||
|
.filter_map(|id| {
|
||||||
|
let (name, alias, _) = identity(p, id)?;
|
||||||
|
let name = name.to_lowercase();
|
||||||
|
let alias = alias.to_lowercase();
|
||||||
|
let matches = if let Some(query) = query.strip_prefix('@') {
|
||||||
|
alias.contains(query)
|
||||||
|
} else {
|
||||||
|
name.contains(&query) || alias.contains(&query)
|
||||||
|
};
|
||||||
|
matches.then_some((name, alias, id))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
candidates.sort();
|
||||||
|
std::iter::once(new_choice())
|
||||||
|
.chain(candidates.into_iter().map(|(_, _, id)| label(p, id)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fixture() -> (Project, Id, Id, Id, Id) {
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let source = p
|
||||||
|
.add_element(Kind::Activity, Layer::Oa, "Assess pressure", None)
|
||||||
|
.unwrap();
|
||||||
|
let first = p
|
||||||
|
.add_element(Kind::Function, Layer::Sa, "Assess pressure", None)
|
||||||
|
.unwrap();
|
||||||
|
let second = p
|
||||||
|
.add_element(Kind::Function, Layer::Sa, "Assess pressure", None)
|
||||||
|
.unwrap();
|
||||||
|
let incompatible = p
|
||||||
|
.add_element(Kind::SystemActor, Layer::Sa, "Assess pressure", None)
|
||||||
|
.unwrap();
|
||||||
|
p.elements.get_mut(&first).unwrap().alias = "alpha_assess".into();
|
||||||
|
p.elements.get_mut(&second).unwrap().alias = "beta_assess".into();
|
||||||
|
(p, source, first, second, incompatible)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ids(choices: Vec<Choice>) -> Vec<Id> {
|
||||||
|
choices.into_iter().map(|c| c.id).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_disambiguates_names_with_aliases_and_retains_creation() {
|
||||||
|
let (p, source, first, second, _) = fixture();
|
||||||
|
let all = choices(&p, source, TransitionDirection::OaToSa, "");
|
||||||
|
assert_eq!(
|
||||||
|
all.iter().map(|c| c.id).collect::<Vec<_>>(),
|
||||||
|
vec![Id::nil(), first, second]
|
||||||
|
);
|
||||||
|
assert!(all[1].label.contains("@alpha_assess"));
|
||||||
|
assert!(all[2].label.contains("@beta_assess"));
|
||||||
|
assert_ne!(all[1].label, all[2].label);
|
||||||
|
for (query, expected) in [
|
||||||
|
(" ASSESS PRESSURE ", vec![Id::nil(), first, second]),
|
||||||
|
(" @BETA_ ", vec![Id::nil(), second]),
|
||||||
|
("ALPHA", vec![Id::nil(), first]),
|
||||||
|
("@pressure", vec![Id::nil()]),
|
||||||
|
("no match", vec![Id::nil()]),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
ids(choices(&p, source, TransitionDirection::OaToSa, query)),
|
||||||
|
expected,
|
||||||
|
"{query}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
ids(choices(&p, first, TransitionDirection::SaToOa, "PRESSURE")),
|
||||||
|
vec![Id::nil(), source]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_preserves_reviewed_request_plan_ids_and_recovery_exactly() {
|
||||||
|
let (mut p, source, first, _, _) = fixture();
|
||||||
|
let second = p
|
||||||
|
.add_element(Kind::Activity, Layer::Oa, "Second source card", None)
|
||||||
|
.unwrap();
|
||||||
|
let before_project = p.clone();
|
||||||
|
// Exercise both an existing reviewed target and generated preview IDs.
|
||||||
|
for chosen in [Some(first), None] {
|
||||||
|
let mut w = Workflow::transition(&p, source).unwrap();
|
||||||
|
let Workflow::Transition(t) = &mut w else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
t.request.sources.push(second);
|
||||||
|
t.plan = plan_transition(&p, &t.request).unwrap();
|
||||||
|
w.update(
|
||||||
|
&p,
|
||||||
|
Event::MappingTarget(
|
||||||
|
source,
|
||||||
|
selected(&p, source, TransitionDirection::OaToSa, chosen),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
w.update(
|
||||||
|
&p,
|
||||||
|
Event::Resolve(
|
||||||
|
source,
|
||||||
|
TransitionField::Name,
|
||||||
|
FieldDecision::Value("Reviewed name".into()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let saved = w.recovery_snapshot().unwrap();
|
||||||
|
let Workflow::Transition(t) = &w else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let plan = t.plan.clone();
|
||||||
|
let request = t.request.clone();
|
||||||
|
for query in ["assess", "@beta", "no match", ""] {
|
||||||
|
assert!(
|
||||||
|
w.update(&p, Event::MappingQuery(source, query.into()))
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(w.recovery_snapshot().unwrap(), saved);
|
||||||
|
let Workflow::Transition(t) = &w else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(t.plan, plan);
|
||||||
|
assert_eq!(t.request, request);
|
||||||
|
assert_eq!(t.counterpart_queries[&source], query);
|
||||||
|
assert_eq!(target(&p, source, &t.request), Some(chosen));
|
||||||
|
assert_eq!(
|
||||||
|
selected(&p, source, t.request.direction, chosen).id,
|
||||||
|
chosen.unwrap_or(Id::nil())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
w.update(&p, Event::MappingQuery(second, "other card".into()))
|
||||||
|
.unwrap();
|
||||||
|
let Workflow::Transition(t) = &w else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(t.counterpart_queries[&source], "");
|
||||||
|
assert_eq!(t.counterpart_queries[&second], "other card");
|
||||||
|
assert_eq!(t.plan, plan);
|
||||||
|
let restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||||
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||||
|
let Workflow::Transition(t) = restored else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(t.counterpart_queries.is_empty());
|
||||||
|
assert_eq!(t.request, request);
|
||||||
|
}
|
||||||
|
assert_eq!(p, before_project);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recovered_invalid_or_missing_mapping_stays_visible_and_refuses_apply() {
|
||||||
|
let (p, source, _, _, incompatible) = fixture();
|
||||||
|
for chosen in [incompatible, Id::from_u128(987654321)] {
|
||||||
|
let mut w = Workflow::transition(&p, source).unwrap();
|
||||||
|
w.update(
|
||||||
|
&p,
|
||||||
|
Event::MappingTarget(
|
||||||
|
source,
|
||||||
|
Choice {
|
||||||
|
id: chosen,
|
||||||
|
label: "Recovered choice".into(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let saved = w.recovery_snapshot().unwrap();
|
||||||
|
let mut restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||||
|
restored
|
||||||
|
.update(&p, Event::MappingQuery(source, "no match".into()))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||||
|
let Workflow::Transition(t) = &restored else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(target(&p, source, &t.request), Some(Some(chosen)));
|
||||||
|
let display = selected(&p, source, t.request.direction, Some(chosen));
|
||||||
|
assert_eq!(display.id, chosen);
|
||||||
|
assert!(display.label.contains(if chosen == incompatible {
|
||||||
|
"incompatible"
|
||||||
|
} else {
|
||||||
|
"Missing counterpart"
|
||||||
|
}));
|
||||||
|
assert!(
|
||||||
|
!choices(&p, source, t.request.direction, "")
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.id == chosen)
|
||||||
|
);
|
||||||
|
assert!(restored.update(&p, Event::Apply).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trace_inference_counts_distinct_targets_and_explicit_mapping_wins() {
|
||||||
|
let (mut p, source, first, second, _) = fixture();
|
||||||
|
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
|
||||||
|
assert_eq!(target(&p, source, &request), Some(None));
|
||||||
|
for (i, counterpart) in [first, first, second].into_iter().enumerate() {
|
||||||
|
let id = Id::from_u128(100 + i as u128);
|
||||||
|
p.traces.insert(
|
||||||
|
id,
|
||||||
|
Trace {
|
||||||
|
id,
|
||||||
|
alias: format!("trace_{i}"),
|
||||||
|
name: format!("Trace {i}"),
|
||||||
|
description: String::new(),
|
||||||
|
oa: source,
|
||||||
|
sa: counterpart,
|
||||||
|
kind: TraceKind::Manual,
|
||||||
|
baseline: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target(&p, source, &request),
|
||||||
|
if i < 2 { Some(Some(first)) } else { None }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
target(
|
||||||
|
&p,
|
||||||
|
first,
|
||||||
|
&TransitionRequest::new(TransitionDirection::SaToOa, vec![first])
|
||||||
|
),
|
||||||
|
Some(Some(source))
|
||||||
|
);
|
||||||
|
for chosen in [Some(second), None] {
|
||||||
|
request.mappings = vec![TransitionMapping {
|
||||||
|
source,
|
||||||
|
target: chosen,
|
||||||
|
target_kind: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(target(&p, source, &request), Some(chosen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,6 +202,7 @@ impl Workflow {
|
|||||||
request,
|
request,
|
||||||
plan,
|
plan,
|
||||||
communication_ports,
|
communication_ports,
|
||||||
|
counterpart_queries: Default::default(),
|
||||||
};
|
};
|
||||||
transition.sync_communication_ports();
|
transition.sync_communication_ports();
|
||||||
transition.plan =
|
transition.plan =
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ P1 extension to frozen relation interface: FunctionalExchange now has `source_po
|
|||||||
|
|
||||||
`transitions::counterpart_kinds(source: Kind, direction: TransitionDirection) -> Vec<Kind>` is the shared choice policy for UI and other clients. It returns supported explicit element counterpart kinds in stable `Kind::ALL` order using the same compatibility predicate as transition validation; an empty list means no automatic element mapping.
|
`transitions::counterpart_kinds(source: Kind, direction: TransitionDirection) -> Vec<Kind>` is the shared choice policy for UI and other clients. It returns supported explicit element counterpart kinds in stable `Kind::ALL` order using the same compatibility predicate as transition validation; an empty list means no automatic element mapping.
|
||||||
|
|
||||||
|
`transitions::counterpart_compatible(&Project, source: Id, target: Id, direction: TransitionDirection) -> bool` checks existing-record candidate eligibility: both IDs must exist in the direction's respective layers, belong to the same element/relation category, and have compatible transition kinds. It does not approve properties, dependencies or structural changes; clients still preview and apply a complete plan. Manual trace creation retains its separate policy. The desktop counterpart search uses this predicate, filters by name or `@alias`, and sorts by name then alias and ID. Search is presentation state: it does not replan, change mappings or reviewed field decisions, or enter workflow recovery data. A selected target remains visible when filtered out; invalid or missing recovered targets remain visible with their error and require correction. Multiple traces to the same target count as one inferred counterpart; distinct traced targets require an explicit choice.
|
||||||
|
|
||||||
`TransitionRequest {direction:OaToSa|SaToOa, sources:Vec<Id>, scope:SelectedOnly|Required|Subtree, mappings:Vec<TransitionMapping>, fields:Vec<TransitionField>, resolutions:Vec<FieldResolution>, destination_diagram:Option<Id>}`. `TransitionRequest::new(direction,sources)` defaults to selected-only and name/description fields. `TransitionMapping {source,target:Option<Id>,target_kind:Option<Kind>}` selects reuse or explicit creation kind. An explicit mapping with target=None means create a new counterpart even if another trace exists. Without mapping, one existing trace is reused; multiple traces require selection.
|
`TransitionRequest {direction:OaToSa|SaToOa, sources:Vec<Id>, scope:SelectedOnly|Required|Subtree, mappings:Vec<TransitionMapping>, fields:Vec<TransitionField>, resolutions:Vec<FieldResolution>, destination_diagram:Option<Id>}`. `TransitionRequest::new(direction,sources)` defaults to selected-only and name/description fields. `TransitionMapping {source,target:Option<Id>,target_kind:Option<Kind>}` selects reuse or explicit creation kind. An explicit mapping with target=None means create a new counterpart even if another trace exists. Without mapping, one existing trace is reused; multiple traces require selection.
|
||||||
|
|
||||||
`FieldResolution {source,field:Name|Description,decision:TakeSource|KeepTarget|Value(String)}`. `TransitionPlan {base_fingerprint,request,changes,conflicts,warnings,commands}`. Changes carry source/target IDs, Create/Update/Reuse and before/after field changes. Conflicts have optional source/target/field and message. Conflicts prevent apply. The deterministic fingerprint guards stale plans, is not a security hash, and does not introduce persistent project revision fields. Planned IDs and aliases remain unchanged during apply. Server owns request idempotency/revision storage. Apply is one candidate transaction; Session adoption can use Command::Replace(candidate) for one Undo.
|
`FieldResolution {source,field:Name|Description,decision:TakeSource|KeepTarget|Value(String)}`. `TransitionPlan {base_fingerprint,request,changes,conflicts,warnings,commands}`. Changes carry source/target IDs, Create/Update/Reuse and before/after field changes. Conflicts have optional source/target/field and message. Conflicts prevent apply. The deterministic fingerprint guards stale plans, is not a security hash, and does not introduce persistent project revision fields. Planned IDs and aliases remain unchanged during apply. Server owns request idempotency/revision storage. Apply is one candidate transaction; Session adoption can use Command::Replace(candidate) for one Undo.
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Transition lifecycle: closed-gate evidence
|
||||||
|
|
||||||
|
Closed Cargo-gate checkpoint with eight runs. Native work remains separate.
|
||||||
|
Run `python verify.py` using
|
||||||
|
Python 3.10 or newer. The five package files are sufficient; no archived code,
|
||||||
|
binary, Git command or network operation is executed by verification.
|
||||||
|
|
||||||
|
Raw logs/receipts preserve the initial lifecycle-1 fixture admission failure as
|
||||||
|
a distinct input state. Successful core-transition-2, desktop-workflow-1 and
|
||||||
|
core-workflows-1 inputs are bound to
|
||||||
|
acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8 by an independent local comparison of
|
||||||
|
all 710 captured input hashes with exact committed bytes using Git cat-file.
|
||||||
|
The readback summary records Git blob identities; its raw Git stream is not
|
||||||
|
archived. Offline verification checks those blob hashes and input relationships,
|
||||||
|
not authenticated membership of a remote commit. Receipt HEADs may precede dirty
|
||||||
|
changes, so per-run input manifests are authoritative for tested source bytes.
|
||||||
|
|
||||||
|
Strict all-target workspace Clippy passed on those tested inputs. The first
|
||||||
|
format check failed only on an inherited tuple layout in diagram/focus.rs;
|
||||||
|
the raw failure is retained. Commit d46854c2dc719bb8b44a0978102de44ecc3a6b39
|
||||||
|
reformats that tuple and the second format check passes. Its separate 710-input
|
||||||
|
inventory is independently compared with that commit; per-run inputCommit
|
||||||
|
bindings and formattingInputChanges distinguish these source states. Earlier
|
||||||
|
execution and lint results are not represented as reruns on the formatted
|
||||||
|
commit. No application behavior change is claimed from formatting evidence.
|
||||||
|
The Windows workspace build completed on d46854c with the same 710 unchanged
|
||||||
|
inputs as format-2 and three executable artifact receipts. Those recorded
|
||||||
|
artifacts identify CLI, server and desktop binaries; binaries are not packaged.
|
||||||
|
|
||||||
|
| Gate | Recorded outcome |
|
||||||
|
| --- | --- |
|
||||||
|
| lifecycle-1 | Initial fixture admission failed: 6 failures |
|
||||||
|
| core-transition-2 | 10 passed |
|
||||||
|
| desktop-workflow-1 | 25 passed, 1,164 filtered out |
|
||||||
|
| core-workflows-1 | 28 passed |
|
||||||
|
| clippy-1 | Strict workspace/all-target lint passed |
|
||||||
|
| format-1 | Initial tuple-format check failed |
|
||||||
|
| format-2 | Format check passed with empty streams |
|
||||||
|
| windows-build-1 | Build passed; three executable artifact records |
|
||||||
|
|
||||||
|
The manifest maps each run to exact manifests and archives; only byte-identical
|
||||||
|
input objects are deduplicated. Verification checks safe exact ZIP membership,
|
||||||
|
hashes/lengths, source inventory and contents, 71 vendored iced_widget files,
|
||||||
|
helper/start/process/result relationships, time order, commands, exit outcomes,
|
||||||
|
unchanged input assertions, exact test summary rows and artifact receipt records.
|
||||||
|
Outer text uses UTF-8 LF before pinning; inner bytes remain raw.
|
||||||
|
|
||||||
|
This is process evidence and internal consistency, not a test rerun, proof of
|
||||||
|
complete coverage, native acceptance or release readiness. Binaries, dependency
|
||||||
|
caches and credentials are excluded; artifact readback hashes are historical
|
||||||
|
observations, not independently verified binaries.
|
||||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"evidence.zip": {
|
||||||
|
"bytes": 8496343,
|
||||||
|
"sha256": "5f44ad5d83024a04a873142e77c5b8961c278a19d76492618c3ef2db63801070"
|
||||||
|
},
|
||||||
|
"manifest.json": {
|
||||||
|
"bytes": 14323,
|
||||||
|
"sha256": "7d48cb7e0788fb409b196f8be94c5947b30c698219bd016141fec136dbce3148"
|
||||||
|
},
|
||||||
|
"README.md": {
|
||||||
|
"bytes": 3017,
|
||||||
|
"sha256": "7946dd1317e326a6319c8e6399a6177ce251c6c8bc6c6c9cc7eac1517130d2db"
|
||||||
|
},
|
||||||
|
"verify.py": {
|
||||||
|
"bytes": 8239,
|
||||||
|
"sha256": "13cd285e8c5331bc9b1b7ada1ee7d3ebce4135570e45c047fb56c2f66b558700"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"testedSourceCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"formattedSourceCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||||
|
"runs": {
|
||||||
|
"lifecycle-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"oa-core",
|
||||||
|
"--test",
|
||||||
|
"transition_lifecycle",
|
||||||
|
"--message-format=json",
|
||||||
|
"--",
|
||||||
|
"--nocapture"
|
||||||
|
],
|
||||||
|
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||||
|
"exitCode": 101,
|
||||||
|
"inputCount": 708,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a.json",
|
||||||
|
"archive": "inputs/6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6.zip",
|
||||||
|
"testRows": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"testTotals": [
|
||||||
|
0,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"core-transition-2": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"oa-core",
|
||||||
|
"--test",
|
||||||
|
"transition_lifecycle",
|
||||||
|
"--test",
|
||||||
|
"transition_counterpart_compatibility",
|
||||||
|
"--message-format=json",
|
||||||
|
"--",
|
||||||
|
"--nocapture"
|
||||||
|
],
|
||||||
|
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||||
|
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||||
|
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"testRows": [
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"testTotals": [
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"desktop-workflow-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"oa-desktop",
|
||||||
|
"--bin",
|
||||||
|
"arcadia-studio",
|
||||||
|
"--message-format=json",
|
||||||
|
"workflow::",
|
||||||
|
"--",
|
||||||
|
"--nocapture"
|
||||||
|
],
|
||||||
|
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||||
|
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||||
|
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"testRows": [
|
||||||
|
[
|
||||||
|
25,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1164
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"testTotals": [
|
||||||
|
25,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1164
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"core-workflows-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"oa-core",
|
||||||
|
"--test",
|
||||||
|
"workflows",
|
||||||
|
"--message-format=json",
|
||||||
|
"--",
|
||||||
|
"--nocapture"
|
||||||
|
],
|
||||||
|
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||||
|
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||||
|
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"testRows": [
|
||||||
|
[
|
||||||
|
28,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"testTotals": [
|
||||||
|
28,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clippy-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"clippy",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"--workspace",
|
||||||
|
"--all-targets",
|
||||||
|
"--message-format=json",
|
||||||
|
"--",
|
||||||
|
"-D",
|
||||||
|
"warnings"
|
||||||
|
],
|
||||||
|
"head": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||||
|
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||||
|
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"testRows": [],
|
||||||
|
"testTotals": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"format-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"fmt",
|
||||||
|
"--all",
|
||||||
|
"--",
|
||||||
|
"--check"
|
||||||
|
],
|
||||||
|
"head": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"exitCode": 1,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||||
|
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||||
|
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||||
|
"testRows": [],
|
||||||
|
"testTotals": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"format-2": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"fmt",
|
||||||
|
"--all",
|
||||||
|
"--",
|
||||||
|
"--check"
|
||||||
|
],
|
||||||
|
"head": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json",
|
||||||
|
"archive": "inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip",
|
||||||
|
"inputCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||||
|
"testRows": [],
|
||||||
|
"testTotals": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"windows-build-1": {
|
||||||
|
"command": [
|
||||||
|
"cargo",
|
||||||
|
"build",
|
||||||
|
"--locked",
|
||||||
|
"--offline",
|
||||||
|
"--workspace",
|
||||||
|
"--bins",
|
||||||
|
"--message-format=json"
|
||||||
|
],
|
||||||
|
"head": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||||
|
"exitCode": 0,
|
||||||
|
"inputCount": 710,
|
||||||
|
"kind": "cargo",
|
||||||
|
"inputs": "inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json",
|
||||||
|
"archive": "inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip",
|
||||||
|
"inputCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||||
|
"testRows": [],
|
||||||
|
"testTotals": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"helpers/commit-readbacks.json": {
|
||||||
|
"bytes": 149015,
|
||||||
|
"sha256": "d78b2c4182f2b1db7413cc3eaa36614ade32686e520d6e565bab959b36067838"
|
||||||
|
},
|
||||||
|
"helpers/gate-transition.py": {
|
||||||
|
"bytes": 3500,
|
||||||
|
"sha256": "065a752452e297ba8a000ee69cfae3996b4e6df9b4edbe002f233a66c32e1c04"
|
||||||
|
},
|
||||||
|
"inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip": {
|
||||||
|
"bytes": 2843714,
|
||||||
|
"sha256": "00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802"
|
||||||
|
},
|
||||||
|
"inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json": {
|
||||||
|
"bytes": 118907,
|
||||||
|
"sha256": "65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26"
|
||||||
|
},
|
||||||
|
"inputs/6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6.zip": {
|
||||||
|
"bytes": 2836501,
|
||||||
|
"sha256": "6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6"
|
||||||
|
},
|
||||||
|
"inputs/c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a.json": {
|
||||||
|
"bytes": 118565,
|
||||||
|
"sha256": "c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a"
|
||||||
|
},
|
||||||
|
"inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip": {
|
||||||
|
"bytes": 2843705,
|
||||||
|
"sha256": "e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b"
|
||||||
|
},
|
||||||
|
"inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json": {
|
||||||
|
"bytes": 118907,
|
||||||
|
"sha256": "feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c"
|
||||||
|
},
|
||||||
|
"runs/clippy-1/process.json": {
|
||||||
|
"bytes": 199,
|
||||||
|
"sha256": "e8aa78d9ea3dd2b623d990a9481e70354707e6aa5a591cb7b93dba5180a90aa9"
|
||||||
|
},
|
||||||
|
"runs/clippy-1/result.json": {
|
||||||
|
"bytes": 1801,
|
||||||
|
"sha256": "b6b63d0069a0194d4a1fa91f17d2f6a69555087ec6a9224c322891a3b3047db2"
|
||||||
|
},
|
||||||
|
"runs/clippy-1/started.json": {
|
||||||
|
"bytes": 1405,
|
||||||
|
"sha256": "165e5bb3b433703336dff190ad594ff3438da491d3396398e616dc5bc700e376"
|
||||||
|
},
|
||||||
|
"runs/clippy-1/stderr.log": {
|
||||||
|
"bytes": 456,
|
||||||
|
"sha256": "e3de1e791b11f30e85b682df843625d4ca6556f7eb298d6a1bf310aa5b38aa30"
|
||||||
|
},
|
||||||
|
"runs/clippy-1/stdout.log": {
|
||||||
|
"bytes": 310455,
|
||||||
|
"sha256": "e2692121e9c3536fe9f673de7388c393f23e6fe22dc08bb191a37610392dc0f9"
|
||||||
|
},
|
||||||
|
"runs/core-transition-2/process.json": {
|
||||||
|
"bytes": 263,
|
||||||
|
"sha256": "39b8ea38cfb392a251a3279cb7d4904545702ef10a1fb870b5e866af843b8b3b"
|
||||||
|
},
|
||||||
|
"runs/core-transition-2/result.json": {
|
||||||
|
"bytes": 4667,
|
||||||
|
"sha256": "3ed748bc46d4e7ebf21881ab6ffea85da6f94a36dde0778068b7a0282212a49d"
|
||||||
|
},
|
||||||
|
"runs/core-transition-2/started.json": {
|
||||||
|
"bytes": 1484,
|
||||||
|
"sha256": "3cca432872797dddf8de33e0cf9f931274851acdc8002d5d3599a4fd43ca853d"
|
||||||
|
},
|
||||||
|
"runs/core-transition-2/stderr.log": {
|
||||||
|
"bytes": 489,
|
||||||
|
"sha256": "92fd897c8f9608b06cdca0033a5008a6f00c5c28c4a88b3cba4bcbb3aeb4f986"
|
||||||
|
},
|
||||||
|
"runs/core-transition-2/stdout.log": {
|
||||||
|
"bytes": 43653,
|
||||||
|
"sha256": "1c35f25e869d6151cbb6de224282a05395caa41d27cd8d2452872c651ba08dbf"
|
||||||
|
},
|
||||||
|
"runs/core-workflows-1/process.json": {
|
||||||
|
"bytes": 202,
|
||||||
|
"sha256": "b6a5f0a3c4887888198f10d8e93b2422a1559c4b1214162f6e0954a8f0826828"
|
||||||
|
},
|
||||||
|
"runs/core-workflows-1/result.json": {
|
||||||
|
"bytes": 3128,
|
||||||
|
"sha256": "89dccdd6655ffb2cd792d25ce76e7da258efbd884dc4ab47d0ba48f8aa23ec6e"
|
||||||
|
},
|
||||||
|
"runs/core-workflows-1/started.json": {
|
||||||
|
"bytes": 1413,
|
||||||
|
"sha256": "a33e662e9743d650f57401b7276b696f97d97b8c5a916c0c783465b02eb3fc3c"
|
||||||
|
},
|
||||||
|
"runs/core-workflows-1/stderr.log": {
|
||||||
|
"bytes": 271,
|
||||||
|
"sha256": "bb006c1bb74a6ce4d938f3eb0db36bb9179395fbc3a1b42a7dfb3edad691ca46"
|
||||||
|
},
|
||||||
|
"runs/core-workflows-1/stdout.log": {
|
||||||
|
"bytes": 43869,
|
||||||
|
"sha256": "a51bf33e8e8767c73efcdc176f615801c63ff3ec9e369586f640fc634d60c528"
|
||||||
|
},
|
||||||
|
"runs/desktop-workflow-1/process.json": {
|
||||||
|
"bytes": 223,
|
||||||
|
"sha256": "3360e8316f62834c74da7c08742e9f2e62856b7a43974f3fd311d95074bfe4cd"
|
||||||
|
},
|
||||||
|
"runs/desktop-workflow-1/result.json": {
|
||||||
|
"bytes": 3170,
|
||||||
|
"sha256": "a9626e778ea9626fe76080c95faeac312455c7fd6b7511f1b5887da07bb6676a"
|
||||||
|
},
|
||||||
|
"runs/desktop-workflow-1/started.json": {
|
||||||
|
"bytes": 1439,
|
||||||
|
"sha256": "cf9cee18e2a6cd06d1ed12306eecb79a056f2a85ca3913557a219302f9b650c5"
|
||||||
|
},
|
||||||
|
"runs/desktop-workflow-1/stderr.log": {
|
||||||
|
"bytes": 539,
|
||||||
|
"sha256": "84734875bfd88145ebb582986937ccaf5fad3fc32ca0b50d03f57554195742e5"
|
||||||
|
},
|
||||||
|
"runs/desktop-workflow-1/stdout.log": {
|
||||||
|
"bytes": 296022,
|
||||||
|
"sha256": "0ec72412bc1b3976e0c7470853a06efdf99f051aace8dad540fb3afc3a99bae2"
|
||||||
|
},
|
||||||
|
"runs/format-1/process.json": {
|
||||||
|
"bytes": 116,
|
||||||
|
"sha256": "3ed4ed5a510a0b116a3c0fd2baf3e4db7ff7de66b7df0a0464cc098841fff115"
|
||||||
|
},
|
||||||
|
"runs/format-1/result.json": {
|
||||||
|
"bytes": 1688,
|
||||||
|
"sha256": "1d5eac33f2174bcb464d89bc858483affe3c73d5016d19db7d0a32412d9ea475"
|
||||||
|
},
|
||||||
|
"runs/format-1/started.json": {
|
||||||
|
"bytes": 1297,
|
||||||
|
"sha256": "60fcb6b008530a620534fba89c0cb8e143d423fab2dd2fe08a19a72d21eab904"
|
||||||
|
},
|
||||||
|
"runs/format-1/stderr.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"runs/format-1/stdout.log": {
|
||||||
|
"bytes": 538,
|
||||||
|
"sha256": "e6eac9f78ecb50fdaa288974ac26df928f6aa437d293d5aaf9ac60190394b88f"
|
||||||
|
},
|
||||||
|
"runs/format-2/process.json": {
|
||||||
|
"bytes": 116,
|
||||||
|
"sha256": "20c7c2177f85bb157b2cfff08b26c447bebed42eb11e0e7f6b43c6664c4a494d"
|
||||||
|
},
|
||||||
|
"runs/format-2/result.json": {
|
||||||
|
"bytes": 1686,
|
||||||
|
"sha256": "b220a0401f478cde47f2e1386a311de58e11bf7937a722490ac630ab223ab279"
|
||||||
|
},
|
||||||
|
"runs/format-2/started.json": {
|
||||||
|
"bytes": 1297,
|
||||||
|
"sha256": "6d87fb899116fed854f2c2f2c358e0810103f462aa2f3f225feaf89b0855af6d"
|
||||||
|
},
|
||||||
|
"runs/format-2/stderr.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"runs/format-2/stdout.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"runs/lifecycle-1/process.json": {
|
||||||
|
"bytes": 213,
|
||||||
|
"sha256": "ef6976e24b19e881d5d435f5dd2d7a54fd228f8013be6cd4a97110a5078f4494"
|
||||||
|
},
|
||||||
|
"runs/lifecycle-1/result.json": {
|
||||||
|
"bytes": 3188,
|
||||||
|
"sha256": "149a0f3c8747b3c5227916aa88e4b94b77415b0c8d765445e92bfdd201069fb0"
|
||||||
|
},
|
||||||
|
"runs/lifecycle-1/started.json": {
|
||||||
|
"bytes": 1425,
|
||||||
|
"sha256": "3e3458c4227da8bfbc4d4b6244fe0510687b8c98c2167a4802b2b691431565fa"
|
||||||
|
},
|
||||||
|
"runs/lifecycle-1/stderr.log": {
|
||||||
|
"bytes": 21458,
|
||||||
|
"sha256": "2e13355db62b04032baa7007f7d7d691eadaad974f4fde8e0bf0ddd4a8b12027"
|
||||||
|
},
|
||||||
|
"runs/lifecycle-1/stdout.log": {
|
||||||
|
"bytes": 42779,
|
||||||
|
"sha256": "9d545eaba4c279233a344b1e8bc845d0df4dbce5de5c4af9de14d288d3d8d7d8"
|
||||||
|
},
|
||||||
|
"runs/windows-build-1/process.json": {
|
||||||
|
"bytes": 167,
|
||||||
|
"sha256": "5292a76450540df9e145cd21cc20118b310ed306126da85e893d0c9c221a8564"
|
||||||
|
},
|
||||||
|
"runs/windows-build-1/result.json": {
|
||||||
|
"bytes": 5583,
|
||||||
|
"sha256": "91b38045319adc1fb9aa0e51d45d79a4be99d2de9aec08a47f73915e3ac9dd79"
|
||||||
|
},
|
||||||
|
"runs/windows-build-1/started.json": {
|
||||||
|
"bytes": 1358,
|
||||||
|
"sha256": "826453a44cd915065caf1db9c0cc547dca0fa5a2b56594e4a72146b19ffe9e8c"
|
||||||
|
},
|
||||||
|
"runs/windows-build-1/stderr.log": {
|
||||||
|
"bytes": 276,
|
||||||
|
"sha256": "0a035e700f490f73889da441e363052b34f33b3de5690c30eb1e6c2666c75bc6"
|
||||||
|
},
|
||||||
|
"runs/windows-build-1/stdout.log": {
|
||||||
|
"bytes": 295257,
|
||||||
|
"sha256": "6ecb53aef4a76dda4174213dd5ba9b03aabc1434577c3141c4ed3e71adf068fa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formattingInputChanges": {
|
||||||
|
"crates/oa-desktop/src/diagram/focus.rs": {
|
||||||
|
"before": {
|
||||||
|
"bytes": 9197,
|
||||||
|
"sha256": "b88a7c43123b08cf6228d8e32257f6ed7fe361f7278f427ec138e233e1ea6ccf"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"bytes": 9276,
|
||||||
|
"sha256": "44e7c13f8f56deb21c62cb7757a412ec8b790344387cebdee983c3a28b6b0160"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Read-only, Python-standard-library verification. Never executes archived code."""
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
def require(ok, message):
|
||||||
|
if not ok:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
def digest(data):
|
||||||
|
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||||
|
|
||||||
|
def members(z, expected):
|
||||||
|
names = z.namelist()
|
||||||
|
require(len(names) == len(set(names)), 'duplicate ZIP members')
|
||||||
|
require(set(names) == set(expected), 'archive membership mismatch')
|
||||||
|
require(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts
|
||||||
|
and '\\' not in n and ':' not in n for n in names), 'unsafe member')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
inventory = json.loads((ROOT / 'files.json').read_text())
|
||||||
|
for name, expected in inventory.items():
|
||||||
|
require(digest((ROOT / name).read_bytes()) == expected, 'external file hash: ' + name)
|
||||||
|
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||||
|
with zipfile.ZipFile(ROOT / 'evidence.zip') as z:
|
||||||
|
members(z, manifest['members'])
|
||||||
|
for name, expected in manifest['members'].items():
|
||||||
|
require(digest(z.read(name)) == expected, 'member hash: ' + name)
|
||||||
|
def raw(name): return z.read(name)
|
||||||
|
def obj(name): return json.loads(raw(name))
|
||||||
|
checked = set()
|
||||||
|
for run, spec in manifest['runs'].items():
|
||||||
|
if spec['kind'] != 'cargo':
|
||||||
|
continue
|
||||||
|
prefix = 'runs/' + run + '/'
|
||||||
|
start, result, process = (obj(prefix + n + '.json') for n in ('started', 'result', 'process'))
|
||||||
|
require(all(result[k] == v for k, v in start.items()), run + ': start/result relationship')
|
||||||
|
require(result['command'] == spec['command'] == process['command'], run + ': command')
|
||||||
|
require(result['head'] == spec['head'], run + ': HEAD')
|
||||||
|
require(result['exitCode'] == spec['exitCode'] and result['inputsUnchanged'] is True, run + ': outcome')
|
||||||
|
require(datetime.fromisoformat(result['finish']) >= datetime.fromisoformat(start['start']), run + ': times')
|
||||||
|
require(result['seconds'] >= 0, run + ': duration')
|
||||||
|
require(process['pid']>0 and datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']), run+': process observation')
|
||||||
|
require(result['helper'] == digest(raw('helpers/gate-transition.py')), run + ': helper')
|
||||||
|
for field, file in [('inputs', spec['inputs']), ('archive', spec['archive']), ('stdout', prefix+'stdout.log'), ('stderr', prefix+'stderr.log')]:
|
||||||
|
require(result[field] == digest(raw(file)), run + ': receipt ' + field)
|
||||||
|
inputs = obj(spec['inputs'])
|
||||||
|
require(len(inputs) == result['inputCount'] == spec['inputCount'], run + ': input count')
|
||||||
|
require(sum(p.startswith('crates/vendor/iced_widget/') for p in inputs) == 71, run + ': vendor count')
|
||||||
|
pair = (spec['inputs'], spec['archive'])
|
||||||
|
if pair not in checked:
|
||||||
|
with zipfile.ZipFile(io.BytesIO(raw(spec['archive']))) as source:
|
||||||
|
members(source, inputs)
|
||||||
|
for name, expected in inputs.items():
|
||||||
|
require(digest(source.read(name)) == expected, run + ': source hash ' + name)
|
||||||
|
checked.add(pair)
|
||||||
|
stdout = raw(prefix+'stdout.log').decode('utf-8', errors='replace')
|
||||||
|
rows = [[int(x) for x in row] for row in re.findall(r'test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', stdout)]
|
||||||
|
require(rows == spec['testRows'], run + ': exact test summaries')
|
||||||
|
require([sum(row[i] for row in rows) for i in range(5)] == spec['testTotals'], run + ': test totals')
|
||||||
|
artifacts = []
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
try: item = json.loads(line)
|
||||||
|
except json.JSONDecodeError: continue
|
||||||
|
if isinstance(item, dict) and item.get('reason') == 'compiler-artifact' and item.get('executable'):
|
||||||
|
artifacts.append(item)
|
||||||
|
require(artifacts == [a['cargo'] for a in result['artifacts']], run + ': artifact receipt relationship')
|
||||||
|
print(run, 'exit', result['exitCode'], 'test totals', spec['testTotals'])
|
||||||
|
readbacks=obj('helpers/commit-readbacks.json')
|
||||||
|
require(set(readbacks)=={spec['inputCommit'] for spec in manifest['runs'].values() if 'inputCommit' in spec}, 'readback commit coverage')
|
||||||
|
base_inputs=obj(manifest['runs']['core-transition-2']['inputs'])
|
||||||
|
for commit,readback in readbacks.items():
|
||||||
|
require(readback['status']=='PASS' and readback['commit']==commit, 'commit readback status')
|
||||||
|
specs=[spec for spec in manifest['runs'].values() if spec.get('inputCommit')==commit]
|
||||||
|
success=specs[0];inputs=obj(success['inputs'])
|
||||||
|
require(all(obj(spec['inputs'])==inputs for spec in specs), 'per-commit input equality')
|
||||||
|
require(readback['inputs']==digest(raw(success['inputs'])) and len(inputs)==readback['inputCount']==710, '710 readback')
|
||||||
|
require(set(readback['gitBlobs'])==set(inputs), 'readback blob inventory')
|
||||||
|
with zipfile.ZipFile(io.BytesIO(raw(success['archive']))) as source:
|
||||||
|
for name,oid in readback['gitBlobs'].items():
|
||||||
|
b=source.read(name)
|
||||||
|
require(hashlib.sha1(b'blob '+str(len(b)).encode()+b'\0'+b).hexdigest()==oid, 'readback blob '+name)
|
||||||
|
if commit==manifest['formattedSourceCommit']:
|
||||||
|
changed={p:{'before':base_inputs.get(p),'after':inputs.get(p)} for p in sorted(set(base_inputs)|set(inputs)) if base_inputs.get(p)!=inputs.get(p)}
|
||||||
|
require(changed==manifest['formattingInputChanges'] and set(changed)=={'crates/oa-desktop/src/diagram/focus.rs'}, 'one-file formatting input difference')
|
||||||
|
with zipfile.ZipFile(io.BytesIO(raw(manifest['runs']['core-transition-2']['archive']))) as old, zipfile.ZipFile(io.BytesIO(raw(success['archive']))) as new:
|
||||||
|
path='crates/oa-desktop/src/diagram/focus.rs'
|
||||||
|
before=old.read(path);after=new.read(path)
|
||||||
|
compact=b' != (self.context.document, self.context.diagram, self.context.epoch)'
|
||||||
|
expanded=b' != (\n self.context.document,\n self.context.diagram,\n self.context.epoch,\n )'
|
||||||
|
require(before.count(compact)==1 and before.replace(compact,expanded,1)==after, 'exact tuple-format-only delta')
|
||||||
|
for run in ('core-transition-2','desktop-workflow-1','core-workflows-1','clippy-1','format-1'):
|
||||||
|
if run in manifest['runs']:require(manifest['runs'][run]['inputCommit']==manifest['testedSourceCommit'], 'earlier execution source scope '+run)
|
||||||
|
for run in ('format-2','windows-build-1'):
|
||||||
|
if run in manifest['runs']:require(manifest['runs'][run]['inputCommit']==manifest['formattedSourceCommit'], 'formatted source scope '+run)
|
||||||
|
if 'format-2' in manifest['runs']:
|
||||||
|
require(not raw('runs/format-2/stdout.log') and not raw('runs/format-2/stderr.log'), 'format2 empty success streams')
|
||||||
|
if 'windows-build-1' in manifest['runs']:
|
||||||
|
build=obj('runs/windows-build-1/result.json')
|
||||||
|
require(build['exitCode']==0 and len(build['artifacts'])==3, 'Windows build outcome')
|
||||||
|
require({a['cargo']['target']['name'] for a in build['artifacts']}=={'arcadia','arcadia-server','arcadia-studio'}, 'Windows build executable membership')
|
||||||
|
require(obj(manifest['runs']['lifecycle-1']['inputs'])!=base_inputs, 'initial failure remains distinct')
|
||||||
|
print('PASS:',len(manifest['runs']),'closed runs;',len(manifest['members']),'members;',len(checked),'input pairs;710 successful input blob bindings.')
|
||||||
|
print('No archived code executed. Local commit-readback summary is not authenticated commit membership.')
|
||||||
|
|
||||||
|
if __name__=='__main__':main()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Transition picker: native comparison evidence
|
||||||
|
|
||||||
|
Run `python verify.py` using Python 3.10 or newer. This five-file package supports
|
||||||
|
read-only verification using the standard library. It does not execute archived
|
||||||
|
helpers, binaries, Git commands or network calls. Outer text is UTF-8 LF before
|
||||||
|
hashing; archived files retain exact original bytes.
|
||||||
|
|
||||||
|
All three trials, launch helpers, copied release1 inputs and original frames are
|
||||||
|
preserved. Baseline picker-baseline-1 used the f714a25 GUI and closed with exit 0.
|
||||||
|
Corrected picker-corrected-1 exited immediately with 3221225781 and empty logs;
|
||||||
|
the operator attributes this to omitted runtime environment setup. That cause
|
||||||
|
is not independently proven by empty logs. Corrected picker-corrected-2 used
|
||||||
|
the d46854c GUI, ran from 15:22:22 to 15:27:13 UTC, and closed with exit 0 and
|
||||||
|
empty logs. Its frame 02 is a loading state, not proof that loading finished.
|
||||||
|
|
||||||
|
The capture-snippet.js source was retained from a previous persistent session.
|
||||||
|
It was not pinned in these trials' launch receipts. Archiving it now does not
|
||||||
|
establish the exact capture function executed during the trials. Frame JSON
|
||||||
|
records timestamps, a window/application path and image references; it is not
|
||||||
|
an action invocation log or independent PID attestation. No frame is fabricated,
|
||||||
|
renumbered, cropped or omitted, including auxiliary dialog images.
|
||||||
|
|
||||||
|
Both exact GUI build result receipts are included. Full build source evidence
|
||||||
|
remains external: the sibling transition-lifecycle-20260913 Cargo package at
|
||||||
|
commit fe43730bec196cc6ebbc06dcf827169f04c120f0 covers the corrected d46854c
|
||||||
|
build, and the ordinary-text-native-20260913 package at
|
||||||
|
commit 3f9e6a18af96b5cd10940da4093be8da28b9c835 includes the baseline f714a25
|
||||||
|
build/source evidence. The manifest pins those related evidence ZIPs. This is
|
||||||
|
not a self-contained source or runtime distribution; the verifier does not
|
||||||
|
require those external packages for its local receipt/frame checks.
|
||||||
|
|
||||||
|
Verification checks exact safe ZIP membership/hashes, launch helper pins,
|
||||||
|
start/process/result timing, expected exit outcomes, empty streams, GUI binary
|
||||||
|
and build receipt relationships, identical copied input membership/bytes across
|
||||||
|
all trials, and complete frame/image references within process intervals. Binary
|
||||||
|
hashes are receipt observations; binaries are excluded. Screenshots require
|
||||||
|
separate interpretation and do not prove action execution, complete model
|
||||||
|
equality, persistence or full usability. The included independent-review.md is
|
||||||
|
a parent-consolidated record of separate human-readable review observations.
|
||||||
|
No credentials or whole user profile are
|
||||||
|
packaged; raw receipts retain original runtime path strings.
|
||||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"evidence.zip": {
|
||||||
|
"bytes": 3412321,
|
||||||
|
"sha256": "c801eda9e33cd796a3594e73c34d6d29691597552a68307d2dc5a5e113c1e470"
|
||||||
|
},
|
||||||
|
"manifest.json": {
|
||||||
|
"bytes": 26082,
|
||||||
|
"sha256": "f80092a483a00e17c3ffcdbf2dd71480ed6ead4d003248de2f9ef708104cfba1"
|
||||||
|
},
|
||||||
|
"README.md": {
|
||||||
|
"bytes": 2673,
|
||||||
|
"sha256": "86251ca15d4950cd0b037ba05b63433877608c1d0d6424fedef8ff4e34340141"
|
||||||
|
},
|
||||||
|
"verify.py": {
|
||||||
|
"bytes": 5143,
|
||||||
|
"sha256": "23c5040f91f29e243e65ad31b4f8f82f7ad1d692efd862b1bc87450c31e5ed5f"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"runs": {
|
||||||
|
"picker-baseline-1": {
|
||||||
|
"helper": "native/run-picker-baseline.py",
|
||||||
|
"build": "build/baseline-result.json",
|
||||||
|
"exitCode": 0,
|
||||||
|
"captures": [
|
||||||
|
"native/picker-baseline-1/01-start.json",
|
||||||
|
"native/picker-baseline-1/02-open-pending.json",
|
||||||
|
"native/picker-baseline-1/03-open-dialog.json",
|
||||||
|
"native/picker-baseline-1/04-teaching-path.json",
|
||||||
|
"native/picker-baseline-1/05-teaching-opened.json",
|
||||||
|
"native/picker-baseline-1/06-teaching-ready.json",
|
||||||
|
"native/picker-baseline-1/07-source-selected.json",
|
||||||
|
"native/picker-baseline-1/08-source-actions.json",
|
||||||
|
"native/picker-baseline-1/09-transition-dialog.json",
|
||||||
|
"native/picker-baseline-1/10-unfiltered-counterparts.json",
|
||||||
|
"native/picker-baseline-1/11-incompatible-selection.json",
|
||||||
|
"native/picker-baseline-1/12-cancel-preserved.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"picker-corrected-1": {
|
||||||
|
"helper": "native/run-picker-corrected.py",
|
||||||
|
"build": "build/corrected-result.json",
|
||||||
|
"exitCode": 3221225781,
|
||||||
|
"captures": []
|
||||||
|
},
|
||||||
|
"picker-corrected-2": {
|
||||||
|
"helper": "native/run-picker-corrected-2.py",
|
||||||
|
"build": "build/corrected-result.json",
|
||||||
|
"exitCode": 0,
|
||||||
|
"captures": [
|
||||||
|
"native/picker-corrected-2/01-opened.json",
|
||||||
|
"native/picker-corrected-2/02-teaching-reopened.json",
|
||||||
|
"native/picker-corrected-2/03-loaded.json",
|
||||||
|
"native/picker-corrected-2/04-source-selected.json",
|
||||||
|
"native/picker-corrected-2/05-actions.json",
|
||||||
|
"native/picker-corrected-2/06-transition-dialog.json",
|
||||||
|
"native/picker-corrected-2/07-compatible-menu.json",
|
||||||
|
"native/picker-corrected-2/08-menu-dismissed.json",
|
||||||
|
"native/picker-corrected-2/09-search-focus.json",
|
||||||
|
"native/picker-corrected-2/10-alias-query.json",
|
||||||
|
"native/picker-corrected-2/11-filtered-menu.json",
|
||||||
|
"native/picker-corrected-2/12-existing-selection.json",
|
||||||
|
"native/picker-corrected-2/13-refocus-search.json",
|
||||||
|
"native/picker-corrected-2/14-query-selected.json",
|
||||||
|
"native/picker-corrected-2/15-no-results-selection-retained.json",
|
||||||
|
"native/picker-corrected-2/16-parent-search-focus.json",
|
||||||
|
"native/picker-corrected-2/17-independent-card-queries.json",
|
||||||
|
"native/picker-corrected-2/18-field-review.json",
|
||||||
|
"native/picker-corrected-2/19-keep-target-reviewed.json",
|
||||||
|
"native/picker-corrected-2/20-reviewed-search-focus.json",
|
||||||
|
"native/picker-corrected-2/21-reviewed-query-selected.json",
|
||||||
|
"native/picker-corrected-2/22-cleared-search-review-retained.json",
|
||||||
|
"native/picker-corrected-2/23-cancel-restores-saved.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"build/baseline-result.json": {
|
||||||
|
"bytes": 5532,
|
||||||
|
"sha256": "5e9e227c66a4b71ade734b8ebeb12070b8a5c64a9a6cf9d8d0b6d70379d4ffc2"
|
||||||
|
},
|
||||||
|
"build/corrected-result.json": {
|
||||||
|
"bytes": 5583,
|
||||||
|
"sha256": "91b38045319adc1fb9aa0e51d45d79a4be99d2de9aec08a47f73915e3ac9dd79"
|
||||||
|
},
|
||||||
|
"native/capture-snippet.js": {
|
||||||
|
"bytes": 1579,
|
||||||
|
"sha256": "55279ed080289ccf5ba482cb24b5a807669a9360012e2d87f794d4465227cb2c"
|
||||||
|
},
|
||||||
|
"native/independent-review.md": {
|
||||||
|
"bytes": 1898,
|
||||||
|
"sha256": "c59d2ff78635f94f3f225c27999b69d24c11977c89026fc8a7331ddbd160c8c6"
|
||||||
|
},
|
||||||
|
"native/input/release1/README.md": {
|
||||||
|
"bytes": 11137,
|
||||||
|
"sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5"
|
||||||
|
},
|
||||||
|
"native/input/release1/evidence/repair-check.md": {
|
||||||
|
"bytes": 1419,
|
||||||
|
"sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5"
|
||||||
|
},
|
||||||
|
"native/input/release1/expected-diagnostics.json": {
|
||||||
|
"bytes": 288,
|
||||||
|
"sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e"
|
||||||
|
},
|
||||||
|
"native/input/release1/model.json": {
|
||||||
|
"bytes": 134520,
|
||||||
|
"sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727"
|
||||||
|
},
|
||||||
|
"native/input/release1/model.rs": {
|
||||||
|
"bytes": 28762,
|
||||||
|
"sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/activities/activity_oa_restore_work.arcadia": {
|
||||||
|
"bytes": 1130,
|
||||||
|
"sha256": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/process_oa_assessment_procedure.arcadia": {
|
||||||
|
"bytes": 550,
|
||||||
|
"sha256": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/process_oa_field_procedure.arcadia": {
|
||||||
|
"bytes": 1178,
|
||||||
|
"sha256": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/process_oa_response.arcadia": {
|
||||||
|
"bytes": 2560,
|
||||||
|
"sha256": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/scenario_oa_conversation.arcadia": {
|
||||||
|
"bytes": 1515,
|
||||||
|
"sha256": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/scenario_oa_full_scenario.arcadia": {
|
||||||
|
"bytes": 1121,
|
||||||
|
"sha256": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/behavior/state_machine_oa_monitor.arcadia": {
|
||||||
|
"bytes": 1358,
|
||||||
|
"sha256": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/capabilities.arcadia": {
|
||||||
|
"bytes": 630,
|
||||||
|
"sha256": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/model.arcadia": {
|
||||||
|
"bytes": 531,
|
||||||
|
"sha256": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/participants.arcadia": {
|
||||||
|
"bytes": 1580,
|
||||||
|
"sha256": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819"
|
||||||
|
},
|
||||||
|
"native/input/release1/oa/types.arcadia": {
|
||||||
|
"bytes": 554,
|
||||||
|
"sha256": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d"
|
||||||
|
},
|
||||||
|
"native/input/release1/project.arcadia": {
|
||||||
|
"bytes": 1958,
|
||||||
|
"sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/process_sa_assessment_procedure.arcadia": {
|
||||||
|
"bytes": 559,
|
||||||
|
"sha256": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/process_sa_field_procedure.arcadia": {
|
||||||
|
"bytes": 1187,
|
||||||
|
"sha256": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/process_sa_response.arcadia": {
|
||||||
|
"bytes": 2569,
|
||||||
|
"sha256": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/scenario_sa_conversation.arcadia": {
|
||||||
|
"bytes": 1847,
|
||||||
|
"sha256": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/scenario_sa_full_scenario.arcadia": {
|
||||||
|
"bytes": 1116,
|
||||||
|
"sha256": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/behavior/state_machine_sa_monitor.arcadia": {
|
||||||
|
"bytes": 1358,
|
||||||
|
"sha256": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/capabilities.arcadia": {
|
||||||
|
"bytes": 630,
|
||||||
|
"sha256": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/functions/function_sa_audit_thresholds.arcadia": {
|
||||||
|
"bytes": 280,
|
||||||
|
"sha256": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/functions/function_sa_restore_work.arcadia": {
|
||||||
|
"bytes": 2251,
|
||||||
|
"sha256": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/model.arcadia": {
|
||||||
|
"bytes": 531,
|
||||||
|
"sha256": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/participants.arcadia": {
|
||||||
|
"bytes": 3604,
|
||||||
|
"sha256": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84"
|
||||||
|
},
|
||||||
|
"native/input/release1/sa/types.arcadia": {
|
||||||
|
"bytes": 903,
|
||||||
|
"sha256": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064"
|
||||||
|
},
|
||||||
|
"native/input/release1/traceability.arcadia": {
|
||||||
|
"bytes": 28041,
|
||||||
|
"sha256": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116"
|
||||||
|
},
|
||||||
|
"native/input/release1/transition-before.json": {
|
||||||
|
"bytes": 35801,
|
||||||
|
"sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d"
|
||||||
|
},
|
||||||
|
"native/input/release1/transition-request.json": {
|
||||||
|
"bytes": 4276,
|
||||||
|
"sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a"
|
||||||
|
},
|
||||||
|
"native/input/release1/views/diagram_oa_allocation_view.arcadia": {
|
||||||
|
"bytes": 1786,
|
||||||
|
"sha256": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624"
|
||||||
|
},
|
||||||
|
"native/input/release1/views/diagram_sa_allocation_view.arcadia": {
|
||||||
|
"bytes": 1797,
|
||||||
|
"sha256": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6"
|
||||||
|
},
|
||||||
|
"native/input/release1/views/diagram_sa_capability_view.arcadia": {
|
||||||
|
"bytes": 1286,
|
||||||
|
"sha256": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2"
|
||||||
|
},
|
||||||
|
"native/input/release1/views/diagram_sa_service_view.arcadia": {
|
||||||
|
"bytes": 1960,
|
||||||
|
"sha256": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c"
|
||||||
|
},
|
||||||
|
"native/input/release1/views/diagram_sa_state_view.arcadia": {
|
||||||
|
"bytes": 1576,
|
||||||
|
"sha256": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/01-start-0.jpg": {
|
||||||
|
"bytes": 116509,
|
||||||
|
"sha256": "d5c669575ffa4f8c0fe2027d39ad0b655bf6557a6315a9f27ef4cc81e3e4a9d8"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/01-start.json": {
|
||||||
|
"bytes": 920,
|
||||||
|
"sha256": "f8ac2638390d3dc2a4bf7d64a9c6b8988c59ef377b53d7067fc41c1d30b5f091"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/02-open-pending-0.jpg": {
|
||||||
|
"bytes": 115140,
|
||||||
|
"sha256": "d06c8963b5b752f6248ecead582c317cadfcf699955f7ecd629d6052ef157f70"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/02-open-pending.json": {
|
||||||
|
"bytes": 927,
|
||||||
|
"sha256": "f326b85978dfa21cc2fd2e4454ebfee6a6d856e439de5ab41a8cec7c2998fb23"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/03-open-dialog-0.jpg": {
|
||||||
|
"bytes": 674,
|
||||||
|
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/03-open-dialog-1.jpg": {
|
||||||
|
"bytes": 52573,
|
||||||
|
"sha256": "23ab19c5286f26ead42aec7e3690fda96cad512518243593f37e26579eb08ef6"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/03-open-dialog.json": {
|
||||||
|
"bytes": 4915,
|
||||||
|
"sha256": "b1d4e6c59104b3d4d36210e855750cc7fc445a8d05e12f3c63cc43196df9df95"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/04-teaching-path-0.jpg": {
|
||||||
|
"bytes": 674,
|
||||||
|
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/04-teaching-path-1.jpg": {
|
||||||
|
"bytes": 55565,
|
||||||
|
"sha256": "569f70974c0347c3accecb412de468751fc2294ba5f577b572765335a1cbca7c"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/04-teaching-path.json": {
|
||||||
|
"bytes": 4919,
|
||||||
|
"sha256": "e7ef7212f637bff8125fa86892cc0b610a0c29a64c3715baeab12a6b36992620"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/05-teaching-opened-0.jpg": {
|
||||||
|
"bytes": 115272,
|
||||||
|
"sha256": "c74bba329bca67a25c83352467842dadb8d393b17785b87929a4b7f10a8a2ae2"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/05-teaching-opened.json": {
|
||||||
|
"bytes": 930,
|
||||||
|
"sha256": "dacbdbc29a7a61de78eb987069ed8e31f5818cdf6269b34836927109badd87ec"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/06-teaching-ready-0.jpg": {
|
||||||
|
"bytes": 148650,
|
||||||
|
"sha256": "89957f78f5744c35f600b09f55dfbe52fddc4aac1b76ea1e820d1063b15c2833"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/06-teaching-ready.json": {
|
||||||
|
"bytes": 1114,
|
||||||
|
"sha256": "4a708684d4469e4b5612ffe10216ad81d492eaf28730f0e11a4f5c4dcd30c903"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/07-source-selected-0.jpg": {
|
||||||
|
"bytes": 146555,
|
||||||
|
"sha256": "ca53af5035524280f3144309a12c931193115e025bec6fe5b6b1799de3c8c949"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/07-source-selected.json": {
|
||||||
|
"bytes": 1115,
|
||||||
|
"sha256": "4a725e4db48f2f8db7bc41a37ce69d49b2cd01a9b046791fee57b1bb580183a0"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/08-source-actions-0.jpg": {
|
||||||
|
"bytes": 153686,
|
||||||
|
"sha256": "367876958aa58ed08c913d33530ba8da663d247ee3b0b9503625e7d3f945480c"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/08-source-actions.json": {
|
||||||
|
"bytes": 1114,
|
||||||
|
"sha256": "4d244ccced055bdff280bbf99269554173fe85c816d15f9dae03aca40ad64ece"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/09-transition-dialog-0.jpg": {
|
||||||
|
"bytes": 147596,
|
||||||
|
"sha256": "f197b031669070f731e354bca993ffc77ff43966349f4699d69e361287f2a704"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/09-transition-dialog.json": {
|
||||||
|
"bytes": 1127,
|
||||||
|
"sha256": "3de33b54e5b15a598a901103acfa48650170d94724c5f5708b90d8fac6bcf6dd"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/10-unfiltered-counterparts-0.jpg": {
|
||||||
|
"bytes": 138703,
|
||||||
|
"sha256": "a3c8464125657ea37550eb46a269f9a0c0646001ba387a8188b987ba74c6a270"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/10-unfiltered-counterparts.json": {
|
||||||
|
"bytes": 1133,
|
||||||
|
"sha256": "977c0bbc366937ce2242b871b12f350cb56af4661f09c214e4945296195f4c55"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/11-incompatible-selection-0.jpg": {
|
||||||
|
"bytes": 144348,
|
||||||
|
"sha256": "53884020ceb9c0b416386ceaa1e06801614ac132760e58c7c3320c820c251d04"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/11-incompatible-selection.json": {
|
||||||
|
"bytes": 1132,
|
||||||
|
"sha256": "94f6814f61b75dc4ad9f9f14babed492865079c45fa5de17e6fc9c8a4fabc349"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/12-cancel-preserved-0.jpg": {
|
||||||
|
"bytes": 145507,
|
||||||
|
"sha256": "d3db7643267cdc532f630c972d6c53823679326910e482179d447efad6289a61"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/12-cancel-preserved.json": {
|
||||||
|
"bytes": 1116,
|
||||||
|
"sha256": "259769191f79cd381e2c8be3230b0da8912d35ef0ce5c362b2dd4f1189773647"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/inputs.json": {
|
||||||
|
"bytes": 5843,
|
||||||
|
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/process.json": {
|
||||||
|
"bytes": 61,
|
||||||
|
"sha256": "a99574647fda39a1529b9733794064c5214005df5a5ea821f7e0d0e61e4136a9"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/result.json": {
|
||||||
|
"bytes": 1247,
|
||||||
|
"sha256": "a4cfe1faa8edc3ae8a7eaef9bb0714b357ecbaf91ad5a5bc6e787fea7fe013d3"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/started.json": {
|
||||||
|
"bytes": 780,
|
||||||
|
"sha256": "b059824ca0e7b1a528ec8ececbe1b8d37335bcd5a34de921c3c9b6fda448acbe"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/stderr.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/picker-baseline-1/stdout.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/inputs.json": {
|
||||||
|
"bytes": 5843,
|
||||||
|
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/process.json": {
|
||||||
|
"bytes": 61,
|
||||||
|
"sha256": "749d13d70bb8c9d7b9ad35a11e63980562b5e2140a1325454d61c6fd2cc5ef7d"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/result.json": {
|
||||||
|
"bytes": 1256,
|
||||||
|
"sha256": "98d9e77ed93d56f2fb79f21c70ba5dceb012605f491fa6d704bd4f2f3bb2f8ea"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/started.json": {
|
||||||
|
"bytes": 780,
|
||||||
|
"sha256": "9260aaee5e9e733eeb38a334a96881a1ed796149d9ece8438e0682f1990a8404"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/stderr.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-1/stdout.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/01-opened-0.jpg": {
|
||||||
|
"bytes": 118158,
|
||||||
|
"sha256": "84db89be21fabc31958b4521587ff708619a4a4cbf0ded6fe8b2c58e94a26a68"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/01-opened.json": {
|
||||||
|
"bytes": 922,
|
||||||
|
"sha256": "44cb8a61c47648bdcdfdc278ebfd43e45942eb5211e8f77624b9982186c89c25"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/02-teaching-reopened-0.jpg": {
|
||||||
|
"bytes": 117821,
|
||||||
|
"sha256": "aa6fd6a99c2f808c6b55c734c83b0aa8b2fbd1ce03a14b02f609e5c84275bc66"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/02-teaching-reopened.json": {
|
||||||
|
"bytes": 933,
|
||||||
|
"sha256": "b8a9dd0a0462f9b2703bfd9d88823b9ca00efba304f0f1cd923d637e8a01b6da"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/03-loaded-0.jpg": {
|
||||||
|
"bytes": 148546,
|
||||||
|
"sha256": "dfe2700568a0e2f688c8c197224d75d7c15dfde5f060dfe2aba09631837687ef"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/03-loaded.json": {
|
||||||
|
"bytes": 1107,
|
||||||
|
"sha256": "e8b18a5e75b52649dd87118f70c55be4b0e9c7e2ee00c2a3db730376a6c36aa4"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/04-source-selected-0.jpg": {
|
||||||
|
"bytes": 146472,
|
||||||
|
"sha256": "74baf9f9059b4f0741d500a1f6d84ccb91474ff48de7466a03736e5decc868ca"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/04-source-selected.json": {
|
||||||
|
"bytes": 1116,
|
||||||
|
"sha256": "45f7cfeab95e2ca2ca2ed2cd45bea9a269c1c6a1515dbe9cbcb3087fb2917c55"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/05-actions-0.jpg": {
|
||||||
|
"bytes": 153725,
|
||||||
|
"sha256": "c00b412af778af51cb2a6658bae4f4e11bb024a0770c3ea5aa4a54383be381c4"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/05-actions.json": {
|
||||||
|
"bytes": 1108,
|
||||||
|
"sha256": "1132f230694873a7e9536fd59f06b976e3b52041b119e2e9632b7c66de53d400"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/06-transition-dialog-0.jpg": {
|
||||||
|
"bytes": 149653,
|
||||||
|
"sha256": "a4736781ac296164ddffe12005f9f1ad29d73ddd2bc752d14fb2b63ac2579153"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/06-transition-dialog.json": {
|
||||||
|
"bytes": 1126,
|
||||||
|
"sha256": "2a2b8e8d9ac75a7c354e79bd5ba764c4024b045db009d5feb2b7f3909423f753"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/07-compatible-menu-0.jpg": {
|
||||||
|
"bytes": 148938,
|
||||||
|
"sha256": "80621a49998cc7fc2a643e7338a6867c722b4365fabc713fac0b39f7812703f9"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/07-compatible-menu.json": {
|
||||||
|
"bytes": 1126,
|
||||||
|
"sha256": "0f3c6a3d512e163953b9773de45df81d4129c0ded48a13c4d79de32dd8ab2d9e"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/08-menu-dismissed-0.jpg": {
|
||||||
|
"bytes": 149416,
|
||||||
|
"sha256": "867236aeac2dcf839104692f73cb63fb45d7dcc38d95eaabd5cba8d0a7ff862a"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/08-menu-dismissed.json": {
|
||||||
|
"bytes": 1125,
|
||||||
|
"sha256": "65ce375a1918a848cc57739f90eb15fbd55e9173c2343701791d0cf9740b818e"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/09-search-focus-0.jpg": {
|
||||||
|
"bytes": 149668,
|
||||||
|
"sha256": "4a08a7955982956d518397832a0befdda951347f04da9af75af787fe5a163332"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/09-search-focus.json": {
|
||||||
|
"bytes": 1123,
|
||||||
|
"sha256": "de505bdab419c6c60a3ad260479036d373f915333eb6fc62cd68c57ce2974af9"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/10-alias-query-0.jpg": {
|
||||||
|
"bytes": 149373,
|
||||||
|
"sha256": "dceaa7f1e0e0959aa6e654d0b9f84cbe20badec8d99315e3fd2ebc3cb47f6f5f"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/10-alias-query.json": {
|
||||||
|
"bytes": 1122,
|
||||||
|
"sha256": "c080f52656f6f603065e763cefb4a6ef790f4cfad96a3ddd3b6d58c5053cd62d"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/11-filtered-menu-0.jpg": {
|
||||||
|
"bytes": 149367,
|
||||||
|
"sha256": "6893983b46da3a6f3da1ee3eac90dc50f34dbb257671835d7e893244d18b1b03"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/11-filtered-menu.json": {
|
||||||
|
"bytes": 1124,
|
||||||
|
"sha256": "f8c5c8573840faef1337a7b429645e46652d29340104c2d6b15a9e1fbc14f152"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/12-existing-selection-0.jpg": {
|
||||||
|
"bytes": 147991,
|
||||||
|
"sha256": "839dee6d22448d12d90758ca76f5503fae69a099af0e055e20cf814d7dd498ec"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/12-existing-selection.json": {
|
||||||
|
"bytes": 1129,
|
||||||
|
"sha256": "84bc484447049dbafa1cda1b4c1ce0ade93c7ddcbf1c0489f2b18167deabed00"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/13-refocus-search-0.jpg": {
|
||||||
|
"bytes": 148017,
|
||||||
|
"sha256": "70f5a9d3ea5d2b82b68e19d5682659ba0bba1abaeac0f56aca4a88ffeec74b76"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/13-refocus-search.json": {
|
||||||
|
"bytes": 1125,
|
||||||
|
"sha256": "c2e42830b2d2364699210f716c375f2e63a447f910e729be0aa60c157f8341f6"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/14-query-selected-0.jpg": {
|
||||||
|
"bytes": 148447,
|
||||||
|
"sha256": "e0e5807ed5f0f535110ceecc6c6aabb9d077ea67fa1d83367d2c50141e8cdc53"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/14-query-selected.json": {
|
||||||
|
"bytes": 1125,
|
||||||
|
"sha256": "5d97ec91b3fd752d9f5e896c6fe97abc97ca40a91b6a708209663d4c198cc2e0"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/15-no-results-selection-retained-0.jpg": {
|
||||||
|
"bytes": 150165,
|
||||||
|
"sha256": "25be189c30acb4c8dab03d458b2fc73bd09d0766513d5c9ee4e0e0bffcee2457"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/15-no-results-selection-retained.json": {
|
||||||
|
"bytes": 1140,
|
||||||
|
"sha256": "a4f3c3e357aed8f66cad8b3c89f0e89d9eaf0662e3d740a06ba8eee6e7a33748"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/16-parent-search-focus-0.jpg": {
|
||||||
|
"bytes": 149957,
|
||||||
|
"sha256": "84bdd2c3ed40178a16452a680932d27d42142220f09f79dc747906732aafb97d"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/16-parent-search-focus.json": {
|
||||||
|
"bytes": 1130,
|
||||||
|
"sha256": "c3346906c51cf087a1fa850b46c867491630c2ec9a1565671e5674f2600ee261"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/17-independent-card-queries-0.jpg": {
|
||||||
|
"bytes": 149082,
|
||||||
|
"sha256": "a48e99ca120c7e34ce2823bc0140a99778b88849eb7a6b245867f129a0e46aff"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/17-independent-card-queries.json": {
|
||||||
|
"bytes": 1135,
|
||||||
|
"sha256": "ced4961b7bffb3d5f82757036ac77dab3cffba705bdd0a596183e686b3a59691"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/18-field-review-0.jpg": {
|
||||||
|
"bytes": 144509,
|
||||||
|
"sha256": "734e83058cc66d1af13098b5b69c7e2cbad8daf2a3afd2a25780e65faa38bf71"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/18-field-review.json": {
|
||||||
|
"bytes": 1123,
|
||||||
|
"sha256": "7489d77a9b1d4718f43aa10edca688e42a76a05507198538bd714ac8de638432"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/19-keep-target-reviewed-0.jpg": {
|
||||||
|
"bytes": 144547,
|
||||||
|
"sha256": "8eafc33857650748a059a2c1dc345112b300a6d16d0395b33ff8f7e596d18d04"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/19-keep-target-reviewed.json": {
|
||||||
|
"bytes": 1131,
|
||||||
|
"sha256": "509c9c3f0237a860c61e2413f05ab7bd9c51e5ffcbf97caede5c7298de0d479a"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/20-reviewed-search-focus-0.jpg": {
|
||||||
|
"bytes": 144893,
|
||||||
|
"sha256": "0e46bb4787aba3c19cb27461cf70a22fdb579f3a63839a91ee68d37d7f9dd1d9"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/20-reviewed-search-focus.json": {
|
||||||
|
"bytes": 1132,
|
||||||
|
"sha256": "33325c0cdac00fd49f25d4606c88a241dc6957f91f38d97b3ebafd4f5c42eaf2"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/21-reviewed-query-selected-0.jpg": {
|
||||||
|
"bytes": 144916,
|
||||||
|
"sha256": "bec9c198292a5d4f6883c003112da1634fc7b8b8694a2910e51ed29079eaeb4b"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/21-reviewed-query-selected.json": {
|
||||||
|
"bytes": 1134,
|
||||||
|
"sha256": "cf01d193ba011ed141e98236dff49fc06f789bbb26bb92b5a848a3f059c4319a"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/22-cleared-search-review-retained-0.jpg": {
|
||||||
|
"bytes": 145507,
|
||||||
|
"sha256": "ff8670ba72a6b516e01bfcb9a8036940682e8f2caec92342b0902d9c0bb26b87"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/22-cleared-search-review-retained.json": {
|
||||||
|
"bytes": 1141,
|
||||||
|
"sha256": "9a6a51578618a6c8aaf238329624102f3f37fda306be1dadffbc97dca178c221"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/23-cancel-restores-saved-0.jpg": {
|
||||||
|
"bytes": 145951,
|
||||||
|
"sha256": "c79b52574c7ad9ea3a442679fbccb7e2683be6c0c8d015260398bc31fe9c705c"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/23-cancel-restores-saved.json": {
|
||||||
|
"bytes": 1122,
|
||||||
|
"sha256": "6524ed5fd1cadc41de60d60818ec69e47a406189d03ede0822ea70ed502798b9"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/inputs.json": {
|
||||||
|
"bytes": 5843,
|
||||||
|
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/process.json": {
|
||||||
|
"bytes": 61,
|
||||||
|
"sha256": "6c4ea33570bd26271c5ec70c884dd4aeeab0642c33f46896ecd8832cb7ff5361"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/result.json": {
|
||||||
|
"bytes": 2837,
|
||||||
|
"sha256": "b483791514deb4c1e16272dfabdf78f5a8092ddc8deeefc01a23baaed2913c2d"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/started.json": {
|
||||||
|
"bytes": 2370,
|
||||||
|
"sha256": "cd97a270ae7e4eae8c6777ec311eb2437c4dd55e8b5882cbd8011c82fca2f7af"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/stderr.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/picker-corrected-2/stdout.log": {
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"native/run-picker-baseline.py": {
|
||||||
|
"bytes": 2106,
|
||||||
|
"sha256": "86c88e94ccb5fcfd8f81db429db9991170839bc4a20dab408ab1d284c7e234f1"
|
||||||
|
},
|
||||||
|
"native/run-picker-corrected-2.py": {
|
||||||
|
"bytes": 2162,
|
||||||
|
"sha256": "e61347fa440337bc9629c0d3b532c39e16cfb8d1966132992676bfa1c8f546f5"
|
||||||
|
},
|
||||||
|
"native/run-picker-corrected.py": {
|
||||||
|
"bytes": 2124,
|
||||||
|
"sha256": "0e82acfca234bade68a6263f1c5a2222e1eeaff932fa7c92eb88063f254f6701"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"externalEvidence": {
|
||||||
|
"correctedCargo": {
|
||||||
|
"repositoryPath": "harness/experiments/transition-lifecycle-20260913/evidence.zip",
|
||||||
|
"commit": "fe43730bec196cc6ebbc06dcf827169f04c120f0",
|
||||||
|
"bytes": 8496343,
|
||||||
|
"sha256": "5f44ad5d83024a04a873142e77c5b8961c278a19d76492618c3ef2db63801070"
|
||||||
|
},
|
||||||
|
"baselineNative": {
|
||||||
|
"repositoryPath": "harness/experiments/ordinary-text-native-20260913/evidence.zip",
|
||||||
|
"commit": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||||
|
"bytes": 5704887,
|
||||||
|
"sha256": "b827d0bdaec76b7b86d8a169530dcf98767c9883187ecdda02b4ea87171a42b5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"captureCount": 35,
|
||||||
|
"imageCount": 37
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Read-only portable receipt and frame verification, without archived execution."""
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib,json,zipfile
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||||
|
def need(ok,msg):
|
||||||
|
if not ok:raise ValueError(msg)
|
||||||
|
def main():
|
||||||
|
files=json.loads((ROOT/'files.json').read_bytes())
|
||||||
|
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
|
||||||
|
manifest=json.loads((ROOT/'manifest.json').read_bytes())
|
||||||
|
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
|
||||||
|
names=z.namelist()
|
||||||
|
need(len(names)==len(set(names)) and set(names)==set(manifest['members']),'exact ZIP membership')
|
||||||
|
need(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n for n in names),'safe members')
|
||||||
|
for n,p in manifest['members'].items():need(pin(z.read(n))==p,'member hash '+n)
|
||||||
|
def obj(n):return json.loads(z.read(n))
|
||||||
|
input_names={n[len('native/input/'):]:n for n in names if n.startswith('native/input/')}
|
||||||
|
reference=None;frame_count=0;image_count=0
|
||||||
|
for run,spec in manifest['runs'].items():
|
||||||
|
prefix='native/'+run+'/'
|
||||||
|
start=obj(prefix+'started.json');result=obj(prefix+'result.json');process=obj(prefix+'process.json')
|
||||||
|
need(all(result[k]==v for k,v in start.items()),run+' start/result relationship')
|
||||||
|
need(result['exitCode']==spec['exitCode'],run+' exit code')
|
||||||
|
need(isinstance(process['pid'],int) and process['pid']>0,run+' process ID')
|
||||||
|
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']),run+' process time order')
|
||||||
|
need(result['helper']==pin(z.read(spec['helper'])),run+' launch helper')
|
||||||
|
need(result['inputs']==pin(z.read(prefix+'inputs.json')) and result['inputUnchanged'] is True,run+' input receipt')
|
||||||
|
inputs=obj(prefix+'inputs.json')
|
||||||
|
need(set(inputs)==set(input_names),run+' input inventory')
|
||||||
|
for n,p in inputs.items():need(pin(z.read(input_names[n]))==p,run+' input bytes '+n)
|
||||||
|
if reference is None:reference=inputs
|
||||||
|
need(inputs==reference,run+' all trials identical fixture')
|
||||||
|
for stream in ['stdout','stderr']:
|
||||||
|
b=z.read(prefix+stream+'.log');need(not b and result[stream]==pin(b),run+' empty stream '+stream)
|
||||||
|
build=obj(spec['build'])
|
||||||
|
need(result['buildReceipt']==pin(z.read(spec['build'])) and result['buildHead']==build['head'],run+' build receipt')
|
||||||
|
need(build['exitCode']==0 and build['inputsUnchanged'] is True,run+' build status')
|
||||||
|
need(datetime.fromisoformat(build['start'])<=datetime.fromisoformat(build['finish'])<=datetime.fromisoformat(start['start']),run+' build before launch')
|
||||||
|
gui=[a for a in build['artifacts'] if a['cargo']['target']['name']=='arcadia-studio']
|
||||||
|
need(len(gui)==1,run+' unique GUI artifact')
|
||||||
|
need(result['binary']==result['binaryAfter']==gui[0]['readback'],run+' unchanged GUI pin')
|
||||||
|
need(result['command']==[gui[0]['cargo']['executable']],run+' exact launch command')
|
||||||
|
images=[]
|
||||||
|
for metadata in spec['captures']:
|
||||||
|
frame=obj(metadata)
|
||||||
|
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(frame['time'])<=datetime.fromisoformat(result['finish']),metadata+' capture interval')
|
||||||
|
need(frame['window']['app'].replace('\\','/')=='process:'+result['command'][0].replace('\\','/'),metadata+' window application path')
|
||||||
|
for image in frame['images']:
|
||||||
|
n=prefix+image['file'];need(n in manifest['members'] and z.read(n).startswith(b'\xff\xd8'),metadata+' JPEG reference')
|
||||||
|
need(image['width']>0 and image['height']>0,metadata+' frame dimensions')
|
||||||
|
images.append(n)
|
||||||
|
actual_images=[n for n in names if n.startswith(prefix) and n.endswith('.jpg')]
|
||||||
|
need(len(images)==len(set(images)) and set(images)==set(actual_images),run+' all images referenced once')
|
||||||
|
actual_metadata=[n for n in names if n.startswith(prefix) and n.endswith('.json') and 'images' in obj(n)]
|
||||||
|
need(set(actual_metadata)==set(spec['captures']) and len(actual_metadata)==len(spec['captures']),run+' all capture metadata')
|
||||||
|
frame_count+=len(actual_metadata);image_count+=len(images)
|
||||||
|
print(run,'exit',result['exitCode'],'captures',len(actual_metadata),'images',len(images))
|
||||||
|
need(frame_count==manifest['captureCount'] and image_count==manifest['imageCount'],'aggregate frame counts')
|
||||||
|
print('PASS:',len(names),'raw members;',len(reference),'unchanged fixture files;',frame_count,'capture records;',image_count,'original JPEGs.')
|
||||||
|
print('Build source inventories are external. Frames prove recorded states, not action execution or PID attestation. Capture helper was not start-pinned.')
|
||||||
|
if __name__=='__main__':main()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Ordinary transition lifecycle and counterpart selection
|
||||||
|
|
||||||
|
Scope: issue 28 C1 cases 1–3 from the [published assessment](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/94a2bb2e5f4ad700f5d34b4fd818d421dcea6d66/harness/issue28-transition-overview-assessment-9e8r6t.md). Candidate branch `codex/transition-lifecycle-9e8r6t` extends ordinary/Text checkpoint `3f9e6a18af96b5cd10940da4093be8da28b9c835`. The behavioral change is `acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8`; `d46854c2dc719bb8b44a0978102de44ecc3a6b39` adds only an inherited tuple-formatting correction. Live ownership and integration status remain in [issue 28](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/28#issuecomment-997).
|
||||||
|
|
||||||
|
## Findings and change
|
||||||
|
|
||||||
|
The existing endpoint deletion and ordinary transition engine conserves the retained counterpart in the six new lifecycle cases. These tests exercise both directions with independently authored root/child work, shared exchange-item definitions, SA typed ports, internal exchanges, placements, collapsed state, route labels and external artifact links. Expected deletions enumerate fixture-owned IDs independently of deletion implementation; whole-project equality detects incidental target changes. Old plans become stale, explicitly deleted target IDs are refused, a fresh implicit retry creates one counterpart, and a fresh subsequent retry reuses it without an undo entry. Recreating the source with the same name/alias but a new UUID does not silently recover its old correspondence. Explicit relinking changes only reviewed fields and adds the new trace.
|
||||||
|
|
||||||
|
These tests prove exact decoded-project JSON and canonical source round trips. They do not establish preservation of original source comments/formatting or file reopening in the packaged GUI. No tombstones, automatic name matching, structural merge policy, or new metamodel are introduced.
|
||||||
|
|
||||||
|
The native baseline exposed a separate obstacle: an Activity's long, unsorted counterpart menu offered opposite-layer ports, scenario records and other incompatible kinds. Selecting an offered output port caused an incompatibility error and disabled Apply. The correction shares the core's existing kind compatibility policy through `counterpart_compatible`, filters existing choices by name or alias, and sorts by name/alias/identity. Labels distinguish duplicate names using kind and alias. Explicit creation remains available.
|
||||||
|
|
||||||
|
Search state is separate from mappings and field resolutions. Typing does not replan or replace preview IDs; selected targets remain visible when filtered out. Missing and incompatible recovered selections remain identifiable and refused by the normal planner. Unique inferred correspondence counts distinct target IDs, so duplicate pair traces do not create false ambiguity; several distinct targets show an unresolved choice.
|
||||||
|
|
||||||
|
The chosen UI adds a text field to the existing picker. A searchable combo box was considered; it brings per-card cached option and selection state, while the separate field supports the needed filtering without changing reviewed mapping semantics. The inherited picker still requires pointer/touch interaction for opening and selection; this change is not complete keyboard accessibility.
|
||||||
|
|
||||||
|
## Verification record
|
||||||
|
|
||||||
|
Windows environment: Rust/Cargo 1.98.1, `stable-x86_64-pc-windows-gnullvm`, LLVM MinGW, locked offline dependencies, one Cargo job, incremental caching disabled. Linux work remains paused by the user.
|
||||||
|
|
||||||
|
- First lifecycle run: compiled, then six failures in the new fixture's incomplete warning expectation. No product failure was established. The fixture now supplies a system of interest and concrete item contracts and checks the exact remaining allocation/context warning multiset by ID, code and field.
|
||||||
|
- Corrected core run: four compatibility tests and six lifecycle tests passed; 710 recorded inputs remained unchanged. Compatibility covers both-direction element/relation families, wrong layers/categories, missing/nonsemantic identities, and the observed real SA-port refusal against the full planner.
|
||||||
|
- Desktop workflow filter: 25 passed, zero failed/ignored, 1,164 filtered; includes query preservation, duplicate-name selection, recovery refusal and trace inference. Existing core workflows: 28 passed, zero failed/ignored/filtered.
|
||||||
|
- Strict Clippy: `cargo clippy --locked --offline --workspace --all-targets -- -D warnings` passed on `acbfb6c`. All successful test/lint input bytes match its 710 recorded Git blobs.
|
||||||
|
- Initial `cargo fmt --all -- --check` found only the inherited tuple layout in `diagram/focus.rs`. Commit `d46854c` fixes those line breaks; the repeated format check passed with empty output. Tests/lint above were not rerun on this formatting-only commit.
|
||||||
|
- Windows workspace/binaries build passed on `d46854c`, with the same 710 unchanged inputs as the final format check. GUI: 117,737,472 bytes, SHA256 `6d3b0996bb153ab55c21672feec1e7303eb6c59eaad01f8dd8aa09609e08086c`. This is a development build, not a packaged release.
|
||||||
|
|
||||||
|
Reproduction commands from the repository root with the [documented Windows toolchain/runtime](../README.md) on PATH (`CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0` for this small disk):
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo test --locked --offline -p oa-core --test transition_lifecycle --test transition_counterpart_compatibility -- --nocapture
|
||||||
|
cargo test --locked --offline -p oa-core --test workflows -- --nocapture
|
||||||
|
cargo test --locked --offline -p oa-desktop --bin arcadia-studio workflow:: -- --nocapture
|
||||||
|
cargo clippy --locked --offline --workspace --all-targets -- -D warnings
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo build --locked --offline --workspace --bins
|
||||||
|
```
|
||||||
|
|
||||||
|
See [closed Cargo evidence](experiments/transition-lifecycle-20260913/README.md) for source inventories, raw results, hashes and an offline verifier. Its initial failures remain separate from corrected runs. The parent independently ran the reviewed verifier against both working and exact staged package bytes.
|
||||||
|
|
||||||
|
## Native picker observation
|
||||||
|
|
||||||
|
The [native comparison package](experiments/transition-picker-native-20260913/README.md) retains all three launch attempts, original frames, copied teaching inputs, both build receipts and the independent frame review. It links the separate build/source evidence. The capture-helper source was retained from the earlier session but was not pinned at launch; frames establish recorded states, not independent action/PID attestation.
|
||||||
|
|
||||||
|
The corrected GUI trial ran 2026-09-13 15:22:22–15:27:13 UTC on `d46854c`. It reopened the copied teaching model from Recent models, selected **Assess pressure alarm**, and used **Create System counterpart…** to open the ordinary transition dialog. All 23 original state captures are retained. The copied files and executable remained byte-identical; the process exited zero with empty stdout/stderr. An earlier launch without the toolchain environment exited immediately with Windows code 3221225781 and empty logs; the same binary launched successfully after loading that environment. This is recorded as an invocation failure, not a passed launch or a reproduced in-app crash.
|
||||||
|
|
||||||
|
- Frames 06–07: two source mapping cards; Create plus six alphabetically sorted Function candidates, with full teaching-model aliases. Baseline frame 10 offered incompatible kinds; baseline 11 shows the resulting port-selection error.
|
||||||
|
- Frames 10–12: uppercase `@SA_REPAIR` narrows the list to the matching function, retaining the current Assess mapping until the user explicitly selects Repair.
|
||||||
|
- Frames 15 and 17: zero results retain Repair; two visible cards retain independent queries.
|
||||||
|
- Frames 18–19: normal Name conflict requires review; Keep target enables Apply. Frame 22 shows that clearing search retains the selected target and Keep target decision.
|
||||||
|
- Frame 23: Cancel dismisses the workflow and restores All changes saved. No reviewed transition was applied in this trial.
|
||||||
|
|
||||||
|
Independent read-only reviews found no blocking defect in the shared predicate, selection/query handling, recovery behavior, or lifecycle conservation evaluator. Two refinements were incorporated: empty-query feedback no longer asks to clear an already empty search, and query-isolation tests use two actual OA source cards. A separate review of original native frames confirmed the bounded observations above without a blocking visual regression. Remaining friction: disabled Apply can have its required field-review explanation below the fold, requiring a scroll. Keyboard-only picker operation and broader transition-dialog feedback remain incomplete.
|
||||||
|
|
||||||
|
The native trial establishes this finite Windows selection/review/cancel workflow. It does not establish native endpoint deletion/recreation/relink, saving an applied transition, packaged launch, or whole-project equality from screenshots. Those lifecycle operations have the core evidence above; the broader C1 native acceptance and integration/release decision remain open.
|
||||||
Reference in New Issue
Block a user