Specify R6 data contracts and verify coupled REST migration
This commit is contained in:
@@ -231,7 +231,7 @@ fn parse_source(body: &[u8]) -> ApiResult<oa_core::persistence::DecodeResult> {
|
||||
}
|
||||
|
||||
fn import_json(body: &[u8]) -> ApiResult<oa_core::persistence::DecodeResult> {
|
||||
// Format is chosen by HTTP. The shared decoder owns explicit schema-1 and schema-2 migration.
|
||||
// Format is chosen by HTTP. The shared decoder owns supported older-schema migration.
|
||||
let _: Value = json_body(body)?;
|
||||
oa_core::persistence::decode_with_report_limited(body, MODEL_LIMITS).map_err(
|
||||
|error| match error {
|
||||
@@ -525,7 +525,7 @@ async fn put_source_project(
|
||||
StatusCode::OK,
|
||||
json!({
|
||||
"revision": stored.revision, "changed": changed, "issues": diagnostics,
|
||||
"source_project": stored.source_project
|
||||
"source_project": stored.source_project, "migration": stored.migration
|
||||
}),
|
||||
))
|
||||
})
|
||||
@@ -947,14 +947,16 @@ async fn validate_candidate(
|
||||
// Advisory locations refer to submitted bytes, before Apply
|
||||
// would insert missing persistent identities.
|
||||
let diagnostics: Vec<_> = source_project::lint_bundle(&source_project, SOURCE_LIMITS).into_iter().map(source_issue).collect();
|
||||
if !diagnostics.iter().any(|issue| issue.severity == "error") {
|
||||
PreparedProject::from_bundle(source_project)?;
|
||||
}
|
||||
Ok(diagnostics)
|
||||
let migration = if !diagnostics.iter().any(|issue| issue.severity == "error") {
|
||||
PreparedProject::from_bundle(source_project)?.migration().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((diagnostics, migration))
|
||||
});
|
||||
let diagnostics = result.unwrap_or_else(candidate_issues);
|
||||
let (diagnostics, migration) = result.unwrap_or_else(|error| (candidate_issues(error), None));
|
||||
let valid = !diagnostics.iter().any(|issue| issue.severity == "error");
|
||||
return Ok(response(stored, StatusCode::OK, json!({"revision":stored.revision,"valid":valid,"issues":diagnostics,"migration":null})));
|
||||
return Ok(response(stored, StatusCode::OK, json!({"revision":stored.revision,"valid":valid,"issues":diagnostics,"migration":migration})));
|
||||
}
|
||||
_ => return Err(ApiError::bad_request("Validation requires one matching format/payload: json with model, arcadia with source, or source-project with source_project.")),
|
||||
};
|
||||
|
||||
@@ -128,9 +128,10 @@ impl PreparedProject {
|
||||
fn from_compiled(compiled: CompiledSourceProject) -> ApiResult<Self> {
|
||||
check_model_size(&compiled.decoded.project)?;
|
||||
check_source_size(&compiled.bundle)?;
|
||||
let migration = compiled.decoded.migration.clone();
|
||||
Ok(Self {
|
||||
compiled,
|
||||
migration: None,
|
||||
migration,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,6 +174,10 @@ impl PreparedProject {
|
||||
&self.compiled.bundle
|
||||
}
|
||||
|
||||
pub(crate) fn migration(&self) -> Option<&MigrationReport> {
|
||||
self.migration.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn source_issues(&self) -> Vec<crate::error::IssueDto> {
|
||||
self.compiled
|
||||
.diagnostics
|
||||
@@ -329,7 +334,12 @@ impl Store {
|
||||
let prepared = if let Some(bundle) = envelope.source_project {
|
||||
let prepared = PreparedProject::from_bundle(bundle.clone())
|
||||
.map_err(|error| corrupt(error.detail))?;
|
||||
if prepared.bundle() != &bundle || prepared.model() != &decoded.project {
|
||||
// Explicit source upgrades may change tokens and generate new
|
||||
// records. They must still agree with independently migrated
|
||||
// JSON. Current-format unstabilized/inconsistent pairs reject.
|
||||
if (prepared.bundle() != &bundle && prepared.migration.is_none())
|
||||
|| prepared.model() != &decoded.project
|
||||
{
|
||||
return Err(corrupt(
|
||||
"Stored source and model disagree, or source lacks stable identities."
|
||||
.into(),
|
||||
@@ -347,7 +357,7 @@ impl Store {
|
||||
revision: envelope.revision,
|
||||
model: decoded.project,
|
||||
source_project: prepared.compiled.bundle,
|
||||
migration: decoded.migration,
|
||||
migration: decoded.migration.or(prepared.migration),
|
||||
};
|
||||
if stored.resource_id != filename_id || stored.revision == 0 {
|
||||
return Err(corrupt(
|
||||
|
||||
@@ -8,6 +8,8 @@ use serde_json::{Value, json};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use tokio::{sync::oneshot, task::JoinHandle};
|
||||
|
||||
#[path = "http/data_contracts.rs"]
|
||||
mod data_contracts;
|
||||
#[path = "http/port_anchors.rs"]
|
||||
mod port_anchors;
|
||||
#[path = "http/source_projects.rs"]
|
||||
@@ -1929,7 +1931,7 @@ async fn legacy_json_import_and_model_reidentification_keep_the_service_address(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn schema3_migration_reports_and_invalid_typed_transaction_preserve_http_state() {
|
||||
async fn current_migration_reports_and_invalid_typed_transaction_preserve_http_state() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let old = v2_missing_ports();
|
||||
@@ -1945,7 +1947,7 @@ async fn schema3_migration_reports_and_invalid_typed_transaction_preserve_http_s
|
||||
assert_eq!(imported.status(), StatusCode::CREATED);
|
||||
let etag = imported.headers()["etag"].to_str().unwrap().to_owned();
|
||||
let summary: Value = imported.json().await.unwrap();
|
||||
assert_eq!(summary["schema_version"], 3);
|
||||
assert_eq!(summary["schema_version"], oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(
|
||||
summary["migration"],
|
||||
serde_json::to_value(&expected.migration).unwrap()
|
||||
@@ -1958,7 +1960,7 @@ async fn schema3_migration_reports_and_invalid_typed_transaction_preserve_http_s
|
||||
let before = std::fs::read(&filename).unwrap();
|
||||
let envelope: Value = serde_json::from_slice(&before).unwrap();
|
||||
assert!(envelope.get("migration").is_none());
|
||||
assert_eq!(envelope["model"]["schema_version"], 3);
|
||||
assert_eq!(envelope["model"]["schema_version"], oa_core::SCHEMA_VERSION);
|
||||
let validation = server
|
||||
.client
|
||||
.post(server.url(&format!("/projects/{id}/validation")))
|
||||
@@ -2000,7 +2002,7 @@ async fn schema3_migration_reports_and_invalid_typed_transaction_preserve_http_s
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(source.starts_with("arcadia 3;"));
|
||||
assert!(source.starts_with("arcadia 4;"));
|
||||
assert_eq!(oa_core::language::parse(&source).unwrap(), stored.model);
|
||||
let no_op = server
|
||||
.client
|
||||
@@ -2049,7 +2051,7 @@ async fn reopening_v2_envelope_reports_migration_without_saving_or_advancing_rev
|
||||
let migrated = server.get(id).await;
|
||||
assert_eq!(migrated.resource_id, id);
|
||||
assert_eq!(migrated.revision, 7);
|
||||
assert_eq!(migrated.model.schema_version, 3);
|
||||
assert_eq!(migrated.model.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(
|
||||
migrated
|
||||
.migration
|
||||
@@ -2074,7 +2076,7 @@ async fn reopening_v2_envelope_reports_migration_without_saving_or_advancing_rev
|
||||
assert_eq!(current.model.name, "Reviewed migrated model");
|
||||
let saved: Value = serde_json::from_slice(&std::fs::read(&filename).unwrap()).unwrap();
|
||||
assert_eq!(saved["revision"], 8);
|
||||
assert_eq!(saved["model"]["schema_version"], 3);
|
||||
assert_eq!(saved["model"]["schema_version"], oa_core::SCHEMA_VERSION);
|
||||
assert!(saved.get("migration").is_none());
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Transport evidence for the published R6 contract, not a second model fixture.
|
||||
use super::*;
|
||||
use oa_core::source_project::{ProjectLimits, SourceBundle, compile_bundle};
|
||||
use oa_core::{DataTypeDefinition, DataValue, SharedBody, TransmissionProtocol};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn documented_data() -> Project {
|
||||
let contract = include_str!("../../../../docs/language.rst");
|
||||
let example = contract
|
||||
.split("This complete data contract example is also exercised through the REST API:")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split(".. code-block:: text")
|
||||
.nth(1)
|
||||
.unwrap();
|
||||
let source = example
|
||||
.lines()
|
||||
.skip_while(|line| line.trim().is_empty())
|
||||
.take_while(|line| line.starts_with(" ") || line.is_empty())
|
||||
.map(|line| line.strip_prefix(" ").unwrap_or(line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let model = oa_core::language::parse(&source).unwrap();
|
||||
oa_core::validate_candidate(&model).unwrap();
|
||||
model
|
||||
}
|
||||
fn id(model: &Project, alias: &str) -> Id {
|
||||
model
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, a)| *a == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn assert_pair(stored: &StoredProject) {
|
||||
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, stored.model);
|
||||
assert_eq!(compiled.bundle, stored.source_project);
|
||||
}
|
||||
async fn commands(
|
||||
server: &TestServer,
|
||||
current: &StoredProject,
|
||||
commands: Vec<Command>,
|
||||
) -> reqwest::Response {
|
||||
server
|
||||
.client
|
||||
.post(server.url(&format!("/projects/{}/transactions", current.resource_id)))
|
||||
.header("if-match", current.etag())
|
||||
.json(&json!({"commands":commands}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn published_data_contract_queries_atomic_edits_identity_and_restart() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let model = documented_data();
|
||||
let (resource, _) = server.import(&model).await;
|
||||
let before = server.get(resource).await;
|
||||
assert_eq!(before.model, model);
|
||||
assert_pair(&before);
|
||||
for (kind, count) in [("InterfaceItemAllocation", 2), ("DataAssociation", 1)] {
|
||||
let result: Value = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{resource}/relations?kind={kind}")))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result["items"].as_array().unwrap().len(), count);
|
||||
}
|
||||
let voltage = id(&model, "voltage");
|
||||
let mut invalid = model.elements[&voltage].clone();
|
||||
let Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Real { maximum, .. },
|
||||
..
|
||||
}) = invalid.shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*maximum = Some(12.0); // Existing default24V is outside this bound.
|
||||
let filename = directory.path().join(format!("{resource}.json"));
|
||||
let disk_before = std::fs::read(&filename).unwrap();
|
||||
assert_eq!(
|
||||
commands(
|
||||
&server,
|
||||
&before,
|
||||
vec![Command::EditElement(voltage, invalid.clone())]
|
||||
)
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::UNPROCESSABLE_ENTITY
|
||||
);
|
||||
assert_eq!(server.get(resource).await.etag(), before.etag());
|
||||
assert_eq!(std::fs::read(&filename).unwrap(), disk_before);
|
||||
|
||||
let Some(SharedBody::DataType { default, .. }) = invalid.shared_mut() else {
|
||||
panic!()
|
||||
};
|
||||
*default = Some(DataValue::Real(12.0));
|
||||
let nav = id(&model, "nav_operation");
|
||||
let other = id(&model, "monitor_operation");
|
||||
let mut allocation = model.relations[&nav].clone();
|
||||
let oa_core::RelationBody::InterfaceItemAllocation { transmission, .. } = &mut allocation.body
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*transmission = TransmissionProtocol::Unicast;
|
||||
let artifact = oa_core::ArtifactLink {
|
||||
id: Id::new_v4(),
|
||||
alias: "navigation_evidence".into(),
|
||||
name: "Navigation CAD evidence".into(),
|
||||
description: "Independent allocation evidence".into(),
|
||||
owner: nav,
|
||||
relation: oa_core::ArtifactRelation::Verifies,
|
||||
uri: "cad://navigation/connector".into(),
|
||||
revision: Some("review-17".into()),
|
||||
fragment: Some("pin-3".into()),
|
||||
};
|
||||
let response = commands(
|
||||
&server,
|
||||
&before,
|
||||
vec![
|
||||
Command::EditElement(voltage, invalid),
|
||||
Command::EditRelation(nav, allocation.clone()),
|
||||
Command::CreateArtifact(artifact.clone()),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::OK,
|
||||
"{}",
|
||||
response.text().await.unwrap()
|
||||
);
|
||||
let changed = server.get(resource).await;
|
||||
assert_eq!(changed.revision, before.revision + 1);
|
||||
assert_eq!(changed.model.relations[&other], model.relations[&other]);
|
||||
assert_eq!(changed.model.relations[&nav], allocation);
|
||||
assert_eq!(changed.model.artifact_links[&artifact.id], artifact);
|
||||
assert_pair(&changed);
|
||||
assert_eq!(
|
||||
commands(&server, &before, vec![Command::Rename(nav, "Stale".into())])
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::PRECONDITION_FAILED
|
||||
);
|
||||
|
||||
let renamed = Id::new_v4();
|
||||
assert_eq!(
|
||||
commands(&server, &changed, vec![Command::Reidentify(nav, renamed)])
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::OK
|
||||
);
|
||||
let reidentified = server.get(resource).await;
|
||||
assert_eq!(
|
||||
reidentified.model.artifact_links[&artifact.id].owner,
|
||||
renamed
|
||||
);
|
||||
// This allocation is not enforcing a live boundary, so removal is valid.
|
||||
assert_eq!(
|
||||
commands(&server, &reidentified, vec![Command::Delete(renamed)])
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::OK
|
||||
);
|
||||
let removed = server.get(resource).await;
|
||||
assert!(removed.model.elements.contains_key(&id(&model, "navigate")));
|
||||
assert_eq!(removed.model.relations[&other], model.relations[&other]);
|
||||
assert!(!removed.model.artifact_links.contains_key(&artifact.id));
|
||||
assert_pair(&removed);
|
||||
let source = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{resource}/source")))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(source.starts_with("arcadia 4;"));
|
||||
assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model);
|
||||
server.stop().await;
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let reopened = server.get(resource).await;
|
||||
assert_eq!(reopened.model, removed.model);
|
||||
assert_eq!(reopened.etag(), removed.etag());
|
||||
assert_pair(&reopened);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_files() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let old = SourceBundle {
|
||||
entry: "legacy.arcadia".into(), files: BTreeMap::from([
|
||||
("legacy.arcadia".into(), "// Entry rationale\narcadia 3; project old_data \"Old data\" { include \"dictionary.arcadia\"; }\n".into()),
|
||||
("dictionary.arcadia".into(), "// Event rationale\nsa_exchange_item notification \"Notification\" { kind Event; }\n// Interface rationale\nsa_interface contract \"Contract\" { items [@notification]; }\n".into())
|
||||
])
|
||||
};
|
||||
let response = server
|
||||
.client
|
||||
.post(server.url("/projects/import-source-project"))
|
||||
.json(&old)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
let summary: Value = response.json().await.unwrap();
|
||||
let resource = summary["resource_id"].as_str().unwrap().parse().unwrap();
|
||||
let stored = server.get(resource).await;
|
||||
let report = stored.migration.as_ref().unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (3, 4));
|
||||
assert_eq!(report.generated_interface_allocations.len(), 1);
|
||||
assert!(!report.semantic_defaults.is_empty());
|
||||
let validated: Value = server
|
||||
.client
|
||||
.post(server.url(&format!("/projects/{resource}/validation")))
|
||||
.json(&json!({"format":"source-project","source_project":old}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(validated["valid"], true);
|
||||
assert_eq!(validated["migration"]["from_schema"], 3);
|
||||
assert_eq!(
|
||||
validated["migration"]["generated_interface_allocations"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(stored.source_project.files.len(), 2);
|
||||
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 4;"));
|
||||
for text in [
|
||||
"// Event rationale",
|
||||
"// Interface rationale",
|
||||
"interface_item_allocation",
|
||||
] {
|
||||
assert!(stored.source_project.files["dictionary.arcadia"].contains(text));
|
||||
}
|
||||
assert_pair(&stored);
|
||||
let filename = directory.path().join(format!("{resource}.json"));
|
||||
let bytes = 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.model, stored.model);
|
||||
assert_eq!(reopened.source_project, stored.source_project);
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(std::fs::read(filename).unwrap(), bytes);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_save() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let resource = Id::new_v4();
|
||||
let filename = directory.path().join(format!("{resource}.json"));
|
||||
let old_model: Value = serde_json::from_str(include_str!(
|
||||
"../../../oa-core/tests/fixtures/v3-data-contract.json"
|
||||
))
|
||||
.unwrap();
|
||||
let old_bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"project.arcadia".into(),
|
||||
include_str!("../../../oa-core/tests/fixtures/v3-data-contract.arcadia").into(),
|
||||
)]),
|
||||
};
|
||||
let original = serde_json::to_vec(&json!({"resource_id":resource,"revision":17,"model":old_model,"source_project":old_bundle})).unwrap();
|
||||
std::fs::write(&filename, &original).unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let opened = server.get(resource).await;
|
||||
assert_eq!(opened.revision, 17);
|
||||
assert_eq!(opened.model.schema_version, 4);
|
||||
assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3);
|
||||
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 4;"));
|
||||
assert_pair(&opened);
|
||||
assert_eq!(std::fs::read(&filename).unwrap(), original);
|
||||
assert_eq!(
|
||||
commands(
|
||||
&server,
|
||||
&opened,
|
||||
vec![Command::Rename(
|
||||
opened.model.id,
|
||||
"Reviewed data migration".into()
|
||||
)]
|
||||
)
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::OK
|
||||
);
|
||||
let saved = server.get(resource).await;
|
||||
assert_eq!(saved.revision, 18);
|
||||
let bytes = std::fs::read(&filename).unwrap();
|
||||
assert_ne!(bytes, original);
|
||||
server.stop().await;
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let reopened = server.get(resource).await;
|
||||
assert_eq!(reopened.model, saved.model);
|
||||
assert_eq!(reopened.source_project, saved.source_project);
|
||||
assert_eq!(reopened.etag(), saved.etag());
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
|
||||
server.stop().await;
|
||||
}
|
||||
@@ -6,10 +6,10 @@ fn authored() -> SourceBundle {
|
||||
let bundle = SourceBundle {
|
||||
entry: "harbor.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("harbor.arcadia".into(), "// Harbor engineering rationale 航海\r\narcadia 3; project harbor \"Harbor rescue\" {\r\n include \"team/first.arcadia\";\r\n include \"team/second.arcadia\";\r\n include \"contracts.arcadia\";\r\n include \"work.arcadia\";\r\n}\r\n".into()),
|
||||
("harbor.arcadia".into(), "// Harbor engineering rationale 航海\r\narcadia 4; project harbor \"Harbor rescue\" {\r\n include \"team/first.arcadia\";\r\n include \"team/second.arcadia\";\r\n include \"contracts.arcadia\";\r\n include \"work.arcadia\";\r\n}\r\n".into()),
|
||||
("team/first.arcadia".into(), "// First payload rationale\r\nsa_exchange_item first \"First payload\" { description \"Reviewed first\"; }\r\n".into()),
|
||||
("team/second.arcadia".into(), "// Second payload rationale\r\nsa_exchange_item second \"Second payload\" {}\r\n".into()),
|
||||
("contracts.arcadia".into(), "// Contract ownership stays here\r\nsa_interface contract \"Contract\" { items [\r\n @first // first reference rationale\r\n ,\r\n @second // second reference rationale\r\n ]; }\r\n".into()),
|
||||
("contracts.arcadia".into(), "// Contract ownership stays here\r\nsa_interface contract \"Contract\" {}\r\ninterface_item_allocation first_allocation \"First allocation\" { source @contract; target @first // first reference rationale\r\n; order 0; }\r\ninterface_item_allocation second_allocation \"Second allocation\" { source @contract; target @second // second reference rationale\r\n; order 1; }\r\n".into()),
|
||||
("work.arcadia".into(), "// Operational rationale\r\noa_activity assess \"Assess assistance\" {}\r\n".into()),
|
||||
]),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user