Complete source 6 engineering workflows and preserve release validation evidence

This commit is contained in:
Codex
2026-09-06 21:49:04 -07:00
parent 2b2d2bc18b
commit 4b6b5fc197
290 changed files with 48271 additions and 1791 deletions
+42 -10
View File
@@ -1065,6 +1065,19 @@ fn check_commands(commands: &[Command]) -> ApiResult<()> {
Ok(())
}
fn check_transition_request(request: &oa_core::transitions::TransitionRequest) -> ApiResult<()> {
if request.sources.len() > 100
|| request.mappings.len() > 200
|| request.communication_endpoints.len() > 200
|| request.resolutions.len() > 400
{
return Err(ApiError::bad_request(
"Transition requests allow at most 100 sources, 200 mappings or communication endpoint decisions and 400 field resolutions.",
));
}
Ok(())
}
async fn preview_transition(
State(state): State<AppState>,
id: ProjectPath,
@@ -1077,21 +1090,38 @@ async fn preview_transition(
let body = bytes(body)?;
blocking(state, move |store| {
let request: oa_core::transitions::TransitionRequest = json_body(&body)?;
if request.sources.len() > 100 || request.mappings.len() > 200 || request.resolutions.len() > 400 {
return Err(ApiError::bad_request("Transition requests allow at most 100 sources, 200 mappings and 400 field resolutions."));
}
check_transition_request(&request)?;
let snapshot = store.checked(id, &expected)?;
let current = &snapshot;
let plan = oa_core::transitions::plan_transition(&current.model, &request)
.map_err(|error| ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, "transition_rejected", error.to_string()))?;
if !plan.commands.is_empty() { check_commands(&plan.commands)?; }
let plan =
oa_core::transitions::plan_transition(&current.model, &request).map_err(|error| {
ApiError::new(
StatusCode::UNPROCESSABLE_ENTITY,
"transition_rejected",
error.to_string(),
)
})?;
if !plan.commands.is_empty() {
check_commands(&plan.commands)?;
}
if plan.conflicts.is_empty() {
let candidate = oa_core::transitions::prepare_transition(&current.model, &plan)
.map_err(|error| ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, "transition_rejected", error.to_string()))?;
.map_err(|error| {
ApiError::new(
StatusCode::UNPROCESSABLE_ENTITY,
"transition_rejected",
error.to_string(),
)
})?;
PreparedProject::from_command(current, candidate)?;
}
Ok(response(current, StatusCode::OK, json!({"base_revision":current.revision,"plan":plan})))
}).await
Ok(response(
current,
StatusCode::OK,
json!({"base_revision":current.revision,"plan":plan}),
))
})
.await
}
#[derive(Deserialize)]
@@ -1164,6 +1194,7 @@ fn check_semantic_request(request: &oa_core::semantic::SemanticTransitionRequest
});
if transition.sources.len() > 100
|| transition.mappings.len() > 200
|| transition.communication_endpoints.len() > 200
|| transition.resolutions.len() > 400
|| request.traces.len() > 100
|| request.references.len() > 5_000
@@ -1173,7 +1204,7 @@ fn check_semantic_request(request: &oa_core::semantic::SemanticTransitionRequest
|| request.accepted_impacts.len() > 200
{
return Err(ApiError::bad_request(
"Semantic transitions allow at most 100 sources/traces/reviews, 200 mappings/candidates/accepted impacts, 400 prose resolutions and 5,000 reference mappings or combined facet resolutions/resets.",
"Semantic transitions allow at most 100 sources/traces/reviews, 200 mappings/communication endpoint decisions/candidates/accepted impacts, 400 prose resolutions and 5,000 reference mappings or combined facet resolutions/resets.",
));
}
Ok(())
@@ -1271,6 +1302,7 @@ async fn commit_transition(
let body = bytes(body)?;
blocking(state, move |store| {
let request: TransitionApplyRequest = json_body(&body)?;
check_transition_request(&request.plan.request)?;
let snapshot = store.checked(id, &expected)?;
let current = &snapshot;
if current.revision != request.base_revision {
+5 -1
View File
@@ -8,12 +8,16 @@ use serde_json::{Value, json};
use std::{path::Path, sync::Arc};
use tokio::{sync::oneshot, task::JoinHandle};
#[path = "http/communication_limits.rs"]
mod communication_limits;
#[path = "http/data_contracts.rs"]
mod data_contracts;
#[path = "http/port_anchors.rs"]
mod port_anchors;
#[path = "http/semantic_transitions.rs"]
mod semantic_transitions;
#[path = "http/source6_views.rs"]
mod source6_views;
#[path = "http/source_projects.rs"]
mod source_projects;
@@ -2015,7 +2019,7 @@ async fn current_migration_reports_and_invalid_typed_transaction_preserve_http_s
.text()
.await
.unwrap();
assert!(source.starts_with("arcadia 5;"));
assert!(source.starts_with("arcadia 6;"));
assert_eq!(oa_core::language::parse(&source).unwrap(), stored.model);
let no_op = server
.client
@@ -0,0 +1,119 @@
//! HTTP admission limits must reject endpoint decisions before transition planning.
use super::*;
use oa_core::{semantic::SemanticTransitionRequest, transitions::*};
async fn post(
server: &TestServer,
id: Id,
route: &str,
etag: &str,
body: &Value,
) -> reqwest::Response {
server
.client
.post(server.url(&format!("/projects/{id}/{route}")))
.header("if-match", etag)
.json(body)
.send()
.await
.unwrap()
}
async fn unchanged(server: &TestServer, before: &StoredProject, disk: &Path, bytes: &[u8]) {
let after = server.get(before.resource_id).await;
assert_eq!(after.revision, before.revision);
assert_eq!(after.etag(), before.etag());
assert_eq!(after.model, before.model);
assert_eq!(after.source_project, before.source_project);
assert_eq!(std::fs::read(disk).unwrap(), bytes);
}
async fn endpoint_limit(semantic: bool) {
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let mut model = Project::empty();
let source = model
.add_element(Kind::Activity, Layer::Oa, "Inspect service", None)
.unwrap();
let (id, etag) = server.import(&model).await;
let before = server.get(id).await;
let disk = directory.path().join(format!("{id}.json"));
let bytes = std::fs::read(&disk).unwrap();
let transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
let (route, mut request, decision_path, applied_decision_path) = if semantic {
(
"semantic-transitions",
serde_json::to_value(SemanticTransitionRequest::new(transition)).unwrap(),
"/transition/communication_endpoints",
"/plan/request/transition/communication_endpoints",
)
} else {
(
"transitions",
serde_json::to_value(transition).unwrap(),
"/communication_endpoints",
"/plan/request/communication_endpoints",
)
};
// Obtain a real prepared envelope first: apply must enforce the same bound,
// even when a client modifies a previously valid plan's embedded request.
let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await;
let status = response.status();
let mut plan: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::OK, "{plan}");
assert_eq!(plan["plan"]["conflicts"], json!([]));
unchanged(&server, &before, &disk, &bytes).await;
// Syntactically typed but semantically inapplicable decisions deliberately
// distinguish capacity admission (400) from core semantic rejection (422 or
// a preview containing conflicts). The bound counts entries, not unique IDs.
let decision = CommunicationEndpointDecision {
source,
source_port: CommunicationPortChoice::Reuse { port: source },
target_port: CommunicationPortChoice::Reuse { port: source },
};
*request.pointer_mut(decision_path).unwrap() = json!(vec![decision.clone(); 200]);
let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await;
let status = response.status();
let body: Value = response.json().await.unwrap();
assert!(
matches!(status, StatusCode::OK | StatusCode::UNPROCESSABLE_ENTITY),
"Exactly 200 decisions must pass capacity admission: {status} {body}"
);
unchanged(&server, &before, &disk, &bytes).await;
let over_limit = json!(vec![decision; 201]);
*request.pointer_mut(decision_path).unwrap() = over_limit.clone();
*plan.pointer_mut(applied_decision_path).unwrap() = over_limit;
for (operation, body) in [("preview", request), ("apply", plan)] {
let response = post(&server, id, &format!("{route}/{operation}"), &etag, &body).await;
let status = response.status();
let error: Value = response.json().await.unwrap();
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"{route}/{operation}: {error}"
);
assert_eq!(error["code"], "invalid_request", "{error}");
let detail = error["detail"].as_str().unwrap().to_lowercase();
assert!(
detail.contains("200") && detail.contains("endpoint"),
"Capacity rejection must identify endpoint decisions: {error}"
);
unchanged(&server, &before, &disk, &bytes).await;
}
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
unchanged(&server, &before, &disk, &bytes).await;
server.stop().await;
}
#[tokio::test]
async fn ordinary_transition_endpoint_decision_limit_rejects_201_without_mutation() {
endpoint_limit(false).await;
}
#[tokio::test]
async fn semantic_transition_endpoint_decision_limit_rejects_201_without_mutation() {
endpoint_limit(true).await;
}
@@ -185,7 +185,7 @@ async fn published_data_contract_queries_atomic_edits_identity_and_restart() {
.text()
.await
.unwrap();
assert!(source.starts_with("arcadia 5;"));
assert!(source.starts_with("arcadia 6;"));
assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model);
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
@@ -218,7 +218,10 @@ async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_fil
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, 5));
assert_eq!(
(report.from_schema, report.to_schema),
(3, oa_core::SCHEMA_VERSION)
);
assert_eq!(report.generated_interface_allocations.len(), 1);
assert!(!report.semantic_defaults.is_empty());
let validated: Value = server
@@ -241,7 +244,7 @@ async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_fil
1
);
assert_eq!(stored.source_project.files.len(), 2);
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 5;"));
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 6;"));
for text in [
"// Event rationale",
"// Interface rationale",
@@ -283,9 +286,9 @@ async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_sav
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, 5);
assert_eq!(opened.model.schema_version, oa_core::SCHEMA_VERSION);
assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3);
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 5;"));
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 6;"));
assert_pair(&opened);
assert_eq!(std::fs::read(&filename).unwrap(), original);
assert_eq!(
@@ -717,7 +720,7 @@ async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_s
.text()
.await
.unwrap();
assert!(source.starts_with("arcadia 5;"));
assert!(source.starts_with("arcadia 6;"));
assert_eq!(oa_core::language::parse(&source).unwrap(), changed.model);
let saved = std::fs::read(&filename).unwrap();
server.stop().await;
@@ -0,0 +1,374 @@
use super::*;
use oa_core::{
ComponentPortKind, ComponentPortOrientation, Position, ProjectionKind, RelationBody,
SharedBody,
source_project::{ProjectLimits, SourceBundle, compile_bundle},
transitions::{
CommunicationEndpointDecision, CommunicationPortChoice, TransitionDirection,
TransitionMapping, TransitionRequest,
},
view_options::{
CategorySelection, EdgeKind, ExchangeFamily, ExchangeLabels, RelationKinds, ViewOptions,
},
};
use std::collections::BTreeMap;
struct Fixture {
bundle: SourceBundle,
mean: Id,
category: Id,
diagram: Id,
item: Id,
oa: [Id; 2],
sa: [Id; 2],
}
fn fixture() -> Fixture {
let mut model = Project::empty();
let a = model
.add_element(Kind::Entity, Layer::Oa, "Rescue team", None)
.unwrap();
let b = model
.add_element(Kind::Actor, Layer::Oa, "Control centre", None)
.unwrap();
let sa = model
.add_element(Kind::System, Layer::Sa, "Radio", None)
.unwrap();
let sb = model
.add_element(Kind::SystemActor, Layer::Sa, "External radio", None)
.unwrap();
let item = model
.add_element(Kind::ExchangeItem, Layer::Oa, "Position & status", None)
.unwrap();
let mean = model.relate(RelationKind::CommunicationMean, "Radio <channel>", a, b);
let category = model
.add_element(Kind::ExchangeCategory, Layer::Oa, "Radio channels", None)
.unwrap();
*model
.elements
.get_mut(&category)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::ExchangeCategory {
family: ExchangeFamily::Participant,
exchanges: vec![mean],
};
let diagram = model.ensure_diagram(Layer::Oa);
model.place(diagram, a, Position { x: 0., y: 0. });
model.place(diagram, b, Position { x: 400., y: 0. });
let view = model.diagrams.get_mut(&diagram).unwrap();
view.projection = ProjectionKind::Allocation;
view.view_options = ViewOptions {
relation_kinds: RelationKinds::Only(vec![EdgeKind::Relation(
RelationKind::CommunicationMean,
)]),
participant_categories: CategorySelection::Any(vec![category]),
exchange_labels: ExchangeLabels::NameAndItems,
..Default::default()
};
let source = format!(
"// Authored source-6 engineering rationale\n{}",
oa_core::language::format(&model).replacen("{\n", "{\n include \"notes.arcadia\";\n", 1)
);
let bundle = SourceBundle {
entry: "radio.arcadia".into(),
files: BTreeMap::from([
("radio.arcadia".into(), source),
(
"notes.arcadia".into(),
"// Retain this exact included note, including CRLF.\r\n".into(),
),
]),
};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, model);
Fixture {
bundle: compiled.bundle,
mean,
category,
diagram,
item,
oa: [a, b],
sa: [sa, sb],
}
}
async fn import(server: &TestServer, bundle: &SourceBundle) -> StoredProject {
let response = server
.client
.post(server.url("/projects/import-source-project"))
.json(bundle)
.send()
.await
.unwrap();
let status = response.status();
let result: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::CREATED, "{result}");
server
.get(result["resource_id"].as_str().unwrap().parse().unwrap())
.await
}
fn assert_pair(stored: &StoredProject) {
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
assert_eq!(compiled.bundle, stored.source_project);
assert_eq!(compiled.decoded.project, stored.model);
assert_eq!(stored.model.schema_version, 6);
}
async fn transact(
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 source6_http_views_categories_and_means_preserve_source_and_reject_invalid_batch_after_restart()
{
let f = fixture();
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let original = import(&server, &f.bundle).await;
let resource = original.resource_id;
assert_eq!(original.source_project, f.bundle);
assert_pair(&original);
let model: Value = server
.client
.get(server.url(&format!("/projects/{resource}/model")))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
assert_eq!(
model["relations"][f.mean.to_string()]["body"]["CommunicationMean"]["oriented"],
false
);
assert_eq!(
model["diagrams"][f.diagram.to_string()]["view_options"]["exchange_labels"],
"NameAndItems"
);
for (route, expected) in [
("relations?kind=CommunicationMean", f.mean),
("elements?kind=ExchangeCategory", f.category),
] {
let result: Value = server
.client
.get(server.url(&format!("/projects/{resource}/{route}")))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
assert_eq!(result["items"].as_array().unwrap().len(), 1);
assert_eq!(result["items"][0]["id"], expected.to_string());
}
// There is no projection HTTP route. The actual returned model supplies the
// shared desktop projection; saved filters must not alter queried relations.
assert!(
oa_core::projection::project_view(&original.model, f.diagram)
.edges
.iter()
.any(|e| e.id == f.mean)
);
let mut relation = original.model.relations[&f.mean].clone();
if let RelationBody::CommunicationMean {
oriented, items, ..
} = &mut relation.body
{
*oriented = true;
*items = vec![f.item];
}
let mut diagram = original.model.diagrams[&f.diagram].clone();
diagram.view_options.hidden_edges = vec![f.mean];
diagram.view_options.exchange_labels = ExchangeLabels::Items;
let response = transact(
&server,
&original,
vec![
Command::EditRelation(f.mean, relation.clone()),
Command::EditDiagram(f.diagram, diagram.clone()),
],
)
.await;
let status = response.status();
let result: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::OK, "{result}");
let changed = server.get(resource).await;
assert_eq!(changed.revision, original.revision + 1);
assert_eq!(changed.model.relations[&f.mean], relation);
assert_eq!(changed.model.diagrams[&f.diagram], diagram);
assert!(
oa_core::projection::project_view(&changed.model, f.diagram)
.edges
.is_empty()
);
assert_eq!(
changed.source_project.files["notes.arcadia"],
f.bundle.files["notes.arcadia"]
);
assert!(
changed.source_project.files["radio.arcadia"]
.starts_with("// Authored source-6 engineering rationale\n")
);
assert_pair(&changed);
let disk = directory.path().join(format!("{resource}.json"));
let accepted_bytes = std::fs::read(&disk).unwrap();
let mut invalid = changed.model.elements[&f.category].clone();
if let Some(SharedBody::ExchangeCategory { family, .. }) = invalid.shared_mut() {
*family = ExchangeFamily::Work; // A communication means cannot join this family.
}
relation.name = "Must not leak from rejected batch".into();
let rejected = transact(
&server,
&changed,
vec![
Command::EditRelation(f.mean, relation),
Command::EditElement(f.category, invalid),
],
)
.await;
assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY);
let retained = server.get(resource).await;
assert_eq!(retained.model, changed.model);
assert_eq!(retained.source_project, changed.source_project);
assert_eq!(retained.etag(), changed.etag());
assert_eq!(std::fs::read(&disk).unwrap(), accepted_bytes);
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.model, changed.model);
assert_eq!(reopened.source_project, changed.source_project);
assert_eq!(reopened.etag(), changed.etag());
assert_pair(&reopened);
server.stop().await;
}
#[tokio::test]
async fn source6_http_explicit_communication_ports_preview_apply_and_restart_preserve_decisions() {
let f = fixture();
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let original = import(&server, &f.bundle).await;
let resource = original.resource_id;
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![f.mean]);
request.mappings =
f.oa.into_iter()
.zip(f.sa)
.map(|(source, target)| TransitionMapping {
source,
target: Some(target),
target_kind: None,
})
.collect();
let ports = [Id::new_v4(), Id::new_v4()];
let create = |id, alias: &str| CommunicationPortChoice::Create {
id,
alias: alias.into(),
name: alias.into(),
description: "Explicit HTTP endpoint choice".into(),
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
};
request
.communication_endpoints
.push(CommunicationEndpointDecision {
source: f.mean,
source_port: create(ports[0], "http_radio_source"),
target_port: create(ports[1], "http_radio_target"),
});
let accepted_bytes = std::fs::read(directory.path().join(format!("{resource}.json"))).unwrap();
let response = server
.client
.post(server.url(&format!("/projects/{resource}/transitions/preview")))
.header("if-match", original.etag())
.json(&request)
.send()
.await
.unwrap();
let status = response.status();
let preview: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::OK, "{preview}");
assert!(
preview["plan"]["conflicts"].as_array().unwrap().is_empty(),
"{preview}"
);
assert_eq!(
preview["plan"]["request"]["communication_endpoints"],
serde_json::to_value(&request.communication_endpoints).unwrap()
);
assert_eq!(
server.get(resource).await.source_project,
original.source_project
);
assert_eq!(
std::fs::read(directory.path().join(format!("{resource}.json"))).unwrap(),
accepted_bytes
);
let response = server
.client
.post(server.url(&format!("/projects/{resource}/transitions/apply")))
.header("if-match", original.etag())
.json(&preview)
.send()
.await
.unwrap();
let status = response.status();
let result: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::OK, "{result}");
let changed = server.get(resource).await;
assert_eq!(changed.revision, original.revision + 1);
assert_pair(&changed);
let target = changed
.model
.traces
.values()
.find(|t| t.oa == f.mean)
.unwrap()
.sa;
assert!(
matches!(changed.model.relations[&target].body, RelationBody::BoundaryExchange { source, target, oriented:false, .. } if [source,target] == ports)
);
for (port, owner) in ports.into_iter().zip(f.sa) {
assert_eq!(changed.model.elements[&port].parent, Some(owner));
}
assert_eq!(
changed.model.relations[&f.mean],
original.model.relations[&f.mean]
);
assert_eq!(
changed.model.elements[&f.category],
original.model.elements[&f.category]
);
assert_eq!(
changed.model.diagrams[&f.diagram],
original.model.diagrams[&f.diagram]
);
assert_eq!(
changed.source_project.files["notes.arcadia"],
original.source_project.files["notes.arcadia"]
);
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.model, changed.model);
assert_eq!(reopened.source_project, changed.source_project);
assert_eq!(reopened.etag(), changed.etag());
assert_pair(&reopened);
server.stop().await;
}
+118 -13
View File
@@ -1,6 +1,7 @@
"""Refresh the closed semantic OpenAPI catalog from the frozen Rust leaf table.
Run from any directory with Python 3: semantic_schema.py [--check].
Run from any directory with Python 3: semantic_schema.py [--check] [--validate].
--validate additionally runs source-6 structural fixtures with jsonschema.
This generates documentation only, never runtime selectors or model mutations.
"""
import copy
@@ -15,6 +16,7 @@ spec = json.loads(PATH.read_text(encoding="utf-8"))
schemas = spec["components"]["schemas"]
types = (ROOT / "crates/oa-core/src/semantic/types.rs").read_text(encoding="utf-8")
model = (ROOT / "crates/oa-core/src/model.rs").read_text(encoding="utf-8")
views = (ROOT / "crates/oa-core/src/view_options.rs").read_text(encoding="utf-8")
def ref(name):
@@ -47,6 +49,7 @@ def variant(name, value):
def typed(name):
name = name.replace("crate::view_options::", "")
if name.startswith("Option<"):
return optional(typed(name[7:-1]))
if name.startswith("Vec<"):
@@ -59,6 +62,7 @@ def typed(name):
def reviewed(name):
name = name.replace("crate::view_options::", "")
key = "Reviewed" + re.sub(r"[^A-Za-z0-9]", "", name)
observation = {"oneOf": [enum(["NotApplicable"]), variant("Value", typed(name))]}
schemas[key] = obj({"oa": observation, "sa": copy.deepcopy(observation)})
@@ -74,8 +78,9 @@ def keypair(name):
for name in ["DataConstructor", "PortConstructor", "OccurrenceConstructor", "EventConstructor",
"MessageConstructor", "ReferenceCollection", "OrderedCollection", "IncidentEnd",
"ArtifactFacet", "ComponentPortKind", "ComponentPortOrientation", "PortDirection",
"FragmentOperator", "StateActionKind", "ArtifactRelation"]:
source = types if f"pub enum {name}" in types else model
"FragmentOperator", "StateActionKind", "ArtifactRelation", "ExchangeFamily",
"Coverage", "ExchangeLabels", "Kind", "RelationKind"]:
source = next(s for s in [types, model, views] if f"pub enum {name}" in s)
body = re.search(r"pub enum " + name + r"\s*\{([^}]+)\}", source).group(1)
body = re.sub(r"//[^\n]*|#\[[^\]]*\]", "", body)
names = [value.strip() for value in body.split(",") if value.strip()]
@@ -84,17 +89,17 @@ for name in ["DataConstructor", "PortConstructor", "OccurrenceConstructor", "Eve
catalog = types.split("macro_rules! scalar_families", 1)[1].split("pub(super) use", 1)[0]
families = re.findall(r"(\w+), (\w+), (\w+) \{([^}]+)\}", catalog)
assert len(families) == 13
assert len(families) == 14
selectors, reviews = [], []
leaf_count = 0
for family, selector, review, body in families:
leaves = re.findall(r"(\w+): ([\w<>]+)", body)
leaves = re.findall(r"(\w+): ([\w:<>]+)", body)
leaf_count += len(leaves)
schemas[selector] = enum([leaf for leaf, _ in leaves])
schemas[review] = {"oneOf": [variant(leaf, reviewed(value)) for leaf, value in leaves]}
selectors.append(variant(family, ref(selector)))
reviews.append(variant(family, ref(review)))
assert leaf_count == 84
assert leaf_count == 85
schemas["AnnotationKey"] = obj({"path": array(ref("Id")), "field": ref("Id")})
id_keys, text_keys, annotation_keys = keypair("Id"), keypair("String"), keypair("AnnotationKey")
artifact_types = {"Presence": "bool", "Name": "String", "Description": "String", "Owner": "Id",
@@ -119,13 +124,49 @@ schemas["FacetReview"] = {"oneOf": reviews}
schemas["SemanticBaseline"] = obj({"facets": array(ref("FacetReview"))})
schemas["TraceBaseline"]["properties"]["semantic"] = {**optional(ref("SemanticBaseline")), "default": None,
"description": "Absent/null means Unreviewed. Historical typed facts never become live graph references."}
schemas["Project"]["properties"]["schema_version"]["const"] = 5
schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4]
schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 5
schemas["MigrationReport"]["description"] = schemas["MigrationReport"]["description"].replace("V1/V2/V3 to V4", "V1/V2/V3/V4 to V5")
spec["info"]["description"] = spec["info"]["description"].replace("schema-4", "schema-5")
spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-5 native JSON is described below. Schema-1/2/3/4 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Prior prose reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 5 / arcadia 5. See docs/language.rst."
schemas["SourceBundle"]["description"] = schemas["SourceBundle"]["description"].replace("source4 workspace (source3 migrates explicitly)", "source5 workspace (source3/4 migrate explicitly)")
schemas["Project"]["properties"]["schema_version"]["const"] = 6
schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4, 5]
schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 6
schemas["MigrationReport"]["description"] = re.sub(r"V1(?:/V[2-5])+ to V[4-6]", "V1/V2/V3/V4/V5 to V6", schemas["MigrationReport"]["description"])
spec["info"]["description"] = re.sub(r"schema-[45]", "schema-6", spec["info"]["description"])
spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-6 native JSON is described below. Schema-1/2/3/4/5 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Earlier versions reject source-6 constructors and saved view fields. Prior prose and typed reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 6 / arcadia 6. See docs/language.rst."
schemas["SourceBundle"]["description"] = re.sub(r"source[45] workspace \(source[^)]+\)", "source6 workspace (source3/4/5 migrate explicitly)", schemas["SourceBundle"]["description"])
# Source-6 additions are closed serde records; selectors with an empty payload
# remain explicit selections rather than reverting to their omitted defaults.
schemas["EdgeKind"] = {"oneOf": [enum(["Trace"]), variant("Relation", ref("RelationKind"))]}
schemas["RelationKinds"] = {"oneOf": [enum(["All"]), variant("Only", array(ref("EdgeKind")))]}
for selection in ["CategorySelection", "ContextSelection"]:
schemas[selection] = {"oneOf": [enum(["All"]), variant("Any", array(ref("Id")))]}
schemas["ViewOptions"] = obj({
"hidden_edges": {**array(ref("Id")), "default": []},
"relation_kinds": {**ref("RelationKinds"), "default": "All"},
"work_categories": {**ref("CategorySelection"), "default": "All"},
"participant_categories": {**ref("CategorySelection"), "default": "All"},
"coverage": {**ref("Coverage"), "default": "All"},
"work_contexts": {**ref("ContextSelection"), "default": "All"},
"exchange_labels": {**ref("ExchangeLabels"), "default": "Name"}}, [])
schemas["ViewOptions"]["description"] = "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions."
schemas["Diagram"]["properties"]["view_options"] = {**ref("ViewOptions"), "default": {}}
def upsert_variant(variants, name, value):
variants[:] = [v for v in variants if name not in v.get("properties", {})]
variants.append(variant(name, value))
upsert_variant(schemas["RelationBody"]["oneOf"], "CommunicationMean", obj({
"source": ref("Id"), "target": ref("Id"), "oriented": typed("bool"), "items": array(ref("Id"))}))
upsert_variant(schemas["SharedBody"]["oneOf"][1]["oneOf"], "ExchangeCategory", obj({
"family": ref("ExchangeFamily"), "exchanges": array(ref("Id"))}))
schemas["CommunicationPortChoice"] = {"oneOf": [
variant("Reuse", obj({"port": ref("Id")})),
variant("Create", obj({"id": ref("Id"), "alias": typed("String"), "name": typed("String"),
"description": typed("String"), "kind": ref("ComponentPortKind"),
"orientation": ref("ComponentPortOrientation")}))]}
schemas["CommunicationEndpointDecision"] = obj({"source": ref("Id"),
"source_port": ref("CommunicationPortChoice"), "target_port": ref("CommunicationPortChoice")})
schemas["TransitionRequest"]["properties"]["communication_endpoints"] = {
**array(ref("CommunicationEndpointDecision"), 200), "default": [],
"description": "Explicit SA component-port pair per selected OA CommunicationMean. Reuse or Create only. Owners follow reviewed participant mappings; no participant-to-port substitution or automatic interface allocation."}
schemas["Drift"] = enum(["Unreviewed", "Unchanged", "OaChanged", "SaChanged", "BothChanged"])
schemas["FacetDrift"] = obj({"selector": ref("FacetSelector"), "current": ref("FacetReview"), "drift": ref("Drift")})
schemas["SemanticDrift"] = obj({"own": ref("Drift"), "linked": ref("Drift"),
@@ -197,6 +238,70 @@ def check_refs(value):
check_refs(spec)
if "--validate" in sys.argv:
from jsonschema import Draft202012Validator, FormatChecker
for schema in schemas.values():
Draft202012Validator.check_schema(schema)
fixture_count = 0
def accepts(name, value, expected=True):
global fixture_count
validator = Draft202012Validator({**ref(name), "components": spec["components"]},
format_checker=FormatChecker())
errors = list(validator.iter_errors(value))
assert (not errors) == expected, (name, value, [e.message for e in errors])
fixture_count += 1
a, b = "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"
mean = {"source": a, "target": b, "oriented": False, "items": []}
accepts("RelationBody", {"CommunicationMean": mean})
accepts("RelationBody", {"CommunicationMean": {**mean, "typo": True}}, False)
for field in mean:
accepts("RelationBody", {"CommunicationMean": {k: v for k, v in mean.items() if k != field}}, False)
category = {"family": "Participant", "exchanges": [a]}
accepts("SharedBody", {"ExchangeCategory": category})
accepts("SharedBody", {"ExchangeCategory": {**category, "family": "Future"}}, False)
accepts("SharedBody", {"ExchangeCategory": {**category, "typo": []}}, False)
accepts("ViewOptions", {})
options = {"hidden_edges": [a], "relation_kinds": {"Only": ["Trace", {"Relation": "CommunicationMean"}]},
"work_categories": {"Any": []}, "participant_categories": {"Any": [b]},
"coverage": "OutsideBehavior", "work_contexts": {"Any": []}, "exchange_labels": "NameAndItems"}
accepts("ViewOptions", options)
accepts("ViewOptions", {**options, "relation_kinds": {"Only": []}})
for field, value in [("typo", []), ("coverage", "Unknown"), ("hidden_edges", ["not-an-id"]),
("relation_kinds", {"Only": [], "typo": []}),
("relation_kinds", {"Only": [{"Relation": "Future"}]}),
("work_contexts", {"Any": [], "typo": []}),
("exchange_labels", "Names")]:
accepts("ViewOptions", {**options, field: value}, False)
create = {"id": a, "alias": "radio", "name": "Radio", "description": "",
"kind": "Standard", "orientation": "Unset"}
accepts("CommunicationPortChoice", {"Create": create})
accepts("CommunicationPortChoice", {"Reuse": {"port": a}})
accepts("CommunicationPortChoice", "Auto", False)
accepts("CommunicationPortChoice", {"Reuse": {"port": a, "owner": b}}, False)
for field in create:
accepts("CommunicationPortChoice", {"Create": {k: v for k, v in create.items() if k != field}}, False)
pair = {"source": a, "source_port": {"Create": create}, "target_port": {"Reuse": {"port": b}}}
accepts("CommunicationEndpointDecision", pair)
accepts("CommunicationEndpointDecision", {k: v for k, v in pair.items() if k != "target_port"}, False)
request = {"direction": "OaToSa", "sources": [a], "scope": "SelectedOnly", "mappings": [],
"fields": [], "resolutions": [], "destination_diagram": None}
accepts("TransitionRequest", request)
accepts("TransitionRequest", {**request, "communication_endpoints": [pair]})
accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 200})
accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 201}, False)
accepts("FacetSelector", {"Category": "Family"})
accepts("FacetSelector", {"Category": "Unknown"}, False)
accepts("FacetReview", {"Category": {"Family": {"oa": {"Value": "Work"}, "sa": {"Value": "Participant"}}}})
accepts("FacetSelector", {"Member": {"collection": "CategoryExchanges", "keys": {"oa": a, "sa": b}}})
assert schemas["Project"]["properties"]["schema_version"]["const"] == 6
assert schemas["MigrationReport"]["properties"]["from_schema"]["enum"] == [1, 2, 3, 4, 5]
print(f"Source-6 schema fixtures: {fixture_count} passed; all {len(schemas)} component schemas well-formed")
rendered = json.dumps(spec, indent=2, ensure_ascii=False) + "\n"
if "--check" in sys.argv:
assert json.loads(PATH.read_text(encoding="utf-8")) == spec, "Run semantic_schema.py to refresh OpenAPI"