Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0908f101a | ||
|
|
24b1eab07d | ||
|
|
47f55199b4 | ||
|
|
41a23fbbe7 | ||
|
|
1a372f1b68 | ||
|
|
bb6e8a44ac | ||
|
|
1a669130fb |
@@ -1,5 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.rs text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.lock text eol=lf
|
||||
@@ -90,7 +89,3 @@ harness/experiments/analysis-source-s2a-windows-20260913/** -text whitespace=-bl
|
||||
|
||||
# Preserve the pinned Iced dependency bytes and upstream manifest.
|
||||
crates/vendor/iced_widget/** -text whitespace=cr-at-eol
|
||||
|
||||
# Final overrides preserve original validation bytes after generic text rules.
|
||||
harness/experiments/navigation-validation-20261002/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/reviews/navigation-validation-packet-20261002/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
|
||||
@@ -340,18 +340,6 @@ pub(crate) fn diagnostic(source: &str, error: Error) -> Diagnostic {
|
||||
},
|
||||
}
|
||||
}
|
||||
pub(crate) fn diagnostic_with_issue(
|
||||
source: &str,
|
||||
error: Error,
|
||||
issue: Option<validation::Issue>,
|
||||
) -> Diagnostic {
|
||||
let mut result = diagnostic(source, error);
|
||||
if let Some(issue) = issue {
|
||||
result.object_id = issue.element;
|
||||
result.field = issue.field;
|
||||
}
|
||||
result
|
||||
}
|
||||
fn reserved(word: &str) -> bool {
|
||||
crate::reserved_alias(word)
|
||||
|| declaration_kind(word).is_some()
|
||||
@@ -634,7 +622,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
|
||||
if tokens.get(1).is_some_and(|t| t.text(source) == "2") {
|
||||
return v2_source::parse(source, tokens, limits);
|
||||
}
|
||||
let mut migration_issue = None;
|
||||
Parser {
|
||||
source,
|
||||
tokens,
|
||||
@@ -642,8 +629,8 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
|
||||
value_depth: 0,
|
||||
limits,
|
||||
}
|
||||
.document(&mut migration_issue)
|
||||
.map_err(|e| vec![diagnostic_with_issue(source, e, migration_issue)])
|
||||
.document()
|
||||
.map_err(|e| vec![diagnostic(source, e)])
|
||||
}
|
||||
fn offset_for(source: &str, line: usize, column: usize) -> usize {
|
||||
let mut offset = 0;
|
||||
@@ -1462,10 +1449,7 @@ impl Parser<'_> {
|
||||
},
|
||||
}
|
||||
}
|
||||
fn document(
|
||||
&mut self,
|
||||
migration_issue: &mut Option<validation::Issue>,
|
||||
) -> Result<ParsedDocument, Error> {
|
||||
fn document(&mut self) -> Result<ParsedDocument, Error> {
|
||||
let unit = self.unit(true, false)?;
|
||||
let header = unit.header.expect("entry header");
|
||||
let mut aliases = BTreeMap::from([(header.project.alias.clone(), header.project.id)]);
|
||||
@@ -1492,14 +1476,9 @@ impl Parser<'_> {
|
||||
.map(|(key, span)| ((d.id, key.clone()), span.start)),
|
||||
);
|
||||
}
|
||||
let decoded = resolve_declarations(
|
||||
header.project,
|
||||
&unit.declarations,
|
||||
&aliases,
|
||||
self.limits,
|
||||
migration_issue,
|
||||
)
|
||||
.map_err(|(_, error)| error)?;
|
||||
let decoded =
|
||||
resolve_declarations(header.project, &unit.declarations, &aliases, self.limits)
|
||||
.map_err(|(_, error)| error)?;
|
||||
Ok(ParsedDocument {
|
||||
project: decoded.project,
|
||||
migration: decoded.migration,
|
||||
@@ -1703,7 +1682,6 @@ pub(crate) fn resolve_declarations(
|
||||
declarations: &[Declaration],
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
migration_issue: &mut Option<validation::Issue>,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let mut legacy_elements = serde_json::Map::new();
|
||||
@@ -2011,38 +1989,6 @@ pub(crate) fn resolve_declarations(
|
||||
crate::persistence::migrate_v6(project, limits)
|
||||
})
|
||||
.map_err(|error| {
|
||||
// These frozen admission checks formerly ran in final validation.
|
||||
// Keep their diagnostic code and reference location after moving
|
||||
// them before schema normalization.
|
||||
if let crate::persistence::FileError::Model { issues, .. } = &error
|
||||
&& let Some(issue) = issues.first()
|
||||
&& let Some(code) = match issue.code.as_str() {
|
||||
"diagram_layer" => Some("diagram_layer"),
|
||||
"view_options" => Some("view_options"),
|
||||
_ => None,
|
||||
}
|
||||
{
|
||||
let index = issue
|
||||
.element
|
||||
.and_then(|id| declarations.iter().position(|d| d.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
let offset = declarations.get(index).map_or(0, |d| {
|
||||
issue
|
||||
.field
|
||||
.as_ref()
|
||||
.and_then(|field| d.field_spans.get(field))
|
||||
.map_or(d.offset, |span| span.start)
|
||||
});
|
||||
*migration_issue = Some(issue.clone());
|
||||
return (
|
||||
index,
|
||||
Error {
|
||||
code,
|
||||
offset,
|
||||
message: issue.message.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
(
|
||||
usize::MAX,
|
||||
Error {
|
||||
|
||||
@@ -69,8 +69,8 @@ impl Project4 {
|
||||
}
|
||||
crate::persistence::check_pre6_relations(self.relations.values())?;
|
||||
crate::persistence::check_pre6_elements(self.elements.values())?;
|
||||
let mut project = Project {
|
||||
schema_version: self.schema_version,
|
||||
Ok(Project {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
id: self.id,
|
||||
alias: self.alias,
|
||||
name: self.name,
|
||||
@@ -84,11 +84,7 @@ impl Project4 {
|
||||
.into_iter()
|
||||
.map(|(id, trace)| (id, trace.into()))
|
||||
.collect(),
|
||||
};
|
||||
// Schema3's adapter also enters here, without calling `finish`.
|
||||
crate::persistence::check_pre7_diagram_admission(&project)?;
|
||||
project.schema_version = SCHEMA_VERSION;
|
||||
Ok(project)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +104,6 @@ pub(crate) fn review_defaults(project: &Project, report: &mut MigrationReport) {
|
||||
}
|
||||
|
||||
pub(crate) fn finish(mut project: Project, limits: ParseLimits) -> Result<DecodeResult, FileError> {
|
||||
// Source4 enters here directly rather than through `Project4`.
|
||||
crate::persistence::check_pre7_diagram_admission(&project)?;
|
||||
crate::persistence::check_pre6_relations(project.relations.values())?;
|
||||
crate::persistence::check_pre6_elements(project.elements.values())?;
|
||||
project.schema_version = SCHEMA_VERSION;
|
||||
|
||||
@@ -341,50 +341,6 @@ pub(crate) fn check_pre6_baseline(baseline: &SemanticBaseline) -> Result<(), Fil
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Frozen sources 3–6 admit opposite-layer placements and visibility references
|
||||
/// only in Traceability. Check before migration discards the advertised version;
|
||||
/// current ordinary projections may retain these records as latent presentation.
|
||||
pub(crate) fn check_pre7_diagram_admission(project: &Project) -> Result<(), FileError> {
|
||||
let mut issues = Vec::new();
|
||||
for diagram in project.diagrams.values() {
|
||||
let option_issues = if diagram.projection == ProjectionKind::Traceability {
|
||||
crate::view_options::validate_options_cross_layer(project, &diagram.view_options)
|
||||
} else {
|
||||
crate::view_options::validate_options(project, diagram.layer, &diagram.view_options)
|
||||
};
|
||||
for issue in option_issues {
|
||||
validation::error(
|
||||
&mut issues,
|
||||
diagram.id,
|
||||
"view_options",
|
||||
issue.field,
|
||||
issue.message,
|
||||
);
|
||||
}
|
||||
for node in diagram.nodes.values() {
|
||||
if !project.elements.get(&node.element).is_some_and(|element| {
|
||||
diagram.projection == ProjectionKind::Traceability
|
||||
|| element.layer() == diagram.layer
|
||||
}) {
|
||||
validation::error(
|
||||
&mut issues,
|
||||
node.id,
|
||||
"diagram_layer",
|
||||
"element",
|
||||
"Diagram nodes reference existing elements in the selected layer.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(issue) = issues.into_iter().next() {
|
||||
return Err(FileError::Model {
|
||||
message: issue.message.clone(),
|
||||
issues: vec![issue],
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn migrate_v5(
|
||||
mut project: Project,
|
||||
limits: crate::language::ParseLimits,
|
||||
@@ -392,7 +348,6 @@ pub(crate) fn migrate_v5(
|
||||
if project.schema_version != 5 {
|
||||
return Err(FileError::Invalid("Expected schema_version 5.".into()));
|
||||
}
|
||||
check_pre7_diagram_admission(&project)?;
|
||||
check_pre6_relations(project.relations.values())?;
|
||||
check_pre6_elements(project.elements.values())?;
|
||||
for baseline in project
|
||||
@@ -465,7 +420,6 @@ pub(crate) fn migrate_v6(
|
||||
if project.schema_version != 6 {
|
||||
return Err(FileError::Invalid("Expected schema_version 6.".into()));
|
||||
}
|
||||
check_pre7_diagram_admission(&project)?;
|
||||
reject_source7_fields(&serde_json::to_value(&project)?).map_err(FileError::Invalid)?;
|
||||
limits.check_model(&project).map_err(FileError::Limit)?;
|
||||
project.schema_version = SCHEMA_VERSION;
|
||||
|
||||
@@ -85,7 +85,6 @@ pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
|
||||
.filter(|n| {
|
||||
p.elements.get(&n.element).is_some_and(|e| {
|
||||
e.kind() != Kind::Port
|
||||
&& (d.projection == ProjectionKind::Traceability || e.layer() == d.layer)
|
||||
&& d.projection.includes(e.kind())
|
||||
&& d.projection
|
||||
.state_machine()
|
||||
|
||||
@@ -589,17 +589,6 @@ fn apply_command(
|
||||
.map(|d| d.id)
|
||||
.ok_or_else(missing)?;
|
||||
if source != *diagram {
|
||||
let destination = &p.diagrams[diagram];
|
||||
let element = p.diagrams[&source].nodes[node].element;
|
||||
if destination.projection != ProjectionKind::Traceability
|
||||
&& p.elements
|
||||
.get(&element)
|
||||
.is_none_or(|e| e.layer() != destination.layer)
|
||||
{
|
||||
return Err(ModelError(
|
||||
"Move the placement to a view in its layer or a traceability view.".into(),
|
||||
));
|
||||
}
|
||||
let placement = p
|
||||
.diagrams
|
||||
.get_mut(&source)
|
||||
|
||||
@@ -573,26 +573,16 @@ fn compile_once(
|
||||
span,
|
||||
}),
|
||||
};
|
||||
let mut migration_issue = None;
|
||||
let decoded = language::resolve_declarations(
|
||||
header.project,
|
||||
&declarations,
|
||||
&aliases,
|
||||
limits.model,
|
||||
&mut migration_issue,
|
||||
)
|
||||
.map_err(|(index, error)| {
|
||||
let path = declaration_paths.get(index).unwrap_or(&bundle.entry);
|
||||
vec![SourceDiagnostic {
|
||||
path: path.clone(),
|
||||
diagnostic: language::diagnostic_with_issue(
|
||||
&bundle.files[path],
|
||||
error,
|
||||
migration_issue,
|
||||
),
|
||||
related: Vec::new(),
|
||||
}]
|
||||
})?;
|
||||
let decoded =
|
||||
language::resolve_declarations(header.project, &declarations, &aliases, limits.model)
|
||||
.map_err(|(index, error)| {
|
||||
let path = declaration_paths.get(index).unwrap_or(&bundle.entry);
|
||||
vec![SourceDiagnostic {
|
||||
path: path.clone(),
|
||||
diagnostic: language::diagnostic(&bundle.files[path], error),
|
||||
related: Vec::new(),
|
||||
}]
|
||||
})?;
|
||||
let diagnostics = model_diagnostics(bundle, &index, &decoded.project);
|
||||
let errors: Vec<_> = diagnostics
|
||||
.iter()
|
||||
|
||||
@@ -620,14 +620,11 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
text(&mut out, d.id, "name", &d.name);
|
||||
text(&mut out, d.id, "description", &d.description);
|
||||
crate::scenario_layout::validate_diagram(p, d, &mut out);
|
||||
// Ordinary projections are reversible filters over the same authored view.
|
||||
// Retain opposite-layer visibility choices when leaving Traceability.
|
||||
let option_issues =
|
||||
if d.projection.state_machine().is_none() && d.projection.scenario().is_none() {
|
||||
crate::view_options::validate_options_cross_layer(p, &d.view_options)
|
||||
} else {
|
||||
crate::view_options::validate_options(p, d.layer, &d.view_options)
|
||||
};
|
||||
let option_issues = if d.projection == ProjectionKind::Traceability {
|
||||
crate::view_options::validate_options_cross_layer(p, &d.view_options)
|
||||
} else {
|
||||
crate::view_options::validate_options(p, d.layer, &d.view_options)
|
||||
};
|
||||
for issue in option_issues {
|
||||
error(&mut out, d.id, "view_options", issue.field, issue.message);
|
||||
}
|
||||
@@ -801,10 +798,8 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
);
|
||||
}
|
||||
match p.elements.get(&n.element) {
|
||||
Some(e)
|
||||
if (d.projection.state_machine().is_none()
|
||||
&& d.projection.scenario().is_none())
|
||||
|| e.layer() == d.layer => {}
|
||||
Some(e) if d.projection == ProjectionKind::Traceability || e.layer() == d.layer => {
|
||||
}
|
||||
_ => error(
|
||||
&mut out,
|
||||
n.id,
|
||||
|
||||
@@ -218,9 +218,7 @@ pub fn validate_options(p: &Project, layer: Layer, options: &ViewOptions) -> Vec
|
||||
validate_options_in(p, Some(layer), options)
|
||||
}
|
||||
|
||||
/// Ordinary saved views retain references from either architecture layer so
|
||||
/// projection changes preserve authored visibility choices. Projection decides
|
||||
/// which layer is visible; typed State/Scenario views use layer-scoped validation.
|
||||
/// Traceability views admit references from either architecture layer.
|
||||
pub fn validate_options_cross_layer(p: &Project, options: &ViewOptions) -> Vec<OptionError> {
|
||||
validate_options_in(p, None, options)
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
//! Current latent presentation must not broaden frozen source/JSON admission.
|
||||
use oa_core::{view_options::*, *};
|
||||
|
||||
fn legacy_json(project: &Project, version: u32) -> Vec<u8> {
|
||||
let mut value = serde_json::to_value(project).unwrap();
|
||||
value["schema_version"] = version.into();
|
||||
for diagram in value["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
let fields = diagram.as_object_mut().unwrap();
|
||||
fields.remove("layout");
|
||||
if version < 6 {
|
||||
fields.remove("view_options");
|
||||
}
|
||||
}
|
||||
serde_json::to_vec(&value).unwrap()
|
||||
}
|
||||
|
||||
fn model_issue(error: persistence::FileError) -> validation::Issue {
|
||||
match error {
|
||||
persistence::FileError::Model { mut issues, .. } => issues.remove(0),
|
||||
other => panic!("Expected a located model issue, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_3_through_6_reject_cross_layer_nodes_but_keep_mixed_traceability() {
|
||||
let node_id = Id::parse_str("00000000-0000-4000-8000-000000000004").unwrap();
|
||||
for version in 3..=6 {
|
||||
let source = format!(
|
||||
"arcadia {version}; project compat \"Compatibility\" id \"00000000-0000-4000-8000-000000000001\" {{\n\
|
||||
sa_function work \"Counterpart\" id \"00000000-0000-4000-8000-000000000002\" {{}}\n\
|
||||
diagram view \"OA concern\" id \"00000000-0000-4000-8000-000000000003\" {{ layer Oa; projection Traceability; }}\n\
|
||||
node occurrence id \"00000000-0000-4000-8000-000000000004\" {{ diagram @view; element @work; }}\n}}"
|
||||
);
|
||||
let mixed = language::parse(&source).unwrap();
|
||||
assert_eq!(
|
||||
persistence::decode(&legacy_json(&mixed, version)).unwrap(),
|
||||
mixed
|
||||
);
|
||||
|
||||
let ordinary = source.replace("projection Traceability", "projection Overview");
|
||||
let same_layer = ordinary.replace("layer Oa", "layer Sa");
|
||||
let ordinary_project = language::parse(&same_layer).unwrap();
|
||||
assert_eq!(
|
||||
persistence::decode(&legacy_json(&ordinary_project, version)).unwrap(),
|
||||
ordinary_project
|
||||
);
|
||||
let errors = language::parse(&ordinary).unwrap_err();
|
||||
// Source3 already reported migration errors with the offending location.
|
||||
let code = if version == 3 {
|
||||
"migration_error"
|
||||
} else {
|
||||
"diagram_layer"
|
||||
};
|
||||
assert!(
|
||||
errors.iter().any(|error| error.code == code
|
||||
&& error.line == 4
|
||||
&& error.message.contains("selected layer")),
|
||||
"{version}: {errors:?}"
|
||||
);
|
||||
|
||||
let mut latent = mixed.clone();
|
||||
let diagram = latent.diagrams.values_mut().next().unwrap();
|
||||
diagram.projection = ProjectionKind::Overview;
|
||||
let node = diagram.nodes.values().next().unwrap().id;
|
||||
assert_eq!(node, node_id);
|
||||
if version >= 4 {
|
||||
let diagnostic = errors
|
||||
.iter()
|
||||
.find(|error| error.code == "diagram_layer")
|
||||
.unwrap();
|
||||
assert_eq!(diagnostic.object_id, Some(node_id));
|
||||
assert_eq!(diagnostic.field.as_deref(), Some("element"));
|
||||
let bundle = source_project::SourceBundle {
|
||||
entry: "model.arcadia".into(),
|
||||
files: std::collections::BTreeMap::from([("model.arcadia".into(), ordinary)]),
|
||||
};
|
||||
let errors =
|
||||
source_project::compile_bundle(&bundle, source_project::ProjectLimits::default())
|
||||
.unwrap_err();
|
||||
let diagnostic = &errors
|
||||
.iter()
|
||||
.find(|error| error.diagnostic.code == "diagram_layer")
|
||||
.unwrap()
|
||||
.diagnostic;
|
||||
assert_eq!(diagnostic.object_id, Some(node_id));
|
||||
assert_eq!(diagnostic.field.as_deref(), Some("element"));
|
||||
}
|
||||
let issue = model_issue(persistence::decode(&legacy_json(&latent, version)).unwrap_err());
|
||||
assert_eq!(issue.code, "diagram_layer");
|
||||
assert_eq!(issue.element, Some(node));
|
||||
assert_eq!(issue.field.as_deref(), Some("element"));
|
||||
// The exact same record is valid under the current presentation contract.
|
||||
assert_eq!(language::parse(&language::format(&latent)).unwrap(), latent);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&latent).unwrap()).unwrap(),
|
||||
latent
|
||||
);
|
||||
let mut dangling = mixed;
|
||||
dangling
|
||||
.diagrams
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap()
|
||||
.nodes
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap()
|
||||
.element = Id::parse_str("00000000-0000-4000-8000-000000000005").unwrap();
|
||||
let issue = model_issue(persistence::decode(&legacy_json(&dangling, version)).unwrap_err());
|
||||
assert_eq!(issue.code, "diagram_layer");
|
||||
assert_eq!(issue.element, Some(node));
|
||||
assert_eq!(issue.field.as_deref(), Some("element"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_6_checks_every_layer_sensitive_saved_selector_before_upgrade() {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "A", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "B", None)
|
||||
.unwrap();
|
||||
let edge = project.relate(RelationKind::Interaction, "Interaction", a, b);
|
||||
let category = project
|
||||
.add_element(Kind::ExchangeCategory, Layer::Oa, "Work category", None)
|
||||
.unwrap();
|
||||
let participant_category = project
|
||||
.add_element(
|
||||
Kind::ExchangeCategory,
|
||||
Layer::Oa,
|
||||
"Participant category",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*project
|
||||
.elements
|
||||
.get_mut(&participant_category)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::ExchangeCategory {
|
||||
family: ExchangeFamily::Participant,
|
||||
exchanges: vec![],
|
||||
};
|
||||
let context = project
|
||||
.add_element(Kind::Process, Layer::Oa, "Process", None)
|
||||
.unwrap();
|
||||
let diagram = project.ensure_diagram(Layer::Sa);
|
||||
for (field, options) in [
|
||||
(
|
||||
"hidden_edges",
|
||||
ViewOptions {
|
||||
hidden_edges: vec![edge],
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"work_categories",
|
||||
ViewOptions {
|
||||
work_categories: CategorySelection::Any(vec![category]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"participant_categories",
|
||||
ViewOptions {
|
||||
participant_categories: CategorySelection::Any(vec![participant_category]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"work_contexts",
|
||||
ViewOptions {
|
||||
work_contexts: ContextSelection::Any(vec![context]),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
] {
|
||||
project.diagrams.get_mut(&diagram).unwrap().view_options = options;
|
||||
let issue = model_issue(persistence::decode(&legacy_json(&project, 6)).unwrap_err());
|
||||
assert_eq!(issue.code, "view_options");
|
||||
assert_eq!(issue.element, Some(diagram));
|
||||
assert_eq!(issue.field.as_deref(), Some(field));
|
||||
let current_source = language::format(&project);
|
||||
assert_eq!(language::parse(¤t_source).unwrap(), project);
|
||||
let source6 = current_source.replacen("arcadia 7;", "arcadia 6;", 1);
|
||||
let errors = language::parse(&source6).unwrap_err();
|
||||
let diagnostic = errors
|
||||
.iter()
|
||||
.find(|error| error.code == "view_options")
|
||||
.unwrap();
|
||||
assert_eq!(diagnostic.object_id, Some(diagram));
|
||||
assert_eq!(diagnostic.field.as_deref(), Some(field));
|
||||
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
assert_eq!(
|
||||
persistence::decode(&legacy_json(&project, 6)).unwrap(),
|
||||
project
|
||||
);
|
||||
let source6 = language::format(&project).replacen("arcadia 7;", "arcadia 6;", 1);
|
||||
assert_eq!(language::parse(&source6).unwrap(), project);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
//! Populated presentation survives mixed-layer projection switches.
|
||||
//! Complements projection_switching.rs without external fixture dependencies.
|
||||
use oa_core::{
|
||||
projection::*, routing::route_diagram, source_project::*, view_options::ViewIndex, *,
|
||||
};
|
||||
|
||||
struct Populated {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
groups: [Id; 2],
|
||||
children: [[Id; 2]; 2],
|
||||
edges: [Id; 2],
|
||||
ports: [Id; 2],
|
||||
trace: Id,
|
||||
}
|
||||
|
||||
fn xy(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn apply(project: &mut Project, command: Command) {
|
||||
*project = prepare_command(project, &command).unwrap().project;
|
||||
}
|
||||
|
||||
fn fixture(layer: Layer, collapsed: bool) -> Populated {
|
||||
let mut p = Project::empty();
|
||||
let oa_group = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Operational response", None)
|
||||
.unwrap();
|
||||
let oa_send = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Request repair", Some(oa_group))
|
||||
.unwrap();
|
||||
let oa_receive = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Perform repair", Some(oa_group))
|
||||
.unwrap();
|
||||
let sa_group = p
|
||||
.add_element(Kind::Function, Layer::Sa, "System response", None)
|
||||
.unwrap();
|
||||
let sa_send = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Dispatch repair", Some(sa_group))
|
||||
.unwrap();
|
||||
let sa_receive = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Execute repair", Some(sa_group))
|
||||
.unwrap();
|
||||
let oa_edge = p.relate(
|
||||
RelationKind::Interaction,
|
||||
"Repair request",
|
||||
oa_send,
|
||||
oa_receive,
|
||||
);
|
||||
let sa_edge = p.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Repair dispatch",
|
||||
sa_send,
|
||||
sa_receive,
|
||||
);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&sa_edge].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let trace = Id::new_v4();
|
||||
let refinement = Trace {
|
||||
id: trace,
|
||||
alias: p.fresh_alias("dispatch_refinement", "trace", trace),
|
||||
name: "Dispatch refines repair request".into(),
|
||||
description: "Keep this cross-layer trace and its authored route.".into(),
|
||||
oa: oa_send,
|
||||
sa: sa_send,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: p.properties(oa_send).unwrap(),
|
||||
sa: p.properties(sa_send).unwrap(),
|
||||
semantic: None,
|
||||
}),
|
||||
};
|
||||
apply(&mut p, Command::CreateTrace(refinement));
|
||||
let d = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
let oa_group_node = p.place(d, oa_group, xy(0., 0.));
|
||||
let oa_send_node = p.place(d, oa_send, xy(40., 120.));
|
||||
let oa_receive_node = p.place(d, oa_receive, xy(360., 120.));
|
||||
let sa_group_node = p.place(d, sa_group, xy(0., 500.));
|
||||
let sa_send_node = p.place(d, sa_send, xy(40., 620.));
|
||||
let sa_receive_node = p.place(d, sa_receive, xy(360., 620.));
|
||||
for (node, port, side, offset) in [
|
||||
(sa_send_node, source_port, PortAnchorSide::Bottom, 0.25),
|
||||
(sa_receive_node, target_port, PortAnchorSide::Top, 0.75),
|
||||
] {
|
||||
apply(
|
||||
&mut p,
|
||||
Command::SetPortAnchor {
|
||||
diagram: d,
|
||||
node,
|
||||
anchor: PortAnchor { port, side, offset },
|
||||
},
|
||||
);
|
||||
}
|
||||
for (edge, y, port_backed) in [
|
||||
(oa_edge, 260., false),
|
||||
(sa_edge, 860., true),
|
||||
(trace, 420., false),
|
||||
] {
|
||||
apply(
|
||||
&mut p,
|
||||
Command::SetEdgeRoute {
|
||||
diagram: d,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: (!port_backed).then_some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 0.3,
|
||||
}),
|
||||
target_attachment: (!port_backed).then_some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.7,
|
||||
}),
|
||||
waypoints: vec![xy(680., y), xy(740., y)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
offset: xy(13., -27.),
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
for node in [oa_group_node, sa_group_node] {
|
||||
let mut placement = p.diagrams[&d].nodes[&node].clone();
|
||||
placement.collapsed = collapsed;
|
||||
placement.size = Size {
|
||||
width: 310.,
|
||||
height: 170.,
|
||||
};
|
||||
apply(
|
||||
&mut p,
|
||||
Command::EditNode {
|
||||
diagram: d,
|
||||
original: node,
|
||||
node: placement,
|
||||
},
|
||||
);
|
||||
}
|
||||
validate_candidate(&p).unwrap();
|
||||
Populated {
|
||||
project: p,
|
||||
diagram: d,
|
||||
groups: [oa_group_node, sa_group_node],
|
||||
children: [
|
||||
[oa_send_node, oa_receive_node],
|
||||
[sa_send_node, sa_receive_node],
|
||||
],
|
||||
edges: [oa_edge, sa_edge],
|
||||
ports: [source_port, target_port],
|
||||
trace,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_round_trips(p: &Project) {
|
||||
assert_eq!(language::parse(&language::format(p)).unwrap(), *p);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(p).unwrap()).unwrap(),
|
||||
*p
|
||||
);
|
||||
assert_eq!(
|
||||
compile_bundle(&format_bundle(p), ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
*p
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_layer_only(p: &Project, d: Id, view: &ProjectedView) {
|
||||
let layer = p.diagrams[&d].layer;
|
||||
for node in &view.nodes {
|
||||
assert_eq!(p.elements[&node.element].layer(), layer);
|
||||
if let Some(parent) = node.visual_parent {
|
||||
let parent = view.nodes.iter().find(|n| n.id == parent).unwrap();
|
||||
assert_eq!(p.elements[&parent.element].layer(), layer);
|
||||
}
|
||||
}
|
||||
for port in &view.ports {
|
||||
assert_eq!(p.elements[&port.element].layer(), layer);
|
||||
let owner = view.nodes.iter().find(|n| n.id == port.owner).unwrap();
|
||||
assert_eq!(p.elements[&owner.element].layer(), layer);
|
||||
}
|
||||
for edge in &view.edges {
|
||||
assert!(
|
||||
!edge.trace,
|
||||
"cross-layer trace leaked into ordinary projection"
|
||||
);
|
||||
assert_eq!(p.layer_of(edge.id), Some(layer));
|
||||
for endpoint in [edge.source, edge.target] {
|
||||
let owner = view.nodes.iter().find(|n| n.id == endpoint).unwrap();
|
||||
assert_eq!(p.elements[&owner.element].layer(), layer);
|
||||
}
|
||||
for port in [edge.source_port, edge.target_port].into_iter().flatten() {
|
||||
assert_eq!(p.elements[&port].layer(), layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn populated_mixed_projection_switch_conserves_authorship_and_restores_geometry() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for collapsed in [false, true] {
|
||||
let f = fixture(layer, collapsed);
|
||||
let baseline = &f.project;
|
||||
let d = f.diagram;
|
||||
let before_view = project_view(baseline, d);
|
||||
let before_routes = route_diagram(baseline, &baseline.diagrams[&d], &before_view);
|
||||
// Guard against a vacuous fixture: Traceability deliberately flattens parents.
|
||||
assert_eq!(before_view.nodes.len(), 6);
|
||||
assert!(before_view.nodes.iter().all(|n| n.visual_parent.is_none()));
|
||||
for id in [f.edges[0], f.edges[1], f.trace] {
|
||||
assert!(before_view.edges.iter().any(|e| e.id == id));
|
||||
let route = baseline.diagrams[&d]
|
||||
.routes
|
||||
.iter()
|
||||
.find(|r| r.edge == id)
|
||||
.unwrap();
|
||||
assert_eq!(route.waypoints.len(), 2);
|
||||
assert!(route.label.is_some());
|
||||
}
|
||||
for port in f.ports {
|
||||
assert!(before_view.ports.iter().any(|p| p.element == port));
|
||||
}
|
||||
assert_round_trips(baseline);
|
||||
for projection in [
|
||||
ProjectionKind::Overview,
|
||||
ProjectionKind::Decomposition,
|
||||
ProjectionKind::Allocation,
|
||||
ProjectionKind::FunctionalDataflow,
|
||||
ProjectionKind::Capabilities,
|
||||
] {
|
||||
let mut expected = baseline.clone();
|
||||
expected.diagrams.get_mut(&d).unwrap().projection = projection;
|
||||
let mut session = Session::new(baseline.clone());
|
||||
session
|
||||
.execute(Command::EditDiagram(d, expected.diagrams[&d].clone()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_round_trips(session.project());
|
||||
let view = project_view(session.project(), d);
|
||||
assert_eq!(
|
||||
view,
|
||||
project_view_with_index(&expected, d, &ViewIndex::new(&expected))
|
||||
);
|
||||
assert_layer_only(session.project(), d, &view);
|
||||
if projection == ProjectionKind::Overview {
|
||||
let own = if layer == Layer::Oa { 0 } else { 1 };
|
||||
assert!(view.nodes.iter().any(|n| n.id == f.groups[own]));
|
||||
for child in f.children[own] {
|
||||
if collapsed {
|
||||
assert!(!view.nodes.iter().any(|n| n.id == child));
|
||||
} else {
|
||||
assert_eq!(
|
||||
view.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == child)
|
||||
.unwrap()
|
||||
.visual_parent,
|
||||
Some(f.groups[own])
|
||||
);
|
||||
}
|
||||
}
|
||||
if !collapsed {
|
||||
assert!(view.edges.iter().any(|e| e.id == f.edges[own]));
|
||||
if layer == Layer::Sa {
|
||||
assert_eq!(view.ports.len(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
session.undo();
|
||||
assert_eq!(session.project(), baseline);
|
||||
assert!(
|
||||
!session.can_undo(),
|
||||
"projection change must be one model history step"
|
||||
);
|
||||
assert_round_trips(session.project());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session
|
||||
.execute(Command::EditDiagram(d, baseline.diagrams[&d].clone()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), baseline);
|
||||
let restored = project_view(session.project(), d);
|
||||
assert_eq!(restored, before_view);
|
||||
assert_eq!(
|
||||
route_diagram(
|
||||
session.project(),
|
||||
&session.project().diagrams[&d],
|
||||
&restored
|
||||
),
|
||||
before_routes
|
||||
);
|
||||
assert_round_trips(session.project());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use oa_core::{source_project::*, view_options::*, *};
|
||||
|
||||
fn fixture(layer: Layer) -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Assess alarm", None)
|
||||
.unwrap();
|
||||
let oa2 = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Assess alarm", None)
|
||||
.unwrap();
|
||||
let edge = p.relate(RelationKind::Interaction, "Alarm", oa, oa2);
|
||||
let category = p
|
||||
.add_element(Kind::ExchangeCategory, Layer::Oa, "Alarm data", None)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory {
|
||||
family: ExchangeFamily::Work,
|
||||
exchanges: vec![edge],
|
||||
};
|
||||
let context = p
|
||||
.add_element(Kind::Process, Layer::Oa, "Alarm handling", None)
|
||||
.unwrap();
|
||||
let d = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
for (i, element) in [oa, oa2, sa].into_iter().enumerate() {
|
||||
p.place(
|
||||
d,
|
||||
element,
|
||||
Position {
|
||||
x: 300. * i as f32,
|
||||
y: 40.,
|
||||
},
|
||||
);
|
||||
}
|
||||
p.diagrams.get_mut(&d).unwrap().view_options = ViewOptions {
|
||||
hidden_edges: vec![edge],
|
||||
work_categories: CategorySelection::Any(vec![category]),
|
||||
work_contexts: ContextSelection::Any(vec![context]),
|
||||
..Default::default()
|
||||
};
|
||||
validate_candidate(&p).unwrap();
|
||||
(p, d, oa, sa, category)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_switching_preserves_complete_model_history_and_all_serializations() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let (baseline, diagram, _, _, _) = fixture(layer);
|
||||
for projection in [
|
||||
ProjectionKind::Overview,
|
||||
ProjectionKind::Decomposition,
|
||||
ProjectionKind::Allocation,
|
||||
ProjectionKind::FunctionalDataflow,
|
||||
ProjectionKind::Capabilities,
|
||||
] {
|
||||
let mut expected = baseline.clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
||||
let mut session = Session::new(baseline.clone());
|
||||
session
|
||||
.execute(Command::EditDiagram(
|
||||
diagram,
|
||||
expected.diagrams[&diagram].clone(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
let view = projection::project_view(session.project(), diagram);
|
||||
assert!(
|
||||
view.nodes
|
||||
.iter()
|
||||
.all(|n| expected.elements[&n.element].layer() == layer)
|
||||
);
|
||||
assert_eq!(
|
||||
view,
|
||||
projection::project_view_with_index(&expected, diagram, &ViewIndex::new(&expected))
|
||||
);
|
||||
let source = language::format(&expected);
|
||||
assert_eq!(language::parse(&source).unwrap(), expected);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&expected).unwrap()).unwrap(),
|
||||
expected
|
||||
);
|
||||
let bundle = format_bundle(&expected);
|
||||
assert_eq!(
|
||||
compile_bundle(&bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
expected
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &baseline);
|
||||
assert!(!session.can_undo());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session
|
||||
.execute(Command::EditDiagram(
|
||||
diagram,
|
||||
baseline.diagrams[&diagram].clone(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &baseline);
|
||||
assert_eq!(
|
||||
projection::project_view(session.project(), diagram),
|
||||
projection::project_view(&baseline, diagram)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_switching_does_not_admit_missing_elements_or_invalid_filter_kinds() {
|
||||
let (mut p, d, oa, _, category) = fixture(Layer::Sa);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation;
|
||||
validate_candidate(&p).unwrap();
|
||||
let valid = p.clone();
|
||||
let node = p.diagrams[&d]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == oa)
|
||||
.unwrap()
|
||||
.id;
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.element = Id::new_v4();
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
p = valid.clone();
|
||||
p.diagrams.get_mut(&d).unwrap().view_options.work_categories = CategorySelection::Any(vec![oa]);
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
p = valid;
|
||||
p.elements.remove(&category);
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_switching_keeps_typed_state_membership_strict() {
|
||||
let (mut p, d, _, _, _) = fixture(Layer::Sa);
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Modes", Some(system))
|
||||
.unwrap();
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::StateMachine { machine };
|
||||
assert!(
|
||||
validation::validate(&p)
|
||||
.iter()
|
||||
.any(|i| i.code == "state_diagram_member")
|
||||
);
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
}
|
||||
@@ -67,18 +67,7 @@ fn source6_traceability_default_keeps_both_layers_and_indexed_projection_matches
|
||||
.any(|e| e.id == interaction)
|
||||
);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Overview;
|
||||
validate_candidate(&p).unwrap();
|
||||
let ordinary = projection::project_view_with_index(&p, d, &index);
|
||||
assert!(
|
||||
ordinary
|
||||
.nodes
|
||||
.iter()
|
||||
.all(|n| p.elements[&n.element].layer() == Layer::Oa)
|
||||
);
|
||||
assert!(ordinary.edges.iter().any(|e| e.id == interaction));
|
||||
assert!(!ordinary.edges.iter().any(|e| e.id == fe));
|
||||
assert_eq!(p.diagrams[&d].nodes.len(), 4);
|
||||
assert_eq!(p.diagrams[&d].view_options.hidden_edges, vec![fe]);
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
p.diagrams.get_mut(&d).unwrap().view_options.relation_kinds = RelationKinds::Only(vec![]);
|
||||
assert!(
|
||||
|
||||
@@ -210,7 +210,6 @@ impl SemanticPlan {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.candidate
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &ViewIndex {
|
||||
&self.index
|
||||
}
|
||||
@@ -485,6 +484,57 @@ pub struct PresentationContext {
|
||||
pub target: Option<TargetPresentation>,
|
||||
}
|
||||
|
||||
impl PresentationContext {
|
||||
fn header_visible(self, header: Rect, outgoing: bool) -> bool {
|
||||
if !outgoing {
|
||||
return self.viewport.contains(header);
|
||||
}
|
||||
// Leaving a performer must not require fitting the entire work card
|
||||
// inside the camera. Retain a 24px grab region so the result stays
|
||||
// directly reachable; geometry/model admission is independent.
|
||||
let width = (header.position.x + header.size.width)
|
||||
.min(self.viewport.position.x + self.viewport.size.width)
|
||||
- header.position.x.max(self.viewport.position.x);
|
||||
let height = (header.position.y + header.size.height)
|
||||
.min(self.viewport.position.y + self.viewport.size.height)
|
||||
- header.position.y.max(self.viewport.position.y);
|
||||
width * self.zoom >= 24. && height * self.zoom >= 24.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment() {
|
||||
let context = PresentationContext {
|
||||
viewport: Rect {
|
||||
position: Position { x: 0., y: 0. },
|
||||
size: Size {
|
||||
width: 800.,
|
||||
height: 400.,
|
||||
},
|
||||
},
|
||||
zoom: 0.75,
|
||||
target: None,
|
||||
};
|
||||
let mut header = Rect {
|
||||
position: Position { x: -100., y: 30. },
|
||||
size: Size {
|
||||
width: 224.,
|
||||
height: 62.,
|
||||
},
|
||||
};
|
||||
assert!(context.header_visible(header, true));
|
||||
assert!(!context.header_visible(header, false));
|
||||
header.position.x = -192.; // exactly 32 world pixels = 24 logical pixels
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.x -= 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
header.position.x = 0.;
|
||||
header.position.y = 368.;
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.y += 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlacementPlan {
|
||||
pub diagram: Diagram,
|
||||
@@ -496,8 +546,8 @@ pub struct PlacementPlan {
|
||||
commands: Vec<Command>,
|
||||
}
|
||||
|
||||
/// `requested` is the original selected placement position plus pointer motion
|
||||
/// in the frozen transform (therefore preserving the press offset). Snap only
|
||||
/// `requested` preserves the frozen pointer offset unless the caller proposes
|
||||
/// bounded outward clearance for an outgoing drop. Snap only
|
||||
/// this root; apply the resulting identical delta to captured Q and its routes.
|
||||
/// No whole Project clone, validator or semantic ViewIndex rebuild occurs here.
|
||||
pub fn place(
|
||||
@@ -577,7 +627,7 @@ pub fn place(
|
||||
height: selected.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !presentation.viewport.contains(header) {
|
||||
if !presentation.header_visible(header, plan.destination == Destination::OutsidePerformers) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -188,7 +188,7 @@ pub fn place(
|
||||
};
|
||||
geometry::check_rigid(&plan.before_expanded, &expanded, &plan.placements, delta)?;
|
||||
scenes?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation.viewport)?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation, false)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -398,7 +398,12 @@ pub fn place(
|
||||
&expanded,
|
||||
)?;
|
||||
geometry::check_rigid(&scope.0.before_expanded, &expanded, scope.affected(), delta)?;
|
||||
geometry::check_headers(&view, scope.selected(), presentation.viewport)?;
|
||||
geometry::check_headers(
|
||||
&view,
|
||||
scope.selected(),
|
||||
presentation,
|
||||
plan.destination == Destination::OutsidePerformers,
|
||||
)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -170,7 +170,8 @@ pub(super) fn check_rigid(
|
||||
pub(super) fn check_headers(
|
||||
view: &ProjectedView,
|
||||
selected: &BTreeMap<Id, Id>,
|
||||
viewport: Rect,
|
||||
presentation: PresentationContext,
|
||||
outgoing: bool,
|
||||
) -> Result<(), Refusal> {
|
||||
for (id, element) in selected {
|
||||
let node = view
|
||||
@@ -185,7 +186,7 @@ pub(super) fn check_headers(
|
||||
height: node.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !viewport.contains(header) {
|
||||
if !presentation.header_visible(header, outgoing) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -4,12 +4,8 @@ pub(crate) mod architecture_authoring;
|
||||
mod architecture_authoring_tests;
|
||||
#[cfg(test)]
|
||||
mod behavior_removal_tests;
|
||||
#[cfg(test)]
|
||||
mod canvas_selection_tests;
|
||||
mod checkpoints;
|
||||
#[cfg(test)]
|
||||
mod connection_begin_regressions;
|
||||
#[cfg(test)]
|
||||
mod diagram_focus_tests;
|
||||
pub(crate) mod diagram_removal;
|
||||
#[cfg(test)]
|
||||
@@ -23,8 +19,6 @@ mod hosted;
|
||||
pub(crate) mod multiselect;
|
||||
#[cfg(test)]
|
||||
mod multiselect_tests;
|
||||
#[cfg(test)]
|
||||
mod placement_relocation_regressions;
|
||||
mod reference_authoring;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
@@ -387,7 +381,6 @@ pub enum Message {
|
||||
Behavior(crate::behavior::Event),
|
||||
Layer(Layer),
|
||||
Select(Id),
|
||||
CanvasSelect(crate::route_edit::Context, Id),
|
||||
EditProperties(Id),
|
||||
SelectNode(Id),
|
||||
DiagramSelection(multiselect::Event),
|
||||
@@ -954,16 +947,6 @@ impl App {
|
||||
return true;
|
||||
}
|
||||
let original = self.selected;
|
||||
let canvas_surface = self.architecture_surface();
|
||||
let placement_owner = self.draft.as_ref().and_then(|draft| match draft.kind {
|
||||
crate::forms::RecordKind::Node(owner) => self
|
||||
.project()
|
||||
.diagrams
|
||||
.get(&owner)
|
||||
.filter(|diagram| diagram.nodes.contains_key(&draft.original))
|
||||
.map(|diagram| diagram.id),
|
||||
_ => None,
|
||||
});
|
||||
let result = self
|
||||
.draft
|
||||
.as_ref()
|
||||
@@ -1018,16 +1001,13 @@ impl App {
|
||||
if let Some(layer) = self.project().layer_of(id) {
|
||||
self.layer = layer;
|
||||
}
|
||||
let selected_surface = self
|
||||
if let Some((diagram, layer, node)) = self
|
||||
.project()
|
||||
.diagrams
|
||||
.values()
|
||||
.find(|d| d.id == id || d.nodes.contains_key(&id))
|
||||
.map(|d| (d.id, d.layer, d.nodes.contains_key(&id).then_some(id)));
|
||||
let relocated_placement = placement_owner
|
||||
.zip(selected_surface.and_then(|(diagram, _, node)| node.map(|_| diagram)))
|
||||
.is_some_and(|(before, after)| before != after);
|
||||
if let Some((diagram, layer, node)) = selected_surface {
|
||||
.map(|d| (d.id, d.layer, d.nodes.contains_key(&id).then_some(id)))
|
||||
{
|
||||
self.layer = layer;
|
||||
if self.page == Page::Behavior
|
||||
&& let Some(machine) =
|
||||
@@ -1041,20 +1021,6 @@ impl App {
|
||||
self.selected_node = node;
|
||||
}
|
||||
}
|
||||
// Ordinary Properties edits retain their saved architecture view.
|
||||
// Follow an explicitly edited diagram identity/layer, never an
|
||||
// inferred replacement for an unrelated queued canvas click.
|
||||
// An accepted placement move explicitly chooses its new view.
|
||||
// Same-owner edits (including a new UUID) still retain inspection.
|
||||
if !relocated_placement && let Some((diagram, _, _)) = canvas_surface {
|
||||
let diagram = if diagram == original { id } else { diagram };
|
||||
if let Some(view) = self.project().diagrams.get(&diagram)
|
||||
&& view.projection.is_architecture()
|
||||
{
|
||||
self.layer = view.layer;
|
||||
self.active_diagram = Some(diagram);
|
||||
}
|
||||
}
|
||||
self.sync();
|
||||
self.status = "Properties applied".into();
|
||||
true
|
||||
@@ -1205,13 +1171,7 @@ impl App {
|
||||
if id != self.selected {
|
||||
self.selected_attachment = None;
|
||||
}
|
||||
if self.selected == id
|
||||
&& !process_preview
|
||||
&& self
|
||||
.project()
|
||||
.layer_of(id)
|
||||
.is_none_or(|layer| layer == self.layer)
|
||||
{
|
||||
if self.selected == id && !process_preview {
|
||||
return;
|
||||
}
|
||||
if !self.apply() {
|
||||
@@ -1257,59 +1217,6 @@ impl App {
|
||||
}
|
||||
self.refresh_draft();
|
||||
}
|
||||
/// The saved architecture surface, independent of the Inspector's object.
|
||||
fn architecture_surface(&self) -> Option<(Id, Layer, ProjectionKind)> {
|
||||
if self.page != Page::Diagram {
|
||||
return None;
|
||||
}
|
||||
let diagram = self.project().diagrams.get(&self.active_diagram?)?;
|
||||
diagram.projection.is_architecture().then_some((
|
||||
diagram.id,
|
||||
diagram.layer,
|
||||
diagram.projection,
|
||||
))
|
||||
}
|
||||
fn canvas_target_visible(&self, diagram: Id, id: Id) -> bool {
|
||||
let view = project_view(self.project(), diagram);
|
||||
view.nodes
|
||||
.iter()
|
||||
.any(|node| node.id == id || node.element == id)
|
||||
|| view.ports.iter().any(|port| port.element == id)
|
||||
|| view.edges.iter().any(|edge| edge.id == id)
|
||||
}
|
||||
/// Inspect a contextual target without following its model layer. An edit
|
||||
/// may invalidate a previously visible occurrence; accept the edit but
|
||||
/// refuse that old hit. Invisible Show-in-view targets remain intentional.
|
||||
fn inspect_canvas_target(&mut self, id: Id) -> bool {
|
||||
if self.selected == id {
|
||||
return true;
|
||||
}
|
||||
let surface = self.architecture_surface();
|
||||
let accepted_draft = self.draft_dirty();
|
||||
let was_visible = accepted_draft
|
||||
&& surface.is_some_and(|(diagram, _, _)| self.canvas_target_visible(diagram, id));
|
||||
if !self.project().contains(id) || !self.apply() {
|
||||
return false;
|
||||
}
|
||||
if !self.project().contains(id)
|
||||
|| (was_visible
|
||||
&& (self.architecture_surface() != surface
|
||||
|| !surface
|
||||
.is_some_and(|(diagram, _, _)| self.canvas_target_visible(diagram, id))))
|
||||
{
|
||||
if accepted_draft {
|
||||
self.status =
|
||||
"Properties applied. The view changed; select a visible target again.".into();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
self.selected = id;
|
||||
self.selected_node = None;
|
||||
self.selected_attachment = None;
|
||||
self.selected_bend = None;
|
||||
self.refresh_draft();
|
||||
true
|
||||
}
|
||||
fn reveal_explorer_selection(&mut self) {
|
||||
if let Some(group) = self.explorer_index.group(self.selected) {
|
||||
self.explorer_groups_collapsed.remove(&group);
|
||||
@@ -1456,11 +1363,6 @@ impl App {
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if let Message::CanvasSelect(captured, _) = &message
|
||||
&& (self.page != Page::Diagram || self.route_context() != Some(*captured))
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Selection can change without advancing the captured canvas context.
|
||||
if let Message::Route(
|
||||
crate::route_edit::Event::Commit(captured, changed, ..)
|
||||
@@ -2216,7 +2118,6 @@ impl App {
|
||||
Message::Command(Command::Layout(diagram)) => self.saved_scenario_diagram(*diagram),
|
||||
Message::Projection(projection) => active || projection.scenario().is_some(),
|
||||
Message::SelectNode(_)
|
||||
| Message::CanvasSelect(..)
|
||||
| Message::PlaceSelected
|
||||
| Message::HideSelected
|
||||
| Message::ToggleCollapse
|
||||
@@ -2817,26 +2718,10 @@ impl App {
|
||||
e.kind() != Kind::Port && d.projection.includes(e.kind())
|
||||
})
|
||||
});
|
||||
// Draw acts on the visible current view even when opened from
|
||||
// Explorer; explicit Select/Open actions still navigate.
|
||||
let draw_in_current_view = match &*action {
|
||||
Message::Connection(crate::connection_edit::Event::Begin(tool, source)) => {
|
||||
*source == menu.request.object
|
||||
&& self.active_diagram.is_some_and(|diagram| {
|
||||
crate::connection_edit::can_begin(
|
||||
self.project(),
|
||||
diagram,
|
||||
*tool,
|
||||
*source,
|
||||
)
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if show_in_mixed_view || draw_in_current_view || menu.request.canvas.is_some() {
|
||||
if !self.inspect_canvas_target(menu.request.object) {
|
||||
return Task::none();
|
||||
}
|
||||
if show_in_mixed_view {
|
||||
self.selected = menu.request.object;
|
||||
self.selected_attachment = None;
|
||||
self.refresh_draft();
|
||||
} else {
|
||||
self.select(menu.request.object);
|
||||
}
|
||||
@@ -2848,15 +2733,7 @@ impl App {
|
||||
}
|
||||
self.reconcile_multiselect();
|
||||
self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i));
|
||||
let action = match *action {
|
||||
Message::EditProperties(id)
|
||||
if menu.request.canvas.is_some() && id == menu.request.object =>
|
||||
{
|
||||
Message::InspectorTab(crate::navigation::InspectorTab::Properties)
|
||||
}
|
||||
action => action,
|
||||
};
|
||||
return self.update(action);
|
||||
return self.update(*action);
|
||||
}
|
||||
Message::CreateContextElement(kind, child) => {
|
||||
self.create_kind = kind;
|
||||
@@ -3012,14 +2889,6 @@ impl App {
|
||||
self.status = "Property draft reverted".into();
|
||||
}
|
||||
Message::Select(id) => self.select(id),
|
||||
Message::CanvasSelect(context, id) => {
|
||||
let view = project_view(self.project(), context.diagram);
|
||||
if view.ports.iter().any(|port| port.element == id)
|
||||
|| view.edges.iter().any(|edge| edge.id == id)
|
||||
{
|
||||
self.inspect_canvas_target(id);
|
||||
}
|
||||
}
|
||||
Message::DiagramSelection(event) => self.multiselect_event(event),
|
||||
Message::EditProperties(id) => {
|
||||
self.select(id);
|
||||
@@ -3039,23 +2908,10 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
let original = self.selected;
|
||||
let surface = self.architecture_surface();
|
||||
let accepted_draft = self.draft_dirty();
|
||||
if !self.apply() {
|
||||
return Task::none();
|
||||
}
|
||||
let id = if id == original { self.selected } else { id };
|
||||
if let Some((diagram, _, _)) = surface
|
||||
&& (self.architecture_surface() != surface
|
||||
|| !self.canvas_target_visible(diagram, id))
|
||||
{
|
||||
if accepted_draft {
|
||||
self.status =
|
||||
"Properties applied. The view changed; select a visible target again."
|
||||
.into();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(d) = self
|
||||
.active_diagram
|
||||
.and_then(|d| self.project().diagrams.get(&d))
|
||||
@@ -3480,7 +3336,7 @@ impl App {
|
||||
self.workflow = Some(crate::workflow::Workflow::relationship_kind(
|
||||
self.project(),
|
||||
self.selected,
|
||||
self.project().layer_of(self.selected).unwrap_or(self.layer),
|
||||
self.layer,
|
||||
kind,
|
||||
incoming,
|
||||
));
|
||||
@@ -4506,18 +4362,13 @@ impl App {
|
||||
self.status = "Select an SA function or participant, then choose a typed port action in Properties.".into();
|
||||
return;
|
||||
}
|
||||
let creation_layer = if child {
|
||||
self.project().layer_of(self.selected).unwrap_or(self.layer)
|
||||
} else {
|
||||
self.layer
|
||||
};
|
||||
let allocated = child
|
||||
&& self.create_kind.is_work()
|
||||
&& self
|
||||
.project()
|
||||
.elements
|
||||
.get(&self.selected)
|
||||
.is_some_and(|e| e.layer() == creation_layer && e.kind().is_performer());
|
||||
.is_some_and(|e| e.layer() == self.layer && e.kind().is_performer());
|
||||
let parent = if child && !allocated {
|
||||
Some(self.selected)
|
||||
} else {
|
||||
@@ -4525,7 +4376,7 @@ impl App {
|
||||
};
|
||||
let Some(e) = self.project().make_element(
|
||||
self.create_kind,
|
||||
creation_layer,
|
||||
self.layer,
|
||||
&format!("New {}", self.create_kind.label().to_lowercase()),
|
||||
parent,
|
||||
) else {
|
||||
@@ -4554,16 +4405,16 @@ impl App {
|
||||
.project()
|
||||
.diagrams
|
||||
.values()
|
||||
.find(|d| d.layer == creation_layer && d.projection == preferred)
|
||||
.find(|d| d.layer == self.layer && d.projection == preferred)
|
||||
.map(|d| d.id);
|
||||
if diagram_target.is_none() {
|
||||
let view = Id::new_v4();
|
||||
commands.push(Command::CreateDiagram(Diagram {
|
||||
id: view,
|
||||
alias: self.project().fresh_alias("engineering_view", "view", view),
|
||||
name: format!("{} {preferred}", creation_layer),
|
||||
name: format!("{} {preferred}", self.layer),
|
||||
description: String::new(),
|
||||
layer: creation_layer,
|
||||
layer: self.layer,
|
||||
projection: preferred,
|
||||
layout: None,
|
||||
routes: Vec::new(),
|
||||
@@ -4696,13 +4547,7 @@ impl App {
|
||||
));
|
||||
}
|
||||
if self.execute(Command::Batch(commands)) {
|
||||
let changed_view = self.active_diagram != diagram_target;
|
||||
self.active_diagram = diagram_target;
|
||||
if changed_view
|
||||
&& let Some(view) = diagram_target.and_then(|id| self.project().diagrams.get(&id))
|
||||
{
|
||||
self.layer = view.layer;
|
||||
}
|
||||
self.selected = id;
|
||||
self.inspector_tab = crate::navigation::InspectorTab::Properties;
|
||||
self.refresh_draft();
|
||||
@@ -4716,7 +4561,7 @@ impl App {
|
||||
self.workflow = Some(crate::workflow::Workflow::relationship(
|
||||
self.project(),
|
||||
self.selected,
|
||||
self.project().layer_of(self.selected).unwrap_or(self.layer),
|
||||
self.layer,
|
||||
));
|
||||
}
|
||||
fn add_trace(&mut self) {
|
||||
@@ -5137,30 +4982,8 @@ impl App {
|
||||
epoch: self.canvas_epoch,
|
||||
})
|
||||
}
|
||||
// Admission belongs to the caller: palette choice, contextual source, or
|
||||
// an already admitted session whose complete context is still current.
|
||||
fn arm_connection(
|
||||
&mut self,
|
||||
context: crate::route_edit::Context,
|
||||
tool: crate::connection_tool::Tool,
|
||||
) -> crate::route_edit::Context {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
let context = crate::route_edit::Context {
|
||||
epoch: self.canvas_epoch,
|
||||
..context
|
||||
};
|
||||
self.connection = Some(crate::connection_edit::Session::new(
|
||||
self.project(),
|
||||
&project_view(self.project(), context.diagram),
|
||||
context,
|
||||
tool,
|
||||
));
|
||||
self.selected_bend = None;
|
||||
self.status = tool.hint().into();
|
||||
context
|
||||
}
|
||||
fn edit_connection(&mut self, event: crate::connection_edit::Event) {
|
||||
use crate::connection_edit::Event;
|
||||
use crate::connection_edit::{Event, Session};
|
||||
let Some(context) = self.route_context() else {
|
||||
return;
|
||||
};
|
||||
@@ -5176,13 +4999,10 @@ impl App {
|
||||
}
|
||||
match event {
|
||||
Event::Begin(tool, source) => {
|
||||
if !crate::connection_edit::can_begin(self.project(), context.diagram, tool, source)
|
||||
{
|
||||
self.status = "Choose a visible compatible source in this diagram.".into();
|
||||
return;
|
||||
self.edit_connection(Event::Choose(Some(tool)));
|
||||
if let Some(context) = self.route_context() {
|
||||
self.edit_connection(Event::Endpoint(context, source));
|
||||
}
|
||||
let context = self.arm_connection(context, tool);
|
||||
self.edit_connection(Event::Endpoint(context, source));
|
||||
}
|
||||
Event::Choose(Some(tool)) => {
|
||||
if !crate::connection_tool::Tool::for_layer(self.layer).contains(&tool) {
|
||||
@@ -5196,7 +5016,16 @@ impl App {
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.arm_connection(context, tool);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
let context = self.route_context().unwrap();
|
||||
self.connection = Some(Session::new(
|
||||
self.project(),
|
||||
&project_view(self.project(), context.diagram),
|
||||
context,
|
||||
tool,
|
||||
));
|
||||
self.selected_bend = None;
|
||||
self.status = tool.hint().into();
|
||||
}
|
||||
Event::Endpoint(captured, id) => {
|
||||
if captured != context {
|
||||
@@ -5257,9 +5086,17 @@ impl App {
|
||||
&project_view(self.project(), context.diagram),
|
||||
id,
|
||||
);
|
||||
if !self.inspect_canvas_target(id) {
|
||||
self.connection = Some(session);
|
||||
return;
|
||||
if session.tool == crate::connection_tool::Tool::Allocation
|
||||
&& self.project().diagrams[&context.diagram].projection
|
||||
== ProjectionKind::Traceability
|
||||
{
|
||||
// Mixed views can allocate either perspective. Choosing
|
||||
// a visible source must not navigate to its preferred view.
|
||||
self.selected = id;
|
||||
self.selected_attachment = None;
|
||||
self.refresh_draft();
|
||||
} else {
|
||||
self.select(id);
|
||||
}
|
||||
self.status = if session.candidates.is_empty() {
|
||||
"No compatible targets are visible. Expand containers or show a compatible endpoint in this view; Esc cancels drawing.".into()
|
||||
@@ -5275,11 +5112,8 @@ impl App {
|
||||
Event::Restart(captured) => {
|
||||
if captured == context
|
||||
&& let Some(session) = &self.connection
|
||||
&& session.context == context
|
||||
{
|
||||
// A contextual mixed-layer tool need not be in this layer's
|
||||
// palette. Its unchanged session context retains admission.
|
||||
self.arm_connection(context, session.tool);
|
||||
self.edit_connection(Event::Choose(Some(session.tool)));
|
||||
}
|
||||
}
|
||||
Event::Choose(None) => {}
|
||||
|
||||
@@ -14,6 +14,8 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
mod clearance;
|
||||
|
||||
pub const FINISH_EDIT: &str =
|
||||
"Finish or cancel the current edit before selecting or moving elements.";
|
||||
|
||||
@@ -56,6 +58,7 @@ pub struct Target {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Frozen {
|
||||
pub targets: Vec<Target>,
|
||||
pub moving: Vec<Rectangle>,
|
||||
cards: Vec<Rectangle>,
|
||||
controls: Vec<Rectangle>,
|
||||
edges: Vec<(Point, Point)>,
|
||||
@@ -82,14 +85,16 @@ impl Frozen {
|
||||
) -> Self {
|
||||
let mut targets = Vec::new();
|
||||
let mut cards = Vec::new();
|
||||
let mut moving = Vec::new();
|
||||
for n in &view.nodes {
|
||||
if affected.contains_key(&n.id) {
|
||||
continue;
|
||||
}
|
||||
let region = Rectangle::new(
|
||||
Point::new(n.position.x, n.position.y),
|
||||
iced::Size::new(n.size.width, n.size.height),
|
||||
);
|
||||
if affected.contains_key(&n.id) {
|
||||
moving.push(region);
|
||||
continue;
|
||||
}
|
||||
let Some(e) = project.elements.get(&n.element) else {
|
||||
cards.push(region);
|
||||
continue;
|
||||
@@ -172,6 +177,7 @@ impl Frozen {
|
||||
}
|
||||
Self {
|
||||
targets,
|
||||
moving,
|
||||
cards,
|
||||
controls,
|
||||
edges,
|
||||
@@ -407,7 +413,6 @@ impl Candidate {
|
||||
Self::Performers(p) => &p.accepted,
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &view_options::ViewIndex {
|
||||
match self {
|
||||
Self::Singleton(p) => p.index(),
|
||||
@@ -525,6 +530,8 @@ pub(crate) struct Feedback {
|
||||
pub inspection_targets: Vec<usize>,
|
||||
pub text: String,
|
||||
pub refused: bool,
|
||||
/// Pointer-following outline only; it never authorizes a model change.
|
||||
pub ghost_delta: Option<Vector>,
|
||||
}
|
||||
impl Feedback {
|
||||
pub fn inspection_identity(&self) -> String {
|
||||
@@ -1055,7 +1062,26 @@ impl App {
|
||||
viewport: stamp.viewport(), zoom: stamp.zoom,
|
||||
target: target.map(|i| plan::TargetPresentation { placement: active.frozen.as_ref().unwrap().targets[i].placement }),
|
||||
};
|
||||
let placed = candidate.place(requested, presentation).map_err(|r| refusal(&r))?;
|
||||
let exact = candidate.place(requested, presentation);
|
||||
let placed = if destination == plan::Destination::OutsidePerformers
|
||||
&& matches!(&exact, Err(plan::Refusal::LayoutRequired(_)))
|
||||
{
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => Some(&scope.placements),
|
||||
Prepared::Assignment(scope) => Some(scope.affected()),
|
||||
_ => None,
|
||||
};
|
||||
affected.and_then(|affected| {
|
||||
clearance::requests(
|
||||
(self.project(), self.document.view_index()),
|
||||
(candidate.project(), candidate.index()), stamp.context.diagram,
|
||||
affected, *anchor, requested,
|
||||
(stamp.world(press.start), stamp.world(pointer.unwrap())),
|
||||
).into_iter().find_map(|position| candidate.place(position, presentation).ok())
|
||||
}).map_or(exact, Ok)
|
||||
} else {
|
||||
exact
|
||||
}.map_err(|r| refusal(&r))?;
|
||||
let label = candidate.label();
|
||||
if release {
|
||||
if let Some(command) = candidate.finish(self.project(), &placed).map_err(|r| refusal(&r))? {
|
||||
@@ -1082,6 +1108,14 @@ impl App {
|
||||
} else {
|
||||
let inspection_targets = active.inspection_targets.clone();
|
||||
let frozen = active.frozen.clone();
|
||||
// A rejected intermediate position is not a return gesture. Keep the
|
||||
// last admitted scene and show current motion separately; release
|
||||
// always recomputes admission and never commits this retained view.
|
||||
if refused && in_bounds && preview.is_none() {
|
||||
preview = self.allocation_drag.preview.take();
|
||||
}
|
||||
let ghost_delta = (refused && in_bounds)
|
||||
.then(|| (pointer.unwrap() - press.start) * (1. / stamp.zoom));
|
||||
self.allocation_drag.active = Some(active);
|
||||
self.allocation_drag.preview = preview;
|
||||
self.allocation_drag.feedback = Some(Feedback {
|
||||
@@ -1092,6 +1126,7 @@ impl App {
|
||||
inspection_targets,
|
||||
text,
|
||||
refused,
|
||||
ghost_delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
//! Bounded outgoing clearance proposals. The ordinary placement and semantic
|
||||
//! validators still decide whether any proposal may be previewed or committed.
|
||||
use iced::Point;
|
||||
use oa_core::{Id, NodePlacement, Position, Project, projection, view_options::ViewIndex};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const GRID: f32 = 10.;
|
||||
const GAP: f32 = 10.;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Bounds {
|
||||
left: f32,
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn node(node: &projection::ProjectedNode) -> Option<Self> {
|
||||
let result = Self {
|
||||
left: node.position.x,
|
||||
top: node.position.y,
|
||||
right: node.position.x + node.size.width,
|
||||
bottom: node.position.y + node.size.height,
|
||||
};
|
||||
([result.left, result.top, result.right, result.bottom]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
&& result.right > result.left
|
||||
&& result.bottom > result.top)
|
||||
.then_some(result)
|
||||
}
|
||||
|
||||
fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
left: self.left.min(other.left),
|
||||
top: self.top.min(other.top),
|
||||
right: self.right.max(other.right),
|
||||
bottom: self.bottom.max(other.bottom),
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(self, other: Self) -> bool {
|
||||
self.left < other.right
|
||||
&& other.left < self.right
|
||||
&& self.top < other.bottom
|
||||
&& other.top < self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// Called only after an OutsidePerformers candidate fails ordinary placement.
|
||||
/// Coordinates use the frozen world transform. The entire affected footprint
|
||||
/// receives one anchor translation; this never moves or packs other cards.
|
||||
pub(super) fn requests(
|
||||
accepted: (&Project, &ViewIndex),
|
||||
candidate: (&Project, &ViewIndex),
|
||||
diagram: Id,
|
||||
affected: &BTreeMap<Id, NodePlacement>,
|
||||
anchor: Id,
|
||||
requested: Position,
|
||||
motion: (Point, Point),
|
||||
) -> Vec<Position> {
|
||||
let (accepted, accepted_index) = accepted;
|
||||
let (candidate, candidate_index) = candidate;
|
||||
let (start, pointer) = motion;
|
||||
if ![
|
||||
requested.x,
|
||||
requested.y,
|
||||
start.x,
|
||||
start.y,
|
||||
pointer.x,
|
||||
pointer.y,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
let (Some(original), Some(base), Some(anchor)) = (
|
||||
accepted.diagrams.get(&diagram),
|
||||
candidate.diagrams.get(&diagram),
|
||||
affected.get(&anchor),
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let snapped = Position {
|
||||
x: (requested.x / GRID).round() * GRID,
|
||||
y: (requested.y / GRID).round() * GRID,
|
||||
};
|
||||
let delta = Position {
|
||||
x: snapped.x - anchor.position.x,
|
||||
y: snapped.y - anchor.position.y,
|
||||
};
|
||||
let before = projection::project_diagram_with_index(accepted, original, accepted_index);
|
||||
let before_nodes: BTreeMap<_, _> = before.nodes.iter().map(|node| (node.id, node)).collect();
|
||||
let mut sources = BTreeSet::new();
|
||||
for node in before
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let mut parent = node.visual_parent;
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = parent {
|
||||
if !seen.insert(id) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(node) = before_nodes.get(&id) else {
|
||||
break;
|
||||
};
|
||||
if !affected.contains_key(&id)
|
||||
&& accepted
|
||||
.elements
|
||||
.get(&node.element)
|
||||
.is_some_and(|element| element.kind().is_performer())
|
||||
{
|
||||
sources.insert(id);
|
||||
}
|
||||
parent = node.visual_parent;
|
||||
}
|
||||
}
|
||||
if sources.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Projection after responsibility removal supplies the former performer's
|
||||
// remaining enclosure, including other allocated work. Projecting the moved
|
||||
// block also includes the full visible descendant enclosure of every root.
|
||||
let mut moved = base.clone();
|
||||
for (id, captured) in affected {
|
||||
let Some(node) = moved.nodes.get_mut(id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
node.position = Position {
|
||||
x: captured.position.x + delta.x,
|
||||
y: captured.position.y + delta.y,
|
||||
};
|
||||
if ![
|
||||
node.position.x,
|
||||
node.position.y,
|
||||
node.size.width,
|
||||
node.size.height,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let after = projection::project_diagram_with_index(candidate, &moved, candidate_index);
|
||||
let mut footprint: Option<Bounds> = None;
|
||||
for node in after
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let Some(bounds) = Bounds::node(node) else {
|
||||
return Vec::new();
|
||||
};
|
||||
footprint = Some(footprint.map_or(bounds, |previous| previous.union(bounds)));
|
||||
}
|
||||
let Some(footprint) = footprint else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut overlaps = Vec::new();
|
||||
for node in after.nodes.iter().filter(|node| sources.contains(&node.id)) {
|
||||
let (Some(old), Some(current)) = (Bounds::node(before_nodes[&node.id]), Bounds::node(node))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if footprint.overlaps(current) {
|
||||
overlaps.push((old, current));
|
||||
}
|
||||
}
|
||||
if overlaps.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Each entry is a half-plane actually crossed by the pointer. At a corner,
|
||||
// prefer the boundary reached first on the press-to-pointer segment. The
|
||||
// direction index is a stable tie-break, independent of placement UUIDs.
|
||||
let mut crossings = [None::<f32>; 4];
|
||||
for (old, _) in &overlaps {
|
||||
for (direction, initial, final_, boundary, outward) in [
|
||||
(
|
||||
0,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.left,
|
||||
pointer.x < old.left && start.x >= old.left,
|
||||
),
|
||||
(
|
||||
1,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.right,
|
||||
pointer.x >= old.right && start.x < old.right,
|
||||
),
|
||||
(
|
||||
2,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.top,
|
||||
pointer.y < old.top && start.y >= old.top,
|
||||
),
|
||||
(
|
||||
3,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.bottom,
|
||||
pointer.y >= old.bottom && start.y < old.bottom,
|
||||
),
|
||||
] {
|
||||
if outward {
|
||||
let fraction = (boundary - initial) / (final_ - initial);
|
||||
if fraction.is_finite() && (0. ..=1.).contains(&fraction) {
|
||||
crossings[direction] =
|
||||
Some(crossings[direction].map_or(fraction, |old| old.min(fraction)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut directions: Vec<_> = crossings
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(direction, fraction)| fraction.map(|fraction| (direction, fraction)))
|
||||
.collect();
|
||||
directions.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
|
||||
let mut result = Vec::new();
|
||||
for (direction, _) in directions {
|
||||
let mut corrected = snapped;
|
||||
match direction {
|
||||
0 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.left)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.x = ((snapped.x + edge - GAP - footprint.right) / GRID).floor() * GRID;
|
||||
}
|
||||
1 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.right)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.x = ((snapped.x + edge + GAP - footprint.left) / GRID).ceil() * GRID;
|
||||
}
|
||||
2 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.top)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.y = ((snapped.y + edge - GAP - footprint.bottom) / GRID).floor() * GRID;
|
||||
}
|
||||
3 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.bottom)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.y = ((snapped.y + edge + GAP - footprint.top) / GRID).ceil() * GRID;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if corrected.x.is_finite() && corrected.y.is_finite() && !result.contains(&corrected) {
|
||||
result.push(corrected);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::allocation_drag as plan;
|
||||
use oa_core::{Kind, Layer, ProjectionKind, RelationKind, Size};
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
performer: Id,
|
||||
work: Id,
|
||||
node: Id,
|
||||
}
|
||||
|
||||
fn xy(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn fixture(layer: Layer) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
let (performer, work) = match layer {
|
||||
Layer::Oa => (Kind::Entity, Kind::Activity),
|
||||
Layer::Sa => (Kind::System, Kind::Function),
|
||||
};
|
||||
let performer = project
|
||||
.add_element(performer, layer, "Performer", None)
|
||||
.unwrap();
|
||||
let work = project.add_element(work, layer, "Work", None).unwrap();
|
||||
let diagram = project.ensure_diagram(layer);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let performer = project.place(diagram, performer, xy(100., 100.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 400.,
|
||||
height: 250.,
|
||||
};
|
||||
let node = project.place(diagram, work, xy(160., 180.));
|
||||
let owner = project.diagrams[&diagram].nodes[&performer].element;
|
||||
project.relate(RelationKind::Allocation, "Responsibility", owner, work);
|
||||
Fixture {
|
||||
project,
|
||||
diagram,
|
||||
performer,
|
||||
work,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic(f: &Fixture) -> (plan::Scope, plan::SemanticPlan) {
|
||||
let scope = plan::capture(&f.project, f.diagram, f.work, f.node).unwrap();
|
||||
let semantic = plan::plan_semantics(
|
||||
&f.project,
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
(scope, semantic)
|
||||
}
|
||||
|
||||
fn presentation() -> plan::PresentationContext {
|
||||
plan::PresentationContext {
|
||||
viewport: plan::Rect {
|
||||
position: xy(-2000., -2000.),
|
||||
size: Size {
|
||||
width: 5000.,
|
||||
height: 5000.,
|
||||
},
|
||||
},
|
||||
zoom: 1.,
|
||||
target: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_outgoing_edge_clears_the_source_and_passes_ordinary_placement() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = fixture(layer);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (start, pointer, requested, expected) in [
|
||||
(
|
||||
Point::new(170., 210.),
|
||||
Point::new(99., 210.),
|
||||
xy(89., 180.),
|
||||
xy(-140., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(340., 210.),
|
||||
Point::new(501., 210.),
|
||||
xy(321., 180.),
|
||||
xy(510., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 190.),
|
||||
Point::new(200., 99.),
|
||||
xy(160., 89.),
|
||||
xy(160., -10.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 260.),
|
||||
Point::new(200., 351.),
|
||||
xy(160., 271.),
|
||||
xy(160., 360.),
|
||||
),
|
||||
] {
|
||||
assert!(matches!(
|
||||
plan::place(&semantic, requested, presentation()),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(start, pointer),
|
||||
);
|
||||
assert_eq!(proposed, vec![expected]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
assert!(
|
||||
plan::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_shrink_uses_candidate_enclosure_not_frozen_target() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&f.performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
let (scope, semantic) = semantic(&f);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(229., 180.),
|
||||
(Point::new(340., 210.), Point::new(409., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(340., 180.)]);
|
||||
plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_order_follows_boundary_crossing_and_is_deterministic() {
|
||||
let f = fixture(Layer::Sa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
// Right crossed at 160/161 of the segment; bottom at 90/91.
|
||||
let args = (
|
||||
xy(321., 271.),
|
||||
Point::new(340., 260.),
|
||||
Point::new(501., 351.),
|
||||
);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(320., 360.), xy(510., 270.)]);
|
||||
assert_eq!(
|
||||
proposed,
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty() {
|
||||
let f = fixture(Layer::Oa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (requested, pointer) in [
|
||||
(xy(170., 180.), Point::new(210., 210.)),
|
||||
(xy(700., 180.), Point::new(740., 210.)),
|
||||
(xy(f32::NAN, 180.), Point::new(99., 210.)),
|
||||
(xy(89., 180.), Point::new(f32::INFINITY, 210.)),
|
||||
] {
|
||||
assert!(
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(Point::new(200., 210.), pointer)
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
requests(
|
||||
(semantic.project(), semantic.index()),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(200., 210.), Point::new(99., 210.))
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descendant_footprint_controls_clearance_without_mutating_projects() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let child = f
|
||||
.project
|
||||
.add_element(kind, layer, "Child", Some(f.work))
|
||||
.unwrap();
|
||||
let child_node = f.project.place(f.diagram, child, xy(240., 270.));
|
||||
// First-child allocation transfer is normally performed by commands;
|
||||
// this fixture expresses the resulting leaf allocation directly.
|
||||
f.project.relations.clear();
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Leaf responsibility",
|
||||
owner,
|
||||
child,
|
||||
);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
assert!(scope.placements.contains_key(&child_node));
|
||||
let before = f.project.clone();
|
||||
let candidate = semantic.project().clone();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(&candidate, semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-240., 180.)]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let child = &placed.diagram.nodes[&child_node];
|
||||
assert_eq!(child.position.x - root.position.x, 80.);
|
||||
assert_eq!(child.position.y - root.position.y, 90.);
|
||||
assert_eq!(f.project, before);
|
||||
assert_eq!(semantic.project(), &candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_selected_roots_keep_one_rigid_group_translation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let second = f
|
||||
.project
|
||||
.add_element(kind, layer, "Second work", None)
|
||||
.unwrap();
|
||||
let second_node = f.project.place(f.diagram, second, xy(240., 300.));
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Second responsibility",
|
||||
owner,
|
||||
second,
|
||||
);
|
||||
let selected = BTreeMap::from([(f.node, f.work), (second_node, second)]);
|
||||
let scope =
|
||||
plan::assignment::capture(&f.project, f.diagram, &selected, f.node).unwrap();
|
||||
let semantic = plan::assignment::plan_semantics(
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
scope.affected(),
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-220., 180.)]);
|
||||
let placed = plan::assignment::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let second = &placed.diagram.nodes[&second_node];
|
||||
assert_eq!(second.position.x - root.position.x, 80.);
|
||||
assert_eq!(second.position.y - root.position.y, 120.);
|
||||
assert!(
|
||||
plan::assignment::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,117 @@ async fn mount() -> (Fixture, Mounted) {
|
||||
mounted.settle(&mut f.app).await;
|
||||
(f, mounted)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_tree_outgoing_boundary_motion_preview_release_and_cancel() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
f.app.layer = layer;
|
||||
f.app.window_size = iced::Size::new(1440., 900.);
|
||||
let _ = f.app.update(Message::Select(f.work));
|
||||
let mut mounted = Mounted::new(&mut f.app).await;
|
||||
f.app.zoom = 0.75;
|
||||
f.app.pan = Vector::new(20., 15.);
|
||||
f.app.fit_to_view = false;
|
||||
mounted.settle(&mut f.app).await;
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(from),
|
||||
)
|
||||
.await;
|
||||
// Separate input/update/layout turns expose the held boundary transition
|
||||
// that a one-batch distant drop cannot exercise.
|
||||
for x in [620., 560., 510., 499., 490.] {
|
||||
let to = at(&mut mounted, &f, xy(x, 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::CursorMoved { position: to })],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(
|
||||
!f.app.allocation_drag.feedback.as_ref().unwrap().refused,
|
||||
"{layer:?} x={x}: {}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.preview.is_some());
|
||||
let _ = mounted.pixels(&f.app);
|
||||
}
|
||||
let expected = f
|
||||
.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.placement
|
||||
.diagram
|
||||
.clone();
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
mounted.settle(&mut f.app).await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: to }),
|
||||
],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[key(keyboard::key::Named::Escape)],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
fn canvas_bounds(mounted: &mut Mounted, app: &App) -> Rectangle {
|
||||
mounted
|
||||
.geometry(app)
|
||||
@@ -89,10 +200,7 @@ async fn refused_click_keeps_field(mounted: &mut Mounted, f: &mut Fixture, field
|
||||
);
|
||||
assert!(!messages.iter().any(|m| matches!(
|
||||
m,
|
||||
Message::Select(_)
|
||||
| Message::CanvasSelect(..)
|
||||
| Message::SelectNode(_)
|
||||
| Message::MoveNode(..)
|
||||
Message::Select(_) | Message::SelectNode(_) | Message::MoveNode(..)
|
||||
)));
|
||||
assert_eq!(f.app.document.bundle(), &before);
|
||||
assert_eq!((f.app.selected, f.app.selected_node), selected);
|
||||
@@ -484,6 +592,7 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
if pan.y == -100. {
|
||||
// A genuine clipping refusal in the same held gesture, then
|
||||
// recovery at the original exact point: no camera rebasing.
|
||||
let retained = f.app.allocation_drag.preview.clone().unwrap();
|
||||
let clipped = at(&mut mounted, &f, xy(160., 110.));
|
||||
mounted
|
||||
.batch(
|
||||
@@ -495,7 +604,19 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
)
|
||||
.await;
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&retained
|
||||
));
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(canvas_bounds(&mut mounted, &f.app), canvas);
|
||||
assert_eq!(destination_bounds(&mut mounted, &f.app), slot);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
|
||||
@@ -237,10 +237,7 @@ fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() {
|
||||
));
|
||||
assert!(!events.iter().any(|m| matches!(
|
||||
m,
|
||||
Message::Select(_)
|
||||
| Message::CanvasSelect(..)
|
||||
| Message::SelectNode(_)
|
||||
| Message::MoveNode(..)
|
||||
Message::Select(_) | Message::SelectNode(_) | Message::MoveNode(..)
|
||||
)));
|
||||
send(&mut f.app, events);
|
||||
assert_eq!((f.app.selected, f.app.selected_node), selection);
|
||||
@@ -1242,6 +1239,7 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
inspection_targets: vec![],
|
||||
text: "A card blocks this drop".into(),
|
||||
refused: true,
|
||||
ghost_delta: None,
|
||||
};
|
||||
assert_eq!(feedback.destination_identity(), "No eligible destination");
|
||||
feedback.text = "Release inside the captured canvas".into();
|
||||
@@ -1249,3 +1247,361 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
feedback.refused = false;
|
||||
assert_eq!(feedback.destination_identity(), "Outside performers");
|
||||
}
|
||||
|
||||
fn outgoing_fixture(layer: Layer) -> Fixture {
|
||||
let mut f = fixture(layer);
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
let mut project = f.app.project().clone();
|
||||
let d = project.diagrams.get_mut(&f.diagram).unwrap();
|
||||
let performer = d.nodes.values_mut().find(|n| n.element == f.a).unwrap();
|
||||
performer.position = xy(500., 100.);
|
||||
performer.size = Size {
|
||||
width: 300.,
|
||||
height: 300.,
|
||||
};
|
||||
d.nodes.get_mut(&f.node).unwrap().position = xy(540., 190.);
|
||||
replace(&mut f, project);
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_boundary_drop_clears_source_without_changing_other_placements() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(!feedback.refused, "{}", feedback.text);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().unwrap();
|
||||
let expected_diagram = preview.placement.diagram.clone();
|
||||
let work = &expected_diagram.nodes[&f.node];
|
||||
assert!(work.position.x + work.size.width <= 490.);
|
||||
assert_eq!(work.position.y, 190.);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&expected_diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected_diagram);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let after = f.app.project().clone();
|
||||
let saved = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &after);
|
||||
assert_eq!(f.app.document.bundle(), &saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_collision_refuses_release_without_source_or_history_mutation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Unrelated work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let obstacle_node = project.place(f.diagram, obstacle, xy(240., 190.));
|
||||
replace(&mut f, project);
|
||||
// A refused release must preserve an existing redo branch, too.
|
||||
let _ = f.app.update(Message::Command(Command::Rename(
|
||||
f.work,
|
||||
"Temporary".into(),
|
||||
)));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let owner = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(owner.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).x < owner.region.x);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None),
|
||||
"The pointer must reach empty canvas, not be occluded by the obstacle"
|
||||
);
|
||||
let candidate = active
|
||||
.cache
|
||||
.iter()
|
||||
.find(|(destination, _)| *destination == plan::Destination::OutsidePerformers)
|
||||
.unwrap()
|
||||
.1
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => &scope.placements,
|
||||
Prepared::Assignment(scope) => scope.affected(),
|
||||
_ => panic!("expected assignment gesture"),
|
||||
};
|
||||
let requested = xy(370., 190.);
|
||||
let presentation = plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
candidate.place(requested, presentation),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = clearance::requests(
|
||||
(f.app.project(), f.app.document.view_index()),
|
||||
(candidate.project(), candidate.index()),
|
||||
f.diagram,
|
||||
affected,
|
||||
f.node,
|
||||
requested,
|
||||
(p.stamp.world(from), p.stamp.world(to)),
|
||||
);
|
||||
assert_eq!(
|
||||
proposed.len(),
|
||||
1,
|
||||
"a genuine left crossing must propose clearance"
|
||||
);
|
||||
assert!(proposed[0].x < requested.x);
|
||||
let work = &before.diagrams[&f.diagram].nodes[&f.node];
|
||||
let obstacle = &before.diagrams[&f.diagram].nodes[&obstacle_node];
|
||||
assert!(obstacle.position.x + obstacle.size.width < 490.);
|
||||
assert!(proposed[0].x + work.size.width <= owner.region.x - 10.);
|
||||
assert!(proposed[0].x < obstacle.position.x + obstacle.size.width);
|
||||
assert!(obstacle.position.x < proposed[0].x + work.size.width);
|
||||
assert_eq!(proposed[0].y, obstacle.position.y);
|
||||
assert!(matches!(
|
||||
candidate.place(proposed[0], presentation),
|
||||
Err(plan::Refusal::LayoutRequired(message)) if message.contains("overlapping cards")
|
||||
));
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(feedback.refused, "{}", feedback.text);
|
||||
assert!(feedback.ghost_delta.is_some());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
let _ = f.app.update(release);
|
||||
assert!(
|
||||
f.app.status.contains("overlapping cards"),
|
||||
"{}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_preserves_source_enclosure_and_allocated_sibling() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let sibling = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Remaining work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let sibling_node = project.place(f.diagram, sibling, xy(540., 450.));
|
||||
let allocation = project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Keep responsibility",
|
||||
f.a,
|
||||
sibling,
|
||||
);
|
||||
replace(&mut f, project);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let view = projection::project_view(&before, f.diagram);
|
||||
let owner = view.nodes.iter().find(|n| n.element == f.a).unwrap();
|
||||
let bottom = owner.position.y + owner.size.height;
|
||||
let stored_owner = &before.diagrams[&f.diagram].nodes[&owner.id];
|
||||
assert!(
|
||||
bottom > stored_owner.position.y + stored_owner.size.height,
|
||||
"The retained sibling must enlarge the projected source enclosure"
|
||||
);
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(660., bottom + 1.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let target = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(target.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).y >= target.region.y + target.region.height);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
||||
);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
||||
assert!(!f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
let requested = xy(540., 190. + to.y - from.y);
|
||||
assert!(matches!(
|
||||
preview.candidate.place(
|
||||
requested,
|
||||
plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
}
|
||||
),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let moved = &preview.placement.diagram.nodes[&f.node];
|
||||
assert_eq!(moved.position.x, 540.);
|
||||
assert!(moved.position.y >= bottom + 10.);
|
||||
assert!(moved.position.y > requested.y);
|
||||
assert_eq!(
|
||||
preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == owner.id)
|
||||
.unwrap(),
|
||||
owner
|
||||
);
|
||||
let remaining = preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == sibling_node)
|
||||
.unwrap();
|
||||
assert_eq!(remaining.visual_parent, Some(owner.id));
|
||||
assert_eq!(
|
||||
remaining.position,
|
||||
before.diagrams[&f.diagram].nodes[&sibling_node].position
|
||||
);
|
||||
assert_eq!(
|
||||
&preview.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&preview.placement.diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
let mut expected = preview.project().clone();
|
||||
expected
|
||||
.diagrams
|
||||
.insert(f.diagram, preview.placement.diagram.clone());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
||||
assert_eq!(f.app.document.revision(), 1);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), f.work),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), sibling),
|
||||
Some(f.a)
|
||||
);
|
||||
assert_eq!(
|
||||
&f.app.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
let after = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_motion_retains_last_scene_but_release_never_commits_it() {
|
||||
let mut f = outgoing_fixture(Layer::Sa);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Unrelated work", None)
|
||||
.unwrap();
|
||||
project.place(f.diagram, obstacle, xy(1250., 120.));
|
||||
replace(&mut f, project);
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let p = admit_press(&mut f);
|
||||
let clear = screen(xy(450., 225.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(clear),
|
||||
)));
|
||||
let last = f.app.allocation_drag.preview.clone().expect(&f.app.status);
|
||||
// Another work card blocks this location: the prior legal outgoing scene is display
|
||||
// state only, and must not become a fallback command on release.
|
||||
let blocked = screen(xy(1260., 130.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&last
|
||||
));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Compact neutral tool palette. Tool state never changes its allocated width.
|
||||
use super::*;
|
||||
use crate::connection_tool::Tool;
|
||||
use iced::{
|
||||
Point, Rectangle, Size, Theme, mouse,
|
||||
widget::{
|
||||
@@ -12,7 +11,7 @@ use iced::{
|
||||
#[derive(Clone, Copy)]
|
||||
enum Icon {
|
||||
Element(Kind),
|
||||
Connection(Tool),
|
||||
Arrow,
|
||||
Existing,
|
||||
Select,
|
||||
Collapse(bool),
|
||||
@@ -77,59 +76,10 @@ impl canvas::Program<Message> for Icon {
|
||||
);
|
||||
}
|
||||
}
|
||||
Self::Connection(connection) => match connection {
|
||||
// The OA and SA work-flow tools share a visual family; they
|
||||
// never coexist in a palette. Keep their small glyph legible.
|
||||
Tool::Interaction | Tool::FunctionalExchange => {
|
||||
for x in [1., 11.] {
|
||||
f.stroke(
|
||||
&Path::rounded_rectangle(
|
||||
Point::new(x, 5.),
|
||||
Size::new(4., 6.),
|
||||
1.5.into(),
|
||||
),
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
f.stroke(&line(&[(5., 8.), (10., 8.)]), stroke);
|
||||
f.stroke(&line(&[(7.5, 5.5), (10., 8.), (7.5, 10.5)]), stroke);
|
||||
}
|
||||
Tool::CommunicationMean => {
|
||||
// Participant endpoints, without implying a fixed direction.
|
||||
for x in [1., 11.] {
|
||||
f.stroke(
|
||||
&Path::rectangle(Point::new(x, 4.), Size::new(4., 8.)),
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
f.stroke(&line(&[(5., 8.), (11., 8.)]), stroke);
|
||||
}
|
||||
Tool::BoundaryExchange => {
|
||||
// Ports on facing participant boundaries. Standard exchanges
|
||||
// are unoriented, so the generic tool has no arrowhead.
|
||||
for x in [2., 14.] {
|
||||
f.stroke(&line(&[(x, 2.), (x, 14.)]), stroke);
|
||||
}
|
||||
for x in [1., 12.] {
|
||||
f.stroke(
|
||||
&Path::rectangle(Point::new(x, 6.), Size::new(3., 4.)),
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
f.stroke(&line(&[(4., 8.), (12., 8.)]), stroke);
|
||||
}
|
||||
Tool::Allocation => {
|
||||
// Work enters a performer: allocation can appear as nesting
|
||||
// rather than a relationship line in allocated-work views.
|
||||
f.stroke(&line(&[(1., 2.), (1., 14.), (15., 14.), (15., 2.)]), stroke);
|
||||
f.stroke(
|
||||
&Path::rounded_rectangle(Point::new(5., 9.), Size::new(6., 3.), 1.into()),
|
||||
stroke,
|
||||
);
|
||||
f.stroke(&line(&[(8., 1.), (8., 7.)]), stroke);
|
||||
f.stroke(&line(&[(5., 4.), (8., 7.), (11., 4.)]), stroke);
|
||||
}
|
||||
},
|
||||
Self::Arrow => {
|
||||
f.stroke(&line(&[(1., 8.), (14., 8.), (10., 4.)]), stroke);
|
||||
f.stroke(&line(&[(14., 8.), (10., 12.)]), stroke);
|
||||
}
|
||||
Self::Existing => {
|
||||
f.stroke(
|
||||
&Path::rectangle(Point::new(1., 1.), Size::new(10., 10.)),
|
||||
@@ -284,7 +234,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
}
|
||||
content = content.push(tool(
|
||||
app,
|
||||
Icon::Connection(*connection),
|
||||
Icon::Arrow,
|
||||
connection.label(),
|
||||
Message::Connection(crate::connection_edit::Event::Choose(Some(*connection))),
|
||||
app.connection
|
||||
|
||||
@@ -1,904 +0,0 @@
|
||||
//! Canvas inspection retains the displayed mixed-layer surface; navigation follows the model.
|
||||
use super::*;
|
||||
use crate::diagram_usage::Target;
|
||||
use crate::model_menu::{Event as MenuEvent, Request};
|
||||
|
||||
struct Fixture {
|
||||
app: App,
|
||||
diagram: Id,
|
||||
process: Id,
|
||||
placement: Id,
|
||||
edge: Id,
|
||||
}
|
||||
|
||||
fn fixture() -> Fixture {
|
||||
let mut p = Project::empty();
|
||||
let process = p
|
||||
.add_element(Kind::Process, Layer::Oa, "Operational process", None)
|
||||
.unwrap();
|
||||
let capability = p
|
||||
.add_element(Kind::Capability, Layer::Oa, "Operational capability", None)
|
||||
.unwrap();
|
||||
let edge = p.relate(
|
||||
RelationKind::ChainInvolvement,
|
||||
"Enables",
|
||||
process,
|
||||
capability,
|
||||
);
|
||||
p.ensure_diagram(Layer::Oa);
|
||||
let diagram = p.ensure_diagram(Layer::Sa);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let placement = p.place(diagram, process, Position { x: 0., y: 0. });
|
||||
p.place(diagram, capability, Position { x: 500., y: 0. });
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Sa;
|
||||
app.active_diagram = Some(diagram);
|
||||
app.selected = diagram;
|
||||
app.refresh_draft();
|
||||
app.reconcile_multiselect();
|
||||
assert!(
|
||||
project_view(app.project(), diagram)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == edge)
|
||||
);
|
||||
Fixture {
|
||||
app,
|
||||
diagram,
|
||||
process,
|
||||
placement,
|
||||
edge,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_surface(f: &Fixture) {
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert_eq!(f.app.layer, Layer::Sa);
|
||||
assert_eq!(f.app.active_diagram, Some(f.diagram));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_projection_switch_restores_all_authored_content_and_one_step_history() {
|
||||
let mut f = fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Projection(ProjectionKind::Allocation));
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert!(project_view(f.app.project(), f.diagram).nodes.is_empty());
|
||||
assert_eq!(f.app.project().diagrams[&f.diagram].nodes.len(), 2);
|
||||
let switched = f.app.document.snapshot();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &switched.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &switched.bundle);
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Projection(ProjectionKind::Traceability));
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert!(
|
||||
project_view(f.app.project(), f.diagram)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == f.edge)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_edge_and_group_inspection_preserve_surface_and_history() {
|
||||
for grouped in [false, true] {
|
||||
let mut f = fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
if grouped {
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::DiagramSelection(multiselect::Event::Choose(
|
||||
f.app.multiselect_context().unwrap(),
|
||||
Target::Placement(f.placement),
|
||||
multiselect::Choice::Replace,
|
||||
)));
|
||||
}
|
||||
let message = if grouped {
|
||||
Message::DiagramSelection(multiselect::Event::Choose(
|
||||
f.app.multiselect_context().unwrap(),
|
||||
Target::Edge(f.edge),
|
||||
multiselect::Choice::Add,
|
||||
))
|
||||
} else {
|
||||
Message::CanvasSelect(f.app.route_context().unwrap(), f.edge)
|
||||
};
|
||||
let _ = f.app.update(message);
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
assert_eq!(f.app.draft.as_ref().unwrap().original, f.edge);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().primary(),
|
||||
Some(&Target::Edge(f.edge))
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().targets().len(),
|
||||
if grouped { 2 } else { 1 }
|
||||
);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
fn invoke(f: &mut Fixture, object: Id, placement: Option<Id>, action: Message) {
|
||||
let mut request = Request::object(object, iced::Point::ORIGIN);
|
||||
request.canvas = f.app.route_context();
|
||||
request.placement = placement;
|
||||
let _ = f.app.update(Message::Menu(MenuEvent::Open(request)));
|
||||
let token = f.app.model_menu.as_ref().unwrap().token;
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Menu(MenuEvent::Invoke(token, Box::new(action))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_context_properties_and_hide_keep_the_captured_surface() {
|
||||
let mut f = fixture();
|
||||
let edge = f.edge;
|
||||
invoke(&mut f, edge, None, Message::EditProperties(edge));
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, edge);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let before = f.app.document.snapshot();
|
||||
let process = f.process;
|
||||
let placement = f.placement;
|
||||
invoke(&mut f, process, Some(placement), Message::HideSelected);
|
||||
assert_surface(&f);
|
||||
assert!(
|
||||
!f.app.project().diagrams[&f.diagram]
|
||||
.nodes
|
||||
.contains_key(&placement)
|
||||
);
|
||||
assert!(f.app.project().elements.contains_key(&process));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_explicit_navigation_still_follows_the_target_layer() {
|
||||
let mut f = fixture();
|
||||
let process = f.process;
|
||||
let edge = f.edge;
|
||||
invoke(&mut f, edge, None, Message::Select(process));
|
||||
assert_eq!(f.app.selected, process);
|
||||
assert_eq!(f.app.layer, Layer::Oa);
|
||||
assert_ne!(f.app.active_diagram, Some(f.diagram));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_navigation_to_the_inspected_cross_layer_object_follows_its_layer() {
|
||||
let mut f = fixture();
|
||||
let _ = f.app.update(Message::CanvasSelect(
|
||||
f.app.route_context().unwrap(),
|
||||
f.edge,
|
||||
));
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
let before = f.app.document.snapshot();
|
||||
let _ = f.app.update(Message::Select(f.edge));
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
assert_eq!(f.app.layer, Layer::Oa);
|
||||
assert_ne!(f.app.active_diagram, Some(f.diagram));
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_invalid_draft_and_stale_packets_preserve_selection() {
|
||||
let mut f = fixture();
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::Field("/id".into(), "-".into()));
|
||||
let draft = serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, f.edge));
|
||||
assert_eq!(f.app.selected, f.diagram);
|
||||
assert_surface(&f);
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
draft
|
||||
);
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::DiagramSelection(multiselect::Event::Choose(
|
||||
f.app.multiselect_context().unwrap(),
|
||||
Target::Edge(f.edge),
|
||||
multiselect::Choice::Replace,
|
||||
)));
|
||||
assert_eq!(f.app.selected, f.diagram);
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
draft
|
||||
);
|
||||
let _ = f.app.update(Message::RevertProperties);
|
||||
f.app.canvas_epoch += 1;
|
||||
let _ = f.app.update(Message::CanvasSelect(context, f.edge));
|
||||
assert_eq!(f.app.selected, f.diagram);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_layer_port_inspection_and_connection_start_retain_the_view() {
|
||||
let mut p = Project::empty();
|
||||
let owner = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
||||
.unwrap();
|
||||
let port = Id::new_v4();
|
||||
let element = make_port(
|
||||
&p,
|
||||
port,
|
||||
owner,
|
||||
"Output",
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p.elements.insert(port, element);
|
||||
p.ensure_diagram(Layer::Sa);
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let placement = p.place(diagram, owner, Position { x: 0., y: 0. });
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
app.selected = diagram;
|
||||
app.refresh_draft();
|
||||
app.reconcile_multiselect();
|
||||
assert!(
|
||||
project_view(app.project(), diagram)
|
||||
.ports
|
||||
.iter()
|
||||
.any(|p| p.element == port)
|
||||
);
|
||||
let _ = app.update(Message::CanvasSelect(app.route_context().unwrap(), port));
|
||||
assert_eq!(app.selected, port);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
assert_eq!(app.layer, Layer::Oa);
|
||||
assert_eq!(
|
||||
app.multiselect_snapshot().unwrap().primary(),
|
||||
Some(&Target::Port { port, placement })
|
||||
);
|
||||
// Reset inspection so the endpoint path cannot pass by selecting the same ID.
|
||||
app.selected = diagram;
|
||||
app.refresh_draft();
|
||||
let context = app.route_context().unwrap();
|
||||
// Mixed canvases can retain a tool from either perspective.
|
||||
app.connection = Some(crate::connection_edit::Session::new(
|
||||
app.project(),
|
||||
&project_view(app.project(), diagram),
|
||||
context,
|
||||
crate::connection_tool::Tool::FunctionalExchange,
|
||||
));
|
||||
let _ = app.update(Message::Connection(
|
||||
crate::connection_edit::Event::Endpoint(context, port),
|
||||
));
|
||||
assert_eq!(app.selected, port);
|
||||
assert_eq!(app.layer, Layer::Oa);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
assert_eq!(app.connection.as_ref().unwrap().source, Some(port));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
fn authoring_fixture(performer: bool) -> (Fixture, Id) {
|
||||
let mut f = fixture();
|
||||
let mut project = f.app.project().clone();
|
||||
let owner = project
|
||||
.add_element(
|
||||
if performer {
|
||||
Kind::Entity
|
||||
} else {
|
||||
Kind::Activity
|
||||
},
|
||||
Layer::Oa,
|
||||
"Operational owner",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let target = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Operational target", None)
|
||||
.unwrap();
|
||||
let placement = project.place(f.diagram, owner, Position { x: 0., y: 400. });
|
||||
project.place(f.diagram, target, Position { x: 500., y: 400. });
|
||||
f.app = App::with_project(project);
|
||||
f.app.page = Page::Diagram;
|
||||
f.app.layer = Layer::Sa;
|
||||
f.app.active_diagram = Some(f.diagram);
|
||||
f.app.selected = f.diagram;
|
||||
f.app.refresh_draft();
|
||||
f.app.reconcile_multiselect();
|
||||
f.process = owner;
|
||||
f.placement = placement;
|
||||
(f, target)
|
||||
}
|
||||
|
||||
fn assert_source_pair(app: &App) {
|
||||
let compiled = oa_core::source_project::compile_bundle(
|
||||
app.document.bundle(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&compiled.decoded.project, app.project());
|
||||
assert_eq!(&compiled.bundle, app.document.bundle());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_relationship_actions_keep_operational_source_and_commit_one_step() {
|
||||
for context_action in [false, true] {
|
||||
let (mut f, target) = authoring_fixture(false);
|
||||
let before = f.app.document.snapshot();
|
||||
let owner = f.process;
|
||||
let placement = f.placement;
|
||||
if context_action {
|
||||
invoke(
|
||||
&mut f,
|
||||
owner,
|
||||
Some(placement),
|
||||
Message::AddRelationKind(RelationKind::Interaction, false),
|
||||
);
|
||||
} else {
|
||||
let _ = f.app.update(Message::SelectNode(placement));
|
||||
let _ = f.app.update(Message::AddRelation);
|
||||
}
|
||||
assert_surface(&f);
|
||||
let Some(crate::workflow::Workflow::Relation(relation)) = &f.app.workflow else {
|
||||
panic!("relationship form did not open: {}", f.app.status);
|
||||
};
|
||||
assert_eq!(
|
||||
relation.source.as_ref().map(|source| source.id),
|
||||
Some(owner)
|
||||
);
|
||||
assert_eq!(relation.kind, RelationKind::Interaction);
|
||||
let saved = f
|
||||
.app
|
||||
.workflow
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.recovery_snapshot()
|
||||
.unwrap();
|
||||
assert_eq!(saved["Relation"]["layer"], "Oa");
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Workflow(crate::workflow::Event::Target(Choice {
|
||||
id: target,
|
||||
label: "Operational target".into(),
|
||||
})));
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Workflow(crate::workflow::Event::Apply));
|
||||
assert!(f.app.workflow.is_none(), "{}", f.app.status);
|
||||
let created: Vec<_> = f
|
||||
.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|relation| !before.decoded.project.relations.contains_key(&relation.id))
|
||||
.collect();
|
||||
assert_eq!(created.len(), 1);
|
||||
assert_eq!(
|
||||
(created[0].kind(), created[0].source(), created[0].target()),
|
||||
(RelationKind::Interaction, owner, target)
|
||||
);
|
||||
assert_source_pair(&f.app);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_add_child_command_uses_parent_layer_for_containment_and_allocation() {
|
||||
for performer in [false, true] {
|
||||
let (mut f, _) = authoring_fixture(performer);
|
||||
let before = f.app.document.snapshot();
|
||||
let owner = f.process;
|
||||
let placement = f.placement;
|
||||
invoke(
|
||||
&mut f,
|
||||
owner,
|
||||
Some(placement),
|
||||
Message::CreateContextElement(Kind::Activity, true),
|
||||
);
|
||||
assert_surface(&f);
|
||||
let created: Vec<_> = f
|
||||
.app
|
||||
.project()
|
||||
.elements
|
||||
.values()
|
||||
.filter(|element| !before.decoded.project.elements.contains_key(&element.id))
|
||||
.collect();
|
||||
assert_eq!(created.len(), 1, "{}", f.app.status);
|
||||
let child = created[0].id;
|
||||
assert_eq!(created[0].kind(), Kind::Activity);
|
||||
assert_eq!(created[0].layer(), Layer::Oa);
|
||||
assert_eq!(
|
||||
created[0].parent,
|
||||
if performer { None } else { Some(owner) }
|
||||
);
|
||||
let allocations: Vec<_> = f
|
||||
.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|relation| !before.decoded.project.relations.contains_key(&relation.id))
|
||||
.collect();
|
||||
assert_eq!(allocations.len(), usize::from(performer));
|
||||
if performer {
|
||||
assert_eq!(
|
||||
(
|
||||
allocations[0].kind(),
|
||||
allocations[0].source(),
|
||||
allocations[0].target()
|
||||
),
|
||||
(RelationKind::Allocation, owner, child)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
f.app.project().diagrams[&f.diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.any(|node| node.element == child)
|
||||
);
|
||||
assert_source_pair(&f.app);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_canvas_root_creation_still_uses_the_active_perspective() {
|
||||
let (mut f, _) = authoring_fixture(false);
|
||||
let _ = f.app.update(Message::SelectNode(f.placement));
|
||||
let before = f.app.document.snapshot();
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::CreateContextElement(Kind::Function, false));
|
||||
assert_surface(&f);
|
||||
let created: Vec<_> = f
|
||||
.app
|
||||
.project()
|
||||
.elements
|
||||
.values()
|
||||
.filter(|element| !before.decoded.project.elements.contains_key(&element.id))
|
||||
.collect();
|
||||
assert_eq!(created.len(), 1, "{}", f.app.status);
|
||||
assert_eq!(created[0].kind(), Kind::Function);
|
||||
assert_eq!(created[0].layer(), Layer::Sa);
|
||||
assert_eq!(created[0].parent, None);
|
||||
assert_source_pair(&f.app);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_creation_navigates_to_a_parent_layer_preferred_view_only_when_needed() {
|
||||
for existing_preferred in [false, true] {
|
||||
let (mut f, _) = authoring_fixture(false);
|
||||
let mut project = f.app.project().clone();
|
||||
for diagram in project
|
||||
.diagrams
|
||||
.values_mut()
|
||||
.filter(|d| d.layer == Layer::Oa)
|
||||
{
|
||||
diagram.projection = if existing_preferred {
|
||||
ProjectionKind::Allocation
|
||||
} else {
|
||||
ProjectionKind::Overview
|
||||
};
|
||||
}
|
||||
project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Capabilities;
|
||||
f.app = App::with_project(project);
|
||||
let _ = f.app.update(Message::Select(f.process));
|
||||
let _ = f.app.update(Message::Diagram(Choice {
|
||||
id: f.diagram,
|
||||
label: "System capabilities".into(),
|
||||
}));
|
||||
assert_eq!(f.app.selected, f.process);
|
||||
assert_eq!(f.app.layer, Layer::Sa);
|
||||
assert_eq!(f.app.active_diagram, Some(f.diagram));
|
||||
let before = f.app.document.snapshot();
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::CreateContextElement(Kind::Activity, true));
|
||||
assert_eq!(f.app.layer, Layer::Oa, "{}", f.app.status);
|
||||
let diagram = f.app.active_diagram.unwrap();
|
||||
assert_ne!(diagram, f.diagram);
|
||||
assert_eq!(f.app.project().diagrams[&diagram].layer, Layer::Oa);
|
||||
assert_eq!(
|
||||
f.app.project().diagrams[&diagram].projection,
|
||||
ProjectionKind::Allocation
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.project().diagrams.len(),
|
||||
before.decoded.project.diagrams.len() + usize::from(!existing_preferred)
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.project().elements[&f.app.selected].parent,
|
||||
Some(f.process)
|
||||
);
|
||||
assert_source_pair(&f.app);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
}
|
||||
}
|
||||
|
||||
// Regression scope: a contextual click may accept a valid property draft, but
|
||||
// must not inherit the draft object's semantic-layer navigation. If acceptance
|
||||
// changes the originating diagram identity or hides the clicked target, retain
|
||||
// that one accepted edit and refuse the old click. Do not infer a new target or
|
||||
// reinterpret an old surface packet on a replacement diagram.
|
||||
fn draft_click_fixture() -> (Fixture, Id) {
|
||||
let mut f = fixture();
|
||||
let mut project = f.app.project().clone();
|
||||
let capability = project
|
||||
.add_element(Kind::Capability, Layer::Oa, "Second capability", None)
|
||||
.unwrap();
|
||||
let other_edge = project.relate(
|
||||
RelationKind::ChainInvolvement,
|
||||
"Also enables",
|
||||
f.process,
|
||||
capability,
|
||||
);
|
||||
project.place(f.diagram, capability, Position { x: 500., y: 300. });
|
||||
f.app = App::with_project(project);
|
||||
f.app.page = Page::Diagram;
|
||||
f.app.layer = Layer::Sa;
|
||||
f.app.active_diagram = Some(f.diagram);
|
||||
f.app.selected = f.diagram;
|
||||
f.app.refresh_draft();
|
||||
f.app.reconcile_multiselect();
|
||||
assert!(
|
||||
project_view(f.app.project(), f.diagram)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|edge| edge.id == other_edge)
|
||||
);
|
||||
(f, other_edge)
|
||||
}
|
||||
|
||||
fn expected_draft_commit(app: &App, command: Command) -> Document {
|
||||
let mut expected = Document::from_bundle(
|
||||
app.document.bundle().clone(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
expected.execute(command).unwrap();
|
||||
expected
|
||||
}
|
||||
|
||||
fn assert_accepted_draft(app: &App, expected: &Document, revision: u64) {
|
||||
assert_eq!(app.project(), expected.project());
|
||||
assert_eq!(app.document.bundle(), expected.bundle());
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
assert!(!app.draft_dirty());
|
||||
assert!(app.document.can_undo());
|
||||
assert!(!app.document.can_redo());
|
||||
assert_source_pair(app);
|
||||
}
|
||||
|
||||
fn assert_one_draft_history(
|
||||
app: &mut App,
|
||||
before: &oa_core::source_project::CompiledSourceProject,
|
||||
expected: &Document,
|
||||
) {
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(
|
||||
!app.document.can_undo(),
|
||||
"the click must not add a second edit"
|
||||
);
|
||||
assert!(app.document.can_redo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), expected.project());
|
||||
assert_eq!(app.document.bundle(), expected.bundle());
|
||||
assert!(!app.document.can_redo());
|
||||
}
|
||||
|
||||
fn pending_edge_name(f: &mut Fixture) {
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, f.edge));
|
||||
assert_surface(f);
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Field("/name".into(), "Reviewed enables".into()));
|
||||
assert!(f.app.draft_dirty());
|
||||
assert!(f.app.draft.as_ref().unwrap().command().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_cross_layer_draft_then_canvas_edge_retains_surface_and_one_edit() {
|
||||
let (mut f, other_edge) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let expected =
|
||||
expected_draft_commit(&f.app, Command::Rename(f.edge, "Reviewed enables".into()));
|
||||
pending_edge_name(&mut f);
|
||||
// Capture after drafting, so this exercises successful draft application,
|
||||
// not refusal of a packet made stale by the property edit itself.
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, other_edge));
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, other_edge);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().primary(),
|
||||
Some(&Target::Edge(other_edge))
|
||||
);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_cross_layer_draft_then_node_click_retains_surface_and_one_edit() {
|
||||
let (mut f, _) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let expected =
|
||||
expected_draft_commit(&f.app, Command::Rename(f.edge, "Reviewed enables".into()));
|
||||
pending_edge_name(&mut f);
|
||||
let _ = f.app.update(Message::SelectNode(f.placement));
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, f.process);
|
||||
assert_eq!(f.app.selected_node, Some(f.placement));
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().primary(),
|
||||
Some(&Target::Placement(f.placement))
|
||||
);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_reidentity_draft_commits_but_refuses_the_old_surface_click() {
|
||||
let (mut f, other_edge) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let replacement = Id::new_v4();
|
||||
let expected = expected_draft_commit(&f.app, Command::Reidentify(f.diagram, replacement));
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Field("/id".into(), replacement.to_string()));
|
||||
assert!(f.app.draft_dirty());
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, other_edge));
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert!(!f.app.project().diagrams.contains_key(&context.diagram));
|
||||
assert!(
|
||||
f.app
|
||||
.active_diagram
|
||||
.is_some_and(|id| f.app.project().diagrams.contains_key(&id)),
|
||||
"never restore a removed diagram identity"
|
||||
);
|
||||
assert_ne!(f.app.selected, other_edge, "refuse the old surface click");
|
||||
assert!(
|
||||
f.app
|
||||
.multiselect_snapshot()
|
||||
.is_none_or(|s| !s.targets().contains(&Target::Edge(other_edge)))
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.status,
|
||||
"Properties applied. The view changed; select a visible target again."
|
||||
);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_draft_commits_but_refuses_the_now_hidden_canvas_target() {
|
||||
let (mut f, other_edge) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut diagram = f.app.project().diagrams[&f.diagram].clone();
|
||||
diagram.projection = ProjectionKind::Allocation;
|
||||
let expected = expected_draft_commit(&f.app, Command::EditDiagram(f.diagram, diagram));
|
||||
let _ = f.app.update(Message::FieldShape(
|
||||
"/projection".into(),
|
||||
serde_json::to_value(ProjectionKind::Allocation).unwrap(),
|
||||
));
|
||||
assert!(f.app.draft_dirty());
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, other_edge));
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_surface(&f);
|
||||
assert!(
|
||||
!project_view(f.app.project(), f.diagram)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|edge| edge.id == other_edge)
|
||||
);
|
||||
assert_ne!(
|
||||
f.app.selected, other_edge,
|
||||
"do not inspect a no-longer-visible hit"
|
||||
);
|
||||
assert!(
|
||||
f.app
|
||||
.multiselect_snapshot()
|
||||
.is_none_or(|s| !s.targets().contains(&Target::Edge(other_edge)))
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.status,
|
||||
"Properties applied. The view changed; select a visible target again."
|
||||
);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_cross_layer_draft_blocks_contextual_clicks_without_changing_history() {
|
||||
for click in 0..3 {
|
||||
let (mut f, other_edge) = draft_click_fixture();
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::CanvasSelect(context, f.edge));
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Field("/id".into(), "not-a-uuid".into()));
|
||||
assert!(f.app.draft_dirty());
|
||||
let expected_error = f.app.draft.as_ref().unwrap().command().unwrap_err();
|
||||
let draft =
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
let selection = f.app.multiselect_snapshot().unwrap();
|
||||
let message = match click {
|
||||
0 => Message::CanvasSelect(f.app.route_context().unwrap(), other_edge),
|
||||
1 => Message::SelectNode(f.placement),
|
||||
_ => Message::DiagramSelection(multiselect::Event::Choose(
|
||||
f.app.multiselect_context().unwrap(),
|
||||
Target::Edge(other_edge),
|
||||
multiselect::Choice::Add,
|
||||
)),
|
||||
};
|
||||
let _ = f.app.update(message);
|
||||
if click < 2 {
|
||||
assert_eq!(f.app.status, expected_error);
|
||||
} else {
|
||||
assert_eq!(
|
||||
f.app.status,
|
||||
"Finish or cancel the active edit before changing the diagram selection."
|
||||
);
|
||||
}
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(!f.app.document.can_redo());
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
draft
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().targets(),
|
||||
selection.targets()
|
||||
);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().primary(),
|
||||
selection.primary()
|
||||
);
|
||||
if click == 0 {
|
||||
// Pre-dispatch stale rejection applies no draft and must not emit
|
||||
// either success feedback or a new validation message.
|
||||
let stale = f.app.route_context().unwrap();
|
||||
f.app.canvas_epoch += 1;
|
||||
f.app.status = "Keep previous feedback".into();
|
||||
let _ = f.app.update(Message::CanvasSelect(stale, other_edge));
|
||||
assert_eq!(f.app.status, "Keep previous feedback");
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
draft
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_select_after_valid_cross_layer_draft_still_navigates_and_commits_once() {
|
||||
let (mut f, other_edge) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let expected =
|
||||
expected_draft_commit(&f.app, Command::Rename(f.edge, "Reviewed enables".into()));
|
||||
pending_edge_name(&mut f);
|
||||
let _ = f.app.update(Message::Select(other_edge));
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_eq!(f.app.selected, other_edge);
|
||||
assert_eq!(f.app.layer, Layer::Oa);
|
||||
assert_ne!(f.app.active_diagram, Some(f.diagram));
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_apply_on_cross_layer_edge_retains_surface_and_one_edit() {
|
||||
let (mut f, _) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let expected =
|
||||
expected_draft_commit(&f.app, Command::Rename(f.edge, "Reviewed enables".into()));
|
||||
pending_edge_name(&mut f);
|
||||
let _ = f.app.update(Message::Apply);
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_eq!(f.app.status, "Properties applied");
|
||||
assert_surface(&f);
|
||||
assert_eq!(f.app.selected, f.edge);
|
||||
assert_eq!(f.app.draft.as_ref().unwrap().original, f.edge);
|
||||
assert_eq!(
|
||||
f.app.multiselect_snapshot().unwrap().primary(),
|
||||
Some(&Target::Edge(f.edge))
|
||||
);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_apply_honors_an_intentional_active_diagram_layer_edit() {
|
||||
let (mut f, _) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut diagram = f.app.project().diagrams[&f.diagram].clone();
|
||||
diagram.layer = Layer::Oa;
|
||||
let expected = expected_draft_commit(&f.app, Command::EditDiagram(f.diagram, diagram));
|
||||
let _ = f.app.update(Message::FieldShape(
|
||||
"/layer".into(),
|
||||
serde_json::to_value(Layer::Oa).unwrap(),
|
||||
));
|
||||
assert!(f.app.draft_dirty());
|
||||
let _ = f.app.update(Message::Apply);
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert_eq!(
|
||||
f.app.layer,
|
||||
Layer::Oa,
|
||||
"honor the edited diagram's own layer"
|
||||
);
|
||||
assert_eq!(f.app.active_diagram, Some(f.diagram));
|
||||
assert_eq!(f.app.selected, f.diagram);
|
||||
assert_eq!(f.app.draft.as_ref().unwrap().original, f.diagram);
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_apply_follows_the_known_active_diagram_reidentity_once() {
|
||||
let (mut f, _) = draft_click_fixture();
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let replacement = Id::new_v4();
|
||||
let expected = expected_draft_commit(&f.app, Command::Reidentify(f.diagram, replacement));
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Field("/id".into(), replacement.to_string()));
|
||||
assert!(f.app.draft_dirty());
|
||||
let _ = f.app.update(Message::Apply);
|
||||
assert_accepted_draft(&f.app, &expected, revision);
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert_eq!(f.app.layer, Layer::Sa);
|
||||
// This identity was explicitly authored in the diagram's own draft. No
|
||||
// queued click is being remapped or inferred from semantic similarity.
|
||||
assert_eq!(f.app.active_diagram, Some(replacement));
|
||||
assert_eq!(f.app.selected, replacement);
|
||||
assert_eq!(f.app.draft.as_ref().unwrap().original, replacement);
|
||||
assert!(!f.app.project().diagrams.contains_key(&f.diagram));
|
||||
assert_one_draft_history(&mut f.app, &before, &expected);
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
// Test-only proposal pinned to peer 6520a89dc042dd433454f6cc63cb23b0e6df7911.
|
||||
// Independent App test module; real popup messages retain menu tokens.
|
||||
use super::*;
|
||||
use crate::model_menu::{Event as MenuEvent, Request};
|
||||
|
||||
struct BeginFixture {
|
||||
app: App,
|
||||
diagram: Id,
|
||||
surface_layer: Layer,
|
||||
source: Id,
|
||||
target: Id,
|
||||
wrong_layer: Id,
|
||||
hidden: Id,
|
||||
tool: crate::connection_tool::Tool,
|
||||
}
|
||||
|
||||
fn begin_fixture(source_layer: Layer, allocation: bool, mixed: bool) -> BeginFixture {
|
||||
use crate::connection_tool::Tool;
|
||||
let mut p = Project::empty();
|
||||
let (work, performer, exchange) = match source_layer {
|
||||
Layer::Oa => (Kind::Activity, Kind::Entity, Tool::Interaction),
|
||||
Layer::Sa => (Kind::Function, Kind::System, Tool::FunctionalExchange),
|
||||
};
|
||||
let opposite = if source_layer == Layer::Oa {
|
||||
Layer::Sa
|
||||
} else {
|
||||
Layer::Oa
|
||||
};
|
||||
let surface_layer = if mixed { opposite } else { source_layer };
|
||||
let source = p
|
||||
.add_element(
|
||||
if allocation { performer } else { work },
|
||||
source_layer,
|
||||
"Source",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let target = p.add_element(work, source_layer, "Target", None).unwrap();
|
||||
let hidden = p
|
||||
.add_element(work, source_layer, "Hidden target", None)
|
||||
.unwrap();
|
||||
let other_work = if opposite == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let wrong_layer = p
|
||||
.add_element(other_work, opposite, "Wrong layer target", None)
|
||||
.unwrap();
|
||||
p.ensure_diagram(source_layer);
|
||||
let diagram = p.ensure_diagram(surface_layer);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = if mixed {
|
||||
ProjectionKind::Traceability
|
||||
} else {
|
||||
ProjectionKind::Capabilities
|
||||
};
|
||||
p.place(diagram, source, Position { x: 0., y: 0. });
|
||||
p.place(diagram, target, Position { x: 400., y: 0. });
|
||||
if mixed {
|
||||
p.place(diagram, wrong_layer, Position { x: 800., y: 0. });
|
||||
}
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = surface_layer;
|
||||
app.active_diagram = Some(diagram);
|
||||
app.selected = diagram;
|
||||
app.refresh_draft();
|
||||
app.reconcile_multiselect();
|
||||
assert!(!app.document.can_undo());
|
||||
BeginFixture {
|
||||
app,
|
||||
diagram,
|
||||
surface_layer,
|
||||
source,
|
||||
target,
|
||||
wrong_layer,
|
||||
hidden,
|
||||
tool: if allocation {
|
||||
Tool::Allocation
|
||||
} else {
|
||||
exchange
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_surface(f: &BeginFixture) {
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert_eq!(f.app.layer, f.surface_layer);
|
||||
assert_eq!(f.app.active_diagram, Some(f.diagram));
|
||||
}
|
||||
|
||||
fn begin_popup_action(f: &mut BeginFixture) -> Option<Message> {
|
||||
begin_popup_action_from(f, true)
|
||||
}
|
||||
|
||||
fn begin_popup_action_from(f: &mut BeginFixture, canvas: bool) -> Option<Message> {
|
||||
let mut request = Request::object(f.source, iced::Point::ORIGIN);
|
||||
if canvas {
|
||||
request.canvas = f.app.route_context();
|
||||
request.placement = f.app.project().diagrams[&f.diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|node| node.element == f.source)
|
||||
.map(|node| node.id);
|
||||
}
|
||||
let _ = f.app.update(Message::Menu(MenuEvent::Open(request)));
|
||||
let popup = crate::model_menu::popup(&f.app).expect("real model popup");
|
||||
let expected = format!("Draw {}", f.tool.label().to_lowercase());
|
||||
for entry in popup.entries {
|
||||
if let crate::context_menu::Entry::Action { label, message, .. } = entry
|
||||
&& label == expected
|
||||
{
|
||||
if let Some(Message::Menu(MenuEvent::Invoke(token, _))) = &message {
|
||||
assert_eq!(*token, popup.token);
|
||||
} else {
|
||||
assert!(message.is_none(), "Draw must use the menu token envelope");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
None // Omission is also a valid unavailable-action policy.
|
||||
}
|
||||
|
||||
fn begin_from_popup(f: &mut BeginFixture) -> Message {
|
||||
begin_from_popup_origin(f, true)
|
||||
}
|
||||
|
||||
fn begin_from_popup_origin(f: &mut BeginFixture, canvas: bool) -> Message {
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let action =
|
||||
begin_popup_action_from(f, canvas).expect("visible semantic source offers enabled Draw");
|
||||
let _ = f.app.update(action.clone());
|
||||
begin_surface(f);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let session = f
|
||||
.app
|
||||
.connection
|
||||
.as_ref()
|
||||
.expect("menu Draw must arm its tool");
|
||||
assert_eq!(session.tool, f.tool);
|
||||
assert_eq!(session.source, Some(f.source));
|
||||
assert_eq!(session.context, f.app.route_context().unwrap());
|
||||
assert!(session.candidates.contains(&f.target));
|
||||
assert!(!session.candidates.contains(&f.wrong_layer));
|
||||
assert!(!session.candidates.contains(&f.hidden));
|
||||
action
|
||||
}
|
||||
|
||||
fn begin_complete_and_check_history(f: &mut BeginFixture) {
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let context = f.app.route_context().unwrap();
|
||||
let _ = f.app.update(Message::Connection(
|
||||
crate::connection_edit::Event::Endpoint(context, f.target),
|
||||
));
|
||||
begin_surface(f);
|
||||
assert!(f.app.connection.is_none());
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
let relations: Vec<_> = f
|
||||
.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| !before.decoded.project.relations.contains_key(&r.id))
|
||||
.collect();
|
||||
assert_eq!(relations.len(), 1);
|
||||
let expected = match f.tool {
|
||||
crate::connection_tool::Tool::Interaction => RelationKind::Interaction,
|
||||
crate::connection_tool::Tool::FunctionalExchange => RelationKind::FunctionalExchange,
|
||||
crate::connection_tool::Tool::Allocation => RelationKind::Allocation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(relations[0].kind(), expected);
|
||||
assert_eq!(
|
||||
(relations[0].source(), relations[0].target()),
|
||||
(f.source, f.target)
|
||||
);
|
||||
// Random identities are accepted only for the precise command delta.
|
||||
// Everything else, including traces, metadata and view options, stays exact.
|
||||
let mut expected_project = before.decoded.project.clone();
|
||||
let created = relations[0];
|
||||
assert!(
|
||||
expected_project
|
||||
.relations
|
||||
.insert(created.id, created.clone())
|
||||
.is_none()
|
||||
);
|
||||
if let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
items,
|
||||
..
|
||||
} = &created.body
|
||||
{
|
||||
assert_eq!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
|
||||
assert!(items.is_empty());
|
||||
assert_ne!(source_port, target_port);
|
||||
let output = &f.app.project().elements[source_port];
|
||||
let input = &f.app.project().elements[target_port];
|
||||
assert_eq!(
|
||||
(output.kind(), output.layer(), output.parent),
|
||||
(Kind::Port, Layer::Sa, Some(f.source))
|
||||
);
|
||||
assert_eq!(
|
||||
(input.kind(), input.layer(), input.parent),
|
||||
(Kind::Port, Layer::Sa, Some(f.target))
|
||||
);
|
||||
assert!(
|
||||
matches!(port_contract(output), Some(PortContract::FunctionOutput { outgoing_items }) if outgoing_items.is_empty())
|
||||
);
|
||||
assert!(
|
||||
matches!(port_contract(input), Some(PortContract::FunctionInput { incoming_items }) if incoming_items.is_empty())
|
||||
);
|
||||
assert!(!before.decoded.project.elements.contains_key(source_port));
|
||||
assert!(!before.decoded.project.elements.contains_key(target_port));
|
||||
assert_eq!(output.id, *source_port);
|
||||
assert_eq!(input.id, *target_port);
|
||||
expected_project
|
||||
.elements
|
||||
.insert(*source_port, output.clone());
|
||||
expected_project
|
||||
.elements
|
||||
.insert(*target_port, input.clone());
|
||||
assert_eq!(
|
||||
f.app.project().elements.len(),
|
||||
before.decoded.project.elements.len() + 2
|
||||
);
|
||||
} else {
|
||||
assert_ne!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
|
||||
assert_eq!(f.app.project().elements, before.decoded.project.elements);
|
||||
}
|
||||
assert_eq!(f.app.project(), &expected_project);
|
||||
assert_eq!(f.app.project().diagrams, before.decoded.project.diagrams);
|
||||
for (id, element) in &before.decoded.project.elements {
|
||||
assert_eq!(&f.app.project().elements[id], element);
|
||||
}
|
||||
begin_assert_source_pair(&f.app);
|
||||
let after = f.app.document.snapshot();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(
|
||||
!f.app.document.can_undo(),
|
||||
"creation is a single history step"
|
||||
);
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &after.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &after.bundle);
|
||||
begin_assert_source_pair(&f.app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_begin_exchange_uses_actual_menu_in_both_mixed_directions() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = begin_fixture(layer, false, true);
|
||||
begin_from_popup(&mut f);
|
||||
begin_complete_and_check_history(&mut f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_begin_cancel_restart_and_stale_menu_preserve_session_ownership() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = begin_fixture(layer, false, true);
|
||||
let original = f.app.document.snapshot();
|
||||
let stale_menu = begin_from_popup(&mut f);
|
||||
let first = f.app.route_context().unwrap();
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Connection(crate::connection_edit::Event::Restart(
|
||||
first,
|
||||
)));
|
||||
begin_surface(&f);
|
||||
let restarted = f
|
||||
.app
|
||||
.connection
|
||||
.as_ref()
|
||||
.expect("restart retains contextual tool");
|
||||
assert_eq!(restarted.tool, f.tool);
|
||||
assert_eq!(restarted.source, None);
|
||||
assert!(restarted.candidates.contains(&f.source));
|
||||
let current = restarted.context;
|
||||
assert_ne!(current, first);
|
||||
let _ = f.app.update(stale_menu);
|
||||
let _ = f.app.update(Message::Connection(
|
||||
crate::connection_edit::Event::Endpoint(first, f.target),
|
||||
));
|
||||
assert_eq!(f.app.connection.as_ref().unwrap().context, current);
|
||||
assert_eq!(f.app.connection.as_ref().unwrap().source, None);
|
||||
let _ = f.app.update(Message::Connection(
|
||||
crate::connection_edit::Event::Endpoint(current, f.source),
|
||||
));
|
||||
assert_eq!(f.app.connection.as_ref().unwrap().source, Some(f.source));
|
||||
let _ = f.app.update(Message::Escape);
|
||||
let _ = f.app.update(Message::Connection(
|
||||
crate::connection_edit::Event::Endpoint(current, f.target),
|
||||
));
|
||||
assert!(f.app.connection.is_none());
|
||||
begin_surface(&f);
|
||||
assert_eq!(f.app.project(), &original.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &original.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_begin_allocation_uses_actual_menu_in_both_mixed_directions() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = begin_fixture(layer, true, true);
|
||||
begin_from_popup(&mut f);
|
||||
begin_complete_and_check_history(&mut f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_begin_allocation_is_not_enabled_when_projection_hides_work() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = begin_fixture(layer, true, false);
|
||||
let view = project_view(f.app.project(), f.diagram);
|
||||
assert!(view.nodes.iter().any(|n| n.element == f.source));
|
||||
assert!(!view.nodes.iter().any(|n| n.element == f.target));
|
||||
assert!(!f.tool.available_in(layer, ProjectionKind::Capabilities));
|
||||
let before = f.app.document.snapshot();
|
||||
assert!(
|
||||
begin_popup_action(&mut f).is_none(),
|
||||
"Draw allocation must be disabled or omitted when work is not projected"
|
||||
);
|
||||
let _ = f.app.update(Message::Menu(MenuEvent::Close));
|
||||
// Defensive handler refusal complements actual-menu enabledness above.
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::Connection(crate::connection_edit::Event::Begin(
|
||||
f.tool, f.source,
|
||||
)));
|
||||
assert!(f.app.connection.is_none());
|
||||
begin_surface(&f);
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_assert_source_pair(app: &App) {
|
||||
let compiled = oa_core::source_project::compile_bundle(
|
||||
app.document.bundle(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&compiled.decoded.project, app.project());
|
||||
assert_eq!(&compiled.bundle, app.document.bundle());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_begin_explorer_draw_retains_visible_mixed_diagram_in_both_directions() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = begin_fixture(layer, false, true);
|
||||
// A real Explorer Request::object has no canvas/placement envelope,
|
||||
// but its generated Draw action still targets the visible current view.
|
||||
begin_from_popup_origin(&mut f, false);
|
||||
begin_complete_and_check_history(&mut f);
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
// Proposed tests against c7065db76ecca3faa733ccb5f09b87fd0a0958be.
|
||||
use super::*;
|
||||
|
||||
struct RelocationFixture {
|
||||
app: App,
|
||||
source: Id,
|
||||
target: Id,
|
||||
node: Id,
|
||||
other_node: Id,
|
||||
edge: Id,
|
||||
}
|
||||
|
||||
fn relocation_fixture(source_layer: Layer, target_layer: Layer) -> RelocationFixture {
|
||||
let mut p = Project::empty();
|
||||
let work = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Observed work", None)
|
||||
.unwrap();
|
||||
let other = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Other work", None)
|
||||
.unwrap();
|
||||
let edge = p.relate(RelationKind::Interaction, "Observed exchange", work, other);
|
||||
let source = p.ensure_diagram(source_layer);
|
||||
p.diagrams.get_mut(&source).unwrap().projection = ProjectionKind::Traceability;
|
||||
let node = p.place(source, work, Position { x: 20., y: 20. });
|
||||
let other_node = p.place(source, other, Position { x: 500., y: 20. });
|
||||
let mut destination = p.diagrams[&source].clone();
|
||||
destination.id = Id::new_v4();
|
||||
destination.alias = "relocation_destination".into();
|
||||
destination.name = "Relocation destination".into();
|
||||
destination.layer = target_layer;
|
||||
destination.nodes.clear();
|
||||
let target = destination.id;
|
||||
p.diagrams.insert(target, destination);
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = source_layer;
|
||||
app.active_diagram = Some(source);
|
||||
app.selected = node;
|
||||
app.refresh_draft();
|
||||
app.reconcile_multiselect();
|
||||
assert!(
|
||||
matches!(app.draft.as_ref().unwrap().kind, crate::forms::RecordKind::Node(d) if d == source)
|
||||
);
|
||||
assert!(!app.document.can_undo());
|
||||
RelocationFixture {
|
||||
app,
|
||||
source,
|
||||
target,
|
||||
node,
|
||||
other_node,
|
||||
edge,
|
||||
}
|
||||
}
|
||||
|
||||
fn relocation_field(app: &mut App, path: &str, value: impl ToString) {
|
||||
let _ = app.update(Message::Field(path.into(), value.to_string()));
|
||||
}
|
||||
|
||||
fn relocation_source_pair(app: &App) {
|
||||
let compiled = oa_core::source_project::compile_bundle(
|
||||
app.document.bundle(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&compiled.decoded.project, app.project());
|
||||
assert_eq!(&compiled.bundle, app.document.bundle());
|
||||
}
|
||||
|
||||
fn relocation_selection(app: &App, diagram: Id, placement: Id) {
|
||||
assert_eq!(app.page, Page::Diagram);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
assert_eq!(app.layer, app.project().diagrams[&diagram].layer);
|
||||
assert_eq!(app.selected, placement);
|
||||
assert_eq!(app.selected_node, Some(placement));
|
||||
let draft = app.draft.as_ref().unwrap();
|
||||
assert_eq!(draft.original, placement);
|
||||
assert!(matches!(draft.kind, crate::forms::RecordKind::Node(d) if d == diagram));
|
||||
}
|
||||
|
||||
fn relocation_expected(app: &App, command: Command) -> Document {
|
||||
let mut expected = Document::from_bundle(
|
||||
app.document.bundle().clone(),
|
||||
oa_core::source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
expected.execute(command).unwrap();
|
||||
expected
|
||||
}
|
||||
|
||||
fn relocation_history(
|
||||
app: &mut App,
|
||||
before: &oa_core::source_project::CompiledSourceProject,
|
||||
expected: &Document,
|
||||
) {
|
||||
assert_eq!(app.project(), expected.project());
|
||||
assert_eq!(app.document.bundle(), expected.bundle());
|
||||
assert!(app.document.can_undo());
|
||||
assert!(!app.document.can_redo());
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
assert!(app.document.can_redo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), expected.project());
|
||||
assert_eq!(app.document.bundle(), expected.bundle());
|
||||
assert!(!app.document.can_redo());
|
||||
relocation_source_pair(app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_placement_relocation_follows_actual_owner_with_optional_new_identity() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for reidentify in [false, true] {
|
||||
let mut f = relocation_fixture(Layer::Oa, layer);
|
||||
let before = f.app.document.snapshot();
|
||||
let before_project = before.decoded.project.clone();
|
||||
let revision = f.app.document.revision();
|
||||
let id = if reidentify { Id::new_v4() } else { f.node };
|
||||
let mut expected = before_project.clone();
|
||||
let mut placement = expected
|
||||
.diagrams
|
||||
.get_mut(&f.source)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&f.node)
|
||||
.unwrap();
|
||||
placement.id = id;
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&f.target)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(id, placement);
|
||||
let expected_document = relocation_expected(
|
||||
&f.app,
|
||||
Command::Batch(vec![
|
||||
Command::EditNode {
|
||||
diagram: f.source,
|
||||
original: f.node,
|
||||
node: expected.diagrams[&f.target].nodes[&id].clone(),
|
||||
},
|
||||
Command::RelocateNode {
|
||||
node: id,
|
||||
diagram: f.target,
|
||||
},
|
||||
]),
|
||||
);
|
||||
assert_eq!(expected_document.project(), &expected);
|
||||
relocation_field(&mut f.app, "/diagram", f.target);
|
||||
if reidentify {
|
||||
relocation_field(&mut f.app, "/id", id);
|
||||
}
|
||||
assert!(f.app.apply(), "{}", f.app.status);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
relocation_selection(&f.app, f.target, id);
|
||||
relocation_source_pair(&f.app);
|
||||
assert_eq!(f.app.document.bundle(), expected_document.bundle());
|
||||
relocation_history(&mut f.app, &before, &expected_document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_owner_placement_position_alias_and_identity_keep_mixed_surface() {
|
||||
for different_current_view in [false, true] {
|
||||
for field in ["/position/x", "/alias", "/id"] {
|
||||
let mut f = relocation_fixture(Layer::Sa, Layer::Oa);
|
||||
// Ordinary Select plus the diagram picker can retain an independent
|
||||
// placement Inspector while showing a different saved view. This is
|
||||
// an App-handler workflow, not a claim of native menu execution.
|
||||
let _ = f.app.update(Message::Select(f.app.project().id));
|
||||
let _ = f.app.update(Message::Select(f.node));
|
||||
let current = if different_current_view {
|
||||
f.target
|
||||
} else {
|
||||
f.source
|
||||
};
|
||||
if different_current_view {
|
||||
let _ = f.app.update(Message::Diagram(Choice {
|
||||
id: current,
|
||||
label: "Other current view".into(),
|
||||
}));
|
||||
}
|
||||
assert_eq!(f.app.selected, f.node);
|
||||
assert_eq!(f.app.active_diagram, Some(current));
|
||||
assert!(
|
||||
matches!(f.app.draft.as_ref().unwrap().kind, crate::forms::RecordKind::Node(d) if d == f.source)
|
||||
);
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut expected = before.decoded.project.clone();
|
||||
let mut placement = expected
|
||||
.diagrams
|
||||
.get_mut(&f.source)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&f.node)
|
||||
.unwrap();
|
||||
let value = match field {
|
||||
"/position/x" => {
|
||||
placement.position.x = 123.;
|
||||
"123".to_owned()
|
||||
}
|
||||
"/alias" => {
|
||||
placement.alias = "renamed_placement".into();
|
||||
placement.alias.clone()
|
||||
}
|
||||
_ => {
|
||||
placement.id = Id::new_v4();
|
||||
placement.id.to_string()
|
||||
}
|
||||
};
|
||||
let id = placement.id;
|
||||
let expected_document = relocation_expected(
|
||||
&f.app,
|
||||
Command::EditNode {
|
||||
diagram: f.source,
|
||||
original: f.node,
|
||||
node: placement.clone(),
|
||||
},
|
||||
);
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&f.source)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(id, placement);
|
||||
assert_eq!(expected_document.project(), &expected);
|
||||
relocation_field(&mut f.app, field, value);
|
||||
assert!(f.app.apply(), "{}", f.app.status);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), expected_document.bundle());
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
assert_eq!(f.app.page, Page::Diagram);
|
||||
assert_eq!(f.app.active_diagram, Some(current));
|
||||
assert_eq!(f.app.layer, f.app.project().diagrams[¤t].layer);
|
||||
assert_eq!(f.app.selected, id);
|
||||
assert_eq!(
|
||||
f.app.selected_node,
|
||||
if different_current_view {
|
||||
None
|
||||
} else {
|
||||
Some(id)
|
||||
}
|
||||
);
|
||||
let draft = f.app.draft.as_ref().unwrap();
|
||||
assert_eq!(draft.original, id);
|
||||
assert!(matches!(draft.kind, crate::forms::RecordKind::Node(d) if d == f.source));
|
||||
relocation_source_pair(&f.app);
|
||||
relocation_history(&mut f.app, &before, &expected_document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepted_relocation_refuses_queued_old_canvas_target_with_feedback() {
|
||||
for edge_hit in [false, true] {
|
||||
let mut f = relocation_fixture(Layer::Oa, Layer::Sa);
|
||||
let before = f.app.document.snapshot();
|
||||
let expected_document = relocation_expected(
|
||||
&f.app,
|
||||
Command::Batch(vec![
|
||||
Command::EditNode {
|
||||
diagram: f.source,
|
||||
original: f.node,
|
||||
node: f.app.project().diagrams[&f.source].nodes[&f.node].clone(),
|
||||
},
|
||||
Command::RelocateNode {
|
||||
node: f.node,
|
||||
diagram: f.target,
|
||||
},
|
||||
]),
|
||||
);
|
||||
let mut expected = before.decoded.project.clone();
|
||||
let placement = expected
|
||||
.diagrams
|
||||
.get_mut(&f.source)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&f.node)
|
||||
.unwrap();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&f.target)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(f.node, placement);
|
||||
let revision = f.app.document.revision();
|
||||
let captured = f.app.route_context().unwrap();
|
||||
relocation_field(&mut f.app, "/diagram", f.target);
|
||||
let message = if edge_hit {
|
||||
Message::CanvasSelect(captured, f.edge)
|
||||
} else {
|
||||
Message::SelectNode(f.other_node)
|
||||
};
|
||||
let _ = f.app.update(message);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
relocation_selection(&f.app, f.target, f.node);
|
||||
assert_eq!(
|
||||
f.app.status,
|
||||
"Properties applied. The view changed; select a visible target again."
|
||||
);
|
||||
assert!(
|
||||
f.app.multiselect_snapshot().is_none_or(|snapshot| {
|
||||
!snapshot.targets().iter().any(|target| {
|
||||
matches!(target, crate::diagram_usage::Target::Placement(id) if *id == f.other_node)
|
||||
|| matches!(target, crate::diagram_usage::Target::Edge(id) if *id == f.edge)
|
||||
})
|
||||
}),
|
||||
"the rejected old hit must not enter the destination selection"
|
||||
);
|
||||
assert_eq!(expected_document.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), expected_document.bundle());
|
||||
relocation_source_pair(&f.app);
|
||||
relocation_history(&mut f.app, &before, &expected_document);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_placement_destination_preserves_owner_model_source_and_draft() {
|
||||
let mut f = relocation_fixture(Layer::Oa, Layer::Sa);
|
||||
let before = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
relocation_field(&mut f.app, "/diagram", Id::new_v4());
|
||||
let draft = serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
assert!(!f.app.apply());
|
||||
assert_eq!(f.app.project(), &before.decoded.project);
|
||||
assert_eq!(f.app.document.bundle(), &before.bundle);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(!f.app.document.can_undo());
|
||||
relocation_selection(&f.app, f.source, f.node);
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
draft
|
||||
);
|
||||
}
|
||||
@@ -39,24 +39,6 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Contextual drawing follows the visible semantic source, not the palette layer.
|
||||
/// The caller separately validates the menu/context lifetime and pending drafts.
|
||||
pub fn can_begin(project: &Project, diagram: Id, tool: Tool, source: Id) -> bool {
|
||||
let Some(diagram) = project.diagrams.get(&diagram) else {
|
||||
return false;
|
||||
};
|
||||
let Some(element) = project.elements.get(&source) else {
|
||||
return false;
|
||||
};
|
||||
diagram.projection.is_architecture()
|
||||
&& (element.layer() == diagram.layer
|
||||
|| diagram.projection == oa_core::ProjectionKind::Traceability)
|
||||
&& Tool::for_layer(element.layer()).contains(&tool)
|
||||
&& tool.can_start(project, source)
|
||||
&& (tool != Tool::Allocation || tool.available_in(element.layer(), diagram.projection))
|
||||
&& visible(&oa_core::projection::project_view(project, diagram.id)).any(|id| id == source)
|
||||
}
|
||||
|
||||
pub fn visible(view: &ProjectedView) -> impl Iterator<Item = Id> + '_ {
|
||||
view.nodes
|
||||
.iter()
|
||||
|
||||
@@ -993,10 +993,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
world,
|
||||
));
|
||||
}
|
||||
return Some(
|
||||
Action::publish(Message::CanvasSelect(self.context, port.element))
|
||||
.and_capture(),
|
||||
);
|
||||
return Some(Action::publish(Message::Select(port.element)).and_capture());
|
||||
}
|
||||
if let Some(id) = self.hit(view, p) {
|
||||
let position = self
|
||||
@@ -1013,7 +1010,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
.hit_label(&scene.routed, p)
|
||||
.or_else(|| self.hit_association_label(&scene.routed, p))
|
||||
{
|
||||
Some(Action::publish(Message::CanvasSelect(self.context, id)).and_capture())
|
||||
Some(Action::publish(Message::Select(id)).and_capture())
|
||||
} else if let Some((id, segment)) = self.hit_edge(&scene.routed, p) {
|
||||
if id == self.selected
|
||||
&& self.editable
|
||||
@@ -1029,7 +1026,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
self.begin_route(state, world, Handle::Bend(index), route);
|
||||
Some(Action::capture())
|
||||
} else {
|
||||
Some(Action::publish(Message::CanvasSelect(self.context, id)).and_capture())
|
||||
Some(Action::publish(Message::Select(id)).and_capture())
|
||||
}
|
||||
} else {
|
||||
state.pan_start = Some((mouse::Button::Left, p));
|
||||
@@ -2444,7 +2441,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.0;
|
||||
assert!(matches!(selected,Some(Message::CanvasSelect(_, id)) if id==port.element));
|
||||
assert!(matches!(selected,Some(Message::Select(id)) if id==port.element));
|
||||
let request = canvas.context_request(
|
||||
&state,
|
||||
click,
|
||||
@@ -3057,7 +3054,7 @@ mod tests {
|
||||
let mut state = State::default();
|
||||
assert_eq!(canvas.hit_association_label(&routed, at), Some(edge));
|
||||
assert!(
|
||||
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),at),Some(Message::CanvasSelect(_, id)) if id==edge)
|
||||
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),at),Some(Message::Select(id)) if id==edge)
|
||||
);
|
||||
assert!(
|
||||
state.route_drag.is_none(),
|
||||
@@ -3107,7 +3104,7 @@ mod tests {
|
||||
let center = scene.routed.edges[&edge].label.center;
|
||||
let screen = point(center) + canvas.pan;
|
||||
assert!(
|
||||
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),screen),Some(Message::CanvasSelect(_, id)) if id==edge)
|
||||
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),screen),Some(Message::Select(id)) if id==edge)
|
||||
);
|
||||
canvas.selected = edge;
|
||||
pointer(
|
||||
|
||||
@@ -179,7 +179,7 @@ fn coalesced_port_drag_commits_once_through_app_and_source_history() {
|
||||
zoom,
|
||||
);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert!(matches!(&messages[0],Message::CanvasSelect(_, id)if *id==output));
|
||||
assert!(matches!(&messages[0],Message::Select(id)if *id==output));
|
||||
assert!(
|
||||
matches!(&messages[1],Message::PortAnchor(port_edit::Event::Commit(_,_,anchor))if anchor.port==output&&anchor.side==PortAnchorSide::Top)
|
||||
);
|
||||
@@ -251,7 +251,7 @@ fn menu_captured_motion_must_not_turn_following_click_into_old_port_drag() {
|
||||
b,
|
||||
);
|
||||
let picked = match &press {
|
||||
Message::CanvasSelect(_, id) => Some(*id),
|
||||
Message::Select(id) => Some(*id),
|
||||
_ => None,
|
||||
};
|
||||
let release_debug = format!("{release:?}");
|
||||
@@ -300,7 +300,7 @@ fn drag_from_already_selected_stationary_port_keeps_source_between_batches() {
|
||||
.filter_map(|e| event(&c, &mut state, e, b))
|
||||
.collect();
|
||||
assert!(
|
||||
matches!(messages.first(),Some(Message::CanvasSelect(_, id)) if *id==output),
|
||||
matches!(messages.first(),Some(Message::Select(id)) if *id==output),
|
||||
"Stationary source was lost between batches: {messages:?}"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -165,7 +165,7 @@ fn nested_owner_palette_selection_drag_and_context_menu_pick_the_same_port() {
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.0;
|
||||
assert!(matches!(press, Some(Message::CanvasSelect(_, id)) if id == port.element));
|
||||
assert!(matches!(press, Some(Message::Select(id)) if id == port.element));
|
||||
assert_eq!(state.port_drag.as_ref().unwrap().anchor.port, port.element);
|
||||
let menu = canvas.context_request(&state, at, bounds);
|
||||
assert_eq!(menu.object, port.element);
|
||||
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
messages.extend(message);
|
||||
}
|
||||
}
|
||||
assert!(matches!(messages.first(), Some(Message::CanvasSelect(_, id)) if *id == port));
|
||||
assert!(matches!(messages.first(), Some(Message::Select(id)) if *id == port));
|
||||
assert!(
|
||||
matches!(messages.last(), Some(Message::PortAnchor(crate::port_edit::Event::Commit(_, _, anchor))) if anchor.port == port && anchor.side == PortAnchorSide::Top)
|
||||
);
|
||||
|
||||
@@ -243,6 +243,21 @@ impl canvas::Program<Message> for WorkFeedback<'_> {
|
||||
let Some(frozen) = &feedback.frozen else {
|
||||
return vec![];
|
||||
};
|
||||
if let Some(delta) = feedback.ghost_delta {
|
||||
for region in &frozen.moving {
|
||||
let top = Point::new(
|
||||
(region.x + delta.x) * self.0.zoom + self.0.pan.x,
|
||||
(region.y + delta.y) * self.0.zoom + self.0.pan.y,
|
||||
);
|
||||
frame.stroke(
|
||||
&Path::rectangle(
|
||||
top,
|
||||
iced::Size::new(region.width * self.0.zoom, region.height * self.0.zoom),
|
||||
),
|
||||
Stroke::default().with_color(color).with_width(2.),
|
||||
);
|
||||
}
|
||||
}
|
||||
for index in &feedback.targets {
|
||||
let target = &frozen.targets[*index];
|
||||
let region = target.region;
|
||||
|
||||
@@ -322,8 +322,6 @@ fn article(topic: Topic) -> Article {
|
||||
"Show a correspondence between OA and SA",
|
||||
&[
|
||||
"Choose OA ↔ SA traceability in View content. Changing this setting does not create counterparts or add their placements, so an existing single-layer diagram can initially look unchanged.",
|
||||
"Switching back to Architecture or another single-layer projection shows only that view's layer. The other layer's placements, routing and saved visibility remain stored; return to OA ↔ SA traceability to show them again, subject to the view's filters. Selecting a displayed card, port or line and opening its Properties keeps the current view and perspective. Applying ordinary Properties edits keeps that saved architecture view open. Changing a placement's Diagram explicitly moves it and follows its destination. Changes you make to the view's own perspective or identity still take effect. If the app asks you to select again, choose a visible target.",
|
||||
"In a mixed traceability view, use Draw from a visible source's canvas menu or the Model explorer sidebar on the Architecture page to connect from that source's OA or SA layer without leaving the view. Choose a highlighted compatible visible target; Escape cancels. The palette offers tools for the view's perspective. Opening an endpoint or selecting a record in the Model explorer sidebar follows that record's perspective. Contextual relationship forms keep the view while opening; applying the reviewed relationship can navigate to its result.",
|
||||
"Use Review layer traces to inspect named Operational and System endpoints and the meaning of their link. This opens the model's layer-trace list with no search restriction; traces between relationships are reviewed here rather than drawn between element cards.",
|
||||
"In the chosen architecture view, use Add existing elements. The chooser offers compatible elements from both layers. Select an existing endpoint and click its position in the canvas; repeat for the other end if needed. This adds a placement, not a new semantic element or trace. For port correspondences, place the ports' owners.",
|
||||
"The view shows correspondence lines alongside ordinary relationships. The shown-line count reflects the current projection and visibility settings, not trace coverage. If a line is hidden, use View visibility and inspect Saved view visibility; placing an endpoint does not remove explicit hides or filters.",
|
||||
|
||||
@@ -557,7 +557,10 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
&format!("Draw {}", tool.label().to_lowercase()),
|
||||
Message::Connection(crate::connection_edit::Event::Begin(*tool, id)),
|
||||
app.active_diagram.is_some_and(|diagram| {
|
||||
crate::connection_edit::can_begin(p, diagram, *tool, id)
|
||||
crate::connection_edit::visible(&projection::project_view(
|
||||
p, diagram,
|
||||
))
|
||||
.any(|element| element == id)
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -372,150 +372,3 @@ async fn source6_http_explicit_communication_ports_preview_apply_and_restart_pre
|
||||
assert_pair(&reopened);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mixed_traceability_projection_roundtrip_preserves_dormant_http_state() {
|
||||
let f = fixture();
|
||||
let mut model = compile_bundle(&f.bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project;
|
||||
let view = model.diagrams.get_mut(&f.diagram).unwrap();
|
||||
view.layer = Layer::Sa;
|
||||
view.projection = ProjectionKind::Traceability;
|
||||
// The OA placements and category are intentionally opposite the view's layer.
|
||||
view.view_options.hidden_edges = vec![f.mean];
|
||||
for (index, element) in f.sa.into_iter().enumerate() {
|
||||
model.place(
|
||||
f.diagram,
|
||||
element,
|
||||
Position {
|
||||
x: index as f32 * 400.,
|
||||
y: 300.,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut bundle = f.bundle.clone();
|
||||
bundle.files.insert(
|
||||
bundle.entry.clone(),
|
||||
format!(
|
||||
"// Authored source-6 engineering rationale\n{}",
|
||||
oa_core::language::format(&model).replacen(
|
||||
"{\n",
|
||||
"{\n include \"notes.arcadia\";\n",
|
||||
1
|
||||
)
|
||||
),
|
||||
);
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let original = import(&server, &bundle).await;
|
||||
assert_pair(&original);
|
||||
let resource = original.resource_id;
|
||||
let mixed = oa_core::projection::project_view(&original.model, f.diagram);
|
||||
assert!(
|
||||
f.oa.iter()
|
||||
.chain(&f.sa)
|
||||
.all(|id| mixed.nodes.iter().any(|n| n.element == *id))
|
||||
);
|
||||
|
||||
let mut ordinary = original.model.diagrams[&f.diagram].clone();
|
||||
ordinary.projection = ProjectionKind::Allocation;
|
||||
let response = transact(
|
||||
&server,
|
||||
&original,
|
||||
vec![Command::EditDiagram(f.diagram, ordinary.clone())],
|
||||
)
|
||||
.await;
|
||||
let status = response.status();
|
||||
let etag = response
|
||||
.headers()
|
||||
.get("etag")
|
||||
.map(|v| v.to_str().unwrap().to_owned());
|
||||
let result: Value = response.json().await.unwrap();
|
||||
assert_eq!(status, StatusCode::OK, "{result}");
|
||||
assert_eq!(result["changed"], true);
|
||||
assert_eq!(result["dry_run"], false);
|
||||
assert_eq!(result["revision"], original.revision + 1);
|
||||
let changed = server.get(resource).await;
|
||||
assert_eq!(etag.as_deref(), Some(changed.etag().as_str()));
|
||||
let mut expected = original.model.clone();
|
||||
expected.diagrams.insert(f.diagram, ordinary.clone());
|
||||
assert_eq!(
|
||||
changed.model, expected,
|
||||
"only the projection changes; dormant state is retained"
|
||||
);
|
||||
assert_pair(&changed);
|
||||
assert_eq!(
|
||||
changed.source_project.files["notes.arcadia"],
|
||||
bundle.files["notes.arcadia"]
|
||||
);
|
||||
assert!(
|
||||
changed.source_project.files[&bundle.entry]
|
||||
.starts_with("// Authored source-6 engineering rationale\n")
|
||||
);
|
||||
let projected = oa_core::projection::project_view(&changed.model, f.diagram);
|
||||
assert!(projected.nodes.iter().all(|n| !f.oa.contains(&n.element)));
|
||||
assert!(
|
||||
f.sa.iter()
|
||||
.all(|id| projected.nodes.iter().any(|n| n.element == *id))
|
||||
);
|
||||
|
||||
// A dormant option still must name an existing category. Rejection must not
|
||||
// advance the ETag or rewrite either side of the persisted model/source pair.
|
||||
let disk = directory.path().join(format!("{resource}.json"));
|
||||
let accepted_bytes = std::fs::read(&disk).unwrap();
|
||||
ordinary.view_options.participant_categories = CategorySelection::Any(vec![Id::new_v4()]);
|
||||
let rejected = transact(
|
||||
&server,
|
||||
&changed,
|
||||
vec![Command::EditDiagram(f.diagram, ordinary)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let retained = server.get(resource).await;
|
||||
assert_eq!(retained.etag(), changed.etag());
|
||||
assert_eq!(
|
||||
serde_json::to_value(&retained).unwrap(),
|
||||
serde_json::to_value(&changed).unwrap()
|
||||
);
|
||||
assert_eq!(std::fs::read(&disk).unwrap(), accepted_bytes);
|
||||
server.stop().await;
|
||||
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let reopened = server.get(resource).await;
|
||||
assert_eq!(
|
||||
serde_json::to_value(&reopened).unwrap(),
|
||||
serde_json::to_value(&changed).unwrap()
|
||||
);
|
||||
assert_pair(&reopened);
|
||||
let response = transact(
|
||||
&server,
|
||||
&reopened,
|
||||
vec![Command::EditDiagram(
|
||||
f.diagram,
|
||||
original.model.diagrams[&f.diagram].clone(),
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
let status = response.status();
|
||||
let result: Value = response.json().await.unwrap();
|
||||
assert_eq!(status, StatusCode::OK, "{result}");
|
||||
assert_eq!(result["changed"], true);
|
||||
assert_eq!(result["revision"], changed.revision + 1);
|
||||
let restored = server.get(resource).await;
|
||||
assert_eq!(restored.model, original.model);
|
||||
assert_pair(&restored);
|
||||
assert_eq!(
|
||||
restored.source_project.files["notes.arcadia"],
|
||||
bundle.files["notes.arcadia"]
|
||||
);
|
||||
let projected = oa_core::projection::project_view(&restored.model, f.diagram);
|
||||
assert!(
|
||||
f.oa.iter()
|
||||
.chain(&f.sa)
|
||||
.all(|id| projected.nodes.iter().any(|n| n.element == *id))
|
||||
);
|
||||
assert!(!projected.edges.iter().any(|edge| edge.id == f.mean));
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ schemas["ViewOptions"] = obj({
|
||||
"coverage": {**ref("Coverage"), "default": "All"},
|
||||
"work_contexts": {**ref("ContextSelection"), "default": "All"},
|
||||
"exchange_labels": {**ref("ExchangeLabels"), "default": "Name"}}, [])
|
||||
schemas["ViewOptions"]["description"] = "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. String projections preserve both-layer hidden/category/context references and latent placements; only Traceability renders both layers. Typed StateMachine/Scenario views retain layer-scoped admission. Filtering preserves unfiltered automatic port positions."
|
||||
schemas["ViewOptions"]["description"] = "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions."
|
||||
schemas["Diagram"]["properties"]["view_options"] = {**ref("ViewOptions"), "default": {}}
|
||||
|
||||
def upsert_variant(variants, name, value):
|
||||
|
||||
@@ -570,15 +570,8 @@ retain their identities and contracts; migration does not silently replace them.
|
||||
``coverage`` and ``work_contexts``, and ``Name`` for ``exchange_labels``.
|
||||
``{"Only":[]}`` hides all relation and trace lines; ``{"Any":[]}`` preserves
|
||||
an explicit empty category/context selection. Category members union within
|
||||
each family, and independent filters intersect. String projections permit saved
|
||||
references and placements from both layers. Only ``Traceability`` renders both;
|
||||
other string projections filter to the diagram's declared layer. ``EditDiagram``
|
||||
may change between these projections without deleting latent placements, routes
|
||||
or visibility choices. Returning to Traceability restores their applicability.
|
||||
Typed StateMachine/Scenario views retain their strict membership and layer checks.
|
||||
Missing objects, wrong reference kinds/families and duplicate selections remain
|
||||
invalid. Frozen versions 1–6 keep their prior layer admission before migration;
|
||||
older version-7 implementations may reject these newly accepted ordinary views.
|
||||
each family, and independent filters intersect. ``Traceability`` permits
|
||||
references from both layers; ordinary projections require the diagram's layer.
|
||||
These presentation choices preserve semantic records, routes, placements and
|
||||
the automatic port positions derived from unfiltered eligible exchanges.
|
||||
|
||||
|
||||
@@ -2702,7 +2702,19 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
@@ -409,12 +409,8 @@ filters rather than widening them to All.
|
||||
|
||||
Traceability views retain their established two-layer projection: default options
|
||||
preserve eligible OA and SA lines. Their hidden-edge, category and context
|
||||
references may therefore select either layer. Ordinary string projections retain
|
||||
these references from either layer as saved presentation choices; they still
|
||||
render only their declared layer. Switching projections MUST preserve those
|
||||
choices, including explicit empty selections. Existing-object, reference-kind,
|
||||
family and uniqueness constraints still apply. Typed StateMachine and Scenario
|
||||
views retain layer-scoped reference admission. A filter never hides the other
|
||||
references may therefore select either layer. All ordinary projections require
|
||||
these references in the declared diagram layer. A filter never hides the other
|
||||
layer merely because the Traceability diagram has an OA or SA layer label.
|
||||
|
||||
Versions 1–5 reject the new relation, category constructor, diagram field (even
|
||||
@@ -1077,20 +1073,6 @@ capabilities with their involvement, exploitation, include, extend and
|
||||
generalization links. Decomposition and Overview retain general containment;
|
||||
Overview also exposes all relationships. These string projections hide
|
||||
inapplicable placements without deleting them or changing semantic records.
|
||||
This includes opposite-layer placements retained after leaving Traceability.
|
||||
Changing between string projections changes only the projection value; placement
|
||||
identities, positions, sizes, collapsed state, port anchors, routes and visibility
|
||||
choices remain authored. Returning to Traceability restores their applicability.
|
||||
Ordinary views MUST NOT render opposite-layer nodes, parents, ports or lines.
|
||||
This current-version admission correction does not broaden frozen source/JSON
|
||||
versions 1–6: their layer-scoped placement and visibility checks remain required
|
||||
before migration. Earlier version-7 implementations can reject ordinary views
|
||||
containing these retained opposite-layer records.
|
||||
|
||||
The explicit RelocateNode operation moves a placement to a different diagram
|
||||
only when its element belongs to the destination layer or the destination is
|
||||
Traceability. Retaining an existing hidden placement through a projection change
|
||||
does not authorize a relocation that immediately hides it in another layer.
|
||||
|
||||
A StateMachine projection MUST reference an existing StateMachine in the
|
||||
diagram's layer. Its placements MUST refer to that machine, its Regions, or
|
||||
@@ -2146,7 +2128,19 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
@@ -12490,7 +12490,7 @@
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false,
|
||||
"description": "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. String projections preserve both-layer hidden/category/context references and latent placements; only Traceability renders both layers. Typed StateMachine/Scenario views retain layer-scoped admission. Filtering preserves unfiltered automatic port positions."
|
||||
"description": "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions."
|
||||
},
|
||||
"CommunicationPortChoice": {
|
||||
"oneOf": [
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
# Architectural and product decisions
|
||||
|
||||
## Canvas inspection, contextual drawing and explicit relocation — 2026-10-02
|
||||
|
||||
An ordinary Properties Apply retains the current Architecture diagram, layer and
|
||||
projection. An accepted edit to the active Diagram itself uses its resulting
|
||||
identity, layer and projection. A queued canvas target is revalidated against that retained surface
|
||||
after Apply; an obsolete target is refused with feedback instead of navigating
|
||||
or selecting hidden content. Explicit Select/Open still follows the requested
|
||||
model target. An accepted change to a placement's actual owning diagram is also
|
||||
explicit navigation: it follows the committed destination and resulting UUID.
|
||||
Changing only geometry, alias or UUID in the same owning diagram retains the
|
||||
inspection surface. Invalid or missing ownership never grants this exception.
|
||||
|
||||
Contextual Draw uses the invoked menu target as its semantic source and the current
|
||||
diagram as its editing surface. Shared admission checks the diagram projection,
|
||||
visibility, family and semantic endpoint roles before arming. Restart clears the
|
||||
pending source while retaining an admitted tool. Explicit model navigation and
|
||||
post-review creation navigation retain their separate behavior. This avoids
|
||||
silently conflating an opposite-layer inspection with a request to change views.
|
||||
|
||||
The source repairs at c706 and 6018, the failed c706 workspace attempt, the
|
||||
full-model/source invariants and corrected Windows acceptance are attributed in
|
||||
[the navigation checkpoint](navigation-validation-20261002.md). The simpler
|
||||
blanket surface restoration was rejected when the unchanged relocation test
|
||||
showed that it hid successful explicit moves. This is bounded Architecture
|
||||
acceptance; diagram label readability and the complete OA/SA goal remain open.
|
||||
|
||||
## Private source composition decision — 2026-09-23
|
||||
|
||||
Adopt the opaque validated original-span rename batch as the next private
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
# Navigation contribution checkpoint — 2026-10-02 UTC
|
||||
|
||||
Verified production source **6018ae30b9296b9f6b37d7fc038175080d15fafb** preserves
|
||||
ordinary opposite-layer Properties inspection and contextual drawing while
|
||||
following accepted explicit placement relocation. Windows full workspace tests
|
||||
pass **2,236 / 0 failures / 8 existing ignores**; formatting, strict workspace
|
||||
all-targets Clippy and matching desktop/server/CLI builds pass. Native creation,
|
||||
projection/history, corrected relocation and saved-source reopen evidence retain
|
||||
their actual c706/6018 attribution and independent reviews.
|
||||
[Source, commands, portable evidence and limits](navigation-validation-20261002.md)
|
||||
are authoritative. The earlier c706 workspace failure remains recorded.
|
||||
|
||||
This is a contribution checkpoint, not the complete OA/SA application or an
|
||||
integrated Help/Save release. Label ellipsis/readability and broader usability
|
||||
remain open. **Linux/WSL is still excluded by the user's explicit pause.**
|
||||
[Issue48](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/48)
|
||||
coordinates the subsequent verified union with delivered Save/Help; ownership,
|
||||
live status and release decisions belong there. Existing released versions are
|
||||
unchanged. Earlier integrated and contribution checkpoints follow with their
|
||||
original source and acceptance boundaries.
|
||||
|
||||
# Integrated prerelease source checkpoint — 2026-09-24 UTC
|
||||
|
||||
This branch assembles peer contributions for the Windows prerelease in issue37.
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Connection tool icons
|
||||
|
||||
Branch candidate for [#41](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/41), based on published Windows preview source `715036e132677dd2bc2fb37ed316d93dc9987aec`. This is not integrated or native acceptance.
|
||||
|
||||
The reported collapsed palette repeats one arrow for three different tools. Extend the existing Rust/Iced canvas glyphs at their actual 16×16 size: work endpoints plus an arrow for Interaction/Functional exchange, neutral participant endpoints for Communication means, facing component ports for Boundary exchange, and work entering a performer for Allocation. The first pair never coexist in one perspective. Unoriented generic tools must not imply a fixed direction.
|
||||
|
||||
Letter badges were rejected as language-dependent and cramped. Different arrow/dash styles risk confusing tool identity with relation direction. Native endpoint/container shapes fit the existing icon system without font or asset dependencies. Preserve button dimensions, explicit labels/tooltips, ordering, messages, availability and selection rules.
|
||||
|
||||
Independent source critique found no blocker and confirmed the tool semantics and Iced API use. Actual-size legibility remains unverified, particularly the exchange arrow, port details and possible download/import interpretation of Allocation. Verify enabled, disabled, hovered and selected palettes in both perspectives, expansion/collapse, tool activation, representative relation creation and cancellation. Existing keyboard/accessibility behavior must be checked, not inferred from hover labels. Reuse semantic connection tests; geometry hashes alone are not an identification or usability evaluator.
|
||||
|
||||
Linux remains excluded by the existing user pause. Live ownership and integration status belong to the linked issue.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Outgoing allocation dragging
|
||||
|
||||
Scope: [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40),
|
||||
based on published Windows source `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This contribution is not integrated release acceptance. User requirements remain
|
||||
in `PROJECT_GOAL.md`; this note records a revision to derived interaction policy.
|
||||
|
||||
## Reproduction and cause
|
||||
|
||||
The released executable SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`
|
||||
opened a copied Allocation/Process companion fixture on Windows. At 1440x900,
|
||||
72% Fit, the OA assign view's X-receive was dragged from window (486,546) to
|
||||
(365,546), just beyond A's left edge. It remained assigned at its original
|
||||
position, reporting enlarged card overlap. A control drag to (625,572) removed
|
||||
assignment; Undo restored the original scene. These are actual native end-state
|
||||
observations; intermediate jitter was diagnosed from source, not frame capture.
|
||||
|
||||
Frozen targets resolve from the pointer, while placement retains its grab
|
||||
offset. The pointer therefore leaves its performer before the full work card
|
||||
does. Former parent/child intersection becomes unrelated overlap after removing
|
||||
allocation. Refused motion previously cleared the preview and redrew the saved
|
||||
scene. Additionally, whole-header viewport containment rejects a useful outgoing
|
||||
position in the fitted reproduction even when its model geometry is valid.
|
||||
|
||||
## Selected policy
|
||||
|
||||
Try exact snapped pointer placement first. Only an outgoing assignment candidate
|
||||
with a placement refusal may propose bounded source clearance: move the entire
|
||||
captured work footprint through a crossed frozen source boundary, clearing the
|
||||
post-removal source enclosure by one 10-world-unit grid cell. Consider at most
|
||||
four directions, ordered by press-to-pointer boundary-crossing fraction and a
|
||||
fixed direction tie-break. Existing placement, collision, semantic, exact-command
|
||||
and source/history admission remain authoritative. No neighbor is moved, no
|
||||
general packing/search occurs, and no semantic refusal is overridden.
|
||||
|
||||
The accepted/candidate semantic indexes are reused during fallback projections.
|
||||
Outgoing headers may be partly clipped but each must retain a 24x24 logical-pixel
|
||||
grab region. Other assignment and Arrange visibility rules remain unchanged.
|
||||
The exact corrected candidate is previewed; release recomputes it. Blocked
|
||||
in-bounds motion retains its last admitted scene and adds an orange pointer
|
||||
outline. This display state confers no permission to commit on refused release.
|
||||
Cancel, outside release and gesture invalidation retire it normally.
|
||||
|
||||
Rejected alternatives: retaining a moving outline alone leaves the boundary
|
||||
dead band; allowing unrelated overlap makes unassigned work look allocated;
|
||||
general auto-layout changes unrelated placement and makes drag intent harder to
|
||||
predict. The previous unconditional pointer-offset rule is narrowed in
|
||||
`docs/language.rst` and its HTML copy. Real neighboring-card collisions and
|
||||
insufficient visible grab area can still refuse a drop.
|
||||
|
||||
## Verification status
|
||||
|
||||
Independent source diagnosis and repair critique found no semantic/stale/cancel
|
||||
bypass. Review exposed the remaining whole-header camera restriction and an
|
||||
obsolete retained-preview assertion; both were revised. Added checks cover
|
||||
OA/SA boundary clearance, corners, source shrinkage, rigid descendants/groups,
|
||||
real App frame-by-frame input, exact source/history, cancellation, blocked release
|
||||
and outgoing grab-area boundaries. At clean source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`, the Windows allocation-drag
|
||||
suite passes 121 tests (none failed or ignored; 1,424 filtered out), strict
|
||||
desktop all-target Clippy passes, and the normal desktop development build
|
||||
passes. [Raw results and reproduction commands](experiments/drag-out-20260925/README.md)
|
||||
preserve the source, platform, failed attempts and exact scope. These checks do
|
||||
not prove native usability. Computer Use was stopped with the physical Escape
|
||||
key before the changed executable could be inspected. Fitted-view dragging,
|
||||
clipped-card identification/regrabbing, native Undo/Redo and Save/reopen remain
|
||||
unverified. The issue remains open; no integration or release acceptance follows
|
||||
from the targeted gates.
|
||||
|
||||
Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` additionally
|
||||
checks an actual fallback blocked by unrelated work, including preservation of
|
||||
an existing redo branch, and an outgoing move while an allocated sibling keeps
|
||||
the source enclosure expanded. Both run for OA and SA. All 123 allocation-drag
|
||||
tests and strict desktop Clippy pass at that successor. Fresh independent review
|
||||
confirms those tests exercise their claimed invariants, with no material source
|
||||
finding in this scope. The same linked evidence includes the new logs and review;
|
||||
production code and native-acceptance limits remain unchanged.
|
||||
|
||||
The first build attempt failed before tests because the sparse checkout omitted
|
||||
`build/windows_manifest.rs`; restoring the tracked build directory corrected the
|
||||
checkout. The local drive initially had about 22 MB free. Git sparse checkout and
|
||||
Cargo's package-scoped cleanup of inactive desktop artifacts recovered capacity.
|
||||
Automatic review rejected recursive deletion of the extracted preview; it was
|
||||
left intact. No published package or unique evidence was deleted.
|
||||
The second attempt also exposed omitted tracked harness fixtures and a `Self`
|
||||
typo in a standalone test helper; both were corrected before the passing run.
|
||||
That failed compiler process was terminated after its errors were captured.
|
||||
@@ -0,0 +1 @@
|
||||
*.log -text
|
||||
@@ -0,0 +1,91 @@
|
||||
# Small architecture diagram CPU investigation
|
||||
|
||||
Question: does core projection/routing explain [issue 44's low FPS](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/44)
|
||||
with roughly eight architecture cards? This experiment narrows that question;
|
||||
it neither reproduces native frame rate nor establishes a complete cause.
|
||||
|
||||
## Workload and source
|
||||
|
||||
The screenshot uses names from `examples/release1`. Its exact model, projection,
|
||||
camera and authored geometry are unavailable. The probe loads the unchanged
|
||||
`examples/release1/model.json`, measures its five saved views, and constructs a
|
||||
clearly labeled eight-card SA Overview approximation using those semantic
|
||||
elements. Approximate coordinates and stable placement IDs are in the probe;
|
||||
it has 8 projected nodes, 6 ports and 13 projected edges. It is not a recovered
|
||||
copy of the user's diagram. All data changes occur in memory.
|
||||
|
||||
Source: `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`; core, manifest and lockfile
|
||||
are unchanged from the drag contribution's `1a669130` production source.
|
||||
Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, LLVM-MinGW
|
||||
20260908 UCRT, reported CPU Intel Core i7-10870H with 4 exposed cores/threads.
|
||||
The probe was optimized with `-O` and linked to the existing **unoptimized
|
||||
development-profile core library** used by the completed desktop test build.
|
||||
This distinction matters: optimizing the driver does not optimize that library.
|
||||
The desktop test process had completed before measurement; no Cargo compilation
|
||||
was started until after measurement. Other operating-system workloads were not
|
||||
controlled, so outliers are retained rather than interpreted as stable capacity.
|
||||
|
||||
## Method and results
|
||||
|
||||
Each phase gets 3 warmups and 40 timed samples, with returned values passed to
|
||||
`black_box`. Reported p50/p95 select sorted index `ceil((n-1)*p)` as implemented
|
||||
in the source. Returned output destruction is outside each timed phase. Diagram
|
||||
movement uses a deterministic small delta sequence and the existing descendant
|
||||
and authored-route translation APIs. Index creation, projection, routing, and
|
||||
combined movement preparation are measured separately. The probe does not model
|
||||
allocation transaction admission or an Iced frame.
|
||||
|
||||
For the eight-card approximation, `performance-01.log` records:
|
||||
|
||||
| Phase | p50 ms | p95 ms | max ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Reused-index projection | 0.3885 | 0.7550 | 1.2965 |
|
||||
| Fresh-index projection | 1.7135 | 2.7022 | 4.2204 |
|
||||
| Settled routing | 2.4525 | 4.4617 | 4.7735 |
|
||||
| Moved routing | 3.1907 | 4.4781 | 4.5672 |
|
||||
| Movement preparation + projection + routing | 4.2640 | 6.8259 | 8.8052 |
|
||||
|
||||
This workload does not expose severe core latency sufficient by itself to
|
||||
explain very low FPS. It does not rule out different authored routes, allocation
|
||||
admission, hardware/rendering, or the user's exact scene. Repeated fresh-index
|
||||
projection has a measurable cost, but no change is justified solely by this
|
||||
small sample. No FPS or performance-fix claim follows.
|
||||
|
||||
## Source observations and next discriminating measurement
|
||||
|
||||
Independent read-only hot-path review found that `DiagramCanvas::scene` already
|
||||
caches projection and routing by semantic context and transient geometry.
|
||||
Ordinary node movement changes preview geometry and routes the full view;
|
||||
route gestures use `route_edge_preview`. Ordinary hover need not reroute.
|
||||
|
||||
`diagram/port_labels.rs` requests redraw on every mouse event, including when
|
||||
no tooltip is visible. `DiagramCanvas::draw` rebuilds geometry and each dot-grid
|
||||
circle on every draw. At a 1900x660 canvas this yields about 2,240 circles at
|
||||
zoom 1 and 6,528 at minimum spacing. Scene caching does not cache that geometry.
|
||||
`views.rs` also performs an unindexed `project_view` during app view rebuilding;
|
||||
legacy node-motion redraw does not itself require that full UI rebuild.
|
||||
|
||||
Next, measure actual canvas draw/frame costs independently for idle hover, pan,
|
||||
ordinary node drag and allocation drag, with grid generation separated from
|
||||
node/edge/text rendering and frame presentation. Compare the same scene/camera
|
||||
and renderer before changing production code. Native automation was stopped
|
||||
with Escape; no GPU/frame measurement was attempted in this experiment. Live
|
||||
ownership and subsequent results belong to issue 44.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Build `oa-core` at the identified source with the stated Windows toolchain, then
|
||||
compile this standalone probe against that exact library and matching dependency
|
||||
directory. For a fresh standard Cargo development build:
|
||||
|
||||
```powershell
|
||||
cargo build --locked -p oa-core
|
||||
rustc --edition=2024 -O harness/experiments/architecture-perf-20260925/architecture_perf_probe.rs --extern oa_core=target/debug/liboa_core.rlib -L dependency=target/debug/deps -C linker=clang.exe -o architecture_perf_probe.exe
|
||||
./architecture_perf_probe.exe examples/release1/model.json 40
|
||||
```
|
||||
|
||||
The actual run linked `target/debug/deps/liboa_core-2f3c5174cb6398fd.rlib`
|
||||
and used the same dependency directory. Set the linker/runtime PATH as required
|
||||
by the Windows toolchain. Compiler flags, core profile, machine load and fixture
|
||||
all affect timings; preserve them when comparing candidates. This standalone
|
||||
probe is experimental evidence, not a production dependency or a timing test.
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Local-only CPU diagnosis. Does not measure native FPS, canvas redraw, or GPU.
|
||||
//! Usage: architecture_perf_probe.exe PATH_TO_RELEASE1_MODEL_JSON [SAMPLES=40]
|
||||
use oa_core::{persistence, projection, routing, view_options::ViewIndex, *};
|
||||
use std::{collections::BTreeMap, hint::black_box, time::Instant};
|
||||
|
||||
fn measure<T>(label: &str, samples: usize, mut f: impl FnMut(usize) -> T) {
|
||||
for i in 0..3 {
|
||||
black_box(f(i));
|
||||
}
|
||||
let mut ms = Vec::with_capacity(samples);
|
||||
for i in 0..samples {
|
||||
let start = Instant::now();
|
||||
let output = black_box(f(i));
|
||||
ms.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
drop(output); // Exclude destruction consistently from each timed phase.
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
let percentile = |p: f64| ms[((samples - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={samples} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
percentile(0.5),
|
||||
percentile(0.95),
|
||||
ms[samples - 1]
|
||||
);
|
||||
}
|
||||
|
||||
fn probe(p: &Project, d: &Diagram, samples: usize) {
|
||||
let index = ViewIndex::new(p);
|
||||
let view = projection::project_diagram_with_index(p, d, &index);
|
||||
println!(
|
||||
"\nview={} projection={:?} nodes={} ports={} edges={}",
|
||||
d.name,
|
||||
d.projection,
|
||||
view.nodes.len(),
|
||||
view.ports.len(),
|
||||
view.edges.len()
|
||||
);
|
||||
measure("index_new", samples, |_| ViewIndex::new(black_box(p)));
|
||||
measure("projection_reused_index", samples, |_| {
|
||||
projection::project_diagram_with_index(black_box(p), black_box(d), black_box(&index))
|
||||
});
|
||||
measure("projection_fresh_index", samples, |_| {
|
||||
projection::project_diagram(black_box(p), black_box(d))
|
||||
});
|
||||
measure("routing_settled", samples, |_| {
|
||||
routing::route_diagram(black_box(p), black_box(d), black_box(&view))
|
||||
});
|
||||
// Deterministic slight motion of the first root card. Includes the same
|
||||
// descendant/route translation as legacy single-card canvas drag. This is
|
||||
// presentation-only, not the allocation transaction pipeline.
|
||||
let Some(root) = view.nodes.iter().find(|n| n.visual_parent.is_none()) else {
|
||||
return;
|
||||
};
|
||||
println!("moving_root={} element={}", root.id, p.named(root.element));
|
||||
let mut moved = projection::descendant_nodes(p, d.id, root.id);
|
||||
moved.push(root.id);
|
||||
let make_moved = |i: usize| {
|
||||
let delta = Position {
|
||||
x: (i % 21) as f32 * 3.0,
|
||||
y: (i % 11) as f32 * 2.0,
|
||||
};
|
||||
let mut preview = d.clone();
|
||||
let deltas = moved
|
||||
.iter()
|
||||
.map(|id| (*id, delta))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for route in projection::translated_routes(p, d, &deltas) {
|
||||
if let Some(stored) = preview.routes.iter_mut().find(|r| r.edge == route.edge) {
|
||||
*stored = route;
|
||||
} else {
|
||||
preview.routes.push(route);
|
||||
}
|
||||
}
|
||||
for id in &moved {
|
||||
if let Some(n) = preview.nodes.get_mut(id) {
|
||||
n.position.x += delta.x;
|
||||
n.position.y += delta.y;
|
||||
}
|
||||
}
|
||||
preview
|
||||
};
|
||||
let previews = (0..samples.max(3)).map(make_moved).collect::<Vec<_>>();
|
||||
let projected = previews
|
||||
.iter()
|
||||
.map(|d| projection::project_diagram_with_index(p, d, &index))
|
||||
.collect::<Vec<_>>();
|
||||
measure("moved_projection", samples, |i| {
|
||||
projection::project_diagram_with_index(p, black_box(&previews[i]), &index)
|
||||
});
|
||||
measure("moved_routing", samples, |i| {
|
||||
routing::route_diagram(p, black_box(&previews[i]), black_box(&projected[i]))
|
||||
});
|
||||
measure("moved_prepare_projection_routing", samples, |i| {
|
||||
let preview = make_moved(i);
|
||||
let view = projection::project_diagram_with_index(p, &preview, &index);
|
||||
routing::route_diagram(p, &preview, &view)
|
||||
});
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
let path = args
|
||||
.get(1)
|
||||
.ok_or("usage: architecture_perf_probe PATH [SAMPLES]")?;
|
||||
let samples = args
|
||||
.get(2)
|
||||
.map(|s| s.parse::<usize>())
|
||||
.transpose()?
|
||||
.unwrap_or(40)
|
||||
.max(1);
|
||||
let p = persistence::decode(&std::fs::read(path)?)?;
|
||||
println!(
|
||||
"CPU-only probe; samples={samples}; fixture={path}; elements={} relations={}",
|
||||
p.elements.len(),
|
||||
p.relations.len()
|
||||
);
|
||||
for d in p.diagrams.values() {
|
||||
probe(&p, d, samples);
|
||||
}
|
||||
// Approximation of the reported eight-card screenshot, not an exact repro:
|
||||
// fixture semantics, Overview projection, synthetic positions/card sizes.
|
||||
let mut approx = p.clone();
|
||||
let id: Id = "f0440000-0000-4000-8000-000000000001".parse()?;
|
||||
approx.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: "fps44_approx_overview".into(),
|
||||
name: "APPROXIMATION eight-card SA Overview".into(),
|
||||
description: "Synthetic positions, not user geometry".into(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
);
|
||||
for (ordinal, (alias, x, y)) in [
|
||||
("sa_assessment_procedure", 30., 240.),
|
||||
("sa_triage", 510., 240.),
|
||||
("sa_control", 990., 240.),
|
||||
("sa_technician", 530., 440.),
|
||||
("sa_assess", 1490., 240.),
|
||||
("sa_repair", 1030., 440.),
|
||||
("sa_confirm", 1530., 440.),
|
||||
("sa_reset", 30., 440.),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let element = approx
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.ok_or_else(|| format!("missing alias {alias}"))?
|
||||
.id;
|
||||
let generated = approx.place(id, element, Position { x, y });
|
||||
let diagram = approx.diagrams.get_mut(&id).unwrap();
|
||||
let mut node = diagram.nodes.remove(&generated).unwrap();
|
||||
node.id = format!("f0440000-0000-4000-8000-{:012}", ordinal + 2).parse()?;
|
||||
diagram.nodes.insert(node.id, node);
|
||||
}
|
||||
probe(&approx, &approx.diagrams[&id], samples);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"core_profile": "dev unoptimized",
|
||||
"driver_optimization": "rustc -O",
|
||||
"samples_per_phase": 40,
|
||||
"warmups_per_phase": 3,
|
||||
"rlib_sha256": "E10C81BF28A33CD0FB6C4C26884CFFA480EF05328E78BCDB138DD4B635B2531C",
|
||||
"fixture_sha256": "EF0EF4006C3E09D5DB6E1B621256B3F7D6642F3A7B09CC34607CFBCCA830C727",
|
||||
"driver_exe_sha256": "4C66E9CDE4F08D6DFD2A0DC3DBF1A80B97E340F3F22CA001A76301F06F47C2E2",
|
||||
"native_fps_measured": false
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
CPU-only probe; samples=40; fixture=.local/drag-out-01a0d6fd/examples/release1/model.json; elements=94 relations=62
|
||||
|
||||
view=1 · OA responsibility and repair traffic projection=Allocation nodes=6 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=1.0106 p95_ms=2.0583 max_ms=2.0910
|
||||
projection_reused_index: n=40 p50_ms=0.5702 p95_ms=1.2320 max_ms=1.3490
|
||||
projection_fresh_index: n=40 p50_ms=1.5505 p95_ms=3.6681 max_ms=5.1533
|
||||
routing_settled: n=40 p50_ms=0.1944 p95_ms=0.3391 max_ms=0.4246
|
||||
moving_root=3191e05c-1c7f-5f0e-beb0-5f3bded54c48 element=Control room
|
||||
moved_projection: n=40 p50_ms=0.4848 p95_ms=1.2745 max_ms=1.5797
|
||||
moved_routing: n=40 p50_ms=0.1994 p95_ms=0.7971 max_ms=1.1414
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.9250 p95_ms=2.6847 max_ms=10.4908
|
||||
|
||||
view=5 · SA pressure supervision projection=StateMachine { machine: 132b3f28-3f88-5327-9868-8c8b645ed412 } nodes=5 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=0.9602 p95_ms=2.1394 max_ms=2.4512
|
||||
projection_reused_index: n=40 p50_ms=0.1697 p95_ms=0.7629 max_ms=1.1780
|
||||
projection_fresh_index: n=40 p50_ms=1.1189 p95_ms=2.6324 max_ms=3.3912
|
||||
routing_settled: n=40 p50_ms=0.7063 p95_ms=1.0812 max_ms=1.1149
|
||||
moving_root=a676fbb9-0833-59a8-98cf-a10d8f4b7434 element=Pressure supervision
|
||||
moved_projection: n=40 p50_ms=0.1565 p95_ms=0.3445 max_ms=0.3790
|
||||
moved_routing: n=40 p50_ms=0.6113 p95_ms=1.3199 max_ms=1.3733
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.8259 p95_ms=1.7640 max_ms=2.1752
|
||||
|
||||
view=2 · SA service boundary and functions projection=Allocation nodes=6 ports=6 edges=3
|
||||
index_new: n=40 p50_ms=0.7820 p95_ms=3.4180 max_ms=6.8439
|
||||
projection_reused_index: n=40 p50_ms=0.7938 p95_ms=1.9946 max_ms=2.1411
|
||||
projection_fresh_index: n=40 p50_ms=2.0597 p95_ms=3.4554 max_ms=3.6616
|
||||
routing_settled: n=40 p50_ms=0.1478 p95_ms=0.7886 max_ms=1.0081
|
||||
moving_root=8345c76a-8d85-5c31-902e-ec1cd33ed4a3 element=Pressure coordination service
|
||||
moved_projection: n=40 p50_ms=0.6914 p95_ms=1.5286 max_ms=1.8658
|
||||
moved_routing: n=40 p50_ms=0.1862 p95_ms=0.5658 max_ms=2.8924
|
||||
moved_prepare_projection_routing: n=40 p50_ms=1.1724 p95_ms=3.1806 max_ms=4.2620
|
||||
|
||||
view=4 · SA mission and included capability projection=Capabilities nodes=4 ports=1 edges=3
|
||||
index_new: n=40 p50_ms=0.6879 p95_ms=1.3582 max_ms=1.5266
|
||||
projection_reused_index: n=40 p50_ms=0.1562 p95_ms=0.6767 max_ms=1.0102
|
||||
projection_fresh_index: n=40 p50_ms=1.2929 p95_ms=2.3543 max_ms=4.0762
|
||||
routing_settled: n=40 p50_ms=0.2553 p95_ms=1.4389 max_ms=1.6677
|
||||
moving_root=069a3fb3-077c-59d8-91da-27b0665143c7 element=Assess an alarm
|
||||
moved_projection: n=40 p50_ms=0.1243 p95_ms=0.1928 max_ms=0.2212
|
||||
moved_routing: n=40 p50_ms=0.2271 p95_ms=0.6733 max_ms=1.2408
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.5542 p95_ms=1.3344 max_ms=1.8957
|
||||
|
||||
view=3 · SA service conversation only projection=Allocation nodes=6 ports=6 edges=1
|
||||
index_new: n=40 p50_ms=0.9618 p95_ms=1.6890 max_ms=2.0995
|
||||
projection_reused_index: n=40 p50_ms=0.6661 p95_ms=2.3388 max_ms=2.6429
|
||||
projection_fresh_index: n=40 p50_ms=2.3913 p95_ms=4.1941 max_ms=4.8120
|
||||
routing_settled: n=40 p50_ms=0.0660 p95_ms=0.2156 max_ms=0.6289
|
||||
moving_root=40210589-69f7-5209-b329-10bb83cf615c element=Field technician
|
||||
moved_projection: n=40 p50_ms=0.5527 p95_ms=1.1239 max_ms=1.1362
|
||||
moved_routing: n=40 p50_ms=0.0495 p95_ms=0.1771 max_ms=0.2209
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.7963 p95_ms=4.1835 max_ms=6.7551
|
||||
|
||||
view=APPROXIMATION eight-card SA Overview projection=Overview nodes=8 ports=6 edges=13
|
||||
index_new: n=40 p50_ms=0.7487 p95_ms=1.9597 max_ms=2.2005
|
||||
projection_reused_index: n=40 p50_ms=0.3885 p95_ms=0.7550 max_ms=1.2965
|
||||
projection_fresh_index: n=40 p50_ms=1.7135 p95_ms=2.7022 max_ms=4.2204
|
||||
routing_settled: n=40 p50_ms=2.4525 p95_ms=4.4617 max_ms=4.7735
|
||||
moving_root=f0440000-0000-4000-8000-000000000002 element=Assess an alarm procedure
|
||||
moved_projection: n=40 p50_ms=0.4691 p95_ms=0.8354 max_ms=1.0408
|
||||
moved_routing: n=40 p50_ms=3.1907 p95_ms=4.4781 max_ms=4.5672
|
||||
moved_prepare_projection_routing: n=40 p50_ms=4.2640 p95_ms=6.8259 max_ms=8.8052
|
||||
@@ -0,0 +1 @@
|
||||
*.log -text
|
||||
@@ -0,0 +1,74 @@
|
||||
# Outgoing allocation drag evidence
|
||||
|
||||
Contribution source: `1a669130fb6d2b65279aab3847e966a1a282c191`, based on
|
||||
published preview `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This is branch-specific Windows evidence for [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40).
|
||||
It is not acceptance of an integrated release or the complete application.
|
||||
The [decision and native baseline](../../drag-out-20260925.md) explain the repair.
|
||||
|
||||
## Environment and commands
|
||||
|
||||
Windows 10.0.26200, Rust/Cargo 1.98.1, host/target
|
||||
`x86_64-pc-windows-gnullvm`, LLVM 22.1.8, LLVM-MinGW 20260908 UCRT linker
|
||||
`clang.exe`, `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`, default development
|
||||
profile. Run at the identified source with the Windows GNU LLVM toolchain and
|
||||
its linker/runtime available on PATH. A sparse checkout must include `build`
|
||||
and `harness/fixtures` as well as crates. The actual commands were:
|
||||
|
||||
```powershell
|
||||
cargo test --locked --offline -p oa-desktop allocation_drag -- --test-threads=1
|
||||
cargo clippy --locked --offline -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked --offline -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
Offline commands require dependencies already cached. A fresh environment must
|
||||
fetch the locked dependencies first. Raw logs retain original absolute paths;
|
||||
these paths identify this run, not required directories on another machine.
|
||||
|
||||
## Results and boundaries
|
||||
|
||||
- Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` adds two
|
||||
OA/SA App regressions: a clearance proposal blocked by unrelated work and an
|
||||
outgoing move with a retained sibling enlarging the source enclosure.
|
||||
`tests-04.log` passes all 123 allocation-drag tests (zero failed/ignored,
|
||||
1,424 filtered) in 54.86s; `clippy-02.log` passes the same strict command in
|
||||
1m27s. Production inputs are unchanged from `1a669130`; its normal binary
|
||||
remains the native candidate. [Independent review](independent-review.md)
|
||||
covers the source, new test design and evidence limits.
|
||||
- `tests-03.log`: 121 passed, zero failed/ignored, 1,424 filtered; 35.33 seconds
|
||||
test execution. This is the allocation-drag subset, not the full desktop suite.
|
||||
- `clippy-01.log`: strict desktop all-target Clippy passed in 4m30s.
|
||||
- `build-01.log`: normal GUI build passed in 5m16s. The resulting local executable
|
||||
SHA256 is `9bda1a6a1d24fdc98c7a8822400c12fea9afe1de828a82ba9273f03b99e47c07`.
|
||||
It was staged with the published preview's `libunwind.dll` and started, but
|
||||
Computer Use was stopped before any changed-binary native observation.
|
||||
- `context.json`: recorded source/toolchain, passing test-log and baseline-image
|
||||
hashes, and clean source after the test run.
|
||||
- `tests-01.log`: compilation failed before tests because `build/windows_manifest.rs`
|
||||
was excluded from the initial sparse checkout.
|
||||
- `tests-02.log`: compilation failed before tests because harness fixture files
|
||||
were excluded and a standalone test helper used `Self` instead of `Fixture`.
|
||||
The compiler was terminated after reporting errors; its interrupted exit is
|
||||
preserved. The missing tracked files and typo were repaired before run 03.
|
||||
|
||||
The tests exercise source-clearance proposals, OA/SA App event sequences,
|
||||
preview/release agreement, exact source/history, cancellation, and retained
|
||||
preview without commit authority on refused release. Existing collision and
|
||||
semantic checks still govern all proposals. Independent source review found the
|
||||
whole-header viewport restriction and obsolete preview assertion; both were
|
||||
corrected before the passing run. This is source/test review, not native review.
|
||||
|
||||
`baseline-left-refusal.png` is an unmodified actual native capture from the
|
||||
published preview executable, SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`.
|
||||
In the copied Allocation companion fixture, OA assign view at 1440x900 content
|
||||
and Fit 72%, dragging X from window (486,546) to (365,546) leaves allocation and
|
||||
placement unchanged and reports overlap. A control drag to (625,572) removes
|
||||
allocation, and Undo restores it. Intermediate jitter was diagnosed from source;
|
||||
this image is end-state evidence only.
|
||||
|
||||
Changed-binary native acceptance is still required: repeat the short fitted-view
|
||||
drag, identify and regrab the partially clipped card, check refused-motion
|
||||
feedback, Cancel and Undo/Redo, save the full bundle, close and reopen in a fresh
|
||||
process, and verify identity/allocation/placement and unchanged sibling data for
|
||||
OA and SA. Linux/WSL remains deferred by the user. No published package changed.
|
||||
|
After Width: | Height: | Size: 129 KiB |
@@ -0,0 +1,5 @@
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling iced v0.14.0
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 5m 16s
|
||||
@@ -0,0 +1,6 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Checking iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Checking iced v0.14.0
|
||||
Finished `dev` profile [unoptimized] target(s) in 4m 30s
|
||||
@@ -0,0 +1,2 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 1m 27s
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"platform": "Microsoft Windows 10.0.26200",
|
||||
"compiler": [
|
||||
"rustc 1.98.1 (48a229cea 2026-09-01)",
|
||||
"binary: rustc",
|
||||
"commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985",
|
||||
"commit-date: 2026-09-01",
|
||||
"host: x86_64-pc-windows-gnullvm",
|
||||
"release: 1.98.1",
|
||||
"LLVM version: 22.1.8"
|
||||
],
|
||||
"cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)",
|
||||
"test_log_sha256": "8EB57F522FA0DCCA327DC13C67A9C1B14F5FAB09C5599C108C0BA6D875C78886",
|
||||
"baseline_image_sha256": "AAF162619582018D1197D255B4BA1122747AA22703D8DEDFC7FCEFCB42AEFE38",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"incremental": "0",
|
||||
"jobs": "1",
|
||||
"working_tree_clean_after_test": true
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Independent source and evidence review
|
||||
|
||||
Fresh read-only GPT-6 Astra review, 2026-09-25. Production source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`; test-only successor
|
||||
`24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`. No GUI actions or test execution
|
||||
were performed by this reviewer.
|
||||
|
||||
No material correctness finding in the bounded scope:
|
||||
|
||||
- The outgoing collision regression in `app/allocation_drag_tests.rs` proves a
|
||||
real source-boundary crossing and unobscured OutsidePerformers target, a
|
||||
nonempty clearance proposal, and intersection with unrelated work. Ordinary
|
||||
placement rejects that proposal. Release preserves project, source bundle,
|
||||
revision, snapshot identity and redo availability.
|
||||
- The retained-sibling regression first requires exact requested placement to
|
||||
fail. The admitted correction clears the enlarged source enclosure while
|
||||
preserving its projection, sibling allocation/placement and all other stored
|
||||
placements. Preview/release and source Undo/Redo comparisons are exact.
|
||||
- In `app/allocation_drag.rs`, clearance remains downstream of semantic
|
||||
admission and retries ordinary placement. Release recomputes placement and
|
||||
passes through `finish` and document execution. No collision, stale-state or
|
||||
retained-preview commit bypass was found.
|
||||
- The committed passing log's bytes and SHA256 match `context.json`; the
|
||||
baseline image hash also matches. Logs support 121 passing tests, strict
|
||||
Clippy and normal development build for the production source. They do not
|
||||
establish execution of the two successor tests; consult the linked raw logs
|
||||
and README for subsequent execution evidence.
|
||||
|
||||
The review supports the source policy and test design, not native usability.
|
||||
The 24x24 visible-header threshold alone does not prove readable identity or
|
||||
practical regrabbing. Changed-binary fitted-view dragging, feedback, recovery
|
||||
and Save/reopen remain unverified. This review is not the broad final
|
||||
requirements/domain/UX/release review required by `PROJECT_GOAL.md`.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"production_source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"test_log": "tests-04.log",
|
||||
"test_log_sha256": "C98E0F70D60788B484146B3C8785891888BA696130ACC4BFD7D30DA570E6B1E1",
|
||||
"passed": 123,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"filtered": 1424,
|
||||
"native_candidate_sha256": "9BDA1A6A1D24FDC98C7A8822400C12FEA9AFE1DE828A82BA9273F03B99E47C07",
|
||||
"native_acceptance": "unverified"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
error: couldn't read `crates\oa-core\../../build/windows_manifest.rs`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-core\build.rs:1:1
|
||||
|
|
||||
1 | include!("../../build/windows_manifest.rs");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: could not compile `oa-core` (build script) due to 1 previous error
|
||||
@@ -0,0 +1,68 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Compiling iced v0.14.0
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\app\reference_authoring\tests\mounted.rs:755:37
|
||||
|
|
||||
755 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
756 | | env!("CARGO_MANIFEST_DIR"),
|
||||
757 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
758 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario\usage\tests.rs:235:37
|
||||
|
|
||||
235 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
236 | | env!("CARGO_MANIFEST_DIR"),
|
||||
237 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
238 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario_operation_tests.rs:6:25
|
||||
|
|
||||
6 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
7 | | env!("CARGO_MANIFEST_DIR"),
|
||||
8 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
9 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/source7-native/base/expected-baseline.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\topology\contextual_routing.rs:61:29
|
||||
|
|
||||
61 | persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________^
|
||||
62 | | env!("CARGO_MANIFEST_DIR"),
|
||||
63 | | "/../../harness/fixtures/source7-native/base/expected-baseline.json"
|
||||
64 | | )))
|
||||
| |__________^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\semantic_review\source7_tests.rs:5:25
|
||||
|
|
||||
5 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
6 | | env!("CARGO_MANIFEST_DIR"),
|
||||
7 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
8 | | )))
|
||||
| |______^
|
||||
|
||||
error[E0411]: cannot find struct, variant or union type `Self` in this scope
|
||||
--> crates\oa-desktop\src\app\allocation_drag\clearance.rs:314:9
|
||||
|
|
||||
287 | fn fixture(layer: Layer) -> Fixture {
|
||||
| ------- `Self` not allowed in a function
|
||||
...
|
||||
314 | Self {
|
||||
| ^^^^ `Self` is only available in impls, traits, and type definitions
|
||||
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 6 previous errors
|
||||
|
||||
Caused by:
|
||||
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=5e2096130f19ead2 -C extra-filename=-c5b42126ba36cd38 --out-dir C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps -C linker=clang.exe -C strip=debuginfo -L dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps --extern axum=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced-594f7d417d69ed72.rlib --extern iced_runtime=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced_runtime-4d31a36fe5cef0b0.rlib --extern oa_core=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_core-2f3c5174cb6398fd.rlib --extern oa_server=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_server-bb42169ca297f969.rlib --extern reqwest=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtokio-5e0ca96735e3073a.rlib --extern unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libunicode_segmentation-85698043f7780858.rlib -L native=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\oa-desktop-307d320bbc13ccd5\out\normal-user-manifest.o` (exit code: 0xffffffff)
|
||||
@@ -0,0 +1,130 @@
|
||||
Blocking waiting for file lock on build directory
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 8m 13s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 121 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 121 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 35.33s
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 6m 05s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 123 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_collision_refuses_release_without_source_or_history_mutation ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_preserves_source_enclosure_and_allocated_sibling ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 123 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 54.86s
|
||||
|
||||
@@ -1,714 +0,0 @@
|
||||
"""Navigation evidence transport. Integrity is not product acceptance.
|
||||
|
||||
Commands: freeze TEMPLATE SOURCE_LOCAL NEW_FROZEN_CONFIG;
|
||||
build FROZEN_CONFIG SOURCE_LOCAL FRESH_PACKET; verify PACKET;
|
||||
selftest FRESH_DIRECTORY. No extraction, subprocesses, or native execution.
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import zipfile
|
||||
import datetime
|
||||
import copy
|
||||
|
||||
MAX_FILE = 128 * 1024 * 1024
|
||||
MAX_TOTAL = 1024 * 1024 * 1024
|
||||
EXCLUDED = {"bin", "profile", "target", ".git", "navigation-oracle-runtime"}
|
||||
BLOCKED_SUFFIXES = {'.exe', '.dll', '.pdb', '.lib', '.obj', '.pfx', '.pem', '.key'}
|
||||
|
||||
|
||||
def need(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def pairs(items):
|
||||
out = {}
|
||||
for key, value in items:
|
||||
need(key not in out, "duplicate JSON key: " + key)
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def decode(data):
|
||||
def reject_constant(value):
|
||||
raise ValueError('nonfinite JSON constant: ' + value)
|
||||
return json.loads(data, object_pairs_hook=pairs, parse_constant=reject_constant)
|
||||
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def safe(name):
|
||||
need(isinstance(name, str) and name and "\\" not in name and ":" not in name,
|
||||
"unsafe path")
|
||||
p = PurePosixPath(name)
|
||||
need(bool(p.parts) and not p.is_absolute() and str(p) == name, "noncanonical path: " + name)
|
||||
need(all(x not in (".", "..") and not x.endswith((".", " ")) for x in p.parts),
|
||||
"unsafe component: " + name)
|
||||
need(not any(re.fullmatch(r"(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?", x)
|
||||
for x in p.parts), "reserved component")
|
||||
need(not any(ord(c) < 32 or c in '<>"|?*' for c in name), "nonportable path")
|
||||
return name
|
||||
|
||||
|
||||
def permitted(name):
|
||||
safe(name)
|
||||
parts = PurePosixPath(name).parts
|
||||
need(not any(x.casefold() in EXCLUDED | {'.ssh', '.codex', '.config', 'credentials', 'auth'}
|
||||
or x.casefold().startswith('.env') for x in parts), 'excluded/private input')
|
||||
need(PurePosixPath(name).suffix.casefold() not in BLOCKED_SUFFIXES
|
||||
and PurePosixPath(name).name.casefold() != 'push-navigation.py', 'binary/private input')
|
||||
|
||||
|
||||
def hash_string(value):
|
||||
need(isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value), 'invalid hash')
|
||||
return value
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
need(isinstance(value, str) and value, 'missing timestamp')
|
||||
parsed = datetime.datetime.fromisoformat(value)
|
||||
need(parsed.tzinfo is not None, 'timestamp needs timezone')
|
||||
return parsed
|
||||
|
||||
|
||||
def inventory(value):
|
||||
need(isinstance(value, dict) and value, 'missing identity inventory')
|
||||
folded = set()
|
||||
for name, sha in value.items():
|
||||
safe(name)
|
||||
need(name.casefold() not in folded, 'identity inventory case collision')
|
||||
folded.add(name.casefold())
|
||||
hash_string(sha)
|
||||
|
||||
|
||||
def validate_config(config, frozen=False):
|
||||
need(config.get('schema') == 'navigation-evidence-config-v2', 'config schema')
|
||||
need(config.get('exclusions') == sorted(EXCLUDED), 'exclusion policy mismatch')
|
||||
includes = config['includes']
|
||||
need(isinstance(includes, list) and includes, 'empty includes')
|
||||
names = []
|
||||
for item in includes:
|
||||
name = safe(item['path'])
|
||||
permitted(name)
|
||||
need(item['kind'] in ('file', 'tree') and isinstance(item['category'], str)
|
||||
and item['category'], 'invalid include')
|
||||
folded = name.casefold()
|
||||
need(not any(folded == old or folded.startswith(old + '/') or old.startswith(folded + '/')
|
||||
for old in names), 'overlapping include roots')
|
||||
names.append(folded)
|
||||
for key in ('gates', 'native_cases', 'oracle_runs'):
|
||||
values = [x['name'] if isinstance(x, dict) else x for x in config[key]]
|
||||
need(len(values) == len(set(values)), 'duplicate case')
|
||||
for name in values:
|
||||
need(re.fullmatch('[a-z0-9-]+', name), 'invalid case name')
|
||||
if frozen:
|
||||
need(config.get('frozen_entries'), 'freeze and review exact file inventory first')
|
||||
|
||||
|
||||
def coverage(config, entries):
|
||||
seen = set()
|
||||
for entry in entries:
|
||||
name = entry['path']
|
||||
permitted(name)
|
||||
need(name.casefold() not in seen, 'duplicate logical path')
|
||||
seen.add(name.casefold())
|
||||
owners = [i for i in config['includes'] if name == i['path'] or
|
||||
(i['kind'] == 'tree' and name.startswith(i['path'] + '/'))]
|
||||
need(len(owners) == 1 and entry['category'] == owners[0]['category'], 'outside whitelist/category')
|
||||
for item in config['includes']:
|
||||
need(any(e['path'] == item['path'] or (item['kind'] == 'tree' and
|
||||
e['path'].startswith(item['path'] + '/')) for e in entries), 'missing required include')
|
||||
for name in config['documentation']:
|
||||
need(safe(name).casefold() in seen, 'missing documentation')
|
||||
|
||||
|
||||
def regular(path):
|
||||
# Also rejects Windows junctions, including ancestors outside the selected root.
|
||||
for item in [path, *path.parents]:
|
||||
s = item.lstat()
|
||||
need(not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s, "st_file_attributes", 0) & 0x400, "link/reparse: " + str(item))
|
||||
need(path.is_file(), "not a regular file: " + str(path))
|
||||
need(path.stat().st_size <= MAX_FILE, "file size cap")
|
||||
|
||||
|
||||
def read(path):
|
||||
regular(path)
|
||||
data = path.read_bytes()
|
||||
need(len(data) <= MAX_FILE, "file grew over cap")
|
||||
return data
|
||||
|
||||
|
||||
def walk_checked(base, exclude=False):
|
||||
# Check before descending, including junctions; os.walk never gets an
|
||||
# unchecked reparse directory to recurse into.
|
||||
for item in [base, *base.parents]:
|
||||
s = item.lstat()
|
||||
need(not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s, "st_file_attributes", 0) & 0x400, "link/reparse ancestor")
|
||||
for current, dirs, files in os.walk(base, followlinks=False):
|
||||
if exclude:
|
||||
dirs[:] = [x for x in dirs if x.casefold() not in EXCLUDED]
|
||||
for name in dirs + files:
|
||||
path = Path(current) / name
|
||||
s = path.lstat()
|
||||
need(not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s, "st_file_attributes", 0) & 0x400, "link/reparse tree")
|
||||
for name in files:
|
||||
yield Path(current) / name
|
||||
|
||||
|
||||
def selected(source, config):
|
||||
found = {}
|
||||
for item in config["includes"]:
|
||||
name = safe(item["path"])
|
||||
base = source / name
|
||||
need(base.exists(), "required input missing: " + name)
|
||||
candidates = [base] if item["kind"] == "file" else sorted(walk_checked(base, exclude=True))
|
||||
need(item["kind"] in ("file", "tree"), "unknown include kind")
|
||||
for file in candidates:
|
||||
relative = file.relative_to(source).as_posix()
|
||||
if any(x.casefold() in EXCLUDED for x in PurePosixPath(relative).parts):
|
||||
continue
|
||||
# Check directories too, before skipping them; never traverse a junction.
|
||||
s = file.lstat()
|
||||
need(not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s, "st_file_attributes", 0) & 0x400, "link/reparse input")
|
||||
if file.is_dir():
|
||||
continue
|
||||
safe(relative)
|
||||
need(relative.casefold() not in found, "overlapping/case-colliding include")
|
||||
regular(file)
|
||||
found[relative.casefold()] = (relative, item["category"], file)
|
||||
return sorted(found.values())
|
||||
|
||||
|
||||
def check_zip(data, source_inputs):
|
||||
need(isinstance(source_inputs, dict) and source_inputs, "missing source inventory")
|
||||
expected = {}
|
||||
for name, sha in source_inputs.items():
|
||||
safe(name)
|
||||
need(name.casefold() not in expected and re.fullmatch("[0-9a-f]{64}", sha),
|
||||
"invalid source inventory")
|
||||
expected[name.casefold()] = (name, sha)
|
||||
seen = set()
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
members = archive.infolist()
|
||||
need(len(members) <= 10000, "ZIP member cap")
|
||||
need(sum(x.file_size for x in members) <= 512 * 1024 * 1024, "ZIP total cap")
|
||||
for member in members:
|
||||
name = safe(member.filename)
|
||||
need(not member.is_dir() and not member.flag_bits & 1, "ZIP directory/encryption")
|
||||
mode = member.external_attr >> 16
|
||||
need(stat.S_IFMT(mode) in (0, stat.S_IFREG), "ZIP nonregular member")
|
||||
need(member.file_size <= 64 * 1024 * 1024, "ZIP member cap")
|
||||
key = name.casefold()
|
||||
need(key not in seen and key in expected and expected[key][0] == name,
|
||||
"ZIP path inventory mismatch")
|
||||
seen.add(key)
|
||||
content = archive.read(member) # zipfile checks CRC; never extracts.
|
||||
need(len(content) == member.file_size and digest(content) == expected[key][1],
|
||||
"ZIP content mismatch")
|
||||
need(seen == set(expected), "ZIP missing inputs")
|
||||
|
||||
|
||||
def streams(record, prefix, get, suffix='.'):
|
||||
for stream in ('stdout', 'stderr'):
|
||||
size = record[stream + '_bytes']
|
||||
need(type(size) is int and size >= 0, 'invalid stream size')
|
||||
path = prefix + suffix + stream if suffix == '.' else prefix + stream + '.log'
|
||||
need(len(get(path)) == size, 'stream size mismatch: ' + path)
|
||||
|
||||
|
||||
def step_sequence(result, planned, get, prefix, stop_on_failure, require_finished=True):
|
||||
steps = result.get('steps')
|
||||
need(isinstance(steps, list) and 0 < len(steps) <= len(planned), 'invalid step count')
|
||||
need(len({s['label'] for s in planned}) == len(planned), 'duplicate plan label')
|
||||
for actual, expected in zip(steps, planned):
|
||||
need(actual['label'] == expected['label'] and actual['command'] == expected['command']
|
||||
and actual.get('expectation', 'exit0') == expected.get('expectation', 'exit0'),
|
||||
'step differs from plan')
|
||||
need(type(actual.get('exit_code')) is int, 'missing step exit')
|
||||
start = actual.get('started', actual.get('start'))
|
||||
need(timestamp(actual['finished']) >= timestamp(start), 'step timestamp order')
|
||||
streams(actual, prefix + safe(actual['label']), get)
|
||||
failed = [i for i,s in enumerate(steps) if s['exit_code'] != 0]
|
||||
if stop_on_failure and failed:
|
||||
need(failed == [len(steps)-1], 'not first-failure stopped prefix')
|
||||
return 'stopped-at-first-failure'
|
||||
need(len(steps) == len(planned), 'gate still open: incomplete successful prefix')
|
||||
if require_finished:
|
||||
need(timestamp(result.get('finished')) >= timestamp(steps[-1]['finished']), 'missing final closure')
|
||||
return 'completed-with-failures' if failed else 'completed-successfully'
|
||||
|
||||
|
||||
def same_fields(before, after, keys):
|
||||
for key in keys:
|
||||
need(key in before and key in after and before[key] is not None and
|
||||
before[key] == after[key], 'identity mismatch/missing: ' + key)
|
||||
|
||||
|
||||
def receipts(config, get, paths):
|
||||
matrix = []
|
||||
for gate in config["gates"]:
|
||||
prefix = "navigation-gates/" + gate["name"] + "/"
|
||||
before = decode(get(prefix + "initial.json"))
|
||||
raw_result = get(prefix + 'result.json')
|
||||
result = decode(raw_result)
|
||||
same_fields(before, result, ('source', 'source_inputs', 'started', 'environment'))
|
||||
need(isinstance(result['source'], str) and re.fullmatch('[0-9a-f]{40}', result['source']), 'source commit')
|
||||
inventory(result['source_inputs'])
|
||||
need(result.get("inputs_unchanged") is True, "gate source conservation missing")
|
||||
if gate['name'] == 'attempt01':
|
||||
need(digest(raw_result) == 'de7c4a7361a4c4991f783c16656af2ac38c49edaaf4d473f26462f3580264486',
|
||||
'historical attempt01 changed')
|
||||
planned = [{'label':a, 'command':b} for a,b in (
|
||||
('fmt','cargo fmt --all -- --check'),
|
||||
('workspace-tests','cargo test --workspace --locked -- --test-threads=1'),
|
||||
('clippy','cargo clippy --workspace --locked --all-targets -- -D warnings'),
|
||||
('native-builds','cargo build --workspace --locked --bins'))]
|
||||
outcome = step_sequence(result, planned, get, prefix, False, False)
|
||||
need(outcome == 'completed-successfully' and gate['zip'] is False and
|
||||
gate.get('missing_zip_reason'), 'historical attempt boundary')
|
||||
outcome = 'historically-closed-no-final-timestamp-no-input-ZIP'
|
||||
else:
|
||||
same_fields(before, result, ('phase', 'plan_sha256'))
|
||||
need(result['phase'] == gate['name'], 'gate phase mismatch')
|
||||
plan_bytes = get(gate['plan'])
|
||||
need(digest(plan_bytes) == result['plan_sha256'], 'gate plan mismatch')
|
||||
plan = decode(plan_bytes)
|
||||
need(plan['head'] == result['source'], 'gate plan source mismatch')
|
||||
outcome = step_sequence(result, plan['steps'], get, prefix, plan.get('stop_on_failure', True))
|
||||
need(gate['zip'] is True, 'unapproved missing source ZIP')
|
||||
if gate["zip"]:
|
||||
check_zip(get(prefix + "inputs.zip"), result["source_inputs"])
|
||||
matrix.append({"kind": "gate", "name": gate["name"], "source": result["source"],
|
||||
"receipt": prefix + "result.json", 'closure':outcome,
|
||||
"exits": [x["exit_code"] for x in result['steps']],
|
||||
"acceptance": "not inferred"})
|
||||
for name in config["native_cases"]:
|
||||
prefix = "navigation-native/" + safe(name) + "/"
|
||||
before = decode(get(prefix + "started.json"))
|
||||
result = decode(get(prefix + "finished.json"))
|
||||
same_fields(before, result, ('case','binary_source','binary_pins','pid','start','argv',
|
||||
'cwd','path','local_app_data'))
|
||||
need(result['case'] == name and type(result['pid']) is int and result['pid'] > 0
|
||||
and isinstance(result['argv'],list) and len(result['argv']) == 1, 'native launch identity')
|
||||
inventory(result['binary_pins'])
|
||||
need(set(result['binary_pins']) == {'arcadia-studio.exe','libunwind.dll'}, 'native binary pins')
|
||||
need(re.fullmatch('[0-9a-f]{40}', result['binary_source']), 'native source identity')
|
||||
if name == 'baseline01':
|
||||
same_fields(before,result,('fixture_sha256','release_tag'))
|
||||
hash_string(result['fixture_sha256'])
|
||||
need(result.get('fixture_unchanged') is True,'legacy fixture changed')
|
||||
else:
|
||||
same_fields(before,result,('original_fixtures',))
|
||||
inventory(result['original_fixtures'])
|
||||
need(result.get('original_fixtures_unchanged') is True,'native fixtures changed')
|
||||
need(result.get('binaries_unchanged') is True,'native binaries changed')
|
||||
need(type(result.get('exit_code')) is int and
|
||||
timestamp(result.get('finished')) >= timestamp(result['start']), 'native still open')
|
||||
streams(result,prefix,get,suffix='native')
|
||||
get(prefix + "observations.md")
|
||||
matrix.append({"kind": "native", "name": name, "source": result.get("binary_source"),
|
||||
"receipt": prefix + "finished.json", "binary_pins": result.get("binary_pins"),
|
||||
"exit": result["exit_code"], "acceptance": "not inferred"})
|
||||
for name in config["oracle_runs"]:
|
||||
prefix = "navigation-oracle-runs/" + safe(name) + "/"
|
||||
before = decode(get(prefix+'initial.json'))
|
||||
raw = get(prefix+'result.json')
|
||||
result = decode(raw)
|
||||
same_fields(before,result,('case','head','started','environment','qa_root','source_inputs'))
|
||||
need(result['case']==name,'oracle case mismatch')
|
||||
need(re.fullmatch('[0-9a-f]{40}',result['head']),'oracle source identity')
|
||||
inventory(result['source_inputs'])
|
||||
need(result.get('inputs_unchanged_except_initial_isolated_lock') is True,'oracle input drift')
|
||||
target = 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/target-gnullvm'
|
||||
planned = [
|
||||
{'label':'tests','command':f'cargo test --offline --manifest-path .local/navigation-oracle/Cargo.toml --target-dir {target} -- --nocapture --test-threads=1'},
|
||||
{'label':'build','command':f'cargo build --locked --offline --manifest-path .local/navigation-oracle/Cargo.toml --target-dir {target}'}]
|
||||
outcome = step_sequence(result,planned,get,prefix,True)
|
||||
final_lock = hash_string(result['final_utility_lock_sha256'])
|
||||
need(all(s['utility_lock_sha256']==final_lock for s in result['steps']), 'oracle lock drift')
|
||||
if name == 'attempt01':
|
||||
need(digest(raw)=='6523dd082eb02ad6f93130320fdfac4bcd3e320e61534a8ae1a54977537d17e7'
|
||||
and outcome=='stopped-at-first-failure','unknown historical oracle failure')
|
||||
else:
|
||||
need(outcome=='completed-successfully','oracle build not complete')
|
||||
hash_string(result['executable']['sha256'])
|
||||
need(type(result['executable']['bytes']) is int and result['executable']['bytes']>0,'oracle executable size')
|
||||
need(digest(get('navigation-oracle-qa-'+name+'/checks.jsonl'))==result['qa_checks_sha256'], 'oracle QA pin')
|
||||
matrix.append({'kind':'oracle-run','name':name,'source':result['head'],'closure':outcome,
|
||||
'receipt':prefix+'result.json','exits':[s['exit_code'] for s in result['steps']],
|
||||
'acceptance':'not inferred'})
|
||||
command_root = 'navigation-oracle-commands/'
|
||||
commands = sorted({p[len(command_root):].split('/')[0] for p in paths if p.startswith(command_root)})
|
||||
if not commands:
|
||||
return matrix
|
||||
build_path = 'navigation-oracle-runs/attempt02/result.json'
|
||||
build_raw = get(build_path)
|
||||
build = decode(build_raw)
|
||||
for name in commands:
|
||||
prefix = command_root+name+'/'
|
||||
safe(name)
|
||||
if name == 'draw-fixture-runtime':
|
||||
need({p for p in paths if p.startswith(prefix)} == {prefix+'failure.json'}, 'recorder failure files changed')
|
||||
failure_raw = get(prefix+'failure.json')
|
||||
failure = decode(failure_raw)
|
||||
need(digest(failure_raw)=='ecfa343366a1e156f9f20e9522cdc89f98352975b9100d3b7b2e6140fb4bafe3'
|
||||
and failure['oracle_started'] is False, 'unknown recorder failure')
|
||||
matrix.append({'kind':'oracle-command','name':name,'closure':'recorder-failed-before-launch',
|
||||
'receipt':prefix+'failure.json','acceptance':'none'})
|
||||
continue
|
||||
raw = get(prefix+'receipt.json')
|
||||
result = decode(raw)
|
||||
need(result['label']==name and type(result['exit_code']) is int and
|
||||
timestamp(result['finished'])>=timestamp(result['started']), 'open/misidentified oracle command')
|
||||
argv = result['argv']
|
||||
need(isinstance(argv,list) and all(isinstance(x,str) and x for x in argv) and
|
||||
len(argv)>=3 and argv[1] in ('fixture','snapshot','compare'), 'oracle argv')
|
||||
need(result['exe_unchanged'] is True and result['exe_sha256']==build['executable']['sha256']
|
||||
and result['build_receipt_sha256']==digest(build_raw), 'oracle build/executable binding')
|
||||
need(re.fullmatch('[0-9a-f]{40}',result['head']), 'oracle source identity')
|
||||
for stream in ('stdout','stderr'):
|
||||
size=result[stream+'_bytes']
|
||||
need(type(size) is int and size>=0 and len(get(prefix+stream))==size,'oracle stream size')
|
||||
if name == 'draw-fixture':
|
||||
need(digest(raw)=='89620b6726a510795a02bd173e0540c48610dbff0135915b0ce3ad81a6868818'
|
||||
and result['exit_code']==3221225781,'unknown runtime failure')
|
||||
outcome='runtime-load-failure-no-admission'
|
||||
else:
|
||||
need(result['exit_code']==0 and get(prefix+'stderr')==b'', 'oracle command failed: review/classify first')
|
||||
need(result['runtime_sha256']==config['oracle_runtime_sha256'] and
|
||||
result['path']=='C:\\Windows\\System32;C:\\Windows', 'oracle runtime environment')
|
||||
out=decode(get(prefix+'stdout'))
|
||||
if argv[1]=='snapshot':
|
||||
need(len(argv)==4 and out['status']=='captured', 'snapshot receipt status')
|
||||
dest=logical_argument(argv[3],config)
|
||||
snapshot=decode(get(dest+'/snapshot.json'))
|
||||
need(out['entry']==snapshot['entry'] and out['files']==len(snapshot['files']) and
|
||||
out['source_bytes']==sum(len(v) for v in snapshot['files'].values()) and
|
||||
out['engineering_diagnostics']==snapshot['diagnostics'], 'snapshot metadata binding')
|
||||
need(decode(get(dest+'/project.json'))==snapshot['project'], 'snapshot exported Project mismatch')
|
||||
elif argv[1]=='compare':
|
||||
need(len(argv) in (4,7) and out['pass'] is True and out['source_contract_pass'] is True
|
||||
and out['expected_entire_project_equal'] is True and
|
||||
out['raw_entry_and_path_set_equal'] is True,'compare status')
|
||||
for arg in argv[2:4]: get(logical_argument(arg,config)+'/snapshot.json')
|
||||
if len(argv)==4:
|
||||
need(out['permitted_projection'] is None and out['raw_source_bytes_equal'] is True,'exact compare scope')
|
||||
else:
|
||||
need(argv[4]=='--projection' and out['permitted_projection']=={'diagram':argv[5],'projection':argv[6]}
|
||||
and out['projection_declaration_diff']['outside_projection_value_exact'] is True,'projection compare scope')
|
||||
else:
|
||||
need(len(argv)==3 and out.get('status')=='fixture_written','fixture status')
|
||||
dest=logical_argument(argv[2],config)
|
||||
need(logical_argument(out['entry'],config)==dest+'/project.arcadia' and
|
||||
logical_argument(out['project_json'],config)==dest+'/project.json','fixture destination')
|
||||
get(dest+'/project.arcadia'); get(dest+'/project.json')
|
||||
outcome='recorded-success-metadata-checked-not-rerun'
|
||||
matrix.append({'kind':'oracle-command','name':name,'closure':outcome,'exit':result['exit_code'],
|
||||
'receipt':prefix+'receipt.json','acceptance':'not inferred'})
|
||||
return matrix
|
||||
|
||||
|
||||
def logical_argument(value, config):
|
||||
# Historical argv is data. This strips only a declared original root; never resolves/extracts it.
|
||||
value=value.replace('\\','/')
|
||||
for prefix in (config['original_source_local'].rstrip('/')+'/', '.local/'):
|
||||
if value.startswith(prefix):
|
||||
return safe(value[len(prefix):])
|
||||
raise ValueError('unknown historical source argument: '+value)
|
||||
|
||||
|
||||
def write_new(path, data):
|
||||
with path.open("xb") as file:
|
||||
file.write(data)
|
||||
|
||||
|
||||
def fresh_directory(path):
|
||||
for item in [path.parent, *path.parent.parents]:
|
||||
s = item.lstat()
|
||||
need(stat.S_ISDIR(s.st_mode) and not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s, "st_file_attributes", 0) & 0x400, "output parent link/reparse")
|
||||
path.mkdir(parents=False, exist_ok=False)
|
||||
|
||||
|
||||
def collect(source, config):
|
||||
entries, blobs = [], {}
|
||||
selection = selected(source, config)
|
||||
total = 0
|
||||
for name, category, file in selection:
|
||||
permitted(name)
|
||||
data = read(file)
|
||||
total += len(data)
|
||||
need(total <= MAX_TOTAL, 'logical packet size cap')
|
||||
sha = digest(data)
|
||||
entries.append({"path": name, "category": category, "size": len(data), "sha256": sha})
|
||||
blobs[sha] = data
|
||||
coverage(config, entries)
|
||||
return selection, entries, blobs
|
||||
|
||||
|
||||
def stable(source, config, selection, entries):
|
||||
need([(a,b) for a,b,_ in selected(source,config)] == [(a,b) for a,b,_ in selection],
|
||||
"input file set changed during collection")
|
||||
for entry in entries:
|
||||
need(digest(read(source / entry["path"])) == entry["sha256"], "input changed during collection")
|
||||
|
||||
|
||||
def freeze(config_path, source, frozen_path):
|
||||
# Creates only a candidate inventory. Human review/pinning must precede build.
|
||||
source=Path(source).absolute()
|
||||
config_bytes=read(Path(config_path).absolute())
|
||||
config=decode(config_bytes)
|
||||
validate_config(config)
|
||||
need('frozen_entries' not in config, 'already frozen')
|
||||
selection,entries,blobs=collect(source,config)
|
||||
index = {x["path"]: blobs[x["sha256"]] for x in entries}
|
||||
receipts(config,index.__getitem__,set(index))
|
||||
stable(source,config,selection,entries)
|
||||
config['frozen_entries']=entries
|
||||
config['template_sha256']=digest(config_bytes)
|
||||
config['inventory_review']='REQUIRED: inspect logical file list and content before build/publication'
|
||||
path=Path(frozen_path).absolute()
|
||||
for item in [path.parent,*path.parent.parents]:
|
||||
s=item.lstat()
|
||||
need(stat.S_ISDIR(s.st_mode) and not stat.S_ISLNK(s.st_mode) and
|
||||
not getattr(s,'st_file_attributes',0)&0x400,'frozen output parent link/reparse')
|
||||
write_new(path,(json.dumps(config,indent=2)+'\n').encode())
|
||||
return {'frozen_config_sha256':digest(read(path)),'logical_files':len(entries),
|
||||
'review_required':True,'acceptance':'not inferred'}
|
||||
|
||||
|
||||
def build(config_path, source, output):
|
||||
source, output = Path(source).absolute(), Path(output).absolute()
|
||||
config_bytes = read(Path(config_path).absolute())
|
||||
config = decode(config_bytes)
|
||||
validate_config(config,frozen=True)
|
||||
selection,entries,blobs=collect(source,config)
|
||||
need(entries==config['frozen_entries'],'source differs from frozen inventory')
|
||||
index = {x["path"]: blobs[x["sha256"]] for x in entries}
|
||||
matrix = receipts(config, index.__getitem__,set(index))
|
||||
verifier = read(Path(__file__).absolute())
|
||||
need(index.get('navigation-evidence.py')==verifier,'running helper differs from frozen helper')
|
||||
fresh_directory(output)
|
||||
(output / "objects").mkdir()
|
||||
for sha, data in blobs.items():
|
||||
write_new(output / "objects" / sha, data)
|
||||
write_new(output / "navigation-evidence.py", verifier)
|
||||
write_new(output / "config.json", config_bytes)
|
||||
stable(source,config,selection,entries)
|
||||
manifest = {"schema": "navigation-evidence-v2", "entries": entries, "matrix": matrix,
|
||||
"config_sha256": digest(config_bytes), "verifier_sha256": digest(verifier),
|
||||
"documentation": config["documentation"], "acceptance": "not inferred"}
|
||||
write_new(output / "manifest.json", (json.dumps(manifest, indent=2) + "\n").encode())
|
||||
report = verify(output)
|
||||
# Read-only attribute is accidental-write protection, not tamper proofing.
|
||||
for file in walk_checked(output):
|
||||
if file.is_file():
|
||||
file.chmod(stat.S_IREAD)
|
||||
return report
|
||||
|
||||
|
||||
def verify(output):
|
||||
output = Path(output).absolute()
|
||||
manifest = decode(read(output / "manifest.json"))
|
||||
need(manifest["schema"] == "navigation-evidence-v2", "manifest schema")
|
||||
config_bytes = read(output / "config.json")
|
||||
need(digest(config_bytes) == manifest["config_sha256"], "config hash")
|
||||
need(digest(read(output / "navigation-evidence.py")) == manifest["verifier_sha256"], "verifier hash")
|
||||
config = decode(config_bytes)
|
||||
validate_config(config,frozen=True)
|
||||
need(manifest['entries']==config['frozen_entries'],'manifest differs from frozen whitelist')
|
||||
need(manifest['documentation']==config['documentation'] and manifest['acceptance']=='not inferred','manifest scope')
|
||||
coverage(config,manifest['entries'])
|
||||
names, objects, index = set(), set(), {}
|
||||
total=0
|
||||
for entry in manifest["entries"]:
|
||||
name = safe(entry["path"])
|
||||
need(name.casefold() not in names, "duplicate manifest path")
|
||||
need(not any(x.casefold() in EXCLUDED for x in PurePosixPath(name).parts), "excluded input")
|
||||
names.add(name.casefold())
|
||||
sha = entry["sha256"]
|
||||
need(isinstance(sha,str) and re.fullmatch("[0-9a-f]{64}", sha), "invalid digest")
|
||||
need(type(entry["size"]) is int and 0 <= entry["size"] <= MAX_FILE, "invalid size")
|
||||
total+=entry['size']
|
||||
need(total<=MAX_TOTAL,'logical size cap')
|
||||
data = read(output / "objects" / sha)
|
||||
need(len(data) == entry["size"] and digest(data) == sha, "object mismatch")
|
||||
index[name] = data
|
||||
objects.add("objects/" + sha)
|
||||
need(index.get('navigation-evidence.py')==read(output/'navigation-evidence.py'),'selfpin object mismatch')
|
||||
expected = objects | {"manifest.json", "config.json", "navigation-evidence.py"}
|
||||
actual = set()
|
||||
for file in walk_checked(output):
|
||||
s = file.lstat()
|
||||
need(not stat.S_ISLNK(s.st_mode) and not getattr(s,"st_file_attributes",0) & 0x400,
|
||||
"packet link/reparse")
|
||||
if file.is_file():
|
||||
actual.add(file.relative_to(output).as_posix())
|
||||
need(actual == expected, "extra or missing packet file")
|
||||
need(receipts(config, index.__getitem__,set(index)) == manifest["matrix"], "matrix mismatch")
|
||||
return {"integrity": "PASS", "logical_files": len(names), "objects": len(objects),
|
||||
"semantic_acceptance": "not evaluated", "manifest_sha256": digest(read(output / "manifest.json"))}
|
||||
|
||||
|
||||
def selftest(output):
|
||||
root = Path(output).absolute()
|
||||
fresh_directory(root)
|
||||
tests = []
|
||||
def reject(label, fn):
|
||||
try:
|
||||
fn()
|
||||
except (ValueError, FileNotFoundError, FileExistsError, KeyError, zipfile.BadZipFile):
|
||||
tests.append(label)
|
||||
return
|
||||
raise ValueError('falsifier unexpectedly accepted: '+label)
|
||||
source=root/'source'; source.mkdir()
|
||||
script=read(Path(__file__).absolute())
|
||||
for name,data in [('a.txt',b'evidence'),('b.txt',b'evidence'),('navigation-evidence.py',script)]:
|
||||
write_new(source/name,data)
|
||||
config={'schema':'navigation-evidence-config-v2','includes':[
|
||||
{'path':name,'category':'test','kind':'file'} for name in ('a.txt','b.txt','navigation-evidence.py')],
|
||||
'gates':[],'native_cases':[],'oracle_runs':[],'documentation':[],
|
||||
'exclusions':sorted(EXCLUDED),'original_source_local':'C:/example/.local',
|
||||
'oracle_runtime_sha256':'0'*64}
|
||||
template=root/'template.json'; write_new(template,json.dumps(config).encode())
|
||||
frozen=root/'frozen.json'; freeze(template,source,frozen)
|
||||
packet=root/'valid'; good=build(frozen,source,packet)
|
||||
need(good['logical_files']==3 and good['objects']==2,'dedup roundtrip')
|
||||
tests.append('valid-freeze-build-verify-dedup')
|
||||
reject('existing output refused',lambda:build(frozen,source,packet))
|
||||
reject('existing freeze refused',lambda:freeze(template,source,frozen))
|
||||
original=decode(read(packet/'manifest.json'))
|
||||
original_config=decode(read(packet/'config.json'))
|
||||
# Each falsifier gets its own create-new packet; valid packet stays read-only.
|
||||
for mutation in ('alter','delete','duplicate','traversal','extra','extra-listed','duplicate-json',
|
||||
'selfpin','omitted-required','unwhitelisted','category'):
|
||||
p=root/mutation; p.mkdir(); (p/'objects').mkdir()
|
||||
m=copy.deepcopy(original); cfg=copy.deepcopy(original_config)
|
||||
if mutation=='duplicate': m['entries'].append(dict(m['entries'][0]))
|
||||
if mutation=='traversal': m['entries'][0]['path']='../a.txt'
|
||||
if mutation=='extra-listed': m['entries'].append({'path':'absent.txt','category':'test','size':3,'sha256':'a'*64})
|
||||
if mutation=='omitted-required':
|
||||
m['entries']=[e for e in m['entries'] if e['path']!='a.txt']; cfg['frozen_entries']=copy.deepcopy(m['entries'])
|
||||
if mutation=='unwhitelisted':
|
||||
e=dict(m['entries'][0]); e['path']='unauthorized.txt'; m['entries'].append(e); cfg['frozen_entries']=copy.deepcopy(m['entries'])
|
||||
if mutation=='category':
|
||||
m['entries'][0]['category']='other'; cfg['frozen_entries']=copy.deepcopy(m['entries'])
|
||||
if mutation=='selfpin':
|
||||
m['verifier_sha256']=digest(b'changed')
|
||||
cb=json.dumps(cfg).encode(); m['config_sha256']=digest(cb)
|
||||
raw=json.dumps(m).encode()
|
||||
if mutation=='duplicate-json': raw=raw.replace(b'"schema":',b'"schema":"bad", "schema":',1)
|
||||
write_new(p/'manifest.json',raw); write_new(p/'config.json',cb)
|
||||
write_new(p/'navigation-evidence.py',b'changed' if mutation=='selfpin' else script)
|
||||
for obj in (packet/'objects').iterdir():
|
||||
if mutation=='delete' and obj.name==digest(b'evidence'): continue
|
||||
write_new(p/'objects'/obj.name,b'changed' if mutation=='alter' and obj.name==digest(b'evidence') else read(obj))
|
||||
if mutation=='extra': write_new(p/'extra.txt',b'extra')
|
||||
reject(mutation,lambda:verify(p))
|
||||
planned=[{'label':'one','command':'first'},{'label':'two','command':'second'}]
|
||||
closed={'started':'2026-01-01T00:00:00+00:00','finished':'2026-01-01T00:00:01+00:00',
|
||||
'exit_code':0,'stdout_bytes':0,'stderr_bytes':0}
|
||||
step=dict(closed,label='one',command='first')
|
||||
partial={'steps':[step]}
|
||||
reject('successful prefix still open',lambda:step_sequence(partial,planned,lambda _:b'','',True))
|
||||
complete={'steps':[step,dict(closed,label='two',command='second')]}
|
||||
reject('successful missing final timestamp',lambda:step_sequence(complete,planned,lambda _:b'','',True))
|
||||
complete['finished']=closed['finished']
|
||||
need(step_sequence(complete,planned,lambda _:b'','',True)=='completed-successfully','valid closure')
|
||||
failed={'steps':[dict(step,exit_code=101)]}
|
||||
need(step_sequence(failed,planned,lambda _:b'','',True)=='stopped-at-first-failure','failed prefix classification')
|
||||
tests.append('complete success and stopped failure distinguished')
|
||||
reject('stream size mismatch',lambda:step_sequence(complete,planned,lambda _:b'x','',True))
|
||||
reject('native/oracle identity mismatch',lambda:same_fields({'pid':1},{'pid':2},['pid']))
|
||||
reject('missing identity on both sides',lambda:same_fields({}, {}, ['binary_source']))
|
||||
reject('duplicate receipt/config JSON',lambda:decode(b'{"steps":[],"steps":[]}'))
|
||||
for constant in (b'NaN', b'Infinity', b'-Infinity'):
|
||||
reject('nonfinite JSON '+constant.decode(),lambda value=constant:decode(b'{"size":'+value+b'}'))
|
||||
reject('dot root path',lambda:safe('.'))
|
||||
native={'case':'baseline01','binary_source':'1'*40,'binary_pins':{'arcadia-studio.exe':'2'*64,'libunwind.dll':'3'*64},
|
||||
'pid':7,'start':closed['started'],'argv':['C:/private/bin/arcadia-studio.exe'],
|
||||
'cwd':'C:/private/working','path':'C:/Windows','local_app_data':'C:/private/profile',
|
||||
'fixture_sha256':'4'*64,'release_tag':'historical-test'}
|
||||
done=dict(native,finished=closed['finished'],exit_code=0,stdout_bytes=0,stderr_bytes=0,
|
||||
binaries_unchanged=True,fixture_unchanged=True)
|
||||
nc=dict(config,native_cases=['baseline01'])
|
||||
np='navigation-native/baseline01/'
|
||||
def native_records(result):
|
||||
data={np+'started.json':json.dumps(native).encode(),np+'finished.json':json.dumps(result).encode(),
|
||||
np+'stdout.log':b'',np+'stderr.log':b'',np+'observations.md':b'bounded selftest'}
|
||||
return receipts(nc,data.__getitem__,set(data))
|
||||
need(native_records(done)[0]['exit']==0,'legacy native fixture valid')
|
||||
for key,value in [('pid',8),('fixture_sha256','5'*64),('binaries_unchanged',False),('finished',None),('stdout_bytes',1)]:
|
||||
reject('native '+key,lambda k=key,v=value:native_records(dict(done,**{k:v})))
|
||||
# A command receipt must bind the included build object and its runtime/status.
|
||||
bp='navigation-oracle-runs/attempt02/result.json'; cp='navigation-oracle-commands/example/'
|
||||
bb=json.dumps({'executable':{'sha256':'6'*64}}).encode()
|
||||
oc=dict(config,oracle_runtime_sha256='7'*64)
|
||||
command={'label':'example','argv':['C:/oracle.exe','compare','.local/a','.local/b'],
|
||||
'started':closed['started'],'finished':closed['finished'],'exit_code':0,
|
||||
'exe_unchanged':True,'exe_sha256':'6'*64,'build_receipt_sha256':digest(bb),
|
||||
'head':'8'*40,'runtime_sha256':'7'*64,'path':'C:\\Windows\\System32;C:\\Windows',
|
||||
'stderr_bytes':0}
|
||||
command_output=json.dumps({'pass':True,'source_contract_pass':True,'expected_entire_project_equal':True,
|
||||
'raw_entry_and_path_set_equal':True,'permitted_projection':None,'raw_source_bytes_equal':True}).encode()
|
||||
command['stdout_bytes']=len(command_output)
|
||||
def command_records(result):
|
||||
data={bp:bb,cp+'receipt.json':json.dumps(result).encode(),cp+'stdout':command_output,cp+'stderr':b'',
|
||||
'a/snapshot.json':b'{}','b/snapshot.json':b'{}'}
|
||||
return receipts(oc,data.__getitem__,set(data))
|
||||
need(command_records(command)[0]['exit']==0,'command metadata valid')
|
||||
for key,value in [('build_receipt_sha256','9'*64),('runtime_sha256','9'*64),('exit_code',2),
|
||||
('finished',None),('stdout_bytes',0),('exe_unchanged',False)]:
|
||||
reject('oracle command '+key,lambda k=key,v=value:command_records(dict(command,**{k:v})))
|
||||
for name in ('../x','C:/x','a\\b','AUX.txt','a/../b','profile/x','auth/settings.json','x.exe'):
|
||||
reject('unsafe/private '+name,lambda n=name:permitted(n))
|
||||
for mutation in ('wrong-content','missing','extra','case-collision','traversal','symlink'):
|
||||
buf=io.BytesIO()
|
||||
with zipfile.ZipFile(buf,'w') as z:
|
||||
if mutation!='missing': z.writestr('a.txt',b'bad' if mutation=='wrong-content' else b'ok')
|
||||
if mutation=='extra': z.writestr('extra.txt',b'ok')
|
||||
if mutation=='case-collision': z.writestr('A.txt',b'ok')
|
||||
if mutation=='traversal': z.writestr('../x',b'ok')
|
||||
if mutation=='symlink':
|
||||
info=zipfile.ZipInfo('link'); info.create_system=3; info.external_attr=(stat.S_IFLNK|0o777)<<16
|
||||
z.writestr(info,b'a.txt')
|
||||
reject('ZIP '+mutation,lambda b=buf.getvalue():check_zip(b,{'a.txt':digest(b'ok')}))
|
||||
crc=io.BytesIO()
|
||||
with zipfile.ZipFile(crc,'w',compression=zipfile.ZIP_STORED) as z:
|
||||
z.writestr('a.txt',b'crc-evidence')
|
||||
damaged=bytearray(crc.getvalue()); at=damaged.index(b'crc-evidence'); damaged[at]^=1
|
||||
reject('ZIP CRC corruption',lambda:check_zip(bytes(damaged),{'a.txt':digest(b'crc-evidence')}))
|
||||
# Real filesystem link test when the platform permits unprivileged link creation.
|
||||
skipped=[]
|
||||
link=root/'linked-source'
|
||||
try:
|
||||
link.symlink_to(source,target_is_directory=True)
|
||||
except (OSError,NotImplementedError) as error:
|
||||
skipped.append('filesystem reparse/symlink creation unavailable: '+type(error).__name__)
|
||||
else:
|
||||
reject('filesystem link ancestor',lambda:read(link/'a.txt'))
|
||||
reject('output link ancestor',lambda:fresh_directory(link/'new'))
|
||||
# Changed source after freeze must fail before packet creation.
|
||||
(source/'a.txt').write_bytes(b'changed')
|
||||
reject('source changed since freeze',lambda:build(frozen,source,root/'changed-source'))
|
||||
report = {"selftest":"PASS", "cases":tests,'skipped':skipped,"scope":"transport integrity only; skipped controls are not passes"}
|
||||
write_new(root / "result.json", (json.dumps(report,indent=2)+"\n").encode())
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mode, *args = sys.argv[1:]
|
||||
need(mode in ("freeze", "build", "verify", "selftest"), "unknown mode")
|
||||
print(json.dumps({"freeze":freeze,"build":build,"verify":verify,"selftest":selftest}[mode](*args),indent=2))
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\peer01\\bin\\arcadia-studio.exe",
|
||||
"id": 25822524,
|
||||
"title": "Restore safe water pressure — release 1 walkthrough — Arcadia Studio"
|
||||
},
|
||||
"accessibility": {
|
||||
"focused_element": "0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise",
|
||||
"tree": "Window: \"Restore safe water pressure — release 1 walkthrough — Arcadia Studio\", App: arcadia-studio.exe.\n\t0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise\n\t\t1 title bar (settable, string) Value: Restore safe water pressure — release 1 walkthrough — Arcadia Studio\n\t\t\t2 menu bar System ID: MenuBar\n\t\t\t\t3 menu item System\n\t\t\t4 button Minimize\n\t\t\t5 button Maximize\n\t\t\t6 button Close\n\nThe focused UI element is 0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise."
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "04-before-oa-edge-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "f267facc804d62ea065ae970b7315cc0a5e580dbf726a291626a354f923adf54"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 131 KiB |
@@ -1,12 +0,0 @@
|
||||
sa_function sa_target "SA execute repair" id "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3" {
|
||||
}
|
||||
|
||||
sa_port functional_exchange_sa_dispatch_repair_sa_execute_repair_input "Functional exchange: SA dispatch repair → SA execute repair input" id "0a6f4b4b-911c-531d-82d5-172bdc8b0564" {
|
||||
contract {
|
||||
FunctionInput {
|
||||
incoming_items [];
|
||||
};
|
||||
};
|
||||
parent @sa_target;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 131 KiB |
@@ -1,60 +0,0 @@
|
||||
sa_scenario sa_conversation "Repair request and measured reply" id "730b27ca-b039-5a3b-8dc6-03418f9061b3" {
|
||||
context @sa_restore;
|
||||
postcondition "The controller has received the measured pressure after repair";
|
||||
precondition "A pressure alarm has been assessed";
|
||||
steps [@sa_call, @sa_reply];
|
||||
viewpoint BoundaryInterface;
|
||||
}
|
||||
|
||||
sa_occurrence sa_call "Request valve repair" id "8bab4783-a098-5984-9e36-493957caf26c" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @sa_contact;
|
||||
message_kind Call;
|
||||
receiver @sa_responder;
|
||||
sender @sa_caller;
|
||||
values [{
|
||||
expression "420.0";
|
||||
field @sa_pressure_parameter;
|
||||
}];
|
||||
};
|
||||
};
|
||||
parent @sa_conversation;
|
||||
}
|
||||
|
||||
sa_lifeline sa_caller "Controller" id "98c61788-2fee-51f4-8cf9-de48e8c70e9e" {
|
||||
parent @sa_conversation;
|
||||
represented @sa_control;
|
||||
}
|
||||
|
||||
sa_occurrence sa_reply "Return pressure after repair" id "4e1c0a56-ce80-5f13-a93a-6956de54880c" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @sa_contact;
|
||||
message_kind Reply;
|
||||
receiver @sa_caller;
|
||||
reply_to @sa_call;
|
||||
sender @sa_responder;
|
||||
values [{
|
||||
expression "320.0";
|
||||
field @sa_pressure_parameter;
|
||||
}];
|
||||
};
|
||||
};
|
||||
parent @sa_conversation;
|
||||
}
|
||||
|
||||
sa_lifeline sa_responder "Technician" id "6249b54a-e396-5338-82b9-89266c55670d" {
|
||||
parent @sa_conversation;
|
||||
represented @sa_technician;
|
||||
}
|
||||
|
||||
artifact repair_test_evidence "Repair conversation acceptance notes" id "f5922bf7-05d8-590f-86e9-1d09f37f4fa6" {
|
||||
description "A local teaching artifact, not proof of a real valve's safety.";
|
||||
fragment "acceptance";
|
||||
owner @sa_conversation;
|
||||
relation Verifies;
|
||||
revision "lesson-1";
|
||||
uri "evidence/repair-check.md";
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\draw01\\bin\\arcadia-studio.exe",
|
||||
"id": 43582746,
|
||||
"title": "Cross-layer native Draw verification — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "04-sa-draw-armed-oa-retained-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "1aea20c087a78156ab476ce8269edd295a906a86cd45476147f8483363cab690"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 132 KiB |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\draw01\\bin\\arcadia-studio.exe",
|
||||
"id": 43582746,
|
||||
"title": "Cross-layer native Draw verification — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "05-wrong-layer-target-refused-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "a73be6efe78cc071da3c4ee352a11023689cbb8ddf0632121be40dd920d9e574"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
oa_activity oa_source "OA request repair" id "e4a68a7d-6949-5c17-9263-c03c03c21af2" {
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"case": "relocation01-reopen",
|
||||
"binary_source": "6018ae30b9296b9f6b37d7fc038175080d15fafb",
|
||||
"start": "2026-10-02T06:55:10.845877+00:00",
|
||||
"binary_pins": {
|
||||
"arcadia-studio.exe": "f86b29555d1e512538ff3f7ee75619b0286add5aa2d60cdc724e8c27cc931165",
|
||||
"libunwind.dll": "f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4"
|
||||
},
|
||||
"original_fixtures": {
|
||||
"eight-card.json": "9db1015369f450d5810c96a01fddb8a009d57f6aafbda5182703f15ded23d9c8",
|
||||
"draw/oracle.json": "b3eb669d353bf9f3490faf50cdf65b9f46d77719de993557c56ea159d4696ae0",
|
||||
"draw/project.arcadia": "2a657d69cf30487364672cd45d0a0fa01a78d64140cc58eeee47e61ea4a112d4",
|
||||
"draw/project.json": "520e5645bcf87380a50be08cb9ef758b85a19414e58ce3bd61926c09767a7963",
|
||||
"draw/views/diagram_mixed_oa.arcadia": "d481e137236c62b13afebae3b14d6eedb61c27b7f6566cb3f6dc325d4aaea927",
|
||||
"draw/views/diagram_mixed_sa.arcadia": "b5c326eafea071ed0f058b8a792271c2bb83c87eab6b7718a51a9cd3782d449d",
|
||||
"draw/oa/activities/activity_oa_source.arcadia": "4d8b0971cc64a3ae13f7bf4b0c79a5ee978facb8ee660c2690c473b0b935ba5f",
|
||||
"draw/oa/activities/activity_oa_target.arcadia": "afef0fbac3fb7fbe42936a0c213e72541b31a471af8257a9462bdafb1f39c8ab",
|
||||
"draw/sa/functions/function_sa_source.arcadia": "536de40f46f64c745af0a41a8dfa70309782e90617be69dd37610cb4e9730ed2",
|
||||
"draw/sa/functions/function_sa_target.arcadia": "cb6d97f9b3c112f9b50bb9bb018ce0e6e34d51c8ae53afd6d6493367f371f1fa",
|
||||
"relocation-fixture.json": "282a3ef1206000c1bd8d32c55bde950831806096fbfcec7c324e182a49d7df4c"
|
||||
},
|
||||
"argv": [
|
||||
"C:\\ar-nav-01a0d717\\.local\\navigation-native\\relocation01-reopen\\bin\\arcadia-studio.exe"
|
||||
],
|
||||
"cwd": "C:\\ar-nav-01a0d717\\.local\\navigation-native\\relocation01-reopen\\working",
|
||||
"path": "C:\\Windows\\System32;C:\\Windows",
|
||||
"local_app_data": "C:\\ar-nav-01a0d717\\.local\\navigation-native\\relocation01-reopen\\profile",
|
||||
"pid": 6776
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"label": "draw01-dataflow",
|
||||
"argv": [
|
||||
"C:\\ar-nav-01a0d717\\.local\\navigation-oracle-runtime\\navigation-oracle.exe",
|
||||
"snapshot",
|
||||
"C:/ar-nav-01a0d717/.local/navigation-native/draw01/working/draw-save/project.arcadia",
|
||||
"C:/ar-nav-01a0d717/.local/navigation-native/draw01/dataflow"
|
||||
],
|
||||
"started": "2026-10-02T06:31:09.966755+00:00",
|
||||
"exe_sha256": "bd2db2e177d9d21f44b45e5efa89071e00155d31509672bf5aea6a57915e09a6",
|
||||
"runtime_sha256": "f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4",
|
||||
"path": "C:\\Windows\\System32;C:\\Windows",
|
||||
"head": "c7065db76ecca3faa733ccb5f09b87fd0a0958be",
|
||||
"build_receipt_sha256": "1ed50fe9c42ff4a67084ed70ce934404f5549ca68bc54e6d2f1436e66ea8d22f",
|
||||
"exit_code": 0,
|
||||
"finished": "2026-10-02T06:31:10.897201+00:00",
|
||||
"stdout_bytes": 7336,
|
||||
"stderr_bytes": 0,
|
||||
"exe_unchanged": true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 117 KiB |
@@ -1,17 +0,0 @@
|
||||
oa_exchange_category oa_contact_category "Field contact" id "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771" {
|
||||
exchanges [@oa_contact];
|
||||
family "Participant";
|
||||
}
|
||||
|
||||
oa_event oa_high_pressure "Pressure alarm detected" id "65d6b8d6-271a-5c84-8b42-abbe310c955e" {
|
||||
body {
|
||||
Change {
|
||||
expression "measured_pressure > operating_limit";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
oa_exchange_category oa_repair_traffic "Repair work exchanges" id "9349b353-53f2-56f8-b571-a05b09335cdf" {
|
||||
exchanges [@oa_repair_request, @oa_repair_report];
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"head": "6018ae30b9296b9f6b37d7fc038175080d15fafb",
|
||||
"steps": [
|
||||
{"label":"native-bins","command":"cargo build --workspace --bins --locked"}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 148 KiB |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\fixed01-reopen\\bin\\arcadia-studio.exe",
|
||||
"id": 17957882,
|
||||
"title": "Restore safe water pressure — release 1 walkthrough — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "05-reopen-saved-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "ab1fde5b53c66d227235610ab3aed1069ab86a9d70e4b10e18148d6275e5f925"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
{
|
||||
"schema_version": 7,
|
||||
"id": "01324fcc-9456-5341-8fd0-2c261bddf186",
|
||||
"alias": "navigation_draw",
|
||||
"name": "Cross-layer native Draw verification",
|
||||
"description": "Synthetic native acceptance fixture; four complete typed work elements, two saved mixed-layer views, initially no relations.",
|
||||
"elements": {
|
||||
"0a6f4b4b-911c-531d-82d5-172bdc8b0564": {
|
||||
"id": "0a6f4b4b-911c-531d-82d5-172bdc8b0564",
|
||||
"alias": "functional_exchange_sa_dispatch_repair_sa_execute_repair_input",
|
||||
"name": "Functional exchange: SA dispatch repair → SA execute repair input",
|
||||
"description": "",
|
||||
"parent": "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Port": {
|
||||
"contract": {
|
||||
"FunctionInput": {
|
||||
"incoming_items": []
|
||||
}
|
||||
},
|
||||
"legacy": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"7957e536-d4b4-50c2-8271-ef829952353f": {
|
||||
"id": "7957e536-d4b4-50c2-8271-ef829952353f",
|
||||
"alias": "functional_exchange_sa_dispatch_repair_sa_execute_repair_output",
|
||||
"name": "Functional exchange: SA dispatch repair → SA execute repair output",
|
||||
"description": "",
|
||||
"parent": "fa0825f4-ae33-5eb3-9222-bf3a6465e72e",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Port": {
|
||||
"contract": {
|
||||
"FunctionOutput": {
|
||||
"outgoing_items": []
|
||||
}
|
||||
},
|
||||
"legacy": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"7b9d5c34-1c45-5048-a3a0-77e5784a5a69": {
|
||||
"id": "7b9d5c34-1c45-5048-a3a0-77e5784a5a69",
|
||||
"alias": "oa_target",
|
||||
"name": "OA perform repair",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Oa": "Activity"
|
||||
}
|
||||
},
|
||||
"c5e2f9d6-ba03-50b6-9860-b2dd717c02b3": {
|
||||
"id": "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3",
|
||||
"alias": "sa_target",
|
||||
"name": "SA execute repair",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": "Function"
|
||||
}
|
||||
},
|
||||
"e4a68a7d-6949-5c17-9263-c03c03c21af2": {
|
||||
"id": "e4a68a7d-6949-5c17-9263-c03c03c21af2",
|
||||
"alias": "oa_source",
|
||||
"name": "OA request repair",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Oa": "Activity"
|
||||
}
|
||||
},
|
||||
"fa0825f4-ae33-5eb3-9222-bf3a6465e72e": {
|
||||
"id": "fa0825f4-ae33-5eb3-9222-bf3a6465e72e",
|
||||
"alias": "sa_source",
|
||||
"name": "SA dispatch repair",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": "Function"
|
||||
}
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"50e20de1-197f-4126-90da-81c6e4c93409": {
|
||||
"id": "50e20de1-197f-4126-90da-81c6e4c93409",
|
||||
"alias": "functional_exchange_sa_dispatch_repair_sa_execute_repair",
|
||||
"name": "Functional exchange: SA dispatch repair → SA execute repair",
|
||||
"description": "",
|
||||
"body": {
|
||||
"FunctionalExchange": {
|
||||
"source": "fa0825f4-ae33-5eb3-9222-bf3a6465e72e",
|
||||
"target": "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3",
|
||||
"source_port": "7957e536-d4b4-50c2-8271-ef829952353f",
|
||||
"target_port": "0a6f4b4b-911c-531d-82d5-172bdc8b0564",
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"a425a1af-e16f-4e25-9fb7-9f063dbe2fda": {
|
||||
"id": "a425a1af-e16f-4e25-9fb7-9f063dbe2fda",
|
||||
"alias": "interaction_oa_request_repair_oa_perform_repair",
|
||||
"name": "Interaction: OA request repair → OA perform repair",
|
||||
"description": "",
|
||||
"body": {
|
||||
"Interaction": {
|
||||
"source": "e4a68a7d-6949-5c17-9263-c03c03c21af2",
|
||||
"target": "7b9d5c34-1c45-5048-a3a0-77e5784a5a69",
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagrams": {
|
||||
"a60ffb09-36c2-5abc-bec5-6eab563c8cb8": {
|
||||
"id": "a60ffb09-36c2-5abc-bec5-6eab563c8cb8",
|
||||
"alias": "mixed_oa",
|
||||
"name": "OA mixed Draw canvas",
|
||||
"description": "",
|
||||
"layer": "Oa",
|
||||
"projection": "Traceability",
|
||||
"view_options": {
|
||||
"hidden_edges": [],
|
||||
"relation_kinds": "All",
|
||||
"work_categories": "All",
|
||||
"participant_categories": "All",
|
||||
"coverage": "All",
|
||||
"work_contexts": "All",
|
||||
"exchange_labels": "Name"
|
||||
},
|
||||
"nodes": {
|
||||
"1f2bb997-b4af-5910-b91b-1948764cff4f": {
|
||||
"id": "1f2bb997-b4af-5910-b91b-1948764cff4f",
|
||||
"alias": "mixed_oa_oa_target",
|
||||
"element": "7b9d5c34-1c45-5048-a3a0-77e5784a5a69",
|
||||
"position": {
|
||||
"x": 520.0,
|
||||
"y": 60.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"371bfc51-edcc-5770-aad1-11b84772d2c8": {
|
||||
"id": "371bfc51-edcc-5770-aad1-11b84772d2c8",
|
||||
"alias": "mixed_oa_oa_source",
|
||||
"element": "e4a68a7d-6949-5c17-9263-c03c03c21af2",
|
||||
"position": {
|
||||
"x": 60.0,
|
||||
"y": 60.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"752822e9-1f95-5e9c-b1d3-58c325d875fa": {
|
||||
"id": "752822e9-1f95-5e9c-b1d3-58c325d875fa",
|
||||
"alias": "mixed_oa_sa_source",
|
||||
"element": "fa0825f4-ae33-5eb3-9222-bf3a6465e72e",
|
||||
"position": {
|
||||
"x": 60.0,
|
||||
"y": 300.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"eb2ce67e-c283-5689-8780-b54e1b06d7cc": {
|
||||
"id": "eb2ce67e-c283-5689-8780-b54e1b06d7cc",
|
||||
"alias": "mixed_oa_sa_target",
|
||||
"element": "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3",
|
||||
"position": {
|
||||
"x": 520.0,
|
||||
"y": 300.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
}
|
||||
},
|
||||
"routes": []
|
||||
},
|
||||
"aa64b55c-9bbb-5444-bda4-c9be0c66242a": {
|
||||
"id": "aa64b55c-9bbb-5444-bda4-c9be0c66242a",
|
||||
"alias": "mixed_sa",
|
||||
"name": "SA mixed Draw canvas",
|
||||
"description": "",
|
||||
"layer": "Sa",
|
||||
"projection": "Traceability",
|
||||
"view_options": {
|
||||
"hidden_edges": [],
|
||||
"relation_kinds": "All",
|
||||
"work_categories": "All",
|
||||
"participant_categories": "All",
|
||||
"coverage": "All",
|
||||
"work_contexts": "All",
|
||||
"exchange_labels": "Name"
|
||||
},
|
||||
"nodes": {
|
||||
"44169450-37f5-59fd-b9ab-32482c943f99": {
|
||||
"id": "44169450-37f5-59fd-b9ab-32482c943f99",
|
||||
"alias": "mixed_sa_sa_target",
|
||||
"element": "c5e2f9d6-ba03-50b6-9860-b2dd717c02b3",
|
||||
"position": {
|
||||
"x": 520.0,
|
||||
"y": 300.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"a0a18bd6-891a-56a4-a46d-d7b7d9911fc9": {
|
||||
"id": "a0a18bd6-891a-56a4-a46d-d7b7d9911fc9",
|
||||
"alias": "mixed_sa_oa_target",
|
||||
"element": "7b9d5c34-1c45-5048-a3a0-77e5784a5a69",
|
||||
"position": {
|
||||
"x": 520.0,
|
||||
"y": 60.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"aca2f538-69bf-561f-9eff-433febee5234": {
|
||||
"id": "aca2f538-69bf-561f-9eff-433febee5234",
|
||||
"alias": "mixed_sa_sa_source",
|
||||
"element": "fa0825f4-ae33-5eb3-9222-bf3a6465e72e",
|
||||
"position": {
|
||||
"x": 60.0,
|
||||
"y": 300.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"f5e4244f-e1c4-59ec-9109-be1e851386c1": {
|
||||
"id": "f5e4244f-e1c4-59ec-9109-be1e851386c1",
|
||||
"alias": "mixed_sa_oa_source",
|
||||
"element": "e4a68a7d-6949-5c17-9263-c03c03c21af2",
|
||||
"position": {
|
||||
"x": 60.0,
|
||||
"y": 60.0
|
||||
},
|
||||
"size": {
|
||||
"width": 224.0,
|
||||
"height": 94.0
|
||||
},
|
||||
"collapsed": false
|
||||
}
|
||||
},
|
||||
"routes": []
|
||||
}
|
||||
},
|
||||
"artifact_links": {},
|
||||
"traces": {}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"recorded_at": "2026-10-02T07:10:23.939491+00:00",
|
||||
"command": [
|
||||
"C:/Users/User/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe",
|
||||
".local/navigation-evidence.py",
|
||||
"selftest",
|
||||
".local/navigation-evidence-selftest-attempt01"
|
||||
],
|
||||
"working_directory": "C:/ar-nav-01a0d717",
|
||||
"tool_exit_code": 0,
|
||||
"helper_sha256": "3b2c6c59c895dd2e40b5dc408e68fe7c3760447c62a7f98413db60ea18ec7825",
|
||||
"result_sha256": "4ef9a80a3de9bc60e21804f2d2ed51d77a78474c5e23f855b06ea5ce25e7cebb",
|
||||
"result_reported_cases": 52,
|
||||
"skipped": [
|
||||
"filesystem reparse/symlink creation unavailable: OSError"
|
||||
],
|
||||
"scope": "Parent executed after independent candidate review. Recorded now from returned tool exit and retained actual result; no fabricated command start/end timestamps. Transport only."
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
# Independent native subset evidence review
|
||||
|
||||
2026-10-02 UTC; reviewer diagram_reading (Astra). Root C:/ar-nav-01a0d717. Read-only review of fixed01, fixed01-reopen and draw01 notes, process receipts, original JPEGs/state manifests, and selected saved-data oracle receipts. No GUI control, Cargo, source/model or Gitea changes; only this note was written. This is evidence review of the existing private c706 run, not a new native run or release acceptance.
|
||||
|
||||
## Finding requiring a wording correction
|
||||
|
||||
The draw01 observation that the OA Interaction label became fully visible at 96% overstates the display. Capture16 shows its label clipped at the top canvas boundary at 113%. Captures28,31,33 show its label box back inside the viewport at 96%, but the text is still ellipsized as Interaction: OA request repair .... The functional-exchange label is also ellipsized in 09 and 33. Say that refitting resolved viewport clipping while full label text remained truncated. This matters to the still-open small-diagram readability requirement; successful creation does not establish readable labels.
|
||||
|
||||
The expanded palette also visibly clips both right-side card contents at 113% in captures25–27. At 96% after perspective navigation, capture28 contains all four cards within the narrower canvas. The screenshots support this width/camera limitation; they cannot prove that authored placements were unchanged. The final saved-data comparator supplies the separate conservation evidence.
|
||||
|
||||
No other material contradiction found in the requested bounded subset. Do not broaden these results to source acceptance: the notes explicitly identify c706's failed workspace placement-relocation regression. This review does not rerun or overturn that failure or accept a later repair.
|
||||
|
||||
## Directly inspected original captures
|
||||
|
||||
All images below were opened individually through view_image at their original JPEG paths; no regenerated diagrams or crops were used.
|
||||
|
||||
- draw01: 04,05,06,07,08,09,16,21,22,23,25,26,27,28,31,33,34,35 (all numbered -0.jpg).
|
||||
- fixed01: 06-oa-edge-inspected, 12-valid-apply-retains-mixed, 17-valid-draft-click-oa-process, 22-allocation-switch-succeeds.
|
||||
- fixed01-reopen: 01-reopened-source-1 (owned warning), 04-reopened-allocation-0, 06-reverse-traceability-0, 09-explicit-open-endpoint-0.
|
||||
|
||||
Draw04 visibly retains Operational and OA mixed Draw canvas while the Inspector identifies SA dispatch repair / sa_source. The source is green and the compatible SA execute card is outlined blue; OA cards are not candidate-highlighted. Draw05 retains that view and shows Choose a highlighted compatible endpoint after the pointer is on the OA card, with a temporary dashed preview. It supports refusal feedback, not a semantic mutation claim. Draw06 shows both SA functions as possible sources and the source-selection hint, consistent with a blank click resetting source choice while keeping the tool. Draw07 shows Select/move and Drawing cancelled; 08 shows Nothing to undo. A screenshot of that status cannot independently count history entries.
|
||||
|
||||
Draw09 shows the Functional exchange between the two SA cards, triangle port glyphs, matching source/target names in Properties, created feedback, and the same OA canvas. Draw16 shows System and SA mixed Draw canvas while Properties identifies an Interaction from OA request repair to OA perform repair (UUID a425a1af-e16f-4e25-9fb7-9f063dbe2fda). The green OA line and existing lower SA line are visible. Exact automatic port identities, contracts, record counts and unchanged records belong to the separately reviewed saved-Project evaluator, not visual inference.
|
||||
|
||||
Draw21 has the Model explorer tab active and its source context menu has no Draw entry. Draw22 has Architecture active with the same sidebar object menu and includes Draw functional exchange. Draw23 confirms the latter starts from sa_source on the retained SA canvas, with the target/port highlighted. Documentation should say Architecture sidebar, not generically Model explorer. This review confirms the visible distinction, not that a later documentation change is already shipped.
|
||||
|
||||
Draw25 shows expanded SA Functional exchange enabled, Boundary exchange and Allocation greyed. Draw26 leaves Select/move active and displays the Boundary exchange compatible-source tooltip. Draw27 activates Functional exchange, adds its source-selection hint and blue compatible SA outlines. Draw28 shows the OA labels/glyphs; Interaction, Communication means and Allocation are visually distinct at this scale, but this is not evidence that an unaided person understands each icon. Draw33/34 show compact OA controls and the disabled Communication means guidance. Draw35 shows the compact Interaction selected and only OA cards highlighted as sources. SA compact selected glyph state is visible in contextual/sidebar capture23; that is not an independent direct click test of the compact SA glyph. The notes distinguish that from the separately clicked expanded control.
|
||||
|
||||
Draw31 shows Functional dataflow, just the two OA cards and their line, and the reduced palette (Activity, Interaction, Add existing; no participant/Communication/Allocation controls). Hidden SA records cannot be inferred as conserved from this image. The recorded projection comparator is the relevant saved-data evidence.
|
||||
|
||||
Fixed01 captures06/12 keep System and the named mixed Traceability view while the OA Chain involvement alias oa_assessment_delivery is inspected/renamed, with UUID 628cb4fc-fe63-5f6b-b364-6268f26b5740. Capture17 shows the OA process selected, Properties applied feedback, and the same canvas. Capture22 shows six SA cards in allocated nesting and Inspector projection Allocation; OA process/capability are absent from the canvas. Fixed01-reopen04 repeats that allocation rendering; 06 restores eight-card traceability. Reopen09 intentionally navigates to Operational and selects oa_assessment_procedure after the explicit endpoint action. This complements inspection retention rather than contradicting it.
|
||||
|
||||
The owned warning JPEG confirms Windows rejected the forward-slash filename before model admission. The detailed Return/UIA recovery sequence is recorded in the observer's note; a still JPEG alone does not independently prove each failed input call. The reviewed draw04 state JSON has accessibility:null, consistent with the tool's limited semantic exposure. Neither that fact nor pointer success establishes screen-reader support, complete Tab navigation, focus semantics, or keyboard-only authoring. Escape/Undo/Redo are bounded exercised keyboard paths; pan hints visible in the UI do not establish that keyboard pan was tested.
|
||||
|
||||
## Process, logs, saved-state evidence and limits
|
||||
|
||||
All three started/finished pairs identify c7065db76ecca3faa733ccb5f09b87fd0a0958be and GUI a9d8b2fe2eecb66e17cd16ab2efe6c270e70588d0282fe2ac5d30e50f827b8d8 with DLL f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4, system-only PATH and separate profile paths. Finished receipts record exit0, unchanged copied binaries/original fixtures; actual stdout.log and stderr.log files are zero bytes for each case. PIDs are fixed01 9668, reopen 9384, draw01 8736. Fresh-process attribution comes from these process records and action notes, not similar screenshots. There is no fresh-process Draw reopen claim.
|
||||
|
||||
Read the fixed01 preparation/inspection/name-undo/implicit-undo/projection-undo/projection-redo exact compare receipts, fixed01-reopen/reverse conservation, draw01 final and dataflow comparison receipts. All inspected command receipts have exit0 and empty actual stderr; comparison outputs pass. Exact comparisons report complete expected Project and raw source-byte equality. Projection comparisons report only the nominated view change and compiler-indexed token replacement, with exact outside-token bytes. The name/implicit saved-delta reports identify the one relation and expected name; their differing evaluator attribution is correctly disclosed in observations.md. The draw delta reports pin helper 6ac2c230... and expressly limit source coverage to inventories. The earlier independent helper review remains .local/native-draw-evaluator-review.md.
|
||||
|
||||
These are existing receipts, not executions performed by this reviewer. Screenshots alone do not establish complete Project conservation, invisible source bytes, one-step transaction history, no unsaved RAM changes, or fresh-process persistence. Counts and warning counts likewise do not establish those facts. Do not infer performance from this run: notes disclose concurrent compiler activity. Synthetic fixtures are useful bounded cases, not recovered user models or comprehensive engineering projects.
|
||||
|
||||
## Evidence pins
|
||||
|
||||
SHA-256 of the reviewed observation/process files:
|
||||
|
||||
| Case | observations.md | started.json | finished.json |
|
||||
| --- | --- | --- | --- |
|
||||
| fixed01 | 7c75d9e04772c35d5084243216caee2781fcebdb24d569c5652de70d554f2403 | f202e806fc935cc43bbf5f53bd64b8cae2627f84785e679c39d4b8b1de63114a | 45c59211f3ce4c78b2dabd93f34b31dec8abbfee0c49a6e553c4a1d3610cf23c |
|
||||
| fixed01-reopen | d6d5724bdbdb02b917b435b7668888886420b31dd8af5ef2d6746c37ca5f9596 | b386c60c93e02883e7b32ec679596f454d1a4f4e23fd799200928b694f4c7bb6 | fdd86e93bd8740245fd0d9e7d0bbe6981b1e7b98b82cdbf8ea2923a13c22bae7 |
|
||||
| draw01 | 52b21b0f80a39808949c7afa531b934528a4069521d04b2e24303fa00ca6a57e | f17a7aa2cc3bc1719e718a40099f38b0b3a8c5c5529e95a8e0c78a71f102d019 | f75847d4d1ad96ee489256e0549ea4569537d4fae53f7659cd5479dab05e017f |
|
||||
|
||||
Read-only hash calculations verified every JPEG against its adjacent original capture manifest: fixed01 28/28, fixed01-reopen 11/11 (including warning image), draw01 38/38. No mismatches. For example draw04 SHA is 1aea20c087a78156ab476ce8269edd295a906a86cd45476147f8483363cab690; its manifest reports 1442 x 932 and the correct private draw01 executable path. The individual manifests remain the authoritative full capture-pin inventory.
|
||||
@@ -1,158 +0,0 @@
|
||||
# Isolated navigation oracle
|
||||
|
||||
Prepared against core commit `6520a89dc042dd433454f6cc63cb23b0e6df7911`. This local crate is not a production CLI feature or workspace member. It has not been compiled or executed by its author. The initial preparation made no generated fixture/admission claim; parent execution evidence and the tested Windows runtime recipe are appended below. The prior `.local/populated-projection-regression.patch` is unchanged.
|
||||
|
||||
## Parent build and first admission
|
||||
|
||||
From `C:/ar-nav-01a0d717`, after the exclusive frozen compiler gate permits it:
|
||||
|
||||
```powershell
|
||||
cargo build --offline --manifest-path .local/navigation-oracle/Cargo.toml --target-dir .local/navigation-oracle/target
|
||||
```
|
||||
|
||||
The utility's Cargo.lock is initially an exact copy of the repository lock, used as a dependency seed. The first isolated build may update that local lock for this new package. Preserve its final bytes and the binary hash with the evidence. Subsequent reproducible builds should add `--locked --offline`. No Cargo command has been run in this task. Use Rust 1.90 or later, matching the repository's declared minimum. Dependencies are the repository path `oa-core` plus the same serde/serde_json major requirements already in the repository; no Python, shell helper or external server is required by the utility. The tested Windows gnullvm executable does require its matching libunwind.dll beside the executable; see the standalone runtime recipe below.
|
||||
|
||||
The `reviewed_core` field is a source-review pin, not an automatic runtime fingerprint. Record actual repository HEAD, tracked diff, utility hashes, final lock and executable hash when building. A later parent core change must not be attributed to this pin.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
$oracle = 'C:/ar-nav-01a0d717/.local/navigation-oracle-runtime/navigation-oracle.exe'
|
||||
& $oracle fixture C:/ar-nav-01a0d717/.local/native-draw-fixture
|
||||
& $oracle snapshot C:/path/to/saved/project.arcadia C:/path/to/new-baseline-snapshot
|
||||
& $oracle snapshot C:/path/to/saved/project.arcadia C:/path/to/new-after-snapshot
|
||||
& $oracle compare C:/path/to/new-baseline-snapshot C:/path/to/new-after-snapshot
|
||||
& $oracle compare C:/path/to/new-baseline-snapshot C:/path/to/new-after-snapshot --projection 00000000-0038-4000-8000-000000000001 Allocation
|
||||
```
|
||||
|
||||
Every output directory must be new, with an existing parent. Existing evidence is never overwritten. Fixture/snapshot writes that fail may leave partial output; preserve the failure, then choose another new directory. `compare` writes a JSON report to stdout and does not modify either snapshot. Exit 0 means that command completed successfully or comparison passed; 1 means a completed comparison failed; 2 means usage, loading, admission, IO or snapshot consistency failed. Capture stderr and the exit code as well as stdout.
|
||||
|
||||
For the projection trial: exact `compare` for selection/Properties; nominated-diagram `Allocation` comparison for switched state; exact baseline-versus-Undo, switched-versus-Redo and switched-versus-fresh-reopen comparisons. The utility does not infer which diagram changed. It never excuses unrelated fields, other diagrams, ID changes or reference changes.
|
||||
|
||||
## Snapshot contract
|
||||
|
||||
`snapshot` uses core `source_files::load_project` with the CLI's explicit policy: 64 MiB aggregate source, 256 files, include depth 32, 1024 include edges, 100000 graph objects, 250000 records and 500000 ordered references. It rejects only discovery/admission diagnostics whose actual enum is `validation::Severity::Error`, and returns full located compiler diagnostics on admission failure. Engineering warnings from successful compilation are retained in the report/snapshot. It compiles the reachable bundle with `source_project::compile_bundle` and exports the complete accepted Project to `project.json`.
|
||||
|
||||
`snapshot.json` contains the accepted Project, logical entry name and every reachable source path mapped to its exact raw captured disk bytes as an integer array. It preserves CR/LF, whitespace, comments and UTF-8 bytes; source is not regenerated or normalized. Disk expectations are checked against compiler input. Two complete source captures must match before evidence is written. This detects changes across the two observations but is not filesystem locking or an atomic multi-file snapshot; keep the saved project quiet.
|
||||
|
||||
The core loader rejects unsafe include paths, symlinks/junction escapes and invalid UTF-8. No snapshot logical path is used to write a file. Comparison revalidates logical paths, aggregate limits and UTF-8, recompiles all raw source, and requires that embedded Project and stored diagnostic strings exactly equal fresh compilation results. Compare reports diagnostics from that fresh compilation. Duplicate JSON source-path keys are rejected by a small serde visitor matching the core SourceBundle boundary pattern. Snapshot input itself is capped at 512 MiB. Source requiring UUID stabilization or migration is rejected so compiler-added text cannot be mistaken for original disk bytes: explicitly save the accepted current-version source first. Detached/unreachable editor buffers and unrelated disk files are outside this saved-project oracle.
|
||||
|
||||
## Comparison contract
|
||||
|
||||
Without an allowance, comparison requires complete Project equality, identical logical entry/path set and byte equality for every captured source file.
|
||||
|
||||
With `--projection DIAGRAM_UUID ProjectionKind`, clone the complete baseline Project and replace exactly the nominated diagram's projection. The after Project must equal that entire expected value. Accepted names are Overview, Decomposition, Allocation, FunctionalDataflow, Capabilities and Traceability; both the baseline and target must be architecture/string projections, so typed StateMachine/Scenario changes have no allowance.
|
||||
|
||||
Source still requires either exact bytes or exactly one changed source file whose complete prefix and suffix outside the compiler-indexed projection value match byte-for-byte. Both versions must have an explicit projection value in the same file and the same reachable path set. The JSON report includes both byte spans and original/replacement field text, plus whether outside-field bytes were exact. Omitted/defaulted projection declarations fail this narrow source allowance rather than broadening it silently. Project mismatches report up to 200 unexpected JSON paths; full snapshots remain available to inspect all differences. Matching counts alone never establishes conservation.
|
||||
|
||||
This is a saved-source oracle. It cannot inspect unsaved desktop state, prove GUI click ordering, prove exact in-memory source history, or establish native selection usability. Save/capture each relevant checkpoint and associate screenshots/action logs with it. Model-changing Draw actions deliberately fail this unchanged/projection-only comparator; inspect their precise creation delta separately instead of adding a broad allowance.
|
||||
|
||||
## Native Draw fixture contract
|
||||
|
||||
`fixture NEW_DIR` constructs typed records through `Project::add_element`, `ensure_diagram`, `place` and `reidentify`. It creates OA Activity source/target and SA Function source/target, with distinct labels and two Traceability diagrams declared OA and SA. Both diagrams contain all four work cards, placed in two uncluttered rows: OA at (60,60)/(520,60), SA at (60,300)/(520,300). Initially there are no relations or ports, allowing native Draw to create the Interaction or FunctionalExchange and its ports using the application's actual workflow.
|
||||
|
||||
UUIDv5 identities and aliases are deterministic; no random identity remains from the construction helpers. The generated `oracle.json` records exact element/view IDs and the source entry. The generator validates the complete Project, compiles the core `format_bundle` output, checks four projected cards per view, saves the bundle using `source_files::new_project`/`expect_new_file`/`save_project`, recompiles the saved source and requires full Project equality. It then writes Project JSON. Generated output is synthetic and must never be described as recovered user source or an already-run native acceptance result.
|
||||
|
||||
Use the SA mixed view to inspect/select OA work before invoking contextual Draw; use the OA mixed view for the SA work counterpart. Semantic source and target must follow the selected work layer even when the canvas perspective differs. Preserve the documented scope distinction: opening the contextual form retains the canvas; successful Apply may perform the existing explicit post-review navigation. The separate architecture palette limitation is not claimed repaired.
|
||||
|
||||
## Reviewed source identities and remaining checks
|
||||
|
||||
SHA-256 at preparation:
|
||||
|
||||
- `src/main.rs`: `69c2c90f5cfebf86f755ea47861e4ab059e7359e442993a996fb9e91e2caeb7c`
|
||||
- `Cargo.toml`: `ba95c25a04dbfd0d6d19bd59f466e4082f840723ebE8180e5061a4a157e139c8`
|
||||
- Repository/seed Cargo.lock: `2aa80d113c5112b31c0767b874845d60f555e4e8ae7eb3ecd0b4b91954b55378`
|
||||
- Core `source_files.rs`: `bb903f8c53456f69c5ac213a192a1eb0d19abf8344bd82a03be8e659ba7bc94f`
|
||||
- Core `source_project.rs`: `d872f2a8f7c4add53f2708bf3fdaea1b4be715d4240f2d9b93baf4624d3f72a7`
|
||||
- CLI limits source `crates/oa-cli/src/lib.rs`: `790f3bdef78025813262c9677d4db5813f32978a398f3b7fb4900427dc0c314e`
|
||||
|
||||
Parent review risks: uncompiled Rust/API compatibility; the isolated lock's initial adjustment; no atomic filesystem snapshot; snapshot memory grows with lossless byte arrays; strict stable-source/explicit-value requirements can reject otherwise valid but insufficiently prepared input. Useful oracle checks before trusting native results: identical snapshots pass, one allowed projection change passes only with the matching nominated view/value, an unrelated comment change fails exact bytes despite equal Project, and unrelated semantic/placement edits fail even under the projection allowance. Those checks are proposed, not executed.
|
||||
|
||||
## Review correction before first execution
|
||||
|
||||
The initial source and README are preserved verbatim under `revisions/01-pre-warning-repair/`, with their hashes. The initial capture check incorrectly rejected all discovery diagnostics, including legitimate `unallocated_leaf` warnings; no successful run is attributed to that version. The correction filters the actual Error severity, keeps compiler warnings, binds stored diagnostic strings to fresh compilation, guards typed baseline projections as well as targets, and rejects duplicate source-path JSON keys. `project.json` is a convenience export; the comparator admits the embedded model and raw sources from `snapshot.json` only.
|
||||
|
||||
Two Rust tests now cover nonfatal engineering warnings/error rejection, deleted/invented warning rejection, and duplicate raw-file-key rejection. Parent command after the frozen gate: `cargo test --offline --manifest-path .local/navigation-oracle/Cargo.toml --target-dir .local/navigation-oracle/target`. These tests and the utility still have not been compiled or run by this author. Runtime fixture generation and the actual warning-bearing source-load path remain required parent checks.
|
||||
|
||||
## Retained filesystem verification module (not executed)
|
||||
|
||||
`src/verification.rs` adds one test, `verification::filesystem_evaluator_contract`. Before any full utility test run, the parent must set `NAVIGATION_ORACLE_QA_DIR` to a fresh absolute path whose parent exists. The test intentionally fails without that explicit destination and refuses an existing destination. It never deletes or replaces evidence.
|
||||
|
||||
```powershell
|
||||
$env:NAVIGATION_ORACLE_QA_DIR = 'C:/ar-nav-01a0d717/.local/navigation-oracle-qa-run-01'
|
||||
cargo test --offline --manifest-path .local/navigation-oracle/Cargo.toml --target-dir .local/navigation-oracle/target verification::filesystem_evaluator_contract -- --nocapture
|
||||
```
|
||||
|
||||
Use a different fresh path for every rerun, including after failure. Capture Cargo stdout/stderr and exit status. Under the supplied path, `checks.jsonl` records each completed control with its result and relevant errors; every source variant, snapshot, corrupt input and sentinel is retained. Comparison JSON reports also go to test stdout; `--nocapture` makes them available to the parent log. An interrupted or failed run retains partial evidence and has no final completion marker.
|
||||
|
||||
The test invokes actual `fixture`, `capture`, `snapshot`, `read_snapshot` and `compare` functions. Positive variants use `prepare_command` and `update_bundle_with_identity_map`, then the core new-project/save APIs; the core crate has no desktop Document type. The projection edit is a real EditDiagram command and the unrelated model edit a real Rename command. Corrupt snapshots are deliberately based on valid captures. Duplicate-key input is syntactically valid JSON with a duplicated existing path, and missing-include input omits an actual discovered dependency from a previously valid complete bundle.
|
||||
|
||||
Controls cover warning-bearing fixture generation/capture; identical snapshots; accepted projection-only change; wrong diagram/value; comment and CRLF changes with equal accepted Project; unrelated coherent semantic change; embedded-model and diagnostic tampering; duplicate/unsafe snapshot paths; invalid UTF-8 and missing include on disk; and preservation of an existing output sentinel for both snapshot and fixture commands. The comment/CRLF cases are checked both against the switched model in exact mode and against the original in projection-allowance mode. Both existing-output controls require a direct boxed std::io::Error with ErrorKind::AlreadyExists and record the actual kind/message; unrelated earlier errors cannot satisfy the refusal check. The previous module and README bytes are retained with hashes under revisions/04-before-output-refusal-check/.
|
||||
|
||||
This module adds no dependency or production behavior. Original reviewed main.rs `2cf2d1ced864ed339eb31470d42b370ba7d6de559c4679965fec4729984b08d6` and README are preserved under `revisions/03-before-filesystem-tests/`; the pre-attempt01 main.rs retained that entire source as an exact prefix and added only the two-line cfg(test) module declaration. The subsequent compiler repair is recorded below. Module SHA-256: `c7d6b184b7bfa579ec6dd2a777a0a417a65418f768d4b8cbf6afce7589ab9a25`. No Cargo, filesystem exercise or GUI run has occurred in this preparation task.
|
||||
|
||||
Bounded omissions: this exercise does not build a typed StateMachine/Scenario fixture to retest the architecture guard; it does not simulate concurrent filesystem races, interrupted multi-file writes, symlink/junction attacks or out-of-memory limits. It also cannot replace native Draw, selection or unsaved desktop Document-history checks.
|
||||
|
||||
## Attempt01 compiler repair (not rerun by this author)
|
||||
|
||||
Parent attempt01 exited 101 before any test executed: Rust E0382 at src/main.rs:261 identified the placement alias being moved by the assignment right-hand side before the left-hand side borrowed it to compute its ID. Evidence is ../navigation-oracle-runs/attempt01/tests.stderr. The isolated fixture generator now computes placement_id once before moving placement_alias and reuses that ID for reidentification and lookup. No alias clone, verification assertion, production repository file or dependency changed.
|
||||
|
||||
The exact pre-repair main.rs and README are preserved with SHA-256 records under revisions/05-before-placement-id-repair/. Main was da515eff05bd34eb0cb354e412db74dbdede72518c99839fc5d75d56cdd7c892; README was 82355e35120e8f0508d146306d37efa8c22eac8d3f0c59029d5cdaff28835257. The parent-adjusted isolated Cargo.lock is e97d03c226c2c3f2328b68bcb6d823df00c0569918fa9891d50504cd12e0af1d and was left untouched. This repair has not been compiled or run by its author; parent owns a rerun with fresh logs and QA root. Earlier no-execution statements describe preparation and remain no test-pass claims.
|
||||
## Tested standalone Windows runtime and parent execution evidence
|
||||
|
||||
This is a receipt review, not a new execution. Parent attempt02 at repository HEAD `c7065db76ecca3faa733ccb5f09b87fd0a0958be` built with Rust 1.98.1 gnullvm / LLVM-MinGW 20260826 UCRT and passed all three tests. Its authoritative input manifest, commands and result are `../navigation-oracle-runs/attempt02/result.json` (SHA-256 `1ed50fe9c42ff4a67084ed70ce934404f5549ca68bc54e6d2f1436e66ea8d22f`); logs and retained QA controls are alongside it and in `../navigation-oracle-qa-attempt02/`. This actual build attribution does not change the source-review `reviewed_core` pin of `6520a89dc042dd433454f6cc63cb23b0e6df7911`.
|
||||
|
||||
The first standalone fixture receipt, `../navigation-oracle-commands/draw-fixture/receipt.json`, records exit 3221225781 (`0xC0000135`, Windows DLL-not-found status), with no stdout or stderr. The same executable succeeded after staging beside it the matching `libunwind.dll`: `../navigation-oracle-commands/draw-fixture-runtime02/receipt.json` records exit 0, unchanged executable, empty stderr, and child PATH `C:\Windows\System32;C:\Windows`. Its stdout reports the saved `../native-draw-fixture/project.arcadia` and Project JSON. The intermediate `draw-fixture-runtime/failure.json` instead records a Python recorder `SystemRoot` key-case failure before the oracle started; it is not an oracle or fixture result.
|
||||
|
||||
Verified current private runtime files in `../navigation-oracle-runtime/`:
|
||||
|
||||
| File | Bytes | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| navigation-oracle.exe | 114890752 | `bd2db2e177d9d21f44b45e5efa89071e00155d31509672bf5aea6a57915e09a6` |
|
||||
| libunwind.dll | 190976 | `f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4` |
|
||||
|
||||
The executable came from `C:/Users/User/Documents/Git/ARCADIA-Studio/.local/target-gnullvm/debug/navigation-oracle.exe`. A verified identical DLL is `C:/ar-save-01a0d717/.local/package46/extracted/ArcadiaStudio-0.2.0-preview.save-diagnostics.20261001-x86_64-pc-windows-gnullvm/libunwind.dll`. For another private runtime copy, use a new directory and verify both hashes before launch. This recipe reproduces the tested arrangement; the particular new paths below have not been executed:
|
||||
|
||||
```powershell
|
||||
$runtimeCopy = 'C:/ar-nav-01a0d717/.local/navigation-oracle-runtime-copy-01'
|
||||
$fixtureOutput = 'C:/ar-nav-01a0d717/.local/native-draw-fixture-copy-01'
|
||||
if ((Test-Path -LiteralPath $runtimeCopy) -or (Test-Path -LiteralPath $fixtureOutput)) { throw 'Choose fresh runtime/output paths' }
|
||||
New-Item -ItemType Directory -Path $runtimeCopy -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath 'C:/ar-nav-01a0d717/.local/navigation-oracle-runtime/navigation-oracle.exe' -Destination $runtimeCopy -ErrorAction Stop
|
||||
Copy-Item -LiteralPath 'C:/ar-nav-01a0d717/.local/navigation-oracle-runtime/libunwind.dll' -Destination $runtimeCopy -ErrorAction Stop
|
||||
$oracle = Join-Path $runtimeCopy 'navigation-oracle.exe'
|
||||
if ((Get-FileHash -LiteralPath $oracle -Algorithm SHA256).Hash -ne 'bd2db2e177d9d21f44b45e5efa89071e00155d31509672bf5aea6a57915e09a6') { throw 'Executable pin mismatch' }
|
||||
if ((Get-FileHash -LiteralPath (Join-Path $runtimeCopy 'libunwind.dll') -Algorithm SHA256).Hash -ne 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4') { throw 'Runtime pin mismatch' }
|
||||
$savedOraclePath = $env:PATH
|
||||
try {
|
||||
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
||||
& $oracle fixture $fixtureOutput
|
||||
if ($LASTEXITCODE -ne 0) { throw "Oracle exited $LASTEXITCODE" }
|
||||
} finally {
|
||||
$env:PATH = $savedOraclePath
|
||||
}
|
||||
```
|
||||
|
||||
The PATH change applies only to this shell and is restored; no compiler directory is needed for this standalone launch. For recorded commands, the existing `../record-native-oracle.py` (SHA-256 `abbe6fae3055673ca3c33f0f87cba21189dd2ca905f63acc2da69b0b605fbc23`) checks executable/DLL pins, sets the child system-only PATH, and retains argv, stdout, stderr, exit, repository HEAD and build-receipt hash in a fresh command directory. The Python recorder is optional evidence tooling, not a runtime dependency of the Rust executable.
|
||||
|
||||
## Fixed01 saved-evidence inventory for scoped review
|
||||
|
||||
At this review, `../navigation-oracle-commands/fixed01-*` contains ten snapshot receipts and seven compare receipts. All 17 record exit 0, empty stderr and an unchanged executable with the runtime pin above. Snapshots are `prepared`, `baseline`, `inspected`, `name-applied`, `name-undo`, `implicit-applied`, `implicit-undo`, `switched`, `projection-undo` and `projection-redo`, under `../navigation-native/fixed01/`. Each compare folder below contains its actual `receipt.json`, `stdout` JSON report and `stderr`:
|
||||
|
||||
| Compare receipt suffix after fixed01- | Before → after | Reported contract/result |
|
||||
| --- | --- | --- |
|
||||
| preparation-equality | prepared → baseline | Exact full Project and raw source, pass |
|
||||
| inspection-equality | baseline → inspected | Exact full Project and raw source, pass |
|
||||
| name-undo-equality | baseline → name-undo | Exact full Project and raw source, pass |
|
||||
| implicit-undo-equality | baseline → implicit-undo | Exact full Project and raw source, pass |
|
||||
| projection-conservation | baseline → switched | Only nominated projection change, pass |
|
||||
| projection-undo-equality | baseline → projection-undo | Exact full Project and raw source, pass |
|
||||
| projection-redo-equality | switched → projection-redo | Exact full Project and raw source, pass |
|
||||
|
||||
The projection report nominates diagram `00000000-0038-4000-8000-000000000001`; only `views/diagram_issue38_representative_mixed.arcadia` changes, from `Traceability` at byte span [338,350) to `Allocation` at [338,348). Full expected Project and entry/path set match; prefix/suffix outside the projection value are byte-exact. Its raw_source_bytes_equal is correctly false because that one permitted token changes. Six exact comparisons instead report no changed files and raw_source_bytes_equal true. These receipts preserve the existing `work_without_behavior_context` warning; success does not mean a warning-free model.
|
||||
|
||||
The separate parameterized `../check-native-name.py` (SHA-256 `617a189b546ded2c9133dd0884152cea265d2f86041a5ef983394e6063c82f72`) reviews full saved Project equality after exactly one nominated relation-name edit, and full raw source equality after exactly one unique declaration-header replacement. It fixes baseline/fixture paths and relation UUID `628cb4fc-fe63-5f6b-b364-6268f26b5740`; only the after-directory, expected name and new report stem are parameters. It is not a general parser/admission substitute: the pinned Rust snapshot receipts supply source admission. Intended invocation for the already-recorded implicit case was `python .local/check-native-name.py implicit-applied "Implicit assessment delivery" implicit-delta-check`; it now refuses overwriting the existing report. Use a fresh report stem for any separately authorized rerun.
|
||||
|
||||
`../navigation-native/fixed01/implicit-delta-check.json` is SHA-256 `eb14e21c5b75dd3ba63200ccf8860cb85aee6f5688b4db2626958060178f5f09`, reports all five checks true, and binds that evaluator hash, baseline snapshot `7695c2ff4aa735110cb198f6451be596c5ac59a65ae9ccd7c7602e0259cb5e9a`, after snapshot `86fe91f1eaf0e8a0a9a509c7f4d2e5f90ec5f2b01a9169e0678dc8342a7a282b`, and original fixture `9db1015369f450d5810c96a01fddb8a009d57f6aafbda5182703f15ded23d9c8`. The changed declaration is in `oa/behavior/process_oa_assessment_procedure.arcadia`. The earlier explicit-name `name-delta-check.json` also reports five true checks but has no evaluator_sha256 field; do not retrospectively attribute it to the current parameterized helper.
|
||||
|
||||
This inventory does not establish GUI click order, one-step interaction history, visible content, fresh reopening, or unsaved RAM conservation. Those claims require the parent's corresponding native capture/action evidence. No GUI, Rust/Python oracle command or Cargo test was executed during this README update. Pre-update README SHA-256 `496bc4eab252671232f6d2269b95fa0bc3596aad36341ca362cca9c140df3e08` is retained under `revisions/06-before-runtime-instructions/`; source and evaluator bytes are unchanged.
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\draw01\\bin\\arcadia-studio.exe",
|
||||
"id": 43582746,
|
||||
"title": "Cross-layer native Draw verification — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "27-enabled-functional-tool-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "81c25b3d2e1a189389b5abdb0b3987d5984fb3491a56600ff36c870b584efab7"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\fixed01\\bin\\arcadia-studio.exe",
|
||||
"id": 15008282,
|
||||
"title": "Restore safe water pressure — release 1 walkthrough * — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "20-click-away-undo-saved-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "6b57d48bfdf74e3b942cb57fadf5e3ade360a6838ae5f7178e4a30de0815ac83"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 141 KiB |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\draw01\\bin\\arcadia-studio.exe",
|
||||
"id": 43582746,
|
||||
"title": "Cross-layer native Draw verification — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "03-sa-source-on-oa-menu-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "b4e365b03c7965b78205703e9f8211f9ef330a0f523a48089ef510edc4140631"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\peer01\\bin\\arcadia-studio.exe",
|
||||
"id": 25822524,
|
||||
"title": "Restore safe water pressure — release 1 walkthrough * — Arcadia Studio"
|
||||
},
|
||||
"accessibility": {
|
||||
"focused_element": "0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise",
|
||||
"tree": "Window: \"Restore safe water pressure — release 1 walkthrough — Arcadia Studio\", App: arcadia-studio.exe.\n\t0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise\n\t\t1 title bar (settable, string) Value: Restore safe water pressure — release 1 walkthrough — Arcadia Studio\n\t\t\t2 menu bar System ID: MenuBar\n\t\t\t\t3 menu item System\n\t\t\t4 button Minimize\n\t\t\t5 button Maximize\n\t\t\t6 button Close\n\nThe focused UI element is 0 window Restore safe water pressure — release 1 walkthrough — Arcadia Studio Secondary Actions: Raise."
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "06-valid-pending-name-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "7dcac290662e627f12d14239f2775fb15370ee4cc5a82d1177bd4a8d0054e33e"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\relocation01-reopen\\bin\\arcadia-studio.exe",
|
||||
"id": 2360006,
|
||||
"title": "Cross-layer native Draw verification — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "09-inspection-no-undo-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "b863bcc65d39147b44f3075aa9aa377383e3868ca90a1bd1c7d1ee41840bc975"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"window": {
|
||||
"app": "process:C:\\ar-nav-01a0d717\\.local\\navigation-native\\baseline01\\bin\\arcadia-studio.exe",
|
||||
"id": 53937404,
|
||||
"title": "Restore safe water pressure — release 1 walkthrough — Arcadia Studio"
|
||||
},
|
||||
"accessibility": null,
|
||||
"screenshots": [
|
||||
{
|
||||
"filename": "04-edge-click-wrong-oa-view-0.jpg",
|
||||
"id": "screenshot-0",
|
||||
"width": 1442,
|
||||
"height": 932,
|
||||
"sha256": "9c437213d3baaa2817c1a134ff2735a9d2d45893cb7fb8afad39f14f08ffd93c"
|
||||
}
|
||||
]
|
||||
}
|
||||