Verify qualified scenario values through HTTP and record R6 evidence

This commit is contained in:
2026-09-06 04:07:49 +03:00
parent b403039d00
commit 15ed1ff3af
7 changed files with 482 additions and 13 deletions
@@ -315,3 +315,420 @@ async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_sav
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
server.stop().await;
}
#[tokio::test]
async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_source_and_restart()
{
use oa_core::{
Element, FieldStorage, MessageKind, MessageValue, OccurrenceBody, ParameterDirection,
PortContract, RelationBody, ScenarioViewpoint, TypeRef,
};
fn values(model: &Project, occurrence: Id) -> &[MessageValue] {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = model.elements[&occurrence].shared()
else {
panic!("Expected message");
};
values
}
fn edited_values(model: &Project, occurrence: Id, next: Vec<MessageValue>) -> Element {
let mut element = model.elements[&occurrence].clone();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = element.shared_mut()
else {
panic!("Expected message");
};
*values = next;
element
}
let mut model = documented_data();
let operation = id(&model, "navigate");
let coordinate = id(&model, "coordinate");
let located = id(&model, "located");
let latitude = id(&model, "latitude");
let origin = id(&model, "origin");
let destination = id(&model, "destination");
let accepted = id(&model, "accepted");
// The second parameter inherits the very same latitude identity.
let Some(SharedBody::DataField { type_ref, .. }) =
model.elements.get_mut(&destination).unwrap().shared_mut()
else {
panic!();
};
*type_ref = TypeRef::Named(located);
let next = model
.add_element(
Kind::DataField,
Layer::Sa,
"Next coordinate",
Some(coordinate),
)
.unwrap();
*model.elements.get_mut(&next).unwrap().shared_mut().unwrap() = SharedBody::DataField {
type_ref: TypeRef::Named(coordinate),
minimum: 0,
maximum: Some(1),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
unit: String::new(),
default: None,
};
let requester = model
.add_element(Kind::Function, Layer::Sa, "Request navigation", None)
.unwrap();
let provider = model
.add_element(Kind::Function, Layer::Sa, "Provide navigation", None)
.unwrap();
let output = Id::new_v4();
let input = Id::new_v4();
for (port, contract) in [
(
output,
PortContract::FunctionOutput {
outgoing_items: vec![operation],
},
),
(
input,
PortContract::FunctionInput {
incoming_items: vec![operation],
},
),
] {
let (owner, name) = if port == output {
(requester, "Navigation output")
} else {
(provider, "Navigation input")
};
let element = oa_core::ports::make_port(&model, port, owner, name, contract).unwrap();
model.elements.insert(port, element);
}
let exchange = model.relate(
RelationKind::FunctionalExchange,
"Navigation request",
requester,
provider,
);
model.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange {
source: requester,
target: provider,
source_port: output,
target_port: input,
items: vec![operation],
};
let scenario = model
.add_element(Kind::Scenario, Layer::Sa, "Navigation conversation", None)
.unwrap();
let Some(SharedBody::Scenario { viewpoint, .. }) =
model.elements.get_mut(&scenario).unwrap().shared_mut()
else {
panic!();
};
*viewpoint = ScenarioViewpoint::Functional;
let sender = model
.add_element(Kind::Lifeline, Layer::Sa, "Requester line", Some(scenario))
.unwrap();
let receiver = model
.add_element(Kind::Lifeline, Layer::Sa, "Provider line", Some(scenario))
.unwrap();
for (line, represented) in [(sender, requester), (receiver, provider)] {
*model.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(represented),
};
}
let call = model
.add_element(
Kind::Occurrence,
Layer::Sa,
"Navigation call",
Some(scenario),
)
.unwrap();
let reply = model
.add_element(
Kind::Occurrence,
Layer::Sa,
"Navigation reply",
Some(scenario),
)
.unwrap();
for (occurrence, message_kind, from, to, reply_to) in [
(call, MessageKind::Call, sender, receiver, None),
(reply, MessageKind::Reply, receiver, sender, Some(call)),
] {
*model
.elements
.get_mut(&occurrence)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: Some(from),
receiver: Some(to),
message_kind,
reply_to,
values: vec![],
},
precondition: String::new(),
postcondition: String::new(),
};
}
model
.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.extend([call, reply]);
oa_core::validate_candidate(&model).unwrap();
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let (resource, _) = server.import(&model).await;
let before = server.get(resource).await;
let call_values = vec![
MessageValue {
path: vec![origin],
field: latitude,
expression: " origin.latitude\n+ 0.25 ".into(),
},
MessageValue {
path: vec![destination],
field: latitude,
expression: "destination.latitude".into(),
},
MessageValue {
path: vec![origin, next],
field: latitude,
expression: "previous.latitude".into(),
},
];
let reply_values = vec![MessageValue {
path: vec![],
field: accepted,
expression: "true".into(),
}];
let response = commands(
&server,
&before,
vec![
Command::EditElement(call, edited_values(&model, call, call_values.clone())),
Command::EditElement(reply, edited_values(&model, reply, reply_values.clone())),
],
)
.await;
assert_eq!(
response.status(),
StatusCode::OK,
"{}",
response.text().await.unwrap()
);
let authored = server.get(resource).await;
assert_eq!(authored.revision, before.revision + 1);
assert_eq!(values(&authored.model, call), call_values);
assert_eq!(values(&authored.model, reply), reply_values);
assert_eq!(
authored.model.elements[&latitude],
model.elements[&latitude]
);
assert_pair(&authored);
let filename = directory.path().join(format!("{resource}.json"));
let bytes = std::fs::read(&filename).unwrap();
for (occurrence, value, code) in [
(
call,
MessageValue {
path: vec![],
field: accepted,
expression: "wrong call phase".into(),
},
"message_parameter_phase",
),
(
reply,
MessageValue {
path: vec![origin],
field: latitude,
expression: "wrong reply phase".into(),
},
"message_parameter_phase",
),
(
call,
MessageValue {
path: vec![origin, accepted],
field: latitude,
expression: "unrelated hop".into(),
},
"message_value_field",
),
] {
let invalid = edited_values(&authored.model, occurrence, vec![value]);
let mut invalid_model = authored.model.clone();
invalid_model.elements.insert(occurrence, invalid.clone());
let response = commands(
&server,
&authored,
vec![Command::EditElement(occurrence, invalid)],
)
.await;
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
let problem: Value = response.json().await.unwrap();
assert_eq!(problem["code"], "command_rejected", "{problem}");
assert!(
problem["detail"].as_str().unwrap().starts_with(code),
"{problem}"
);
// Transactions report the first rejection; validation supplies located issues.
let validation: Value = server
.client
.post(server.url(&format!("/projects/{resource}/validation")))
.json(&json!({"format":"json", "model":invalid_model}))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
assert_eq!(validation["valid"], false);
assert!(
validation["issues"]
.as_array()
.unwrap()
.iter()
.any(|issue| issue["code"] == code
&& issue["object_id"] == json!(occurrence)
&& issue["field"] == "values.path"),
"{problem}"
);
let unchanged = server.get(resource).await;
assert_eq!(unchanged.etag(), authored.etag());
assert_eq!(unchanged.model, authored.model);
assert_eq!(unchanged.source_project, authored.source_project);
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
}
// Source transport changes one argument without conflating the shared leaf.
let source = server
.client
.get(server.url(&format!("/projects/{resource}/source")))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.text()
.await
.unwrap();
let edited_source = source.replace(
"\"destination.latitude\"",
"\"destination.corrected_latitude\"",
);
assert_ne!(edited_source, source);
let response = server
.client
.put(server.url(&format!("/projects/{resource}/source")))
.header("if-match", authored.etag())
.header("content-type", "text/plain")
.body(edited_source)
.send()
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"{}",
response.text().await.unwrap()
);
let source_edited = server.get(resource).await;
assert_eq!(source_edited.revision, authored.revision + 1);
let mut expected_call = call_values.clone();
expected_call[1].expression = "destination.corrected_latitude".into();
assert_eq!(values(&source_edited.model, call), expected_call);
assert_eq!(values(&source_edited.model, reply), reply_values);
assert_pair(&source_edited);
// Reidentify every kind of enclosing hop, the shared terminal, and its base type.
let mapping = [origin, destination, next, latitude, coordinate]
.into_iter()
.map(|old| (old, Id::new_v4()))
.collect::<BTreeMap<_, _>>();
let response = commands(
&server,
&source_edited,
mapping
.iter()
.map(|(old, new)| Command::Reidentify(*old, *new))
.collect(),
)
.await;
assert_eq!(
response.status(),
StatusCode::OK,
"{}",
response.text().await.unwrap()
);
let changed = server.get(resource).await;
assert_eq!(changed.revision, source_edited.revision + 1);
for value in &mut expected_call {
value.path = value.path.iter().map(|hop| mapping[hop]).collect();
value.field = mapping[&value.field];
}
assert_eq!(values(&changed.model, call), expected_call);
assert_eq!(values(&changed.model, reply), reply_values);
assert_eq!(
changed.model.elements[&mapping[&latitude]].parent,
Some(mapping[&coordinate])
);
assert!(
matches!(changed.model.elements[&located].shared(), Some(SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: Some(base) }, ..
}) if *base == mapping[&coordinate])
);
assert!(
oa_core::data::effective_fields(&changed.model, located)
.iter()
.any(|field| field.id == mapping[&latitude])
);
for old in mapping.keys() {
assert!(!changed.model.contains(*old));
}
assert!(
matches!(changed.model.elements[&reply].shared(), Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { reply_to: Some(correlation), .. }, ..
}) if *correlation == call)
);
assert_pair(&changed);
let source = server
.client
.get(server.url(&format!("/projects/{resource}/source")))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.text()
.await
.unwrap();
assert!(source.starts_with("arcadia 4;"));
assert_eq!(oa_core::language::parse(&source).unwrap(), changed.model);
let saved = std::fs::read(&filename).unwrap();
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.etag(), changed.etag());
assert_eq!(reopened.model, changed.model);
assert_eq!(reopened.source_project, changed.source_project);
assert_eq!(values(&reopened.model, call), expected_call);
assert_eq!(values(&reopened.model, reply), reply_values);
assert_pair(&reopened);
assert_eq!(std::fs::read(&filename).unwrap(), saved);
server.stop().await;
}
+4 -3
View File
@@ -984,9 +984,10 @@ declares <span class="docutils literal">outgoing_items</span>; <span class="docu
The lists contain distinct same-layer ExchangeItem references. A functional
exchange MUST reference output/input ports owned by its respective functions,
and each carried item MUST fit both declarations. Empty declarations accept
no items. Item compatibility is identity equality; future specialization is
not inferred from names. Port declarations remain independent of incident
exchanges after creation.</p>
no items. A carried item satisfies an identical declaration or a declaration
of one of its explicit base items. A base item does not satisfy a more
specialized declaration; names imply no compatibility. Port declarations
remain independent of incident exchanges after creation.</p>
<p>A <span class="docutils literal">Component</span> contract belongs to an SA System or SystemActor and stores
<span class="docutils literal">kind</span> (Standard or Flow) and <span class="docutils literal">orientation</span> (Unset, In, Out, InOut).
Standard requires Unset. Flow Unset is an incomplete draft, not InOut. Typed
+4 -3
View File
@@ -311,9 +311,10 @@ declares ``outgoing_items``; ``FunctionInput`` declares ``incoming_items``.
The lists contain distinct same-layer ExchangeItem references. A functional
exchange MUST reference output/input ports owned by its respective functions,
and each carried item MUST fit both declarations. Empty declarations accept
no items. Item compatibility is identity equality; future specialization is
not inferred from names. Port declarations remain independent of incident
exchanges after creation.
no items. A carried item satisfies an identical declaration or a declaration
of one of its explicit base items. A base item does not satisfy a more
specialized declaration; names imply no compatibility. Port declarations
remain independent of incident exchanges after creation.
A ``Component`` contract belongs to an SA System or SystemActor and stores
``kind`` (Standard or Flow) and ``orientation`` (Unset, In, Out, InOut).
+1 -1
View File
@@ -4,7 +4,7 @@
Deliver the first release of Arcadia Studio for system engineers: complete Operational Analysis and System Analysis/System Architecture workflows, equivalent graphical and textual models, clear artifact traceability and a deployable/programmatically accessible Rust service. Logical/Physical Architecture and later Arcadia layers are future releases, as the new objective explicitly states.
The authoritative objective is attachment07aee762-7d27-4cbe-81c3-2e82c7170711/goal-objective.md. It retains first-release scope and source/include, drag-selection and direct behavior editing requirements, and strengthens domain coherence, alternative evaluation and target-user task evidence. TASK_LEDGER.md separates given/verified facts, assumptions and open candidates; STATE.md records progress. The accepted0.2.0 prototype is the baseline, not proof of release completeness. Its source is preserved at D:/CodexBuilds/Capella2-release1-01a06e2e/v2-source-baseline.zip and existing archives remain in dist until replacement passes acceptance.
The authoritative objective is attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md. It retains first-release scope and source/include, drag-selection and direct behavior editing requirements, and strengthens domain coherence, alternative evaluation and target-user task evidence. TASK_LEDGER.md separates given/verified facts, assumptions and open candidates; STATE.md records progress. The accepted0.2.0 prototype is the baseline, not proof of release completeness. Its source is preserved at D:/CodexBuilds/Capella2-release1-01a06e2e/v2-source-baseline.zip and existing archives remain in dist until replacement passes acceptance.
## Existing architecture and retained decisions
+5 -5
View File
@@ -7,7 +7,7 @@ Operational Analysis and System Analysis/System Architecture, equivalent graphic
textual and REST semantics, artifact traceability, directly editable behavior,
offline guidance, and Windows/Linux release evidence. Later Arcadia layers remain
future releases. The full authoritative objective was reread on 2026-09-06:
attachment07aee762-7d27-4cbe-81c3-2e82c7170711/goal-objective.md.
attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md.
PLAN.md retains the entire release scope; no subsystem acceptance completes it.
Direct state-machine editing is accepted through production316dc55. The main
@@ -82,9 +82,9 @@ Core15 R6 +28workflow tests passed again after final allocation-arrow suppressio
Linux0b58107 full core307 tests passed (one explicit probe ignored by default),
with strict all-target core Clippy. Earlier13ports/14scenarios and115 source/lib
tests also passed. Desktop final257 tests and strict all-target Clippy pass;
the optimized Windows desktop build is in progress for native acceptance.
optimized Windows desktop build b403039 passed. Its SHA256 is recorded in the R6 verification note; native acceptance remains pending.
Root41 server tests pass, including3 new actual HTTP workflows for the published
Root41 server tests passed; a fourth R6 HTTP workflow now also passes, alongside all4 data HTTP tests and strict server Clippy. The original3 actual HTTP workflows for the published
data example, protocol/artifact identity, atomic rejected defaults, query/export,
reidentify/delete/restart, source3 bundle migration reports and old coupled3
envelope upgrade without write/revision. Tests exposed dropped bundle reports and
@@ -107,8 +107,7 @@ and imported finite paths; same-phase styles/same-exchange reselection preserve
valid drafts. Read-only follow-up found no new path/style defect. Paging resets
only on actual layer/document changes; its regression failed before and passes now.
Root found another unresolved scale issue: scenario creation still enumerates every
carried field into both a dropdown and individual input rows. Native reproduction
and a bounded focused value editor remain required before R6 acceptance.
carried field into both a dropdown and individual input rows. A bounded focused value editor is now implemented and under automated/independent review; actual native operation remains required before R6 acceptance.
Current ownership: drawing_correctness owns desktop except root-owned guide.rs and
the dedicated Windows Cargo target. state_architecture_review independently reviews
@@ -131,6 +130,7 @@ Root owns API/OpenAPI/reST, docs, integration and native input. No concurrent wr
## Environment and native ownership
Native input is paused after physical Escape; no R6 native trial has run.
Root exclusively owns native input. Latest saved Windows trial is window1706782,
ui-state-navigation-windows,49objects/28links/1 expected missing-owner warning.
Linux navigation trial3148598 closed normally, exec46107 exit0; baseline Linux
+1 -1
View File
@@ -1,6 +1,6 @@
# Task Ledger — first release
Authoritative objective: attachment07aee762-7d27-4cbe-81c3-2e82c7170711/
Authoritative objective: attachment10134df9-bea2-4c02-a820-67b53d6b86f7/
goal-objective.md, read fully2026-09-06. STATE.md records execution; PLAN.md retains
scope; DECISIONS.md and linked contracts preserve rationale.
+50
View File
@@ -0,0 +1,50 @@
# R6 data contracts — verification in progress
R6 is not accepted. Main remains at state-editing acceptance a8f451e. The updated
objective10134df9-bea2-4c02-a820-67b53d6b86f7 was read fully on2026-09-06; product
scope is unchanged and reasoning effort must match the delegated task. The full
first release remains incomplete. Native input is paused after physical Escape.
## Current evidence
- Core0b58107: Linux307 tests pass, one explicitly ignored benchmark; strict
all-target core Clippy passes. Windows15 R6+28 workflows pass after final
allocation-edge filtering. Source migration/compatibility evidence is retained.
- Desktopb403039: Windows257 tests, strict all-target desktop Clippy and release
build pass. Binary D:/CodexBuilds/Capella2-data-target/release/arcadia-studio.exe,
SHA256 d4aa6ae7d36f429c0cb639cdf57e33cb8b99d158a9629c95a1510d24e406fab5.
The later bounded scenario editor is not covered by this binary.
- Optimized empty inheritance chains250/500/1000: Windows21.070/89.088/391.590ms;
Linux22.381/111.662/408.129ms. These are single-run validation measurements.
- Server41 prior tests passed. The new qualified Call/Reply workflow and all4 R6
HTTP tests now pass with strict all-target server Clippy. Actual HTTP proves
distinct reused/inherited/recursive field paths, preserved expression whitespace,
phase/path422 with unchanged ETag/model/source/disk, source edits, enclosing-hop
and terminal/base reidentification, correlation and exact restart persistence.
Initial new-test failures were fixture misuse of untyped Port construction and
incorrect assumption that transaction errors carry structured issues. Tests now
use make_port and the documented command_rejected/detail plus validation endpoint.
- Published data example was imported into actual frozen server32033FFE. Complete
Project/StoredProject responses passed served/local OpenAPI validation; obsolete
shapes rejected. reST strict rendering and81 OpenAPI schemas passed.
- Independent spec review found one stale identity-only port-compatibility paragraph;
fixed to directional explicit ancestry and strict HTML rendering passed. No other
material R6 spec/API contradiction found in that review.
Logs are under D:/CodexBuilds/Capella2-release1-01a06e2e:
r6-http-paths-final.log, r6-http-all-data.log, r6-http-paths-clippy.log,
r6-server-tests-after.log and api-r6-contract. Linux core evidence is under
D:/CodexBuilds/Capella2-data-linux-review/target/linux-verification/report.md.
## Active checks and remaining gates
The isolated Linux checkout was fast-forwarded to b403039; full workspace tests are
running in exec98587 with its dedicated data-linux-target. Initial filesystem wait
was verified as a live Cargo process in p9_client_rpc; it subsequently compiled.
Do not restart from an observation timeout. Bounded scenario editor production and
regressions are being tested by the desktop worker. Independent review found stale
values hiding an alternative reply call; its fix is included in that worker pass.
Before acceptance: integrated Windows/Linux checks, native data/association/scenario
value workflows at normal/minimum sizes, source3 migration diagnostics, Save/reopen,
and closure of material review findings. No R6 visual or real-app acceptance claim.