From 15ed1ff3afecbe4d511ce78a9c954b40d4f777f0 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sun, 6 Sep 2026 04:07:49 +0300 Subject: [PATCH] Verify qualified scenario values through HTTP and record R6 evidence --- crates/oa-server/tests/http/data_contracts.rs | 417 ++++++++++++++++++ docs/language.html | 7 +- docs/language.rst | 7 +- harness/PLAN.md | 2 +- harness/STATE.md | 10 +- harness/TASK_LEDGER.md | 2 +- harness/release-data-verification.md | 50 +++ 7 files changed, 482 insertions(+), 13 deletions(-) create mode 100644 harness/release-data-verification.md diff --git a/crates/oa-server/tests/http/data_contracts.rs b/crates/oa-server/tests/http/data_contracts.rs index a56ec8ec..7b59d154 100644 --- a/crates/oa-server/tests/http/data_contracts.rs +++ b/crates/oa-server/tests/http/data_contracts.rs @@ -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) -> 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::>(); + 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; +} diff --git a/docs/language.html b/docs/language.html index 0ec2ddf1..ab46b44c 100644 --- a/docs/language.html +++ b/docs/language.html @@ -984,9 +984,10 @@ declares outgoing_items; Component contract belongs to an SA System or SystemActor and stores kind (Standard or Flow) and orientation (Unset, In, Out, InOut). Standard requires Unset. Flow Unset is an incomplete draft, not InOut. Typed diff --git a/docs/language.rst b/docs/language.rst index c87c8aa3..65ee36c1 100644 --- a/docs/language.rst +++ b/docs/language.rst @@ -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). diff --git a/harness/PLAN.md b/harness/PLAN.md index 4d95a328..30793f45 100644 --- a/harness/PLAN.md +++ b/harness/PLAN.md @@ -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 diff --git a/harness/STATE.md b/harness/STATE.md index 8f7641d8..8654f789 100644 --- a/harness/STATE.md +++ b/harness/STATE.md @@ -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 diff --git a/harness/TASK_LEDGER.md b/harness/TASK_LEDGER.md index 7fa57765..d7a6147e 100644 --- a/harness/TASK_LEDGER.md +++ b/harness/TASK_LEDGER.md @@ -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. diff --git a/harness/release-data-verification.md b/harness/release-data-verification.md new file mode 100644 index 00000000..beb28433 --- /dev/null +++ b/harness/release-data-verification.md @@ -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.