Review nested message kind groups with exact timer scope

This commit is contained in:
2026-09-06 07:06:19 +03:00
parent bd098ee6b7
commit 43dd566939
8 changed files with 970 additions and 45 deletions
+25
View File
@@ -46,6 +46,7 @@ pub struct Draft {
pub inputs: BTreeMap<String, Input>, pub inputs: BTreeMap<String, Input>,
pub exact_strings: bool, pub exact_strings: bool,
context: Option<Value>, context: Option<Value>,
message_timer_draft: Option<Input>,
port_contract_drafts: BTreeMap<String, Value>, port_contract_drafts: BTreeMap<String, Value>,
pub value_browsers: BTreeMap<String, ValueBrowser>, pub value_browsers: BTreeMap<String, ValueBrowser>,
} }
@@ -85,6 +86,7 @@ impl Draft {
inputs: BTreeMap::new(), inputs: BTreeMap::new(),
exact_strings: false, exact_strings: false,
context: None, context: None,
message_timer_draft: None,
port_contract_drafts: BTreeMap::new(), port_contract_drafts: BTreeMap::new(),
value_browsers: BTreeMap::new(), value_browsers: BTreeMap::new(),
}; };
@@ -248,6 +250,29 @@ impl Draft {
self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned())); self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned()));
Ok(()) Ok(())
} }
/// Message-kind selection preserves the timer's exact input/editor state
/// while its payload is temporarily hidden by another kind.
pub fn message_kind_variant(&mut self, path: &str, value: Value) -> Result<(), String> {
serde_json::from_value::<MessageKind>(value.clone()).map_err(|e| e.to_string())?;
let current = self
.value
.pointer(path)
.ok_or("Message kind no longer exists.")?;
if same_variant(current, &value) {
return Ok(());
}
let timer_path = format!("{path}/Timer/expression");
if let Some(input) = self.inputs.remove(&timer_path) {
self.message_timer_draft = Some(input);
}
self.shape_scoped(path, value)?;
if self.inputs.contains_key(&timer_path)
&& let Some(input) = self.message_timer_draft.take()
{
self.inputs.insert(timer_path, input);
}
Ok(())
}
fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) { fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) {
let previous = std::mem::take(&mut self.inputs); let previous = std::mem::take(&mut self.inputs);
self.rebuild(); self.rebuild();
+77 -12
View File
@@ -8,10 +8,13 @@ use oa_core::{transitions::*, *};
use serde_json::Value; use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
mod message_kind;
mod scalar; mod scalar;
mod view; mod view;
pub(crate) use view::summary; pub(crate) use view::summary;
#[cfg(test)] #[cfg(test)]
mod message_kind_tests;
#[cfg(test)]
mod tests; mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -54,6 +57,9 @@ pub struct State {
pub decisions: BTreeMap<FacetSelector, SemanticDecision>, pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
pub references: BTreeMap<Id, Id>, pub references: BTreeMap<Id, Id>,
pub draft: Option<Draft>, pub draft: Option<Draft>,
// Group editing cannot reshape the independently authored TimerExpression
// scalar draft. Both drafts survive decision changes until this trace closes.
pub message_draft: Option<Draft>,
pub plan: Option<SemanticTransitionPlan>, pub plan: Option<SemanticTransitionPlan>,
pub proposed: Option<Project>, pub proposed: Option<Project>,
pub error: Option<String>, pub error: Option<String>,
@@ -96,6 +102,7 @@ impl State {
decisions: BTreeMap::new(), decisions: BTreeMap::new(),
references: BTreeMap::new(), references: BTreeMap::new(),
draft: None, draft: None,
message_draft: None,
plan: None, plan: None,
proposed: None, proposed: None,
error: None, error: None,
@@ -128,6 +135,12 @@ impl State {
self.draft = self self.draft = self
.target(project) .target(project)
.and_then(|id| Draft::from_project(project, id)); .and_then(|id| Draft::from_project(project, id));
self.message_draft = self
.trace
.and_then(|id| project.traces.get(&id))
.filter(|trace| message_kind::available(project, trace))
.and_then(|_| self.target(project))
.and_then(|id| Draft::from_project(project, id));
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
self.references.insert(trace.oa, trace.sa); self.references.insert(trace.oa, trace.sa);
} }
@@ -188,7 +201,10 @@ impl State {
self.edit(project, &path, |d| d.shape_scoped(&path, value)) self.edit(project, &path, |d| d.shape_scoped(&path, value))
} }
Event::Variant(path, value) => self.edit(project, &path, |d| { Event::Variant(path, value) => self.edit(project, &path, |d| {
if d.value if path.ends_with("/message_kind") {
d.message_kind_variant(&path, value)
} else if d
.value
.pointer(&path) .pointer(&path)
.is_some_and(|v| crate::forms::same_variant(v, &value)) .is_some_and(|v| crate::forms::same_variant(v, &value))
{ {
@@ -286,9 +302,15 @@ impl State {
.and_then(|id| scalar::binding(project, id, facet)) .and_then(|id| scalar::binding(project, id, facet))
.ok_or("Selected scalar property is unavailable.")?; .ok_or("Selected scalar property is unavailable.")?;
if path != root && !path.starts_with(&format!("{root}/")) { if path != root && !path.starts_with(&format!("{root}/")) {
return Err("Only the selected scalar property can change in this draft.".into()); return Err("Only the selected review property can change in this draft.".into());
} }
let draft = self.draft.as_mut().ok_or("Choose an existing target.")?; let draft = if message_kind::handles(facet) {
&mut self.message_draft
} else {
&mut self.draft
}
.as_mut()
.ok_or("Choose an existing target.")?;
// The scoped view sends only descendants of the selected scalar property. // The scoped view sends only descendants of the selected scalar property.
// Protected identity and top-level body/perspective controls are never exposed. // Protected identity and top-level body/perspective controls are never exposed.
if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" { if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" {
@@ -301,7 +323,21 @@ impl State {
self.target(project) self.target(project)
.and_then(|id| scalar::binding(project, id, facet)) .and_then(|id| scalar::binding(project, id, facet))
.is_some() .is_some()
&& !scalar::grouped(facet) && (!scalar::grouped(facet)
|| (message_kind::handles(facet)
&& self.message_draft.is_some()
&& self
.trace
.and_then(|id| project.traces.get(&id))
.is_some_and(|trace| message_kind::available(project, trace))))
}
fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> {
if message_kind::handles(facet) {
self.message_draft.as_ref()
} else {
self.draft.as_ref()
}
} }
pub fn can_acknowledge_constructor(&self, project: &Project, facet: &FacetSelector) -> bool { pub fn can_acknowledge_constructor(&self, project: &Project, facet: &FacetSelector) -> bool {
@@ -319,13 +355,12 @@ impl State {
{ {
return false; return false;
} }
self.facts capture_facet(project, trace.oa, trace.sa, facet)
.iter() .ok()
.find(|f| f.selector == *facet) .is_some_and(|current| {
.is_some_and(|f| { scalar::value(&current, Layer::Oa).is_some()
scalar::value(&f.current, Layer::Oa).is_some() && scalar::observation(&current, Layer::Oa)
&& scalar::observation(&f.current, Layer::Oa) == scalar::observation(&current, Layer::Sa)
== scalar::observation(&f.current, Layer::Sa)
}) })
} }
@@ -399,7 +434,6 @@ impl State {
let mut final_value = Draft::from_project(project, target) let mut final_value = Draft::from_project(project, target)
.ok_or("Target has no editable record.")? .ok_or("Target has no editable record.")?
.value; .value;
let draft = self.draft.as_ref().ok_or("Target draft is unavailable.")?;
let mut map = BTreeMap::new(); let mut map = BTreeMap::new();
for (oa, sa) in &self.references { for (oa, sa) in &self.references {
let (from, to) = if self.direction == TransitionDirection::OaToSa { let (from, to) = if self.direction == TransitionDirection::OaToSa {
@@ -412,6 +446,7 @@ impl State {
} }
} }
map.insert(source, target); map.insert(source, target);
let mut message_candidate = None;
for (facet, decision) in &self.decisions { for (facet, decision) in &self.decisions {
if *decision == SemanticDecision::KeepTarget { if *decision == SemanticDecision::KeepTarget {
continue; continue;
@@ -421,6 +456,9 @@ impl State {
let value = match decision { let value = match decision {
SemanticDecision::KeepTarget => continue, SemanticDecision::KeepTarget => continue,
SemanticDecision::Candidate => { SemanticDecision::Candidate => {
let draft = self
.candidate_draft(facet)
.ok_or("Target draft is unavailable.")?;
let value = match draft.materialize_scopes(std::slice::from_ref(&path)) { let value = match draft.materialize_scopes(std::slice::from_ref(&path)) {
Ok(value) => value, Ok(value) => value,
Err(_) if form_context => draft.value.clone(), Err(_) if form_context => draft.value.clone(),
@@ -431,15 +469,39 @@ impl State {
.cloned() .cloned()
.ok_or("Candidate property is no longer available.")? .ok_or("Candidate property is no longer available.")?
} }
SemanticDecision::TakeSource if message_kind::handles(facet) => {
// First equal kinds acknowledge only the discriminator.
// An unselected Timer payload must stay at the destination.
message_kind::source_value(
project,
if self.can_acknowledge_constructor(project, facet) {
target
} else {
source
},
)?
}
SemanticDecision::TakeSource => { SemanticDecision::TakeSource => {
let fact = capture_facet(project, trace.oa, trace.sa, facet)?; let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
scalar::source_value(&fact, self.direction.source_layer(), &map)? scalar::source_value(&fact, self.direction.source_layer(), &map)?
} }
}; };
if message_kind::handles(facet) && *decision == SemanticDecision::Candidate {
message_candidate = Some((path.clone(), value.clone()));
}
*final_value *final_value
.pointer_mut(&path) .pointer_mut(&path)
.ok_or("Final target property is unavailable.")? = value; .ok_or("Final target property is unavailable.")? = value;
} }
if !form_context
&& let Some((path, candidate)) = message_candidate
&& let Ok(current) = message_kind::source_value(project, target)
&& crate::forms::same_variant(&candidate, &current)
&& candidate != current
&& final_value.pointer(&path) != Some(&candidate)
{
return Err("The message-kind candidate and Timer Expression decision propose different timer values. Keep the kind or make their final values agree; both drafts are retained.".into());
}
Ok(final_value) Ok(final_value)
} }
@@ -449,6 +511,9 @@ impl State {
self.error = None; self.error = None;
let context = self.combined_value(project, true).ok(); let context = self.combined_value(project, true).ok();
if let Some(draft) = &mut self.draft { if let Some(draft) = &mut self.draft {
draft.set_context(context.clone());
}
if let Some(draft) = &mut self.message_draft {
draft.set_context(context); draft.set_context(context);
} }
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else { let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
@@ -0,0 +1,32 @@
//! The first supported constructor group is the closed nested MessageKind.
//! The core owns its acknowledgment/retirement and validation policies.
use super::*;
pub(super) fn selector() -> FacetSelector {
FacetSelector::Occurrence(OccurrenceFacet::MessageKind)
}
pub(super) fn timer_selector() -> FacetSelector {
FacetSelector::Occurrence(OccurrenceFacet::TimerExpression)
}
pub(super) fn handles(facet: &FacetSelector) -> bool {
*facet == selector()
}
pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
match project.elements.get(&id)?.shared()? {
SharedBody::Occurrence {
body: OccurrenceBody::Exchange { message_kind, .. },
..
} => Some(message_kind),
_ => None,
}
}
pub(super) fn available(project: &Project, trace: &Trace) -> bool {
value(project, trace.oa).is_some() && value(project, trace.sa).is_some()
}
/// MessageConstructor observations contain only the discriminator. Taking a
/// replacing group needs the complete typed payload, including Timer.expression.
pub(super) fn source_value(project: &Project, id: Id) -> Result<Value, String> {
let value =
value(project, id).ok_or("Message kind requires an existing Exchange occurrence.")?;
serde_json::to_value(value).map_err(|e| e.to_string())
}
@@ -0,0 +1,691 @@
use super::{tests::*, *};
use crate::{app::App, document::Document};
use serde_json::json;
fn fixture(oa_kind: MessageKind, sa_kind: MessageKind) -> (Project, Id, Id, Id) {
let mut p = Project::empty();
let mut ids = vec![];
for (layer, message_kind) in [(Layer::Oa, oa_kind), (Layer::Sa, sa_kind)] {
let scenario = p
.add_element(Kind::Scenario, layer, "Scenario", None)
.unwrap();
let id = p
.add_element(Kind::Occurrence, layer, "Message", Some(scenario))
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: None,
sender: None,
receiver: None,
message_kind,
reply_to: None,
values: vec![],
},
precondition: "retain precondition".into(),
postcondition: "retain postcondition".into(),
};
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
ids.push(id);
}
let trace = add_trace(&mut p, ids[0], ids[1], "Message-kind review");
(p, ids[0], ids[1], trace)
}
fn timer(value: &str) -> MessageKind {
MessageKind::Timer {
expression: value.into(),
}
}
fn path(layer: &str) -> String {
format!("/body/{layer}/Shared/Occurrence/body/Exchange/message_kind")
}
fn set_kind(p: &mut Project, id: Id, kind: MessageKind) {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { message_kind, .. },
..
}) = p.elements.get_mut(&id).unwrap().shared_mut()
else {
panic!()
};
*message_kind = kind;
}
fn seed_history(p: &mut Project, trace: Id) {
let t = &p.traces[&trace];
let baseline = TraceBaseline {
oa: p.properties(t.oa).unwrap(),
sa: p.properties(t.sa).unwrap(),
semantic: Some(SemanticBaseline {
facets: [message_kind::selector(), message_kind::timer_selector()]
.iter()
.map(|selector| capture_facet(p, t.oa, t.sa, selector).unwrap())
.collect(),
}),
};
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
}
fn acknowledged(app: &App) -> BTreeSet<FacetSelector> {
state(app).plan.as_ref().unwrap().reviews[0]
.acknowledged
.iter()
.cloned()
.collect()
}
fn failures(app: &App) -> String {
state(app)
.error
.iter()
.chain(state(app).plan.iter().flat_map(|p| &p.conflicts))
.cloned()
.collect::<Vec<_>>()
.join("; ")
}
#[test]
fn message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions()
{
for reverse in [false, true] {
let expression = " after β ≥ 5 s\n\tkeep exact whitespace ";
let (p, oa, sa, trace) = if reverse {
fixture(MessageKind::Asynchronous, timer(expression))
} else {
fixture(timer(expression), MessageKind::Asynchronous)
};
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
let mut app = App::with_project(p);
let mut bundle = app.document.bundle().clone();
bundle
.files
.get_mut(&bundle.entry)
.unwrap()
.insert_str(0, "// Keep the engineering review rationale.\r\n");
app.document.apply_bundle(bundle).unwrap();
let before = app.document.snapshot();
let revision = app.document.revision();
start(&mut app, source);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::TakeSource,
);
decide(
&mut app,
FacetSelector::Header(HeaderFacet::Name),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Input("/name".into(), "Reviewed message".into()),
);
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
assert_eq!(
acknowledged(&app),
BTreeSet::from([
message_kind::selector(),
message_kind::timer_selector(),
FacetSelector::Header(HeaderFacet::Name)
])
);
assert!(
state(&app).plan.as_ref().unwrap().reviews[0]
.retired
.is_empty()
);
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
let expected = state(&app).proposed.clone().unwrap();
assert_eq!(
message_kind::value(&expected, target),
Some(&timer(expression))
);
assert_eq!(app.document.bundle(), &before.bundle);
apply(&mut app);
assert_eq!(app.project(), &expected);
assert_eq!(app.document.revision(), revision + 1);
assert_eq!(
app.project().elements[&source],
before.decoded.project.elements[&source]
);
let saved = app.document.bundle().clone();
assert!(saved.files[&saved.entry].starts_with("// Keep the engineering review rationale."));
assert_eq!(
Document::from_bundle(saved.clone(), source_project::ProjectLimits::default())
.unwrap()
.snapshot()
.decoded
.project,
expected
);
let facts = &app.project().traces[&trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets;
assert_eq!(facts.len(), 3);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &saved);
}
}
#[test]
fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit() {
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("original target"));
let mut app = App::with_project(p);
let before = app.document.snapshot();
let kind = path("Sa");
let expression = format!("{kind}/Timer/expression");
start(&mut app, oa);
decide(
&mut app,
message_kind::timer_selector(),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Input(expression.clone(), "independent scalar timer".into()),
);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
assert_eq!(state(&app).decisions.len(), 2);
assert!(!state(&app).can_apply());
send(
&mut app,
Event::Input(expression.clone(), " group β timer\n\t ".into()),
);
send(
&mut app,
Event::Text(expression.clone(), text_editor::Action::SelectAll),
);
let cursor = state(&app).message_draft.as_ref().unwrap().inputs[&expression]
.editor
.cursor();
let selection = state(&app).message_draft.as_ref().unwrap().inputs[&expression]
.editor
.selection();
send(
&mut app,
Event::Variant(kind.clone(), json!("Asynchronous")),
);
send(
&mut app,
Event::Variant(kind, json!({"Timer":{"expression":""}})),
);
assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression].raw,
" group β timer\n\t "
);
assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression]
.editor
.cursor(),
cursor
);
assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression]
.editor
.selection(),
selection
);
assert_eq!(
state(&app).draft.as_ref().unwrap().inputs[&expression].raw,
"independent scalar timer"
);
assert!(!state(&app).can_apply());
assert_eq!(app.document.bundle(), &before.bundle);
// The visible overlap-resolution actions only change the explicitly chosen
// decision. Neither independent candidate buffer is discarded.
send(&mut app, Event::Decide(message_kind::selector(), None));
assert!(state(&app).can_apply());
send(
&mut app,
Event::Decide(message_kind::timer_selector(), None),
);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
assert_eq!(
acknowledged(&app),
BTreeSet::from([message_kind::selector(), message_kind::timer_selector()])
);
let _ = state(&app).view(app.project());
let expected = state(&app).proposed.clone().unwrap();
apply(&mut app);
assert_eq!(app.project(), &expected);
}
#[test]
fn message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects() {
let (mut p, oa, sa, trace) = fixture(timer("old OA"), timer("old SA"));
seed_history(&mut p, trace);
set_kind(&mut p, oa, MessageKind::Asynchronous);
set_kind(&mut p, sa, MessageKind::Asynchronous);
let mut app = App::with_project(p);
start(&mut app, oa);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::KeepTarget,
);
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
assert_eq!(
acknowledged(&app),
BTreeSet::from([message_kind::selector()])
);
assert_eq!(
state(&app).plan.as_ref().unwrap().reviews[0].retired,
vec![message_kind::timer_selector()]
);
apply(&mut app);
assert_eq!(
app.project().traces[&trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.len(),
1
);
start(&mut app, oa);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::KeepTarget,
);
assert!(!state(&app).can_apply());
assert!(
failures(&app).contains("No constructor switch"),
"{}",
failures(&app)
);
}
#[test]
fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions() {
for reverse in [false, true] {
let (mut p, oa, sa, _) = if reverse {
fixture(MessageKind::Asynchronous, MessageKind::Reply)
} else {
fixture(MessageKind::Reply, MessageKind::Asynchronous)
};
let (source, target, layer) = if reverse {
(sa, oa, "Oa")
} else {
(oa, sa, "Sa")
};
let mut calls = vec![];
for message in [oa, sa] {
let scenario = p.elements[&message].parent.unwrap();
let layer = p.elements[&message].layer();
let call = p
.add_element(Kind::Occurrence, layer, "Correlated call", Some(scenario))
.unwrap();
*p.elements.get_mut(&call).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: None,
sender: None,
receiver: None,
message_kind: MessageKind::Call,
reply_to: None,
values: vec![],
},
precondition: String::new(),
postcondition: String::new(),
};
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.insert(0, call);
if message == source {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { reply_to, .. },
..
}) = p.elements.get_mut(&message).unwrap().shared_mut()
else {
panic!()
};
*reply_to = Some(call);
}
calls.push(call);
}
let target_call = if reverse { calls[0] } else { calls[1] };
let mut app = App::with_project(p);
let before = app.document.snapshot();
start(&mut app, source);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
send(&mut app, Event::Variant(path(layer), json!("Reply")));
decide(
&mut app,
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo),
SemanticDecision::TakeSource,
);
assert!(!state(&app).can_apply());
assert!(
failures(&app).contains("reference counterparts"),
"{}",
failures(&app)
);
send(
&mut app,
Event::ReferenceOa(Choice {
id: calls[0],
label: "OA call".into(),
}),
);
send(
&mut app,
Event::ReferenceSa(Choice {
id: calls[1],
label: "SA call".into(),
}),
);
send(&mut app, Event::AddReference);
assert!(state(&app).can_apply(), "{}", failures(&app));
assert_eq!(
acknowledged(&app),
BTreeSet::from([
message_kind::selector(),
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo)
])
);
let final_value =
serde_json::to_value(&state(&app).proposed.as_ref().unwrap().elements[&target])
.unwrap();
assert_eq!(
final_value.pointer(&format!(
"/body/{layer}/Shared/Occurrence/body/Exchange/reply_to"
)),
Some(&json!(target_call))
);
assert_eq!(app.document.bundle(), &before.bundle);
apply(&mut app);
assert_eq!(
message_kind::value(app.project(), target),
Some(&MessageKind::Reply)
);
for call in calls {
assert_eq!(
app.project().elements[&call],
before.decoded.project.elements[&call]
);
}
}
}
#[test]
fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() {
for reverse in [false, true] {
for kind in [
MessageKind::Asynchronous,
MessageKind::Call,
MessageKind::Reply,
MessageKind::Create,
MessageKind::Delete,
timer("after 2 s"),
] {
let (p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
let (source, target, layer) = if reverse {
(sa, oa, "Oa")
} else {
(oa, sa, "Sa")
};
let mut app = App::with_project(p);
start(&mut app, source);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Variant(path(layer), serde_json::to_value(&kind).unwrap()),
);
assert!(state(&app).can_apply(), "{kind:?}: {}", failures(&app));
assert_eq!(
message_kind::value(state(&app).proposed.as_ref().unwrap(), target),
Some(&kind)
);
assert_eq!(
acknowledged(&app).contains(&message_kind::timer_selector()),
matches!(kind, MessageKind::Timer { .. })
);
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
}
}
let (mut p, oa, _, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
let mut review = State::new(&p, oa).unwrap();
let Some(SharedBody::Occurrence { body, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut()
else {
panic!()
};
*body = OccurrenceBody::Action {
action: None,
lifeline: None,
};
review.refresh(&p);
assert!(
!review.editable(&p, &message_kind::selector()),
"An externally changed outer occurrence requires its own constructor review"
);
review.update(
&p,
Event::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)),
);
assert!(review.decisions.is_empty());
assert!(!review.can_apply());
}
#[test]
fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision() {
let (mut p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
let sender = p
.add_element(Kind::Function, Layer::Sa, "Sender", None)
.unwrap();
let receiver = p
.add_element(Kind::Function, Layer::Sa, "Receiver", None)
.unwrap();
let exchange = p.relate(
RelationKind::FunctionalExchange,
"Message exchange",
sender,
receiver,
);
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: selected, ..
},
..
}) = p.elements.get_mut(&sa).unwrap().shared_mut()
else {
panic!()
};
*selected = Some(exchange);
let mut app = App::with_project(p);
let before = app.document.snapshot();
start(&mut app, oa);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})),
);
assert!(!state(&app).can_apply());
assert!(
failures(&app).contains("timer_contract"),
"{}",
failures(&app)
);
assert_eq!(state(&app).decisions.len(), 1);
assert_eq!(app.document.bundle(), &before.bundle);
decide(
&mut app,
FacetSelector::Occurrence(OccurrenceFacet::Exchange),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Shape(
"/body/Sa/Shared/Occurrence/body/Exchange/exchange".into(),
Value::Null,
),
);
assert!(state(&app).can_apply(), "{}", failures(&app));
assert_eq!(
acknowledged(&app),
BTreeSet::from([
message_kind::selector(),
message_kind::timer_selector(),
FacetSelector::Occurrence(OccurrenceFacet::Exchange)
])
);
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
apply(&mut app);
assert_eq!(
app.project().relations[&exchange],
before.decoded.project.relations[&exchange]
);
}
#[test]
fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement() {
let (p, oa, sa, _) = fixture(timer("source timer"), timer("target timer"));
let mut app = App::with_project(p);
start(&mut app, oa);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::TakeSource,
);
decide(
&mut app,
FacetSelector::Header(HeaderFacet::Name),
SemanticDecision::Candidate,
);
send(&mut app, Event::Input("/name".into(), "Final name".into()));
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
assert_eq!(
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
Some(&timer("target timer"))
);
assert!(!acknowledged(&app).contains(&message_kind::timer_selector()));
let expression = format!("{}/Timer/expression", path("Sa"));
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Input(expression.clone(), "authored group timer".into()),
);
assert!(
!state(&app).can_apply(),
"An unchanged initial kind cannot silently authorize a timer edit"
);
decide(
&mut app,
message_kind::timer_selector(),
SemanticDecision::Candidate,
);
send(
&mut app,
Event::Input(expression.clone(), "different scalar timer".into()),
);
assert!(!state(&app).can_apply());
assert!(
state(&app)
.error
.as_ref()
.unwrap()
.contains("different timer values")
);
send(
&mut app,
Event::Input(expression, "authored group timer".into()),
);
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
assert!(acknowledged(&app).contains(&message_kind::timer_selector()));
assert_eq!(
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
Some(&timer("authored group timer"))
);
}
#[test]
fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer() {
for source_failure in [false, true] {
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("target"));
let mut app = App::with_project(p);
if source_failure {
let bundle = app.document.bundle().clone();
let mut limits = source_project::ProjectLimits::default();
limits.model.max_source_bytes =
bundle.files.values().map(String::len).sum::<usize>() + 10;
app.document = Document::from_bundle(bundle, limits).unwrap();
}
start(&mut app, oa);
decide(
&mut app,
message_kind::selector(),
SemanticDecision::Candidate,
);
let expression = format!("{}/Timer/expression", path("Sa"));
let raw = "retain exact timer β\n".repeat(40);
send(&mut app, Event::Input(expression.clone(), raw.clone()));
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
send(
&mut app,
Event::Input("/name".into(), "Unselected protected scope".into()),
);
assert!(!state(&app).can_apply());
assert!(
state(&app)
.error
.as_ref()
.unwrap()
.contains("selected review")
);
send(&mut app, Event::Preview);
assert!(state(&app).can_apply());
if !source_failure {
app.document
.execute(Command::Rename(oa, "External source edit".into()))
.unwrap();
}
let before = app.document.snapshot();
let revision = app.document.revision();
apply(&mut app);
assert!(state(&app).error.is_some());
assert_eq!(state(&app).decisions.len(), 1);
assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression].raw,
raw
);
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.document.revision(), revision);
}
}
@@ -2,13 +2,13 @@ use super::*;
use crate::{app::App, document::Document, workflow::Workflow}; use crate::{app::App, document::Document, workflow::Workflow};
use serde_json::json; use serde_json::json;
fn send(app: &mut App, event: Event) { pub(super) fn send(app: &mut App, event: Event) {
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event))); let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event)));
} }
fn apply(app: &mut App) { pub(super) fn apply(app: &mut App) {
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply)); let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
} }
fn state(app: &App) -> &State { pub(super) fn state(app: &App) -> &State {
let Some(Workflow::Semantic(state)) = &app.workflow else { let Some(Workflow::Semantic(state)) = &app.workflow else {
panic!("Semantic workflow missing: {}", app.status) panic!("Semantic workflow missing: {}", app.status)
}; };
@@ -35,7 +35,7 @@ fn fixture() -> (Project, Id, Id, Id) {
let trace = add_trace(&mut p, oa, sa, "Range rationale"); let trace = add_trace(&mut p, oa, sa, "Range rationale");
(p, oa, sa, trace) (p, oa, sa, trace)
} }
fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id { pub(super) fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
let id = Id::new_v4(); let id = Id::new_v4();
p.traces.insert( p.traces.insert(
id, id,
@@ -52,7 +52,7 @@ fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
); );
id id
} }
fn start(app: &mut App, source: Id) { pub(super) fn start(app: &mut App, source: Id) {
let _ = app.update(Message::StartSemanticReview(source)); let _ = app.update(Message::StartSemanticReview(source));
} }
fn facet(value: DataFacet) -> FacetSelector { fn facet(value: DataFacet) -> FacetSelector {
@@ -262,7 +262,7 @@ fn independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity
assert!(second_title.contains(&b.to_string())); assert!(second_title.contains(&b.to_string()));
assert_ne!(super::view::title(&p, &annotation(vec![])), first_title); assert_ne!(super::view::title(&p, &annotation(vec![])), first_title);
} }
fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) { pub(super) fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) {
send(app, Event::Select(selected.clone())); send(app, Event::Select(selected.clone()));
send(app, Event::Decide(selected, Some(decision))); send(app, Event::Decide(selected, Some(decision)));
} }
@@ -386,7 +386,7 @@ fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() {
.error .error
.as_ref() .as_ref()
.unwrap() .unwrap()
.contains("selected scalar") .contains("selected review")
); );
assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.project(), &before.decoded.project);
send(&mut app, Event::Preview); send(&mut app, Event::Preview);
+81 -11
View File
@@ -266,7 +266,7 @@ impl State {
.iter() .iter()
.filter(|f| f.drift == Drift::Unreviewed) .filter(|f| f.drift == Drift::Unreviewed)
.count(); .count();
content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Constructors and structured collections require further review.", self.decisions.len())).size(10).color(style::MUTED)); content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Other constructor groups and structured collections require further review.", self.decisions.len())).size(10).color(style::MUTED));
if let Some(trace) = trace { if let Some(trace) = trace {
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED)); content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
} }
@@ -364,7 +364,32 @@ impl State {
.and_then(|t| t.baseline.as_ref()) .and_then(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref()) .and_then(|b| b.semantic.as_ref())
.and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector)); .and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector));
let is_message_kind = message_kind::handles(&fact.selector);
let timer_previous = trace
.and_then(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref())
.and_then(|b| {
b.facets
.iter()
.find(|f| f.selector() == message_kind::timer_selector())
});
let timer_current = self
.facts
.iter()
.find(|f| f.selector == message_kind::timer_selector());
let side = |layer, label: &str| { let side = |layer, label: &str| {
let mut before = observed(project, previous, layer);
let mut current = observed(project, Some(&fact.current), layer);
if is_message_kind {
before.push_str(&format!(
"\nTimer expression: {}",
observed(project, timer_previous, layer)
));
current.push_str(&format!(
"\nTimer expression: {}",
observed(project, timer_current.map(|f| &f.current), layer)
));
}
column![ column![
text(format!( text(format!(
"{label}{}", "{label}{}",
@@ -377,9 +402,9 @@ impl State {
.size(12) .size(12)
.color(style::TEAL), .color(style::TEAL),
text("Previously reviewed").size(10).color(style::MUTED), text("Previously reviewed").size(10).color(style::MUTED),
text(observed(project, previous, layer)).size(12), text(before).size(12),
text("Current").size(10).color(style::MUTED), text("Current").size(10).color(style::MUTED),
text(observed(project, Some(&fact.current), layer)).size(12), text(current).size(12),
] ]
.spacing(5) .spacing(5)
.width(Fill) .width(Fill)
@@ -401,11 +426,14 @@ impl State {
.color(style::TEAL), .color(style::TEAL),
) )
.push( .push(
text(observed( text(if is_message_kind {
proposed, self.target(project)
Some(&after), .and_then(|id| message_kind::source_value(proposed, id).ok())
self.direction.target_layer(), .map(|value| formatted(proposed, &value, &BTreeSet::new()))
)) .unwrap_or_else(|| "Message kind is not applicable.".into())
} else {
observed(proposed, Some(&after), self.direction.target_layer())
})
.size(12), .size(12),
); );
} else { } else {
@@ -416,6 +444,12 @@ impl State {
); );
} }
if self.editable(project, &fact.selector) { if self.editable(project, &fact.selector) {
if is_message_kind {
detail = detail.push(text("A replacing message-kind decision reviews its Timer Expression as a group. The exact acknowledgment and retirement appear below.").size(12));
if self.can_acknowledge_constructor(project, &fact.selector) {
detail = detail.push(text("These initial kinds are equal. Keep or Take acknowledges only the kind; an unchanged final kind also needs a separate Timer Expression decision for expression edits.").size(11).color(style::MUTED));
}
}
let destination = if self.direction.target_layer() == Layer::Sa { let destination = if self.direction.target_layer() == Layer::Sa {
"System" "System"
} else { } else {
@@ -438,15 +472,19 @@ impl State {
] ]
.spacing(5); .spacing(5);
buttons = buttons.push(action( buttons = buttons.push(action(
"Set final value…", if is_message_kind {
"Set final message kind…"
} else {
"Set final value…"
},
Event::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)), Event::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)),
)); ));
detail = detail.push(buttons).push(action( detail = detail.push(buttons.wrap()).push(action(
"Leave unselected", "Leave unselected",
Event::Decide(fact.selector.clone(), None), Event::Decide(fact.selector.clone(), None),
)); ));
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = &self.draft && let Some(draft) = self.candidate_draft(&fact.selector)
&& let Some(path) = self && let Some(path) = self
.target(project) .target(project)
.and_then(|id| scalar::binding(project, id, &fact.selector)) .and_then(|id| scalar::binding(project, id, &fact.selector))
@@ -463,6 +501,38 @@ impl State {
} else { } else {
detail = detail.push(text(if scalar::grouped(&fact.selector) { "Constructor groups are shown for comparison. Group replacement and its complete descendant review are a later milestone." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED)); detail = detail.push(text(if scalar::grouped(&fact.selector) { "Constructor groups are shown for comparison. Group replacement and its complete descendant review are a later milestone." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED));
} }
if is_message_kind || fact.selector == message_kind::timer_selector() {
if self.decisions.contains_key(&message_kind::selector())
&& self.decisions.contains_key(&message_kind::timer_selector())
{
detail = detail.push(text("Both kind and timer decisions are selected. An initial equal-kind acknowledgment can coexist with a timer decision; a replacing group requires one combined decision. Resolve any overlap explicitly; both drafts are retained.").size(11))
.push(action("Replace timer decision with message-kind group", Event::Decide(message_kind::timer_selector(), None)))
.push(action("Leave message kind unselected; use timer decision", Event::Decide(message_kind::selector(), None)));
}
if let Some(review) = self.plan.as_ref().and_then(|p| p.reviews.first()) {
detail = detail.push(text("Exact review scope from preview").size(13));
for (label, selectors) in [
("Acknowledge", &review.acknowledged),
("Retire previous observation", &review.retired),
] {
detail = detail.push(
text(format!(
"{label}: {}",
if selectors.is_empty() {
"None".into()
} else {
selectors
.iter()
.map(|s| title(project, s))
.collect::<Vec<_>>()
.join(", ")
}
))
.size(11),
);
}
}
}
if matches!( if matches!(
fact.selector, fact.selector,
FacetSelector::Child(_) FacetSelector::Child(_)
+1
View File
@@ -744,6 +744,7 @@ fn traces(app: &App) -> View<'_, Message> {
action("+ Artifact", Message::AddArtifact) action("+ Artifact", Message::AddArtifact)
] ]
.spacing(8) .spacing(8)
.wrap()
] ]
.spacing(16); .spacing(16);
for t in p.traces.values() { for t in p.traces.values() {
+56 -15
View File
@@ -1,7 +1,8 @@
# Semantic desktop review — milestones 1 and 2 # Semantic desktop review — scalars and nested message kind
Status: milestones 1–2 implemented and desktop tests/build verified; not full GUI Status: milestones 1–2 implemented; the bounded MessageKind/TimerExpression
parity, native acceptance or release acceptance. constructor slice is implemented. Full milestone 3, GUI parity, native acceptance
and release acceptance remain outstanding.
Base: `d130013`. Dedicated build target: Base: `d130013`. Dedicated build target:
`D:/CodexBuilds/Capella2-semantic-desktop-target`. Native verification is root-owned. `D:/CodexBuilds/Capella2-semantic-desktop-target`. Native verification is root-owned.
@@ -16,7 +17,8 @@ Base: `d130013`. Dedicated build target:
- Support every applicable scalar leaf through Keep, Take and typed final-value - Support every applicable scalar leaf through Keep, Take and typed final-value
forms. Candidate records contain the complete combined selected result. Identity, forms. Candidate records contain the complete combined selected result. Identity,
alias and layer are protected. The closed core remains the validation boundary. alias and layer are protected. The closed core remains the validation boundary.
- State.Kind is a single structural scalar. Other constructor switches, keyed - State.Kind is a single structural scalar. Occurrence.MessageKind now supports
its complete nested group, including TimerExpression. Other constructor switches, keyed
membership/order/annotation/binding/attachment decisions and pairing resets belong membership/order/annotation/binding/attachment decisions and pairing resets belong
to milestone 3; show them read-only with an honest explanation. to milestone 3; show them read-only with an honest explanation.
- Core `6bb2583` permits first equal constructor acknowledgment without descendant - Core `6bb2583` permits first equal constructor acknowledgment without descendant
@@ -36,14 +38,16 @@ Base: `d130013`. Dedicated build target:
2. Scalar decisions and typed candidates; immutable initial observations; combined 2. Scalar decisions and typed candidates; immutable initial observations; combined
final records; exact impact acceptance; atomic document/source commit. Focused final records; exact impact acceptance; atomic document/source commit. Focused
App event/source/history tests, strict Clippy and a dedicated desktop build. App event/source/history tests, strict Clippy and a dedicated desktop build.
3. Constructor closure, keyed and ordered facts, reset, complete artifact lifecycle, 3. Nested MessageKind/TimerExpression closure is implemented. Other constructor
multi-source review and remaining source/REST parity cases. Still outstanding. groups, keyed and ordered facts, reset, complete artifact lifecycle, multi-source
review and remaining source/REST parity cases are outstanding.
4. Root-owned native review, cross-surface fixtures, package/release acceptance. 4. Root-owned native review, cross-surface fixtures, package/release acceptance.
## Verification ## Verification
Verified after the independent scalar review fixes on SEM `6ad3cb`, using only the Scalar fixes were verified on SEM `6ad3cb`; the nested message-kind slice builds
dedicated desktop target (core behavior remains based on `6bb2583`): on scalar commit `5a51a75`. Verification uses only the dedicated desktop target
(core behavior remains based on `6bb2583`):
```powershell ```powershell
$env:CARGO_TARGET_DIR='D:/CodexBuilds/Capella2-semantic-desktop-target' $env:CARGO_TARGET_DIR='D:/CodexBuilds/Capella2-semantic-desktop-target'
@@ -52,8 +56,8 @@ cargo clippy --locked -p oa-desktop --all-targets -- -D warnings
cargo build --locked -p oa-desktop cargo build --locked -p oa-desktop
``` ```
All **277 desktop tests passed**, including **11 original semantic review tests** All **285 desktop tests passed**, including **11 original semantic review tests**,
and **5 scalar review regression tests**. **5 scalar review regression tests**, and **8 nested message-kind tests**.
Strict Clippy and the desktop build passed. The existing migration test's header Strict Clippy and the desktop build passed. The existing migration test's header
assertion now follows `SCHEMA_VERSION` instead of expecting schema 4. assertion now follows `SCHEMA_VERSION` instead of expecting schema 4.
@@ -85,19 +89,56 @@ The independent scalar review found three gaps, now corrected:
The regression tests exercise App events, the actual default-form helpers and The regression tests exercise App events, the actual default-form helpers and
button availability, exact source retention before Apply, and one-step undo. button availability, exact source retention before Apply, and one-step undo.
## Bounded constructor slice
Occurrence.MessageKind is a complete closed nested family: Asynchronous, Call,
Reply, Create, Delete and Timer. It is available only when both traced counterparts
are Exchange occurrences. The only constructor-specific payload is the timer
expression. Outer occurrence replacement remains read-only.
- Keep/Take/Candidate use the core's switch and first-equal acknowledgment policy.
Taking a replacing group copies the typed MessageKind, including its expression.
Taking an initial equal discriminator does not copy an unselected timer value.
- Candidate MessageKind has its own scoped draft. The independent TimerExpression
scalar draft survives group decisions; the group timer's exact raw text, editor
cursor and selection survive switching away from Timer and back.
- An overlapping kind/timer draft retains both decisions and blocks Apply until
explicitly resolved. The dialog offers **Replace timer decision with message-kind
group** and **Leave message kind unselected; use timer decision**. First equal-kind
acknowledgment can coexist with an independent timer decision. Divergent authored
candidate values reject; neither candidate silently overwrites the other.
- The kind detail includes baseline/current timer observations and the complete
proposed kind. It displays the exact plan-provided acknowledged/retired lists.
Exchange, reply-to, lifelines, annotations and pre/postconditions stay separately
scoped. Incompatible Timer contracts remain core errors; no dependency or payload
is silently removed. Existing additional-impact approval remains in force.
- Group changes compose with scalar choices and execute the exact prepared Document
batch. Tests cover both directions, all six kinds, mapped reply correlation,
historical retirement, repeated no-switch rejection, invalid combinations,
protected scopes, stale preview, source failure, comments and one-step undo/redo.
- The Traceability contextual toolbar now wraps at the available width so +Artifact
remains reachable in the compact layout. This is a single `row.wrap()` change in
`views.rs`; other toolbars are outside this slice.
Root reported native scalar coverage on frozen integrated `e7fa061`: mixed numeric
Keep/Take/Candidate, invalid default retention/repair, zero additional changes,
atomic Apply, comment/value inspection, Undo/Redo and Save All at 1440×900 and
1100×700. Reverse/type/reference/history/reopen native checks were still continuing
when this slice was implemented. This task launched no native application.
Built executable: Built executable:
`D:/CodexBuilds/Capella2-semantic-desktop-target/debug/arcadia-studio.exe`. `D:/CodexBuilds/Capella2-semantic-desktop-target/debug/arcadia-studio.exe`.
The build target is idle. No native application was launched by this task. The build target is idle. No native application was launched by this task.
## Remaining work ## Remaining work
Milestone 3 must add explicit constructor closure/replacement, keyed and ordered Milestone 3 must add the remaining constructor closure/replacement, keyed and ordered
decisions, historical pairing reset, complete artifact lifecycle and multi-source decisions, historical pairing reset, complete artifact lifecycle and multi-source
review. These facts are currently inspectable, with unsupported writes visibly review. These facts are currently inspectable, with unsupported writes visibly
identified. The reference table supplies explicit mappings for scalar reference identified. The reference table supplies explicit mappings for scalar reference
Take; it does not claim to complete a reset or establish keyed acknowledgments. Take; it does not claim to complete a reset or establish keyed acknowledgments.
Fresh independent scalar GUI review and root-owned native verification are still Fresh independent requirements/correctness review and root-owned native checks of
required before merging this milestone into release acceptance. In particular, the message-kind slice are required before release acceptance. Verify the compact
validate the 1100×700 layout, keyboard/focus continuity, long historical values, kind picker, timer text/focus retention, overlap choices, exact closure/retirement,
reference selection, source reopening and prepared-impact inspection in the app. mapped Reply, blocked Timer contract, source reopening and toolbar wrapping.