Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b272200b83 | ||
|
|
e44e01ac54 | ||
|
|
01ef0f425b | ||
|
|
fd5025bf39 | ||
|
|
5e40b775cd | ||
|
|
881d7aa243 | ||
|
|
65b1a0d7d0 | ||
|
|
4fa95914d9 | ||
|
|
04316a75fa | ||
|
|
9dbe386a8e | ||
|
|
4101434157 | ||
|
|
a8f451ebae | ||
|
|
316dc559d7 |
@@ -0,0 +1,73 @@
|
||||
# Agent working agreement
|
||||
|
||||
## Working method
|
||||
|
||||
- Follow `PROJECT_GOAL.md` and the user's current instructions. Work autonomously within scope. Resolve discoverable questions yourself; record, test, and revise reversible assumptions. Ask only when a consequential decision needs information or authorization not already available. Continue independent useful work while blocked elsewhere.
|
||||
- Map unfamiliar domains through primary sources, comparable systems, and real user workflows. Identify underlying needs, valuable concepts, and avoidable friction; evaluate improvements rather than merely reproducing features. Synthesize findings into decisions or experiments, and stop research when another action offers greater value.
|
||||
- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid/invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it.
|
||||
- For consequential uncertain choices, compare materially different candidates against explicit user-derived criteria. Ask independent critics to challenge assumptions and attempt to falsify proposals; refine or replace candidates as evidence warrants. Record the choice, evidence, tradeoffs, rejected approaches, and reasons to revisit it. Use proportionate effort for reversible local choices.
|
||||
- Plan milestones that resolve uncertainty or deliver useful capability, with explicit dependencies. Test foundational assumptions before substantial downstream work. At checkpoints, assess progress toward the actual objective and replan when evidence invalidates the strategy or progress stalls.
|
||||
- Define observable success and suitable evaluators before repeated improvement: invariants, examples, round trips, transformations, performance checks, realistic user tasks, or explicit review rubrics. Ensure the evaluator measures the intended outcome. For experiments, record the question, expectations, method, result, and implications; isolate exploratory prototypes from production architecture.
|
||||
- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing/selection, navigation, feedback, recovery, loading/empty/failure states, accessibility, layout, focus/disabled states, and responsiveness.
|
||||
- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human/AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability.
|
||||
- Favor the simplest maintainable design satisfying known requirements and justified extensibility. Avoid speculative abstraction, unrelated refactoring, and repeating approaches already invalidated by evidence.
|
||||
|
||||
## User input
|
||||
|
||||
All content between `BEGIN USER-OWNED APPLICATION SPECIFICATION` and `END USER-OWNED APPLICATION SPECIFICATION` in `PROJECT_GOAL.md` belongs to the user. Its headings and structure are unrestricted. Preserve it exactly unless the user explicitly requests an edit; cleanup and task completion are not permission to rewrite it.
|
||||
|
||||
Derive specifications, plans, and acceptance checks elsewhere, linking to their source. Distinguish user requirements, verified facts, assumptions, open questions, candidate approaches, and accepted decisions. Interpretations must not silently replace the user's intent. When input changes, reconcile affected work. Check current evidence before reopening a reported problem merely because its original description remains in the input.
|
||||
|
||||
## Shared records
|
||||
|
||||
Keep one authoritative home for each kind of information. Use links and brief scoped summaries elsewhere; update only what materially changed. Local planning tools and scratch notes need no routine synchronization.
|
||||
|
||||
| Information | Authoritative home |
|
||||
| --- | --- |
|
||||
| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues/milestones; linked PRs hold change review and validation. |
|
||||
| Specifications, architecture, interfaces, and build/run documentation | Established version-controlled project documents. |
|
||||
| Durable decisions and reusable evidence | `harness/DECISIONS.md` and relevant topic notes, or existing equivalents. |
|
||||
| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit/release, capabilities, and material limitations. No live task ledger. |
|
||||
| Shared entrypoint | Gitea Wiki: overview and links, without duplicating versioned documentation. |
|
||||
| Published deliverables | Gitea releases, tied to source commits and verification. |
|
||||
|
||||
Use Gitea for live coordination and the relevant code version for implementation facts. Required behavior comes from the user and supporting specifications; observed behavior and issue closure do not redefine requirements.
|
||||
|
||||
## Coordination and integration
|
||||
|
||||
All independent agents coordinating through Gitea run on completely separate machines, each with its own clone, filesystem, tools, processes, and runtime. Gitea and pushed Git history are the shared communication and delivery channels. Local files (including `harness`), unpushed commits, worktrees, test results, and localhost services are not accessible to peers. Matching branch names or paths do not imply synchronized contents.
|
||||
|
||||
1. On starting or resuming, including after compaction, inspect the working branch, local changes, applicable instructions, relevant shared records, and existing implementation. Recover context from authoritative records. Check for newer shared instructions on the actual remote default branch without overwriting local work. Read what is relevant; avoid repeated full-history scans and constant polling.
|
||||
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository/branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
|
||||
3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration/release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files/interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines.
|
||||
4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
|
||||
5. Follow project review and merge conventions. Fetch the relevant remote branches and inspect the identified commits before reviewing or integrating another machine's work. Resolve conflicts while preserving others' edits, and verify the combined result locally; report material environment differences. Local instruction-file edits reach peers only after publication and retrieval. Do not overwrite others' work or force-push shared history to resolve coordination problems.
|
||||
6. Close work when its acceptance criteria are met and the result is integrated or delivered as appropriate. Mark duplicates and superseded tasks explicitly with replacement links. Then choose further useful unowned work while the requested objective remains incomplete.
|
||||
|
||||
Use Gitea MCP for shared coordination. If unavailable, report the failure, buffer necessary handoff information locally, and continue already-owned or clearly non-overlapping work. Reconcile with current Gitea state when access returns; do not claim an ownership update, review, or publication succeeded without confirmation.
|
||||
|
||||
## Delegation and reasoning
|
||||
|
||||
Use GPT-6 Astra exclusively. Gitea peers are independent remote sessions, not subagents controlled by this session. Spawned subagents use the execution environment actually provided by the agent tools; do not assume they share files, and publish their results before Gitea peers need them. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
|
||||
|
||||
Optimize reasoning effort for verified outcomes per cost, including review, retries, and repair:
|
||||
|
||||
- Main agent: default to high for planning, synthesis, integration, and acceptance decisions; medium for dispatching a settled plan and routine coordination.
|
||||
- Subagents: default to medium for bounded implementation and targeted research; low for straightforward retrieval, mechanical edits, and running established checks; high for complex debugging, correctness review, adversarial analysis, or ambiguous design.
|
||||
- Use extra-high selectively for difficult foundational semantic, methodological, formal, or cross-domain decisions. Reserve max for exceptional unresolved problems where additional reasoning is likely to justify its cost. Retain higher settings when evidence supports their value.
|
||||
|
||||
Allocate by task difficulty and consequences, not role or length alone. Start higher when a clearly difficult decision has expensive downstream consequences. Escalate for concrete reasoning difficulties; resolve missing evidence or unclear requirements at their source. Set subagent effort explicitly through supported controls to avoid unintended inheritance. Do not claim an unapplied change or alter global configuration merely to enforce these preferences.
|
||||
|
||||
## Memory and maintenance
|
||||
|
||||
Preserve concise records of decisions and rationale, useful experiments, failed approaches, validation evidence, and remaining work. Link substantial evidence rather than copying logs into indexes. Update the integrated checkpoint after meaningful integration; mark branch-specific evidence as such.
|
||||
|
||||
Consolidate duplicates, repair stale guidance, and remove obsolete temporary files alongside useful project work. Before removing material, check references, active ownership, and ongoing use; preserve unique knowledge and update affected links. Prefer closing, superseding, or archiving history over deletion. User input is excluded from autonomous cleanup.
|
||||
|
||||
If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions/evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony.
|
||||
|
||||
## Delivery
|
||||
|
||||
Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases/tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps.
|
||||
|
||||
Include runnable builds or appropriate artifacts, concise release notes, build/run/deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release/upload operation. Do not silently replace published versions with different contents.
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Project goal
|
||||
|
||||
## Objective
|
||||
|
||||
Take the existing project to the complete, coherent, usable, and verified outcome defined by the user input below. Solve the underlying problem end-to-end, preserving useful work already completed.
|
||||
|
||||
Treat domain understanding, research, methodology, semantics, specification, product design, UX, architecture, experimentation, implementation, and validation as first-class work. Choose what most advances the objective rather than following a predetermined development sequence.
|
||||
|
||||
Follow `AGENTS.md` for execution, coordination, record ownership, and delivery. Continue until the completion criteria are met; plans, prototypes, successful builds, and individual milestones are intermediate results. Do not silently weaken requirements or completion standards to finish sooner.
|
||||
|
||||
## Completion criteria
|
||||
|
||||
Apply these criteria to the scope requested by the user:
|
||||
|
||||
- Required capabilities address the intended problem and support realistic user workflows.
|
||||
- Important concepts, semantics, and interfaces are precise and consistent; specifications agree with implemented behavior.
|
||||
- Consequential decisions have evidence and rationale. Major assumptions have been validated, explicitly accepted, or isolated without compromising required behavior.
|
||||
- The result builds, runs, and operates on the required platforms. Relevant tests and evaluators pass; integrations, edge cases, and failure states have been exercised against the actual result.
|
||||
- User-facing behavior and usability have been exercised directly, using representative data and workflows.
|
||||
- A fresh independent review covers requirements, domain/semantic consistency, usability, architecture, correctness, regressions, runtime behavior, documentation, and remaining risks. Valid material findings are addressed, blocking and high-impact issues resolved, and affected behavior reverified; no known blocker or high-severity defect remains.
|
||||
- Deliverables are maintainable, appropriately extensible, documented, and published as required by `AGENTS.md`. Obsolete scaffolding is removed; documentation and shared state accurately describe the result. Another engineer or agent can reproduce the verification and continue the work.
|
||||
|
||||
If required work or verification is blocked, record the specific gap and continue independent useful work without claiming completion.
|
||||
|
||||
## Final handoff
|
||||
|
||||
Link the delivered result and summarize capabilities, discoveries, architecture and decisions, relevant rejected approaches, verification and independent review, remaining limitations, and exact build/run/deployment/verification instructions. Refer to authoritative records instead of repeating the project history.
|
||||
|
||||
## USER-OWNED APPLICATION SPECIFICATION
|
||||
|
||||
The block below accepts all project-specific requirements, context, priorities, and feedback, with any headings or organization the user chooses.
|
||||
|
||||
<!-- BEGIN USER-OWNED APPLICATION SPECIFICATION -->
|
||||
## Application specification
|
||||
|
||||
### General description
|
||||
|
||||
Create a modern clone of ARCADIA Capella using Rust Iced framework. It should be cross platform (supporting Windows and Linux at least). Use Rust exclusively (wherever possible & practical).
|
||||
|
||||
Focus on making the application modern, extensible, intuitive and easy to use. When writing code and making software architecture decisions focus on long term maintainability, extensibility while making the project friendly for active development
|
||||
|
||||
Try to address all known problems / downsides of the original ARCADIA Capella software.
|
||||
|
||||
Version 1 MVP in the form of working Operational Analysis layer is done.
|
||||
Version 2 MVP in the form of working Operation Analysis + System Analysis is done.
|
||||
You should make the 1st release version of the project with fully feature complete Operation Analysis + System Architecture, according to this application specification.
|
||||
Remaining ARCADIA layers will be added in future releases.
|
||||
|
||||
Transitioning of model elements between layers (in both directions) should be easy and intuitive.
|
||||
|
||||
Keep in mind that the target audience for the application are system engineers. And you should satisfy their needs and use cases
|
||||
|
||||
The application should be a general systems modeling tool, like ARCADIA Capella or SysML v2
|
||||
|
||||
You should implement most of the ARCADIA semantics, while adressing original ARCADIA Capella problems / downsides
|
||||
|
||||
The original ARCADIA Capella UI is very bloated, clunky, unintuitive and hard to use. In addition, it lacks a number of quality of life features. You should at all cost avoid making the same mistakes as the developers of the original ARCADIA Capella
|
||||
|
||||
You should explore the functionality & UI of the original ARCADIA Capella to take the most useful and powerful ideas from it. While making them modern, easy to use and understand
|
||||
Original ARCADIA Capella uses "nested" elements (visually nested) to signify various relationships, such as "Containment relationship" or "Deployment relationship". Instead of using visually noisy arrows / graph edges. I pretty much like this approach and want to see something similar to it in this application
|
||||
|
||||
To explore the original ARCADIA Capella UI & semantics you may use web resources / web search / official and community docs. Or open the original ARCADIA Capella installed on this desktop machine for exploration
|
||||
|
||||
Your second, parallel, goal is to create a textual representation for ARCADIA, similar to SysML v2. So a user can either edit a system model using UI (graphical representation) or text. Both graphical and textual representations should be fully equivalent and have full parity. You may take inspiration from SysML v2. Both graphical and textual representations should have one overarching, concrete, unambiguous spec in reST format, similar to SysML v2 strictness
|
||||
The application should support both forms of editing
|
||||
The textual representation should be easily editable and understandble for both humans and AI
|
||||
The textual representation should be easily handled by version control tools such as Git
|
||||
The underlying semantics of the textual / graphical representation should support traceability of various development artifacts (for example - code, CAD files, electrical schematics, Docx files and anything in between)
|
||||
The textual representation editor should have syntax highlighting and linting
|
||||
|
||||
The project should also have a backend server / service that you can deploy. The backend server purpose is to give programmatic access to stored system models. So, there also should be a clear REST API for querying, editing and working with models. The purpose of the REST API is to allow automation, scripting and AI-driven workflows
|
||||
An example could be - writing custom plugins for various domain specific software (version control software, CAD software etc...) to establish traceability between development artifacts and a system model
|
||||
|
||||
The desktop application should contain built-in guide / help / tutorial with clear and concise info on modeling concepts, underlying semantics, common modeling scenarios and application features / how to use the app. It should be easy to navigate and explore
|
||||
|
||||
@Computer
|
||||
|
||||
### Semantic model / metamodel
|
||||
|
||||
Requirements:
|
||||
- Textual notation.
|
||||
- Graphical notation.
|
||||
- Constraints and calculations.
|
||||
- Quantities and units.
|
||||
- State and behavior modeling.
|
||||
- Reusable model libraries.
|
||||
- Strong, standardized specification.
|
||||
- Strong, standardized API.
|
||||
|
||||
#### Textual notation
|
||||
|
||||
Requirements:
|
||||
- Should not have visual noise and clutter.
|
||||
- Should have means to reduce cognitive load when developing system models.
|
||||
- Should be easily editable by humans. And AI agents.
|
||||
- System engineers should be able to easily develop system models (both big and small) using textual notation only.
|
||||
|
||||
### UI
|
||||
|
||||
The focus should be on reducing system engineer's cognitive load when creating / working with a system model. Regardless of the model size (big or small).
|
||||
|
||||
Long, homogeneous lists in the UI that require user to scroll far or search for a desired option for too long - is a bad design.
|
||||
|
||||
## Active issues / current guidance
|
||||
|
||||
Right now, the behavior & scenarios UI is hard to use. It contains static / uneditable diagrams, which is highly dubious. In addition, user workflow with the behavior & scenarios UI differ significantly from the other UIs - you can't edit diagrams and instead suppose to click on menus for model development
|
||||
|
||||
---
|
||||
|
||||
The current project state is nowhere near the usability of the original ARCADIA Capella (despite its downfalls) or its functionality / semantics. You should focus on that by studying original ARCADIA Capella very closely and implementing its features in a modern, ergonomic and easy to use way
|
||||
<!-- END USER-OWNED APPLICATION SPECIFICATION -->
|
||||
File diff suppressed because it is too large
Load Diff
+11
-24
@@ -34,7 +34,10 @@ fn occurrence(
|
||||
id
|
||||
}
|
||||
fn port(p: &mut Project, owner: Id, name: &str, direction: PortDirection, interface: Id) -> Id {
|
||||
let items = crate::data::interface_items(p, interface);
|
||||
let items = match p.elements[&interface].shared() {
|
||||
Some(SharedBody::Interface { items }) => items.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let contract = if p.elements[&owner].kind() == Kind::Function {
|
||||
if direction == PortDirection::Out {
|
||||
PortContract::FunctionOutput {
|
||||
@@ -165,11 +168,6 @@ pub fn coastal_rescue() -> Project {
|
||||
"Distress report",
|
||||
None,
|
||||
);
|
||||
if let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||
p.elements.get_mut(&oa_item).unwrap().shared_mut()
|
||||
{
|
||||
*mechanism = ExchangeMechanism::Event;
|
||||
}
|
||||
p.relate(RelationKind::Allocation, "Reports distress", vessel, report);
|
||||
p.relate(
|
||||
RelationKind::Allocation,
|
||||
@@ -310,11 +308,6 @@ pub fn coastal_rescue() -> Project {
|
||||
"Digital distress report",
|
||||
None,
|
||||
);
|
||||
if let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||
p.elements.get_mut(&information).unwrap().shared_mut()
|
||||
{
|
||||
*mechanism = ExchangeMechanism::Event;
|
||||
}
|
||||
let material = add(
|
||||
&mut p,
|
||||
Kind::ExchangeItem,
|
||||
@@ -324,8 +317,6 @@ pub fn coastal_rescue() -> Project {
|
||||
);
|
||||
*p.elements.get_mut(&material).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Material,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
let energy = add(
|
||||
&mut p,
|
||||
@@ -336,8 +327,6 @@ pub fn coastal_rescue() -> Project {
|
||||
);
|
||||
*p.elements.get_mut(&energy).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Energy,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
let field = add(
|
||||
&mut p,
|
||||
@@ -351,9 +340,6 @@ pub fn coastal_rescue() -> Project {
|
||||
minimum: 1,
|
||||
maximum: Some(1),
|
||||
unit: "degrees north".into(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
let interface = add(
|
||||
&mut p,
|
||||
@@ -362,12 +348,13 @@ pub fn coastal_rescue() -> Project {
|
||||
"Distress reporting interface",
|
||||
None,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Distress payload",
|
||||
interface,
|
||||
information,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface {
|
||||
items: vec![information],
|
||||
};
|
||||
let vessel_port = port(
|
||||
&mut p,
|
||||
actor,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3 migration.
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2 migration.
|
||||
#[path = "migration_v2/source.rs"]
|
||||
mod v2_source;
|
||||
use crate::*;
|
||||
@@ -340,8 +340,6 @@ fn reference_field(key: &str) -> bool {
|
||||
| "referenced"
|
||||
| "local"
|
||||
| "field"
|
||||
| "path"
|
||||
| "supertype"
|
||||
| "sender"
|
||||
| "receiver"
|
||||
| "action"
|
||||
@@ -380,14 +378,10 @@ fn enum_field(key: &str) -> bool {
|
||||
| "message_kind"
|
||||
| "transition_kind"
|
||||
| "side"
|
||||
| "mechanism"
|
||||
| "storage"
|
||||
| "transmission"
|
||||
| "acquisition"
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse V4, or explicitly migrate a valid V1/V2/V3 source candidate. Parsing never
|
||||
/// Parse V3, or explicitly migrate a valid V1/V2 source candidate. Parsing never
|
||||
/// adopts a model and never overwrites an editor draft.
|
||||
pub fn parse(source: &str) -> Result<Project, Vec<Diagnostic>> {
|
||||
parse_limited(source, ParseLimits::UNLIMITED)
|
||||
@@ -552,7 +546,7 @@ fn offset_for(source: &str, line: usize, column: usize) -> usize {
|
||||
/// nested records are ordinary language fields; JSON documents are not embedded.
|
||||
pub fn format(project: &Project) -> String {
|
||||
let mut out = std::format!(
|
||||
"arcadia 4;\nproject {} {} id \"{}\" {{\n description {};\n\n",
|
||||
"arcadia 3;\nproject {} {} id \"{}\" {{\n description {};\n\n",
|
||||
project.alias,
|
||||
quote(&project.name),
|
||||
project.id,
|
||||
@@ -1343,12 +1337,12 @@ impl Parser<'_> {
|
||||
.map(|(key, span)| ((d.id, key.clone()), span.start)),
|
||||
);
|
||||
}
|
||||
let decoded =
|
||||
let project =
|
||||
resolve_declarations(header.project, &unit.declarations, &aliases, self.limits)
|
||||
.map_err(|(_, error)| error)?;
|
||||
Ok(ParsedDocument {
|
||||
project: decoded.project,
|
||||
migration: decoded.migration,
|
||||
project,
|
||||
migration: None,
|
||||
locations,
|
||||
fields,
|
||||
})
|
||||
@@ -1356,11 +1350,9 @@ impl Parser<'_> {
|
||||
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
|
||||
let mut header = if entry {
|
||||
self.take("arcadia")?;
|
||||
if !matches!(self.text(), "3" | "4") {
|
||||
return Err(self
|
||||
.error("Unsupported schema version: expected 'arcadia 3;' or 'arcadia 4;'."));
|
||||
if self.text() != "3" {
|
||||
return Err(self.error("Unsupported schema version: expected 'arcadia 3;'."));
|
||||
}
|
||||
let schema_version = if self.text() == "3" { 3 } else { 4 };
|
||||
self.cursor += 1;
|
||||
self.take(";")?;
|
||||
let start = self.current().span.start;
|
||||
@@ -1373,7 +1365,7 @@ impl Parser<'_> {
|
||||
self.take("{")?;
|
||||
Some(SourceHeader {
|
||||
project: Project {
|
||||
schema_version,
|
||||
schema_version: SCHEMA_VERSION,
|
||||
id,
|
||||
alias,
|
||||
name,
|
||||
@@ -1548,9 +1540,7 @@ pub(crate) fn resolve_declarations(
|
||||
declarations: &[Declaration],
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let mut legacy_elements = serde_json::Map::new();
|
||||
) -> Result<Project, (usize, Error)> {
|
||||
let mut nodes = Vec::new();
|
||||
for (index, declaration) in declarations.iter().enumerate() {
|
||||
(|| -> Result<(), Error> {
|
||||
@@ -1571,9 +1561,6 @@ pub(crate) fn resolve_declarations(
|
||||
};
|
||||
match d.kind {
|
||||
DeclKind::Element(kind, layer) => {
|
||||
if legacy && !crate::persistence::v3::element_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) {
|
||||
return Err(conversion_error("Element constructor was not part of schema3.".into()));
|
||||
}
|
||||
let description =
|
||||
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
|
||||
let explicit_parent = d.fields.contains_key("parent");
|
||||
@@ -1614,17 +1601,9 @@ pub(crate) fn resolve_declarations(
|
||||
*viewpoint = ScenarioViewpoint::MixedLegacy;
|
||||
}
|
||||
let mut body = serde_json::to_value(initial).unwrap();
|
||||
let defaults = if legacy {
|
||||
crate::persistence::v3::legacy_defaults(kind)
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_else(|| payload(&body))
|
||||
} else { payload(&body) };
|
||||
let body_fields = merge_fields(defaults, fields).map_err(&conversion_error)?;
|
||||
let body_fields =
|
||||
merge_fields(payload(&body), fields).map_err(&conversion_error)?;
|
||||
replace_payload(&mut body, body_fields);
|
||||
if legacy {
|
||||
legacy_elements.insert(d.id.to_string(), serde_json::json!({"id":d.id,"alias":d.alias,"name":d.name,"description":description,"parent":parent,"body":body}));
|
||||
return Ok(());
|
||||
}
|
||||
let body: ElementBody = serde_json::from_value(body)
|
||||
.map_err(|e| conversion_error(e.to_string()))?;
|
||||
if body.kind() != kind || body.layer() != layer {
|
||||
@@ -1645,9 +1624,6 @@ pub(crate) fn resolve_declarations(
|
||||
);
|
||||
}
|
||||
DeclKind::Relation(kind) => {
|
||||
if legacy && !crate::persistence::v3::relation_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) {
|
||||
return Err(conversion_error("Relation constructor was not part of schema3.".into()));
|
||||
}
|
||||
let description =
|
||||
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
|
||||
let required: &[&str] = if kind == RelationKind::FunctionalExchange {
|
||||
@@ -1749,58 +1725,6 @@ pub(crate) fn resolve_declarations(
|
||||
.map_err(|error| (index, error))?;
|
||||
target.nodes.insert(node.id, node);
|
||||
}
|
||||
if legacy {
|
||||
let mut value = serde_json::to_value(&project).expect("project serialization");
|
||||
value["elements"] = Value::Object(legacy_elements);
|
||||
let migrated = serde_json::from_value(value)
|
||||
.map_err(crate::persistence::FileError::Json)
|
||||
.and_then(|old| crate::persistence::v3::migrate(old, limits))
|
||||
.map_err(|error| {
|
||||
let issue = if let crate::persistence::FileError::Model { issues, .. } = &error {
|
||||
issues.first()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let index = issue
|
||||
.and_then(|issue| issue.element)
|
||||
.and_then(|id| declarations.iter().position(|d| d.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
let offset = declarations.get(index).map_or(0, |d| {
|
||||
issue
|
||||
.and_then(|issue| issue.field.as_ref())
|
||||
.and_then(|field| d.field_spans.get(field))
|
||||
.map_or(d.offset, |span| span.start)
|
||||
});
|
||||
(
|
||||
index,
|
||||
Error {
|
||||
code: if matches!(error, crate::persistence::FileError::Limit(_)) {
|
||||
"model_limit"
|
||||
} else {
|
||||
"migration_error"
|
||||
},
|
||||
offset,
|
||||
message: error.to_string(),
|
||||
},
|
||||
)
|
||||
})?;
|
||||
for (index, d) in declarations.iter().enumerate() {
|
||||
if let DeclKind::Element(kind, layer) = d.kind {
|
||||
let body = &migrated.project.elements[&d.id].body;
|
||||
if body.kind() != kind || body.layer() != layer {
|
||||
return Err((
|
||||
index,
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: d.offset,
|
||||
message: "Declaration kind disagrees with its typed body.".into(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(migrated);
|
||||
}
|
||||
limits.check_model(&project).map_err(|message| {
|
||||
(
|
||||
usize::MAX,
|
||||
@@ -1811,10 +1735,7 @@ pub(crate) fn resolve_declarations(
|
||||
},
|
||||
)
|
||||
})?;
|
||||
Ok(crate::persistence::DecodeResult {
|
||||
project,
|
||||
migration: None,
|
||||
})
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
fn header(fields: &mut Map<String, Value>, d: &Declaration, named: bool) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::source_project::{ProjectLimits, SourceBundle, compile_bundle, lint_bu
|
||||
|
||||
fn project_source(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 4; project model \"Model\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}"
|
||||
"arcadia 3; project model \"Model\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ fn nested_fragment_id_stabilization_preserves_comments_and_indexes_real_bytes()
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project model \"Model\" { include \"children.arcadia\"; }".into(),
|
||||
"arcadia 3; project model \"Model\" { include \"children.arcadia\"; }".into(),
|
||||
),
|
||||
(
|
||||
"children.arcadia".into(),
|
||||
@@ -300,7 +300,7 @@ fn deep_declaration_parsing_and_positions_use_a_bounded_rust_stack() {
|
||||
|
||||
#[test]
|
||||
fn tolerant_nested_analysis_retains_closed_children_before_an_unfinished_sibling() {
|
||||
let source = "arcadia 4; project model \"航海\" {\noa_activity outer_one \"Outer\" {\n oa_activity ready \"Ready\" { description \"} is text\"; }\n oa_activity unfinished \"Unfinished\" { description \"unterminated";
|
||||
let source = "arcadia 3; project model \"航海\" {\noa_activity outer_one \"Outer\" {\n oa_activity ready \"Ready\" { description \"} is text\"; }\n oa_activity unfinished \"Unfinished\" { description \"unterminated";
|
||||
let analysis = analyze(source);
|
||||
let ready = analysis
|
||||
.declarations
|
||||
@@ -406,34 +406,12 @@ fn checked_in_source_project() -> SourceBundle {
|
||||
fn checked_in_nested_example_has_stable_ids_typed_exchanges_and_three_clear_views() {
|
||||
let input = checked_in_source_project();
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 3);
|
||||
for (path, source) in &input.files {
|
||||
let unit = parse_unit(source, path == &input.entry, ParseLimits::UNLIMITED).unwrap();
|
||||
assert!(
|
||||
unit.header
|
||||
.as_ref()
|
||||
.is_none_or(|header| header.id_insertion.is_none())
|
||||
);
|
||||
for declaration in unit.declarations {
|
||||
assert!(
|
||||
declaration.id_insertion.is_none(),
|
||||
"every example identity is already explicit"
|
||||
);
|
||||
assert_eq!(
|
||||
identity(&compiled.decoded.project, &declaration.alias),
|
||||
declaration.id
|
||||
);
|
||||
}
|
||||
}
|
||||
let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.bundle, compiled.bundle);
|
||||
assert!(reopened.decoded.migration.is_none());
|
||||
assert_eq!(compiled.diagnostics.len(), 2);
|
||||
assert_eq!(
|
||||
compiled.bundle, input,
|
||||
"every example identity is already explicit"
|
||||
);
|
||||
assert!(
|
||||
compiled
|
||||
.diagnostics
|
||||
.iter()
|
||||
.all(|issue| issue.diagnostic.code == "unspecified_exchange_mechanism"),
|
||||
compiled.diagnostics.is_empty(),
|
||||
"{:?}",
|
||||
compiled.diagnostics
|
||||
);
|
||||
@@ -495,15 +473,7 @@ fn checked_in_example_supports_cross_file_reparent_without_changing_allocations(
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.diagnostics.len(), 2);
|
||||
assert!(
|
||||
result
|
||||
.diagnostics
|
||||
.iter()
|
||||
.all(|issue| issue.diagnostic.code == "unspecified_exchange_mechanism"),
|
||||
"{:?}",
|
||||
result.diagnostics
|
||||
);
|
||||
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
|
||||
assert_eq!(
|
||||
result.index.declarations[&assess].path,
|
||||
"sa/assurance.arcadia"
|
||||
@@ -530,6 +500,6 @@ fn checked_in_example_supports_cross_file_reparent_without_changing_allocations(
|
||||
"sa/participants.arcadia",
|
||||
"views.arcadia",
|
||||
] {
|
||||
assert_eq!(result.bundle.files[path], compiled.bundle.files[path]);
|
||||
assert_eq!(result.bundle.files[path], input.files[path]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Framework-independent OA/SA model, transactional commands and editing services.
|
||||
pub mod data;
|
||||
pub mod language;
|
||||
pub mod model;
|
||||
pub mod persistence;
|
||||
|
||||
@@ -11,12 +11,10 @@ but constructs and validates the frozen DTO, preserves located old-contract
|
||||
errors, then calls `adapter.rs`. Original staged scenario/state defaults and
|
||||
generic port/optional FE endpoint contracts therefore remain explicit.
|
||||
|
||||
`adapter.rs` budgets generated data, validates V2 and transforms only the changed
|
||||
port/endpoint payloads into an explicit schema 3 candidate. The schema 3 reader
|
||||
then performs the data-contract upgrade and validates schema 4. Serialization
|
||||
between compatibility DTOs copies unchanged fields; original input bytes are
|
||||
never deserialized into the new `Project`. V1 normalization also constructs this
|
||||
V2 DTO before the shared stages. Combined generation stays within one budget.
|
||||
`adapter.rs` budgets generated data, validates V2, transforms only the changed
|
||||
payloads, and validates schema 3. Serialization between the two typed DTOs copies
|
||||
unchanged fields; original input bytes are never deserialized into the new
|
||||
`Project`. V1 normalization also constructs this V2 DTO before the shared stage.
|
||||
|
||||
Do not update these frozen shapes when extending the current model. Add another
|
||||
versioned migration when the persistent contract changes again. Golden V2
|
||||
|
||||
@@ -62,9 +62,16 @@ pub(crate) fn migrate(
|
||||
}],
|
||||
});
|
||||
}
|
||||
let mut report = MigrationReport::data_upgrade(2);
|
||||
let mut report = MigrationReport {
|
||||
from_schema: 2,
|
||||
to_schema: 3,
|
||||
preserved_ports: vec![],
|
||||
generated_endpoints: vec![],
|
||||
orientation_defaults: vec![],
|
||||
required_resolutions: vec![],
|
||||
};
|
||||
let mut value = serde_json::to_value(&old)?;
|
||||
value["schema_version"] = Value::from(3);
|
||||
value["schema_version"] = Value::from(SCHEMA_VERSION);
|
||||
for element in old.elements.values() {
|
||||
if let Some(super::model::SharedBody::Port {
|
||||
direction,
|
||||
@@ -197,23 +204,11 @@ pub(crate) fn migrate(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Carry the endpoint-generation reference charge into the next migration;
|
||||
// independent per-stage checks would undercount the combined generated graph.
|
||||
let data_limits = ParseLimits {
|
||||
max_step_references: limits
|
||||
.max_step_references
|
||||
.saturating_sub(additional_references),
|
||||
..limits
|
||||
};
|
||||
let migrated = super::super::v3::migrate(serde_json::from_value(value)?, data_limits)?;
|
||||
let data = migrated
|
||||
.migration
|
||||
.expect("the schema3 adapter reports its defaults");
|
||||
report.semantic_defaults = data.semantic_defaults;
|
||||
report.generated_interface_allocations = data.generated_interface_allocations;
|
||||
report.unresolved_message_values = data.unresolved_message_values;
|
||||
let project: Project = serde_json::from_value(value)?;
|
||||
limits.check_model(&project).map_err(FileError::Limit)?;
|
||||
super::super::validate_project(&project)?;
|
||||
Ok(DecodeResult {
|
||||
project: migrated.project,
|
||||
project,
|
||||
migration: Some(report),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Schema 3 to 4 reader
|
||||
|
||||
Schema 3 is a real compatibility input. Its shapes were frozen at `5ce3eef`.
|
||||
The small DTOs in `mod.rs` own the changed data payloads and old message values;
|
||||
closed element/relation constructor lists prevent new vocabulary from entering
|
||||
the old reader. Unchanged ports, diagram placements/routes, artifacts and traces
|
||||
reuse their typed records. The intermediate JSON bridge rejects duplicate keys.
|
||||
|
||||
Nature, bounded types, field defaults/direction/storage, and single supertypes
|
||||
receive explicit documented defaults. Interface lists become identified ordered
|
||||
allocations with UUID v5 namespace equal to the project UUID and name
|
||||
`v2-interface-item:<interface-uuid>:<item-uuid>`. The name remains the same for
|
||||
V2 and V3 inputs so the migration route cannot change the generated identity.
|
||||
Existing IDs never move; deterministic collisions fail with a located diagnostic.
|
||||
Aliases use the normal deterministic collision suffix. Unique legacy message
|
||||
paths are qualified; ambiguous annotations retain their expression and warning.
|
||||
|
||||
V1 first normalizes to the frozen V2 DTO. V2 port/endpoint normalization emits
|
||||
an explicit schema3 candidate and then runs this reader before current decoding.
|
||||
Generated object, record and reference budgets cover the combined stages.
|
||||
|
||||
`language::resolve_declarations` shares lexical/AST parsing, including nesting,
|
||||
but chooses frozen source3 payload defaults and rejects new constructors. Both
|
||||
single-file parsing and `source_project::compile_bundle` take this explicit path.
|
||||
The bundle compiler first stabilizes omitted IDs, then performs targeted token
|
||||
edits in `source_project/source_migration.rs`. Original files, includes, lexical
|
||||
ownership and comments survive. New allocation declarations stay in the source
|
||||
interface's module. The returned source and index describe schema4 bytes; loading
|
||||
does not rewrite original disk files. Canonical output and second-save stability
|
||||
are schema4. No whole-project formatting fallback handles authored source3.
|
||||
|
||||
Frozen source/JSON fixtures live under `tests/fixtures/v3-data-contract.*`; the
|
||||
original V2 golden files remain unchanged.
|
||||
@@ -1,630 +0,0 @@
|
||||
//! Explicit schema-3 compatibility. Only changed payloads are frozen here;
|
||||
//! unchanged typed records are shared after a closed old-constructor check.
|
||||
use crate::language::ParseLimits;
|
||||
use crate::persistence::{DecodeResult, FileError, MigrationReport};
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Project3 {
|
||||
pub schema_version: u32,
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub elements: BTreeMap<Id, Element3>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub relations: BTreeMap<Id, Relation3>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub diagrams: BTreeMap<Id, Diagram>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub artifact_links: BTreeMap<Id, ArtifactLink>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub traces: BTreeMap<Id, Trace>,
|
||||
}
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Element3 {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parent: Option<Id>,
|
||||
#[serde(deserialize_with = "strict_value")]
|
||||
pub body: Value,
|
||||
}
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Relation3 {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(deserialize_with = "strict_value")]
|
||||
pub body: Value,
|
||||
}
|
||||
|
||||
// A Value bridge must retain the strict duplicate-field behavior of typed DTOs.
|
||||
fn strict_value<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Value, D::Error> {
|
||||
struct Strict;
|
||||
impl<'de> serde::de::Visitor<'de> for Strict {
|
||||
type Value = Value;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("a value with unique object fields")
|
||||
}
|
||||
fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Value, E> {
|
||||
serde_json::Number::from_f64(v)
|
||||
.map(Value::Number)
|
||||
.ok_or_else(|| E::custom("Non-finite number"))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Value, E> {
|
||||
Ok(v.into())
|
||||
}
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn visit_none<E: serde::de::Error>(self) -> Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<Value, A::Error> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(v) = a.next_element::<StrictValue>()? {
|
||||
out.push(v.0);
|
||||
}
|
||||
Ok(Value::Array(out))
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut a: A) -> Result<Value, A::Error> {
|
||||
let mut out = serde_json::Map::new();
|
||||
while let Some((k, v)) = a.next_entry::<String, StrictValue>()? {
|
||||
if out.insert(k.clone(), v.0).is_some() {
|
||||
return Err(serde::de::Error::custom(format!("Duplicate field '{k}'")));
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
}
|
||||
}
|
||||
d.deserialize_any(Strict)
|
||||
}
|
||||
struct StrictValue(Value);
|
||||
impl<'de> Deserialize<'de> for StrictValue {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
strict_value(d).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum ItemKind3 {
|
||||
Information,
|
||||
Material,
|
||||
Energy,
|
||||
Event,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum Definition3 {
|
||||
Boolean,
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration { literals: Vec<String> },
|
||||
Record,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Item3 {
|
||||
kind: ItemKind3,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Type3 {
|
||||
definition: Definition3,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Field3 {
|
||||
type_ref: TypeRef,
|
||||
minimum: u32,
|
||||
maximum: Option<u32>,
|
||||
unit: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Interface3 {
|
||||
items: Vec<Id>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct MessageValue3 {
|
||||
field: Id,
|
||||
expression: String,
|
||||
}
|
||||
|
||||
pub(crate) fn relation_allowed(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"Allocation"
|
||||
| "PortAllocation"
|
||||
| "Interaction"
|
||||
| "CapabilityInvolvement"
|
||||
| "ChainInvolvement"
|
||||
| "RoleRealization"
|
||||
| "RoleAssignment"
|
||||
| "FunctionalExchange"
|
||||
| "BoundaryExchange"
|
||||
| "MissionCapability"
|
||||
| "CapabilityInclude"
|
||||
| "CapabilityExtend"
|
||||
| "CapabilityGeneralization"
|
||||
| "MissionInvolvement"
|
||||
| "SequenceLink"
|
||||
| "StateTransition"
|
||||
| "Availability"
|
||||
| "ExchangeAllocation"
|
||||
| "InterfaceProvision"
|
||||
| "InterfaceRequirement"
|
||||
| "StateAction"
|
||||
)
|
||||
}
|
||||
pub(crate) fn element_allowed(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"Entity"
|
||||
| "Actor"
|
||||
| "Role"
|
||||
| "Activity"
|
||||
| "Capability"
|
||||
| "OperationalMission"
|
||||
| "Process"
|
||||
| "System"
|
||||
| "SystemActor"
|
||||
| "Function"
|
||||
| "SystemCapability"
|
||||
| "Mission"
|
||||
| "FunctionalChain"
|
||||
| "ExchangeItem"
|
||||
| "DataType"
|
||||
| "DataField"
|
||||
| "Interface"
|
||||
| "Port"
|
||||
| "Scenario"
|
||||
| "Lifeline"
|
||||
| "Occurrence"
|
||||
| "StateMachine"
|
||||
| "Region"
|
||||
| "State"
|
||||
| "Mode"
|
||||
| "Initial"
|
||||
| "Final"
|
||||
| "Choice"
|
||||
| "Junction"
|
||||
| "Fork"
|
||||
| "Join"
|
||||
| "ShallowHistory"
|
||||
| "DeepHistory"
|
||||
| "EntryPoint"
|
||||
| "ExitPoint"
|
||||
| "Terminate"
|
||||
| "Event"
|
||||
| "Constraint"
|
||||
)
|
||||
}
|
||||
pub(crate) fn legacy_defaults(kind: Kind) -> Option<Value> {
|
||||
Some(match kind {
|
||||
Kind::ExchangeItem => json!({"kind":"Information"}),
|
||||
Kind::DataType => json!({"definition":"String"}),
|
||||
Kind::DataField => json!({"type_ref":"String","minimum":1,"maximum":1,"unit":""}),
|
||||
Kind::Interface => json!({"items":[]}),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn at(id: Id, field: &str, code: &str, message: impl Into<String>) -> FileError {
|
||||
let message = message.into();
|
||||
FileError::Model {
|
||||
message: format!("V3 {code} at {id}.{field}: {message}"),
|
||||
issues: vec![validation::Issue {
|
||||
severity: validation::Severity::Error,
|
||||
element: Some(id),
|
||||
code: code.into(),
|
||||
field: Some(field.into()),
|
||||
message,
|
||||
}],
|
||||
}
|
||||
}
|
||||
fn singleton(value: &Value) -> Option<(&str, &Value)> {
|
||||
let object = value.as_object()?;
|
||||
(object.len() == 1)
|
||||
.then(|| object.iter().next().unwrap())
|
||||
.map(|(k, v)| (k.as_str(), v))
|
||||
}
|
||||
fn old_payload<T: serde::de::DeserializeOwned>(
|
||||
id: Id,
|
||||
field: &str,
|
||||
value: Value,
|
||||
) -> Result<T, FileError> {
|
||||
serde_json::from_value(value).map_err(|error| at(id, field, "legacy_shape", error.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn migrate(old: Project3, limits: ParseLimits) -> Result<DecodeResult, FileError> {
|
||||
if old.schema_version != 3 {
|
||||
return Err(FileError::Invalid("Expected schema_version 3.".into()));
|
||||
}
|
||||
let graph = old.elements.len().saturating_add(old.relations.len());
|
||||
let records = graph
|
||||
.saturating_add(old.diagrams.len())
|
||||
.saturating_add(old.diagrams.values().map(|d| d.nodes.len()).sum::<usize>())
|
||||
.saturating_add(old.traces.len())
|
||||
.saturating_add(old.artifact_links.len())
|
||||
.saturating_add(1);
|
||||
limits
|
||||
.check_counts(graph, records, 0)
|
||||
.map_err(FileError::Limit)?;
|
||||
let mut report = MigrationReport::data_upgrade(3);
|
||||
let mut value = serde_json::to_value(&old)?;
|
||||
let mut allocations = Vec::new();
|
||||
for (key, e) in &old.elements {
|
||||
if *key != e.id {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"id",
|
||||
"identity",
|
||||
"Element map key differs from its identity.",
|
||||
));
|
||||
}
|
||||
let mut body = e.body.clone();
|
||||
let (layer, inner) = singleton(&body)
|
||||
.filter(|(name, _)| matches!(*name, "Oa" | "Sa"))
|
||||
.ok_or_else(|| {
|
||||
at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Expected one OA or SA constructor.",
|
||||
)
|
||||
})?;
|
||||
let layer = layer.to_owned();
|
||||
let outer = inner
|
||||
.as_str()
|
||||
.or_else(|| singleton(inner).map(|(name, _)| name));
|
||||
if !outer.is_some_and(|name| {
|
||||
if layer == "Oa" {
|
||||
matches!(
|
||||
name,
|
||||
"Entity"
|
||||
| "Actor"
|
||||
| "Role"
|
||||
| "Activity"
|
||||
| "Capability"
|
||||
| "Mission"
|
||||
| "Process"
|
||||
| "Shared"
|
||||
)
|
||||
} else {
|
||||
matches!(
|
||||
name,
|
||||
"System"
|
||||
| "Actor"
|
||||
| "Function"
|
||||
| "Capability"
|
||||
| "Mission"
|
||||
| "FunctionalChain"
|
||||
| "Shared"
|
||||
)
|
||||
}
|
||||
}) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Unknown schema3 layer constructor.",
|
||||
));
|
||||
}
|
||||
if let Some(("Shared", shared)) = singleton(inner) {
|
||||
let name = shared
|
||||
.as_str()
|
||||
.or_else(|| singleton(shared).map(|(name, _)| name))
|
||||
.ok_or_else(|| {
|
||||
at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Expected one schema3 shared constructor.",
|
||||
)
|
||||
})?
|
||||
.to_owned();
|
||||
if !matches!(
|
||||
name.as_str(),
|
||||
"ExchangeItem"
|
||||
| "DataType"
|
||||
| "DataField"
|
||||
| "Interface"
|
||||
| "Port"
|
||||
| "Scenario"
|
||||
| "Lifeline"
|
||||
| "Occurrence"
|
||||
| "StateMachine"
|
||||
| "Event"
|
||||
| "Region"
|
||||
| "State"
|
||||
| "Constraint"
|
||||
) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
format!("Unknown schema3 shared constructor '{name}'."),
|
||||
));
|
||||
}
|
||||
let payload = shared.get(&name).cloned().unwrap_or(Value::Null);
|
||||
let changed = match name.as_str() {
|
||||
"ExchangeItem" => {
|
||||
let old: Item3 = old_payload(e.id, "body.kind", payload)?;
|
||||
let (kind, mechanism) = match old.kind {
|
||||
ItemKind3::Information => ("Information", "Unspecified"),
|
||||
ItemKind3::Material => ("Material", "Unspecified"),
|
||||
ItemKind3::Energy => ("Energy", "Unspecified"),
|
||||
ItemKind3::Event => ("Information", "Event"),
|
||||
};
|
||||
Some(json!({"kind":kind,"mechanism":mechanism,"supertype":null}))
|
||||
}
|
||||
"DataType" => {
|
||||
let old: Type3 = old_payload(e.id, "body.definition", payload)?;
|
||||
let definition = match old.definition {
|
||||
Definition3::Boolean => json!("Boolean"),
|
||||
Definition3::Integer => json!({"Integer":{"minimum":null,"maximum":null}}),
|
||||
Definition3::Real => json!({"Real":{"minimum":null,"maximum":null}}),
|
||||
Definition3::String => {
|
||||
json!({"String":{"minimum_length":0,"maximum_length":null}})
|
||||
}
|
||||
Definition3::Enumeration { literals } => {
|
||||
json!({"Enumeration":{"literals":literals}})
|
||||
}
|
||||
Definition3::Record => json!({"Record":{"supertype":null}}),
|
||||
};
|
||||
Some(json!({"definition":definition,"default":null,"unit":""}))
|
||||
}
|
||||
"DataField" => {
|
||||
let old: Field3 = old_payload(e.id, "body", payload)?;
|
||||
Some(
|
||||
json!({"type_ref":old.type_ref,"minimum":old.minimum,"maximum":old.maximum,"unit":old.unit,"direction":"Unspecified","storage":"Value","default":null}),
|
||||
)
|
||||
}
|
||||
"Interface" => {
|
||||
let old: Interface3 = old_payload(e.id, "items", payload)?;
|
||||
let mut seen = BTreeSet::new();
|
||||
for (order, item) in old.items.into_iter().enumerate() {
|
||||
if !seen.insert(item) {
|
||||
return Err(at(
|
||||
e.id,
|
||||
"items",
|
||||
"duplicate_reference",
|
||||
"Interface items must be unique.",
|
||||
));
|
||||
}
|
||||
allocations.push((e.id, item, order));
|
||||
}
|
||||
Some(json!({"supertype":null}))
|
||||
}
|
||||
"Occurrence" => {
|
||||
let mut payload = payload;
|
||||
if let Some(values) = payload.pointer_mut("/body/Exchange/values") {
|
||||
let old: Vec<MessageValue3> =
|
||||
old_payload(e.id, "body.values", values.clone())?;
|
||||
*values=Value::Array(old.into_iter().map(|v|json!({"field":v.field,"expression":v.expression,"path":[]})).collect());
|
||||
}
|
||||
Some(payload)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(changed) = changed {
|
||||
let description = match name.as_str() {
|
||||
"ExchangeItem" if changed["mechanism"] == "Event" => {
|
||||
"Legacy Event nature becomes Information with Event mechanism; supertype is absent."
|
||||
}
|
||||
"ExchangeItem" => {
|
||||
"Nature is retained; mechanism is Unspecified and supertype is absent."
|
||||
}
|
||||
"DataType" => {
|
||||
"Existing type is retained with unbounded numeric/string limits, no scalar default or unit, and no Record supertype."
|
||||
}
|
||||
"DataField" => {
|
||||
"Type, ownership, multiplicity and unit are retained; direction is Unspecified, storage is Value and default is absent."
|
||||
}
|
||||
"Interface" => {
|
||||
"Items become identified ordered allocations with Unspecified protocols; supertype is absent."
|
||||
}
|
||||
_ => {
|
||||
"Value expressions are retained; unique enclosing field paths are resolved and ambiguous legacy paths remain unresolved."
|
||||
}
|
||||
};
|
||||
body[&layer]["Shared"][&name] = changed;
|
||||
report
|
||||
.semantic_defaults
|
||||
.push(crate::persistence::SemanticDefault {
|
||||
object: e.id,
|
||||
field: "body".into(),
|
||||
description: description.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Current deserialization checks the reused, unchanged outer/typed parts.
|
||||
let _: ElementBody = serde_json::from_value(body.clone())
|
||||
.map_err(|error| at(e.id, "body", "legacy_shape", error.to_string()))?;
|
||||
value["elements"][key.to_string()]["body"] = body;
|
||||
}
|
||||
for (key, r) in &old.relations {
|
||||
if *key != r.id {
|
||||
return Err(at(
|
||||
r.id,
|
||||
"id",
|
||||
"identity",
|
||||
"Relation map key differs from its identity.",
|
||||
));
|
||||
}
|
||||
if !singleton(&r.body).is_some_and(|(name, _)| relation_allowed(name)) {
|
||||
return Err(at(
|
||||
r.id,
|
||||
"body",
|
||||
"legacy_shape",
|
||||
"Relation constructor was not part of schema3.",
|
||||
));
|
||||
}
|
||||
}
|
||||
value["schema_version"] = SCHEMA_VERSION.into();
|
||||
let mut project: Project = serde_json::from_value(value)?;
|
||||
let mut references = project
|
||||
.elements
|
||||
.values()
|
||||
.map(|e| e.steps().map_or(0, Vec::len))
|
||||
.sum::<usize>()
|
||||
.saturating_add(allocations.len().saturating_mul(2));
|
||||
limits
|
||||
.check_counts(
|
||||
project
|
||||
.elements
|
||||
.len()
|
||||
.saturating_add(project.relations.len())
|
||||
.saturating_add(allocations.len()),
|
||||
project.identities().len().saturating_add(allocations.len()),
|
||||
references,
|
||||
)
|
||||
.map_err(FileError::Limit)?;
|
||||
let mut ids: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||
for (source, target, order) in allocations {
|
||||
let id = Id::new_v5(
|
||||
&project.id,
|
||||
format!("v2-interface-item:{source}:{target}").as_bytes(),
|
||||
);
|
||||
if !ids.insert(id) {
|
||||
return Err(at(
|
||||
source,
|
||||
"items",
|
||||
"identity_collision",
|
||||
format!(
|
||||
"Deterministic interface allocation {id} already belongs to another object."
|
||||
),
|
||||
));
|
||||
}
|
||||
let name = format!("{} · {}", project.named(source), project.named(target));
|
||||
let alias = project.fresh_alias(
|
||||
&format!(
|
||||
"{}_{}_allocation",
|
||||
project
|
||||
.elements
|
||||
.get(&source)
|
||||
.map_or("interface", |e| e.alias.as_str()),
|
||||
project
|
||||
.elements
|
||||
.get(&target)
|
||||
.map_or("item", |e| e.alias.as_str())
|
||||
),
|
||||
"interface_item_allocation",
|
||||
id,
|
||||
);
|
||||
project.relations.insert(
|
||||
id,
|
||||
Relation {
|
||||
id,
|
||||
alias: alias.clone(),
|
||||
name,
|
||||
description: String::new(),
|
||||
body: RelationBody::InterfaceItemAllocation {
|
||||
source,
|
||||
target,
|
||||
order: order.try_into().map_err(|_| {
|
||||
at(source, "items", "model_limit", "Too many interface items.")
|
||||
})?,
|
||||
transmission: TransmissionProtocol::Unspecified,
|
||||
acquisition: AcquisitionProtocol::Unspecified,
|
||||
},
|
||||
},
|
||||
);
|
||||
report.generated_interface_allocations.push(
|
||||
crate::persistence::GeneratedInterfaceAllocation {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
alias,
|
||||
order: order as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
let annotations: Vec<_> = project
|
||||
.elements
|
||||
.values()
|
||||
.filter_map(|e| match e.shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
values,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) => Some((e.id, *exchange, values.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (owner, exchange, values) in annotations {
|
||||
let Some(relation) = project.relations.get(&exchange) else {
|
||||
continue;
|
||||
};
|
||||
for (index, value) in values.into_iter().enumerate() {
|
||||
let resolution =
|
||||
crate::data::resolve_legacy_field_path(&project, relation, value.field)
|
||||
.map_err(|message| at(owner, "body.values", "model_limit", message))?;
|
||||
if let crate::data::LegacyFieldPathResolution::Unique(path) = resolution {
|
||||
if !path.is_empty() {
|
||||
references = references.saturating_add(path.len());
|
||||
limits
|
||||
.check_counts(
|
||||
project
|
||||
.elements
|
||||
.len()
|
||||
.saturating_add(project.relations.len()),
|
||||
project.identities().len(),
|
||||
references,
|
||||
)
|
||||
.map_err(FileError::Limit)?;
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project
|
||||
.elements
|
||||
.get_mut(&owner)
|
||||
.and_then(Element::shared_mut)
|
||||
{
|
||||
values[index].path = path;
|
||||
}
|
||||
}
|
||||
} else if matches!(
|
||||
resolution,
|
||||
crate::data::LegacyFieldPathResolution::Ambiguous
|
||||
) {
|
||||
report.unresolved_message_values.push(crate::persistence::UnresolvedMessageValue { occurrence:owner,field:value.field,reason:"Several carried paths reach this legacy field; choose an explicit argument path.".into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
limits.check_model(&project).map_err(FileError::Limit)?;
|
||||
super::validate_project(&project)?;
|
||||
Ok(DecodeResult {
|
||||
project,
|
||||
migration: Some(report),
|
||||
})
|
||||
}
|
||||
+13
-148
@@ -6,7 +6,7 @@ use std::{
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type Id = Uuid;
|
||||
pub const SCHEMA_VERSION: u32 = 4;
|
||||
pub const SCHEMA_VERSION: u32 = 3;
|
||||
pub const MAX_COORDINATE: f32 = 1_000_000.;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
@@ -265,79 +265,17 @@ pub enum ExchangeItemKind {
|
||||
Information,
|
||||
Material,
|
||||
Energy,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum ExchangeMechanism {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Flow,
|
||||
Event,
|
||||
Operation,
|
||||
SharedData,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum ParameterDirection {
|
||||
#[default]
|
||||
Unspecified,
|
||||
In,
|
||||
Out,
|
||||
InOut,
|
||||
Return,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum FieldStorage {
|
||||
#[default]
|
||||
Value,
|
||||
Reference,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum DataValue {
|
||||
Boolean(bool),
|
||||
Integer(i64),
|
||||
Real(f64),
|
||||
String(String),
|
||||
Enumeration(String),
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum TransmissionProtocol {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Synchronous,
|
||||
Asynchronous,
|
||||
Unicast,
|
||||
Multicast,
|
||||
Broadcast,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum AcquisitionProtocol {
|
||||
#[default]
|
||||
Unspecified,
|
||||
Read,
|
||||
Accept,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum DataTypeDefinition {
|
||||
Boolean,
|
||||
Integer {
|
||||
minimum: Option<i64>,
|
||||
maximum: Option<i64>,
|
||||
},
|
||||
Real {
|
||||
minimum: Option<f64>,
|
||||
maximum: Option<f64>,
|
||||
},
|
||||
String {
|
||||
minimum_length: u32,
|
||||
maximum_length: Option<u32>,
|
||||
},
|
||||
Enumeration {
|
||||
literals: Vec<String>,
|
||||
},
|
||||
Record {
|
||||
supertype: Option<Id>,
|
||||
},
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration { literals: Vec<String> },
|
||||
Record,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -515,8 +453,6 @@ pub struct LifelineBinding {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MessageValue {
|
||||
#[serde(default)]
|
||||
pub path: Vec<Id>,
|
||||
pub field: Id,
|
||||
pub expression: String,
|
||||
}
|
||||
@@ -566,25 +502,18 @@ pub enum OccurrenceBody {
|
||||
pub enum SharedBody {
|
||||
ExchangeItem {
|
||||
kind: ExchangeItemKind,
|
||||
mechanism: ExchangeMechanism,
|
||||
supertype: Option<Id>,
|
||||
},
|
||||
DataType {
|
||||
definition: DataTypeDefinition,
|
||||
default: Option<DataValue>,
|
||||
unit: String,
|
||||
},
|
||||
DataField {
|
||||
type_ref: TypeRef,
|
||||
minimum: u32,
|
||||
maximum: Option<u32>,
|
||||
unit: String,
|
||||
direction: ParameterDirection,
|
||||
storage: FieldStorage,
|
||||
default: Option<Vec<DataValue>>,
|
||||
},
|
||||
Interface {
|
||||
supertype: Option<Id>,
|
||||
items: Vec<Id>,
|
||||
},
|
||||
Port {
|
||||
contract: PortContract,
|
||||
@@ -743,27 +672,17 @@ impl ElementBody {
|
||||
match k {
|
||||
Kind::ExchangeItem => SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Unspecified,
|
||||
supertype: None,
|
||||
},
|
||||
Kind::DataType => SharedBody::DataType {
|
||||
definition: DataTypeDefinition::String {
|
||||
minimum_length: 0,
|
||||
maximum_length: None,
|
||||
},
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
definition: DataTypeDefinition::String,
|
||||
},
|
||||
Kind::DataField => SharedBody::DataField {
|
||||
type_ref: TypeRef::String,
|
||||
minimum: 1,
|
||||
maximum: Some(1),
|
||||
unit: String::new(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
},
|
||||
Kind::Interface => SharedBody::Interface { supertype: None },
|
||||
Kind::Interface => SharedBody::Interface { items: vec![] },
|
||||
// Port creation requires an explicit contract and owner.
|
||||
Kind::Port => return None,
|
||||
Kind::Scenario => SharedBody::Scenario {
|
||||
@@ -833,8 +752,6 @@ impl SharedBody {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RelationKind {
|
||||
InterfaceItemAllocation,
|
||||
DataAssociation,
|
||||
Allocation,
|
||||
PortAllocation,
|
||||
Interaction,
|
||||
@@ -858,9 +775,7 @@ pub enum RelationKind {
|
||||
StateAction,
|
||||
}
|
||||
impl RelationKind {
|
||||
pub const ALL: [Self; 23] = [
|
||||
Self::InterfaceItemAllocation,
|
||||
Self::DataAssociation,
|
||||
pub const ALL: [Self; 21] = [
|
||||
Self::Allocation,
|
||||
Self::PortAllocation,
|
||||
Self::Interaction,
|
||||
@@ -885,8 +800,6 @@ impl RelationKind {
|
||||
];
|
||||
pub fn permits(self, from: Kind, to: Kind) -> bool {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation => from == Kind::Interface && to == Kind::ExchangeItem,
|
||||
Self::DataAssociation => from == Kind::DataType && to == Kind::DataType,
|
||||
Self::Allocation => from.is_performer() && to.is_work(),
|
||||
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
|
||||
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
|
||||
@@ -942,8 +855,6 @@ impl RelationKind {
|
||||
}
|
||||
pub fn hint(self) -> &'static str {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation => "Interface → allocated exchange item",
|
||||
Self::DataAssociation => "Record type → record type",
|
||||
Self::Allocation => "Performer → leaf activity/function",
|
||||
Self::PortAllocation => "Component port → allocated function port",
|
||||
Self::Interaction => "OA activity → OA activity",
|
||||
@@ -975,8 +886,6 @@ impl RelationKind {
|
||||
impl fmt::Display for RelationKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::InterfaceItemAllocation => "Interface item allocation",
|
||||
Self::DataAssociation => "Data association",
|
||||
Self::Allocation => "Allocation",
|
||||
Self::PortAllocation => "Port allocation",
|
||||
Self::Interaction => "Interaction",
|
||||
@@ -1013,25 +922,6 @@ pub struct Relation {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum RelationBody {
|
||||
InterfaceItemAllocation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
order: u32,
|
||||
transmission: TransmissionProtocol,
|
||||
acquisition: AcquisitionProtocol,
|
||||
},
|
||||
DataAssociation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
source_role: String,
|
||||
target_role: String,
|
||||
source_minimum: u32,
|
||||
source_maximum: Option<u32>,
|
||||
target_minimum: u32,
|
||||
target_maximum: Option<u32>,
|
||||
source_navigable: bool,
|
||||
target_navigable: bool,
|
||||
},
|
||||
Allocation {
|
||||
source: Id,
|
||||
target: Id,
|
||||
@@ -1149,8 +1039,6 @@ impl Relation {
|
||||
impl RelationBody {
|
||||
pub fn kind(&self) -> RelationKind {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation { .. } => RelationKind::InterfaceItemAllocation,
|
||||
Self::DataAssociation { .. } => RelationKind::DataAssociation,
|
||||
Self::Allocation { .. } => RelationKind::Allocation,
|
||||
Self::PortAllocation { .. } => RelationKind::PortAllocation,
|
||||
Self::Interaction { .. } => RelationKind::Interaction,
|
||||
@@ -1176,9 +1064,7 @@ impl RelationBody {
|
||||
}
|
||||
pub fn endpoints(&self) -> (Id, Id) {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation { source, target, .. }
|
||||
| Self::DataAssociation { source, target, .. }
|
||||
| Self::Allocation { source, target }
|
||||
Self::Allocation { source, target }
|
||||
| Self::PortAllocation { source, target }
|
||||
| Self::Interaction { source, target, .. }
|
||||
| Self::CapabilityInvolvement { source, target }
|
||||
@@ -1203,9 +1089,7 @@ impl RelationBody {
|
||||
}
|
||||
pub fn set_endpoints(&mut self, a: Id, b: Id) {
|
||||
match self {
|
||||
Self::InterfaceItemAllocation { source, target, .. }
|
||||
| Self::DataAssociation { source, target, .. }
|
||||
| Self::Allocation { source, target }
|
||||
Self::Allocation { source, target }
|
||||
| Self::PortAllocation { source, target }
|
||||
| Self::Interaction { source, target, .. }
|
||||
| Self::CapabilityInvolvement { source, target }
|
||||
@@ -1241,25 +1125,6 @@ impl RelationBody {
|
||||
}
|
||||
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
|
||||
match kind {
|
||||
RelationKind::InterfaceItemAllocation => Self::InterfaceItemAllocation {
|
||||
source,
|
||||
target,
|
||||
order: 0,
|
||||
transmission: TransmissionProtocol::Unspecified,
|
||||
acquisition: AcquisitionProtocol::Unspecified,
|
||||
},
|
||||
RelationKind::DataAssociation => Self::DataAssociation {
|
||||
source,
|
||||
target,
|
||||
source_role: String::new(),
|
||||
target_role: String::new(),
|
||||
source_minimum: 0,
|
||||
source_maximum: None,
|
||||
target_minimum: 0,
|
||||
target_maximum: None,
|
||||
source_navigable: false,
|
||||
target_navigable: false,
|
||||
},
|
||||
RelationKind::Allocation => Self::Allocation { source, target },
|
||||
RelationKind::PortAllocation => Self::PortAllocation { source, target },
|
||||
RelationKind::Interaction => Self::Interaction {
|
||||
|
||||
@@ -5,8 +5,6 @@ use thiserror::Error;
|
||||
#[path = "migration_v2/mod.rs"]
|
||||
pub(crate) mod v2;
|
||||
pub(crate) use v2::adapter::migrate as migrate_v2_limited;
|
||||
#[path = "migration_v3/mod.rs"]
|
||||
pub(crate) mod v3;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -17,50 +15,6 @@ pub struct MigrationReport {
|
||||
pub generated_endpoints: Vec<GeneratedEndpoint>,
|
||||
pub orientation_defaults: Vec<OrientationDefault>,
|
||||
pub required_resolutions: Vec<Id>,
|
||||
#[serde(default)]
|
||||
pub semantic_defaults: Vec<SemanticDefault>,
|
||||
#[serde(default)]
|
||||
pub generated_interface_allocations: Vec<GeneratedInterfaceAllocation>,
|
||||
#[serde(default)]
|
||||
pub unresolved_message_values: Vec<UnresolvedMessageValue>,
|
||||
}
|
||||
impl MigrationReport {
|
||||
pub(crate) fn data_upgrade(from_schema: u32) -> Self {
|
||||
Self {
|
||||
from_schema,
|
||||
to_schema: SCHEMA_VERSION,
|
||||
preserved_ports: vec![],
|
||||
generated_endpoints: vec![],
|
||||
orientation_defaults: vec![],
|
||||
required_resolutions: vec![],
|
||||
semantic_defaults: vec![],
|
||||
generated_interface_allocations: vec![],
|
||||
unresolved_message_values: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SemanticDefault {
|
||||
pub object: Id,
|
||||
pub field: String,
|
||||
pub description: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GeneratedInterfaceAllocation {
|
||||
pub id: Id,
|
||||
pub source: Id,
|
||||
pub target: Id,
|
||||
pub alias: String,
|
||||
pub order: u32,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UnresolvedMessageValue {
|
||||
pub occurrence: Id,
|
||||
pub field: Id,
|
||||
pub reason: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -147,14 +101,13 @@ pub fn decode_with_report_limited(
|
||||
migrate_v1_with_report_limited(old, limits)?
|
||||
}
|
||||
Some(2) => migrate_v2_limited(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(3) => v3::migrate(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(4) => DecodeResult {
|
||||
Some(3) => DecodeResult {
|
||||
project: serde_json::from_slice(bytes)?,
|
||||
migration: None,
|
||||
},
|
||||
_ => {
|
||||
return Err(FileError::Invalid(
|
||||
"Unsupported or missing schema_version; expected 1, 2, 3 or 4.".into(),
|
||||
"Unsupported or missing schema_version; expected 1, 2 or 3.".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ pub struct LegacyWorkPortSplitRequest {
|
||||
pub outgoing_items: Vec<Id>,
|
||||
}
|
||||
|
||||
/// A specialized carried item satisfies a declared base item.
|
||||
pub fn item_satisfies(project: &Project, carried: Id, declared: Id) -> bool {
|
||||
crate::data::item_satisfies(project, carried, declared)
|
||||
/// R6 specialization has one explicit seam; current models support identity only.
|
||||
pub fn item_satisfies(_project: &Project, carried: Id, declared: Id) -> bool {
|
||||
carried == declared
|
||||
}
|
||||
pub fn items_satisfy(project: &Project, carried: &[Id], declared: &[Id]) -> bool {
|
||||
carried.iter().all(|item| {
|
||||
@@ -130,7 +130,13 @@ fn declared_interface_items(p: &Project, port: Id) -> Option<Vec<Id>> {
|
||||
Some(
|
||||
interfaces
|
||||
.into_iter()
|
||||
.flat_map(|id| crate::data::interface_items(p, id))
|
||||
.filter_map(|id| p.elements.get(&id))
|
||||
.filter_map(|e| match e.shared() {
|
||||
Some(SharedBody::Interface { items }) => Some(items),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.copied()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -897,10 +903,14 @@ fn validate_carried_contract(p: &Project, r: &Relation, port: &Element, out: &mu
|
||||
..
|
||||
}) => {
|
||||
let mut allowed = declared_interface_items(p, port.id);
|
||||
if let Some(interface) = legacy.as_ref().and_then(|l| l.interface) {
|
||||
let items = crate::data::interface_items(p, interface);
|
||||
if let Some(SharedBody::Interface { items }) = legacy
|
||||
.as_ref()
|
||||
.and_then(|l| l.interface)
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.and_then(Element::shared)
|
||||
{
|
||||
if r.kind() == RelationKind::BoundaryExchange
|
||||
&& !items_satisfy(p, r.body.items(), &items)
|
||||
&& !items_satisfy(p, r.body.items(), items)
|
||||
{
|
||||
error(
|
||||
out,
|
||||
@@ -1163,7 +1173,7 @@ pub(crate) fn validate_boundary_interface_requirements(
|
||||
if !p.relations.values().any(|role| {
|
||||
role.kind() == RelationKind::InterfaceProvision
|
||||
&& role.source() == peer.id
|
||||
&& crate::data::interface_satisfies(p, role.target(), required.target())
|
||||
&& role.target() == required.target()
|
||||
}) {
|
||||
warning(
|
||||
out,
|
||||
|
||||
@@ -335,11 +335,6 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
.collect();
|
||||
let mut edges = Vec::new();
|
||||
for r in p.relations.values() {
|
||||
// Interface allocations are selectable nested dictionary rows. Their
|
||||
// independently identified contracts do not add noisy diagram arrows.
|
||||
if r.kind() == RelationKind::InterfaceItemAllocation {
|
||||
continue;
|
||||
}
|
||||
if d.projection == ProjectionKind::Allocation && r.kind() == RelationKind::Allocation {
|
||||
continue;
|
||||
}
|
||||
@@ -375,8 +370,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
}
|
||||
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
||||
&& (source != target
|
||||
|| ((d.projection.state_machine().is_some()
|
||||
|| r.kind() == RelationKind::DataAssociation)
|
||||
|| (d.projection.state_machine().is_some()
|
||||
&& r.source() == r.target()
|
||||
&& nodes
|
||||
.iter()
|
||||
@@ -402,7 +396,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
||||
RelationBody::BoundaryExchange {
|
||||
oriented: false,
|
||||
..
|
||||
} | RelationBody::DataAssociation { .. }
|
||||
}
|
||||
),
|
||||
source_port: source_port.filter(|id| visible_ports.contains(id)),
|
||||
target_port: target_port.filter(|id| visible_ports.contains(id)),
|
||||
|
||||
@@ -33,13 +33,8 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
||||
type_ref: TypeRef::Named(id),
|
||||
..
|
||||
} => visit(id),
|
||||
SharedBody::Interface { supertype }
|
||||
| SharedBody::ExchangeItem { supertype, .. }
|
||||
| SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype },
|
||||
..
|
||||
} => {
|
||||
if let Some(id) = supertype {
|
||||
SharedBody::Interface { items } => {
|
||||
for id in items {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
@@ -88,9 +83,6 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
||||
visit(id);
|
||||
}
|
||||
for value in values {
|
||||
for id in &mut value.path {
|
||||
visit(id);
|
||||
}
|
||||
visit(&mut value.field);
|
||||
}
|
||||
}
|
||||
@@ -385,12 +377,7 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
}
|
||||
if let Some(shared) = e.shared_mut() {
|
||||
match shared {
|
||||
SharedBody::Interface { supertype }
|
||||
| SharedBody::ExchangeItem { supertype, .. }
|
||||
| SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype },
|
||||
..
|
||||
} => clear(supertype),
|
||||
SharedBody::Interface { items } => items.retain(|id| !removed.contains(id)),
|
||||
SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
} => items.retain(|id| !removed.contains(id)),
|
||||
@@ -426,10 +413,7 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
clear(sender);
|
||||
clear(receiver);
|
||||
clear(reply_to);
|
||||
values.retain(|value| {
|
||||
!removed.contains(&value.field)
|
||||
&& value.path.iter().all(|id| !removed.contains(id))
|
||||
});
|
||||
values.retain(|value| !removed.contains(&value.field));
|
||||
}
|
||||
OccurrenceBody::Action { action, lifeline } => {
|
||||
clear(action);
|
||||
@@ -501,37 +485,20 @@ fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||
exchange: Some(id), ..
|
||||
},
|
||||
..
|
||||
}) => p.relations.get(id).map(|r| {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = e.shared()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let retained: Vec<_> = values
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
crate::data::carried_value_path(p, r, &value.path, value.field).is_some()
|
||||
|| (value.path.is_empty()
|
||||
&& crate::data::carried_field_paths(p, r)
|
||||
.iter()
|
||||
.any(|f| f.field == value.field))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
(e.id, retained)
|
||||
}),
|
||||
}) => p
|
||||
.relations
|
||||
.get(id)
|
||||
.map(|r| (e.id, crate::validation::carried_fields(p, r))),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (id, retained) in allowed {
|
||||
for (id, fields) in allowed {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&id).and_then(Element::shared_mut)
|
||||
{
|
||||
*values = retained;
|
||||
values.retain(|value| fields.contains(&value.field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Deterministic, bounded diagram geometry. Only authored pins belong in the model;
|
||||
//! callers share this derived geometry for drawing, picking, fitting and export.
|
||||
use crate::projection::{self, ProjectedEdge, ProjectedNode, ProjectedView};
|
||||
use crate::{Diagram, Id, Position, Project, Relation, RelationBody, RelationKind, Size};
|
||||
use crate::{Diagram, Id, Position, Project, RelationKind, Size};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
||||
use std::fmt;
|
||||
@@ -55,122 +55,6 @@ pub struct EdgeLabelGeometry {
|
||||
pub bounds: RoutingBounds,
|
||||
}
|
||||
|
||||
/// Derived labels and navigation markers for the two named association ends.
|
||||
/// Canvas and SVG consume these same coordinates; no extra diagram state exists.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AssociationEndGeometry {
|
||||
pub source_end: bool,
|
||||
pub label: EdgeLabelGeometry,
|
||||
pub tip: Position,
|
||||
pub inward: Position,
|
||||
pub navigable: bool,
|
||||
}
|
||||
|
||||
pub fn association_end_geometry(
|
||||
relation: &Relation,
|
||||
routed: &RoutedEdge,
|
||||
) -> Vec<AssociationEndGeometry> {
|
||||
let RelationBody::DataAssociation {
|
||||
source_navigable,
|
||||
target_navigable,
|
||||
..
|
||||
} = &relation.body
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut ends: Vec<AssociationEndGeometry> = Vec::new();
|
||||
for (source_end, navigable) in [(true, *source_navigable), (false, *target_navigable)] {
|
||||
let Some(tip) = (if source_end {
|
||||
routed.points.first()
|
||||
} else {
|
||||
routed.points.last()
|
||||
})
|
||||
.copied() else {
|
||||
continue;
|
||||
};
|
||||
let inward = if source_end {
|
||||
routed.points.iter().find(|p| **p != tip)
|
||||
} else {
|
||||
routed.points.iter().rev().find(|p| **p != tip)
|
||||
};
|
||||
let Some(inward) = inward.copied() else {
|
||||
continue;
|
||||
};
|
||||
let full = crate::data::association_end_label(relation, source_end).unwrap_or_default();
|
||||
let text = if let Some((role, multiplicity)) = full.rsplit_once(" [") {
|
||||
let role = if role.chars().count() > 32 {
|
||||
role.chars()
|
||||
.take(31)
|
||||
.chain(std::iter::once('…'))
|
||||
.collect::<String>()
|
||||
} else {
|
||||
role.to_owned()
|
||||
};
|
||||
format!("{role} [{multiplicity}")
|
||||
} else {
|
||||
full
|
||||
};
|
||||
let size = Size {
|
||||
width: text.chars().count() as f32 * 7.0 + 12.0,
|
||||
height: 22.0,
|
||||
};
|
||||
let dx = inward.x - tip.x;
|
||||
let dy = inward.y - tip.y;
|
||||
let length = dx.hypot(dy);
|
||||
let direction = pos(dx / length, dy / length);
|
||||
// One normal sign puts opposite endpoints of a straight edge on
|
||||
// opposite sides because their inward tangent directions are reversed.
|
||||
let along =
|
||||
direction.x.abs() * size.width / 2.0 + direction.y.abs() * size.height / 2.0 + 12.0;
|
||||
let normal =
|
||||
direction.y.abs() * size.width / 2.0 + direction.x.abs() * size.height / 2.0 + 8.0;
|
||||
let mut center = pos(
|
||||
tip.x + direction.x * along - direction.y * normal,
|
||||
tip.y + direction.y * along + direction.x * normal,
|
||||
);
|
||||
if let Some(other) = ends.first()
|
||||
&& (center.x - other.label.center.x).abs()
|
||||
< (size.width + other.label.bounds.size.width) / 2.0 + 6.0
|
||||
&& (center.y - other.label.center.y).abs()
|
||||
< (size.height + other.label.bounds.size.height) / 2.0 + 6.0
|
||||
{
|
||||
// Same-side anchors on a self-loop can still be close. Separate
|
||||
// their complete bounds along this end's normal in one step.
|
||||
let perpendicular = pos(-direction.y, direction.x);
|
||||
let distance = (center.x - other.label.center.x) * perpendicular.x
|
||||
+ (center.y - other.label.center.y) * perpendicular.y;
|
||||
let clearance = (perpendicular.x.abs() * (size.width + other.label.bounds.size.width)
|
||||
+ perpendicular.y.abs() * (size.height + other.label.bounds.size.height))
|
||||
/ 2.0
|
||||
+ 8.0;
|
||||
let shift = if distance < 0.0 {
|
||||
-clearance - distance
|
||||
} else {
|
||||
clearance - distance
|
||||
};
|
||||
center.x += perpendicular.x * shift;
|
||||
center.y += perpendicular.y * shift;
|
||||
}
|
||||
ends.push(AssociationEndGeometry {
|
||||
source_end,
|
||||
label: EdgeLabelGeometry {
|
||||
text,
|
||||
fraction: if source_end { 0.0 } else { 1.0 },
|
||||
anchor: tip,
|
||||
center,
|
||||
bounds: RoutingBounds {
|
||||
position: pos(center.x - size.width / 2.0, center.y - size.height / 2.0),
|
||||
size,
|
||||
},
|
||||
},
|
||||
tip,
|
||||
inward,
|
||||
navigable,
|
||||
});
|
||||
}
|
||||
ends
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RouteWarning {
|
||||
PinInsideObstacle { pin: usize, obstacle: Id },
|
||||
@@ -582,14 +466,6 @@ fn route_diagram_impl(
|
||||
// never changes paths or authored metadata. Previews retain other cached
|
||||
// labels and avoid the paths actually shown during the gesture.
|
||||
let mut previous_labels = Vec::new();
|
||||
for routed in edges.values() {
|
||||
if let Some(relation) = project.relations.get(&routed.id) {
|
||||
for end in association_end_geometry(relation, routed) {
|
||||
bounds.rect(end.label.bounds.position, end.label.bounds.size);
|
||||
previous_labels.push(end.label.bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in ordered {
|
||||
let Some(routed) = edges.get_mut(&edge.id) else {
|
||||
continue;
|
||||
@@ -703,7 +579,7 @@ fn route_edge(
|
||||
let mut candidates = Vec::new();
|
||||
for a in endpoints(source, edge.source_port, view) {
|
||||
for b in endpoints(target, edge.target_port, view) {
|
||||
// A real self-relation must leave and return at distinct anchors.
|
||||
// A real self-transition must leave and return at distinct anchors.
|
||||
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||
if edge.source == edge.target
|
||||
&& edge.source_port.is_none()
|
||||
@@ -1231,8 +1107,7 @@ fn label_text(project: &Project, edge: Id) -> String {
|
||||
let text = match relation.kind() {
|
||||
RelationKind::Interaction
|
||||
| RelationKind::FunctionalExchange
|
||||
| RelationKind::BoundaryExchange
|
||||
| RelationKind::DataAssociation => relation.name.as_str(),
|
||||
| RelationKind::BoundaryExchange => relation.name.as_str(),
|
||||
RelationKind::CapabilityInclude => "«include»",
|
||||
RelationKind::CapabilityExtend => "«extend»",
|
||||
RelationKind::CapabilityGeneralization => "specializes",
|
||||
|
||||
@@ -114,7 +114,6 @@ pub struct PreparedCommand {
|
||||
struct IdentityProvenance {
|
||||
original: BTreeSet<Id>,
|
||||
current_to_original: BTreeMap<Id, Id>,
|
||||
explicit_replacement: bool,
|
||||
}
|
||||
|
||||
impl IdentityProvenance {
|
||||
@@ -124,12 +123,10 @@ impl IdentityProvenance {
|
||||
Self {
|
||||
original,
|
||||
current_to_original,
|
||||
explicit_replacement: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(&mut self, project: &Project) {
|
||||
self.explicit_replacement = true;
|
||||
let present: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||
self.current_to_original = self
|
||||
.original
|
||||
@@ -183,13 +180,6 @@ pub fn prepare_command(
|
||||
)?;
|
||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||
provenance.retain_present(&candidate);
|
||||
if !provenance.explicit_replacement {
|
||||
crate::data::prune_removed_inheritance(
|
||||
project,
|
||||
&mut candidate,
|
||||
&provenance.current_to_original,
|
||||
);
|
||||
}
|
||||
validate_candidate(&candidate)?;
|
||||
Ok(PreparedCommand {
|
||||
project: candidate,
|
||||
@@ -297,13 +287,6 @@ pub fn execute_batch(project: &Project, commands: &[Command]) -> Result<Project,
|
||||
)?;
|
||||
}
|
||||
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||
if !provenance.explicit_replacement {
|
||||
crate::data::prune_removed_inheritance(
|
||||
project,
|
||||
&mut candidate,
|
||||
&provenance.current_to_original,
|
||||
);
|
||||
}
|
||||
validate_candidate(&candidate)?;
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
@@ -999,7 +999,7 @@ mod transaction_tests {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(directory.path().join("a.arcadia"), "oa_activity a \"A\" {}").unwrap();
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
//! Pure, bounded source projects. All includes resolve against supplied UTF-8
|
||||
//! Pure, bounded source3 projects. All includes resolve against supplied UTF-8
|
||||
//! documents; this module never reads a filesystem, URL, or process directory.
|
||||
use crate::{Id, Kind, Project, language, persistence::DecodeResult, validation};
|
||||
use language::{Diagnostic, ParseLimits, Span};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
#[path = "source_project/source_migration.rs"]
|
||||
mod source_migration;
|
||||
#[path = "source_project/source_structure.rs"]
|
||||
mod source_structure;
|
||||
|
||||
@@ -280,24 +278,21 @@ pub fn lint_bundle(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDi
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile reachable source3/4 files, explicitly upgrade data contracts, and stabilize UUIDs.
|
||||
/// Compile all reachable files as one source3 model and stabilize omitted UUIDs.
|
||||
/// Missing/unreachable files and every admission or engineering error leave the
|
||||
/// input untouched. V1/V2 single-file migration remains in `language::parse`.
|
||||
pub fn compile_bundle(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<CompiledSourceProject, Vec<SourceDiagnostic>> {
|
||||
let (mut compiled, insertions) = compile_once(bundle, limits)?;
|
||||
if !insertions.is_empty() {
|
||||
let mut stable = bundle.clone();
|
||||
apply_edits(&mut stable, insertions)?;
|
||||
// Re-index actual returned bytes before performing the schema upgrade.
|
||||
compiled = compile_once(&stable, limits)?.0;
|
||||
let (compiled, insertions) = compile_once(bundle, limits)?;
|
||||
if insertions.is_empty() {
|
||||
return Ok(compiled);
|
||||
}
|
||||
if compiled.decoded.migration.is_some() {
|
||||
return source_migration::upgrade(&compiled, limits);
|
||||
}
|
||||
Ok(compiled)
|
||||
let mut stable = bundle.clone();
|
||||
apply_edits(&mut stable, insertions)?;
|
||||
// Re-index the actual returned bytes. Never expose offsets into pre-ID text.
|
||||
compile_once(&stable, limits).map(|(compiled, _)| compiled)
|
||||
}
|
||||
|
||||
fn compile_once(
|
||||
@@ -573,7 +568,7 @@ fn compile_once(
|
||||
span,
|
||||
}),
|
||||
};
|
||||
let decoded =
|
||||
let project =
|
||||
language::resolve_declarations(header.project, &declarations, &aliases, limits.model)
|
||||
.map_err(|(index, error)| {
|
||||
let path = declaration_paths.get(index).unwrap_or(&bundle.entry);
|
||||
@@ -583,7 +578,7 @@ fn compile_once(
|
||||
related: Vec::new(),
|
||||
}]
|
||||
})?;
|
||||
let diagnostics = model_diagnostics(bundle, &index, &decoded.project);
|
||||
let diagnostics = model_diagnostics(bundle, &index, &project);
|
||||
let errors: Vec<_> = diagnostics
|
||||
.iter()
|
||||
.filter(|issue| issue.diagnostic.severity == validation::Severity::Error)
|
||||
@@ -595,7 +590,10 @@ fn compile_once(
|
||||
Ok((
|
||||
CompiledSourceProject {
|
||||
bundle: bundle.clone(),
|
||||
decoded,
|
||||
decoded: DecodeResult {
|
||||
project,
|
||||
migration: None,
|
||||
},
|
||||
index,
|
||||
diagnostics,
|
||||
},
|
||||
@@ -720,7 +718,7 @@ fn apply_edits(bundle: &mut SourceBundle, edits: Edits) -> Result<(), Vec<Source
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Canonical, readable source4 export. Like `language::format`, the caller is
|
||||
/// Canonical, readable source3 export. Like `language::format`, the caller is
|
||||
/// responsible for supplying a valid model; compilation checks the result.
|
||||
/// Existing source layout is preserved by `update_bundle`, not this formatter.
|
||||
pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
@@ -736,7 +734,7 @@ pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
.push_str(&unindent(&declaration));
|
||||
}
|
||||
let mut entry = format!(
|
||||
"arcadia 4;\n{}{{\n description {};\n",
|
||||
"arcadia 3;\n{}{{\n description {};\n",
|
||||
project_header(project),
|
||||
quote(&project.description)
|
||||
);
|
||||
@@ -925,7 +923,6 @@ pub fn update_bundle_with_identity_map(
|
||||
after: new_text,
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
};
|
||||
let local = edits.entry(path.clone()).or_default();
|
||||
update.header(&actual.header, &previous.header, &next.header, local);
|
||||
@@ -954,7 +951,6 @@ pub fn update_bundle_with_identity_map(
|
||||
after: &after,
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
};
|
||||
let mut local = Vec::new();
|
||||
update.header(
|
||||
@@ -1123,9 +1119,6 @@ struct SyntaxUpdate<'a> {
|
||||
after: &'a str,
|
||||
old_aliases: &'a BTreeMap<String, String>,
|
||||
new_aliases: &'a BTreeMap<String, String>,
|
||||
// Source3 migration only adds path qualification; the existing field identity
|
||||
// must keep its authored argument trivia during that one-time transformation.
|
||||
legacy_value_paths: bool,
|
||||
}
|
||||
impl SyntaxUpdate<'_> {
|
||||
fn header(
|
||||
@@ -1345,11 +1338,11 @@ impl SyntaxUpdate<'_> {
|
||||
let previous = before.unwrap_or(&[]);
|
||||
let old_keys: Option<Vec<_>> = previous
|
||||
.iter()
|
||||
.map(|item| item_key(item, self.before, self.old_aliases, self.legacy_value_paths))
|
||||
.map(|item| item_key(item, self.before, self.old_aliases))
|
||||
.collect();
|
||||
let new_keys: Option<Vec<_>> = after
|
||||
.iter()
|
||||
.map(|item| item_key(item, self.after, self.new_aliases, self.legacy_value_paths))
|
||||
.map(|item| item_key(item, self.after, self.new_aliases))
|
||||
.collect();
|
||||
let keyed = old_keys.zip(new_keys).filter(|(old, new)| {
|
||||
old.iter().collect::<BTreeSet<_>>().len() == old.len()
|
||||
@@ -1456,7 +1449,6 @@ fn item_key(
|
||||
value: &language::EditValue,
|
||||
source: &str,
|
||||
aliases: &BTreeMap<String, String>,
|
||||
legacy_value_paths: bool,
|
||||
) -> Option<String> {
|
||||
use language::EditKind;
|
||||
match &value.kind {
|
||||
@@ -1470,31 +1462,7 @@ fn item_key(
|
||||
.find_map(|key| {
|
||||
fields.get(key).and_then(|field| {
|
||||
if let EditKind::Reference(alias) = &field.value.kind {
|
||||
aliases.get(alias).map(|id| {
|
||||
if key == "field" && !legacy_value_paths {
|
||||
let path = fields
|
||||
.get("path")
|
||||
.and_then(|field| {
|
||||
if let EditKind::List(path) = &field.value.kind {
|
||||
path.iter()
|
||||
.map(|hop| {
|
||||
if let EditKind::Reference(alias) = &hop.kind {
|
||||
aliases.get(alias).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
format!("field:{}:{id}", path.join("/"))
|
||||
} else {
|
||||
format!("{key}:{id}")
|
||||
}
|
||||
})
|
||||
aliases.get(alias).map(|id| format!("{key}:{id}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//! Source3 data upgrade through token edits; authored modules, nesting and trivia survive.
|
||||
use super::*;
|
||||
|
||||
pub(super) fn upgrade(
|
||||
current: &CompiledSourceProject,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<CompiledSourceProject, Vec<SourceDiagnostic>> {
|
||||
let report = current
|
||||
.decoded
|
||||
.migration
|
||||
.as_ref()
|
||||
.expect("an explicit source migration report");
|
||||
let project = ¤t.decoded.project;
|
||||
let canonical: BTreeMap<_, _> = language::format_declarations(project).into_iter().collect();
|
||||
let changed: BTreeSet<_> = report
|
||||
.semantic_defaults
|
||||
.iter()
|
||||
.map(|default| default.object)
|
||||
.collect();
|
||||
let aliases: BTreeMap<_, _> = project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.map(|(id, alias)| (alias.to_owned(), id.to_string()))
|
||||
.collect();
|
||||
let mut edits: Edits = BTreeMap::new();
|
||||
for (path, source) in ¤t.bundle.files {
|
||||
let declarations =
|
||||
language::edit_declarations(source, path == ¤t.bundle.entry, limits.model)
|
||||
.map_err(|diagnostic| {
|
||||
vec![SourceDiagnostic {
|
||||
path: path.clone(),
|
||||
diagnostic: *diagnostic,
|
||||
related: vec![],
|
||||
}]
|
||||
})?;
|
||||
for (id, actual) in declarations {
|
||||
if !changed.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
let Some(after) = canonical.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
let mut next = language::edit_declaration(after).map_err(|diagnostic| {
|
||||
vec![SourceDiagnostic {
|
||||
path: path.clone(),
|
||||
diagnostic: *diagnostic,
|
||||
related: vec![],
|
||||
}]
|
||||
})?;
|
||||
if actual.lexical_owner.is_some()
|
||||
&& let language::EditKind::Record(fields) = &actual.fields.kind
|
||||
&& !fields.contains_key("parent")
|
||||
&& let language::EditKind::Record(fields) = &mut next.fields.kind
|
||||
{
|
||||
fields.remove("parent");
|
||||
}
|
||||
let update = SyntaxUpdate {
|
||||
source,
|
||||
before: source,
|
||||
after,
|
||||
old_aliases: &aliases,
|
||||
new_aliases: &aliases,
|
||||
legacy_value_paths: true,
|
||||
};
|
||||
update.value(
|
||||
&actual.fields,
|
||||
Some(&actual.fields),
|
||||
&next.fields,
|
||||
edits.entry(path.clone()).or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let source = ¤t.bundle.files[¤t.bundle.entry];
|
||||
let version = significant(source)
|
||||
.get(1)
|
||||
.map(|token| token.span)
|
||||
.expect("a parsed version token");
|
||||
edits
|
||||
.entry(current.bundle.entry.clone())
|
||||
.or_default()
|
||||
.push((version, "4".into()));
|
||||
// New allocation relations are emitted into the allocating interface's
|
||||
// existing module, outside its lexical element body. Existing source never moves.
|
||||
let mut added: BTreeMap<String, String> = BTreeMap::new();
|
||||
for allocation in &report.generated_interface_allocations {
|
||||
let path = current
|
||||
.index
|
||||
.declarations
|
||||
.get(&allocation.source)
|
||||
.map(|location| location.path.clone())
|
||||
.unwrap_or_else(|| current.bundle.entry.clone());
|
||||
let ending = newline(¤t.bundle.files[&path]);
|
||||
let text = added.entry(path).or_default();
|
||||
text.push_str(ending);
|
||||
text.push_str(&canonical[&allocation.id].replace('\n', ending));
|
||||
}
|
||||
for (path, text) in added {
|
||||
let offset = if path == current.bundle.entry {
|
||||
current.index.project_body.span.end
|
||||
} else {
|
||||
current.bundle.files[&path].len()
|
||||
};
|
||||
edits.entry(path).or_default().push((
|
||||
Span {
|
||||
start: offset,
|
||||
end: offset,
|
||||
},
|
||||
text,
|
||||
));
|
||||
}
|
||||
let mut bundle = current.bundle.clone();
|
||||
apply_edits(&mut bundle, edits)?;
|
||||
let (mut result, insertions) = compile_once(&bundle, limits)?;
|
||||
if !insertions.is_empty() || result.decoded.project != *project {
|
||||
return Err(vec![problem(
|
||||
&bundle,
|
||||
&bundle.entry,
|
||||
Span::default(),
|
||||
"migration_source_mismatch",
|
||||
"Targeted source3 upgrade did not reproduce the migrated project.",
|
||||
)]);
|
||||
}
|
||||
result.decoded.migration = Some(report.clone());
|
||||
Ok(result)
|
||||
}
|
||||
@@ -251,8 +251,6 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option<Relat
|
||||
}
|
||||
(
|
||||
RelationBody::Allocation { .. }
|
||||
| RelationBody::InterfaceItemAllocation { .. }
|
||||
| RelationBody::DataAssociation { .. }
|
||||
| RelationBody::ExchangeAllocation { .. }
|
||||
| RelationBody::CapabilityInvolvement { .. }
|
||||
| RelationBody::CapabilityInclude { .. }
|
||||
@@ -423,24 +421,6 @@ pub fn plan_transition(
|
||||
}
|
||||
if let Some(e) = project.elements.get(&id) {
|
||||
selected.extend(crate::references::element_references(e));
|
||||
// Allocations are owned by interfaces. Independent data
|
||||
// associations travel only when both endpoint types are
|
||||
// already required; they do not expand the dictionary.
|
||||
if matches!(e.kind(), Kind::Interface | Kind::DataType) {
|
||||
let contracts: Vec<_> = project
|
||||
.relations
|
||||
.values()
|
||||
.filter_map(|r| {
|
||||
((r.kind() == RelationKind::InterfaceItemAllocation
|
||||
&& r.source() == id)
|
||||
|| (r.kind() == RelationKind::DataAssociation
|
||||
&& selected.contains(&r.source())
|
||||
&& selected.contains(&r.target())))
|
||||
.then_some(r.id)
|
||||
})
|
||||
.collect();
|
||||
selected.extend(contracts);
|
||||
}
|
||||
// Contextual links are owned by the process/chain shared by
|
||||
// their endpoint involvements, independently of list order.
|
||||
if matches!(e.kind(), Kind::Process | Kind::FunctionalChain) {
|
||||
|
||||
@@ -388,10 +388,68 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
}
|
||||
if let Some(shared) = e.shared() {
|
||||
match shared {
|
||||
SharedBody::DataType { .. }
|
||||
| SharedBody::DataField { .. }
|
||||
| SharedBody::Interface { .. }
|
||||
| SharedBody::ExchangeItem { .. } => crate::data::validate_element(p, e, &mut out),
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration { literals },
|
||||
} => {
|
||||
let mut seen = BTreeSet::new();
|
||||
for literal in literals {
|
||||
text(&mut out, e.id, "literals", literal);
|
||||
if !seen.insert(literal) {
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"duplicate_literal",
|
||||
"literals",
|
||||
"Enumeration literals must be unique.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SharedBody::DataField {
|
||||
type_ref,
|
||||
minimum,
|
||||
maximum,
|
||||
unit,
|
||||
} => {
|
||||
if maximum.is_some_and(|max| max < *minimum) {
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"invalid_multiplicity",
|
||||
"maximum",
|
||||
"Maximum multiplicity must be at least minimum, or unbounded.",
|
||||
);
|
||||
}
|
||||
text(&mut out, e.id, "unit", unit);
|
||||
if let TypeRef::Named(id) = type_ref {
|
||||
expect_element(p, &mut out, e.id, *id, "type_ref", |k| k == Kind::DataType);
|
||||
}
|
||||
if let Some(parent) = e.parent.and_then(|id| p.elements.get(&id))
|
||||
&& parent.kind() == Kind::DataType
|
||||
&& !matches!(
|
||||
parent.shared(),
|
||||
Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record
|
||||
})
|
||||
)
|
||||
{
|
||||
error(
|
||||
&mut out,
|
||||
e.id,
|
||||
"field_owner",
|
||||
"parent",
|
||||
"Fields under a data type require a Record type.",
|
||||
);
|
||||
}
|
||||
}
|
||||
SharedBody::Interface { items } => {
|
||||
check_ids(&mut out, e.id, "items", items);
|
||||
for id in items {
|
||||
expect_element(p, &mut out, e.id, *id, "items", |k| {
|
||||
k == Kind::ExchangeItem
|
||||
});
|
||||
}
|
||||
}
|
||||
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
|
||||
SharedBody::Scenario {
|
||||
viewpoint,
|
||||
@@ -585,11 +643,9 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
text(&mut out, r.id, "name", &r.name);
|
||||
text(&mut out, r.id, "description", &r.description);
|
||||
validate_relation(p, r, &mut out);
|
||||
crate::data::validate_relation(p, r, &mut out);
|
||||
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
|
||||
validate_relation_context(p, r, &mut out);
|
||||
}
|
||||
crate::data::validate_record_cycles(p, &mut out);
|
||||
for (key, d) in &p.diagrams {
|
||||
if *key != d.id {
|
||||
error(
|
||||
@@ -1380,10 +1436,23 @@ fn enclosing_loops(p: &Project, occurrence: Id) -> BTreeSet<Id> {
|
||||
}
|
||||
/// Fields carried by an exchange, including fields of nested named records.
|
||||
pub fn carried_fields(p: &Project, relation: &Relation) -> BTreeSet<Id> {
|
||||
crate::data::carried_field_paths(p, relation)
|
||||
.into_iter()
|
||||
.map(|f| f.field)
|
||||
.collect()
|
||||
let mut pending = relation.body.items().to_vec();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut fields = BTreeSet::new();
|
||||
while let Some(owner) = pending.pop() {
|
||||
if !seen.insert(owner) {
|
||||
continue;
|
||||
}
|
||||
for field in p.children(owner) {
|
||||
if let Some(SharedBody::DataField { type_ref, .. }) = field.shared() {
|
||||
fields.insert(field.id);
|
||||
if let TypeRef::Named(id) = type_ref {
|
||||
pending.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fields
|
||||
}
|
||||
fn occurrence_uses_lifeline(body: &OccurrenceBody, line: Id) -> bool {
|
||||
match body {
|
||||
@@ -1614,125 +1683,36 @@ fn validate_message(p: &Project, e: &Element, body: &OccurrenceBody, out: &mut V
|
||||
}
|
||||
}
|
||||
}
|
||||
let relation = exchange.and_then(|id| p.relations.get(&id));
|
||||
if message_kind == &MessageKind::Call
|
||||
&& relation.is_some_and(|r| {
|
||||
!r.body
|
||||
.items()
|
||||
.iter()
|
||||
.any(|id| crate::data::item_mechanism(p, *id) == Some(ExchangeMechanism::Operation))
|
||||
})
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
"call_without_operation",
|
||||
"exchange",
|
||||
"This generic call carries no Operation item; define an operation contract when appropriate.",
|
||||
);
|
||||
}
|
||||
let allowed = exchange
|
||||
.and_then(|id| p.relations.get(&id))
|
||||
.map(|r| carried_fields(p, r));
|
||||
let mut fields = BTreeSet::new();
|
||||
for value in values {
|
||||
expect_element(p, out, e.id, value.field, "values.field", |kind| {
|
||||
kind == Kind::DataField
|
||||
});
|
||||
for hop in &value.path {
|
||||
expect_element(p, out, e.id, *hop, "values.path", |kind| {
|
||||
kind == Kind::DataField
|
||||
});
|
||||
}
|
||||
text(out, e.id, "values.expression", &value.expression);
|
||||
if !fields.insert((&value.path, value.field)) {
|
||||
if !fields.insert(value.field) {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"duplicate_message_value",
|
||||
"values",
|
||||
"Each qualified carried field has at most one contextual value annotation.",
|
||||
"Each carried field has at most one contextual value annotation in a message.",
|
||||
);
|
||||
}
|
||||
if value.path.len() > crate::data::MAX_VALUE_PATH_DEPTH {
|
||||
if allowed
|
||||
.as_ref()
|
||||
.is_some_and(|allowed| !allowed.contains(&value.field))
|
||||
{
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_value_path_limit",
|
||||
"values.path",
|
||||
"The value path exceeds the bounded authoring depth.",
|
||||
"message_value_field",
|
||||
"values.field",
|
||||
"Value annotations must reference fields carried by this message's exchange, including nested record fields.",
|
||||
);
|
||||
}
|
||||
if let Some(r) = relation {
|
||||
if let Some(path) = crate::data::carried_value_path(p, r, &value.path, value.field) {
|
||||
if crate::data::item_mechanism(p, path.item) == Some(ExchangeMechanism::Operation) {
|
||||
let permitted = match message_kind {
|
||||
MessageKind::Call => matches!(
|
||||
path.direction,
|
||||
ParameterDirection::Unspecified
|
||||
| ParameterDirection::In
|
||||
| ParameterDirection::InOut
|
||||
),
|
||||
MessageKind::Reply => matches!(
|
||||
path.direction,
|
||||
ParameterDirection::Unspecified
|
||||
| ParameterDirection::Out
|
||||
| ParameterDirection::InOut
|
||||
| ParameterDirection::Return
|
||||
),
|
||||
_ => true,
|
||||
};
|
||||
if !permitted {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_parameter_phase",
|
||||
"values.path",
|
||||
"Call values address In/InOut parameters; replies address Out/InOut/Return parameters.",
|
||||
);
|
||||
}
|
||||
if path.direction == ParameterDirection::Unspecified {
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
"message_parameter_direction",
|
||||
"values.path",
|
||||
"The containing operation parameter has no direction yet.",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let legacy = value
|
||||
.path
|
||||
.is_empty()
|
||||
.then(|| crate::data::resolve_legacy_field_path(p, r, value.field).ok())
|
||||
.flatten();
|
||||
if matches!(
|
||||
legacy,
|
||||
Some(
|
||||
crate::data::LegacyFieldPathResolution::Unique(_)
|
||||
| crate::data::LegacyFieldPathResolution::Ambiguous
|
||||
)
|
||||
) {
|
||||
warning(
|
||||
out,
|
||||
e.id,
|
||||
if legacy == Some(crate::data::LegacyFieldPathResolution::Ambiguous) {
|
||||
"message_value_ambiguous_path"
|
||||
} else {
|
||||
"message_value_missing_path"
|
||||
},
|
||||
"values.path",
|
||||
"This legacy nested field annotation needs an explicit containing parameter path; its expression is preserved.",
|
||||
);
|
||||
} else {
|
||||
error(
|
||||
out,
|
||||
e.id,
|
||||
"message_value_field",
|
||||
"values.path",
|
||||
"The qualified field must be carried by the exchange, following Value record fields only.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if value.expression.trim().is_empty() {
|
||||
warning(
|
||||
out,
|
||||
@@ -2150,11 +2130,7 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
||||
};
|
||||
if a.layer() != b.layer()
|
||||
|| !r.kind().permits(a.kind(), b.kind())
|
||||
|| (a.id == b.id
|
||||
&& !matches!(
|
||||
r.kind(),
|
||||
RelationKind::StateTransition | RelationKind::DataAssociation
|
||||
))
|
||||
|| (a.id == b.id && r.kind() != RelationKind::StateTransition)
|
||||
{
|
||||
error(
|
||||
out,
|
||||
@@ -2405,10 +2381,8 @@ pub fn can_parent(p: &Project, child_kind: Kind, layer: Layer, parent: Option<Id
|
||||
owner.layer() == layer
|
||||
&& parent_permits(owner.kind(), child_kind)
|
||||
&& match owner.shared() {
|
||||
Some(SharedBody::DataType { definition, .. })
|
||||
if child_kind == Kind::DataField =>
|
||||
{
|
||||
matches!(definition, DataTypeDefinition::Record { .. })
|
||||
Some(SharedBody::DataType { definition }) if child_kind == Kind::DataField => {
|
||||
*definition == DataTypeDefinition::Record
|
||||
}
|
||||
Some(SharedBody::Occurrence { body, .. }) if child_kind == Kind::Occurrence => {
|
||||
matches!(
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// Frozen authored schema3 compatibility input. Do not rewrite as current syntax.
|
||||
arcadia 3;
|
||||
project legacy_data "Legacy data contract" id "00000000-0000-0000-0000-000000000001" {
|
||||
sa_function produce "Produce" id "00000000-0000-0000-0000-000000000002" {}
|
||||
sa_function consume "Consume" id "00000000-0000-0000-0000-000000000003" {}
|
||||
sa_exchange_item packet "Position event" id "00000000-0000-0000-0000-000000000004" {
|
||||
kind Event;
|
||||
// Reused record parameters retain their own identities.
|
||||
sa_data_field origin "Origin" id "00000000-0000-0000-0000-000000000007" { type_ref { Named @coordinate; }; }
|
||||
sa_data_field destination "Destination" id "00000000-0000-0000-0000-000000000008" { type_ref { Named @coordinate; }; }
|
||||
}
|
||||
sa_data_type coordinate "Coordinate" id "00000000-0000-0000-0000-000000000005" {
|
||||
definition Record;
|
||||
sa_data_field latitude "Latitude" id "00000000-0000-0000-0000-000000000006" { type_ref Real; unit "deg"; }
|
||||
}
|
||||
sa_interface sender_contract "Sender contract" id "00000000-0000-0000-0000-000000000009" { items [@packet]; }
|
||||
sa_interface receiver_contract "Receiver contract" id "00000000-0000-0000-0000-00000000000a" { items [@packet]; }
|
||||
sa_port output "Output" id "00000000-0000-0000-0000-00000000000b" {
|
||||
parent @produce; contract { FunctionOutput { outgoing_items [@packet]; }; }; legacy none;
|
||||
}
|
||||
sa_port input "Input" id "00000000-0000-0000-0000-00000000000c" {
|
||||
parent @consume; contract { FunctionInput { incoming_items [@packet]; }; }; legacy none;
|
||||
}
|
||||
functional_exchange position_exchange "Position exchange" id "00000000-0000-0000-0000-00000000000d" {
|
||||
source @produce; target @consume; source_port @output; target_port @input; items [@packet];
|
||||
}
|
||||
sa_scenario scenario "Position scenario" id "00000000-0000-0000-0000-00000000000e" {
|
||||
viewpoint Functional; steps [@message];
|
||||
sa_lifeline sender "Sender" id "00000000-0000-0000-0000-00000000000f" { represented @produce; }
|
||||
sa_lifeline receiver "Receiver" id "00000000-0000-0000-0000-000000000010" { represented @consume; }
|
||||
sa_occurrence message "Position" id "00000000-0000-0000-0000-000000000011" {
|
||||
body { Exchange {
|
||||
exchange @position_exchange; sender @sender; receiver @receiver;
|
||||
values [{ field @latitude; expression "navigation.latitude"; }];
|
||||
}; };
|
||||
}
|
||||
}
|
||||
}
|
||||
-312
@@ -1,312 +0,0 @@
|
||||
{
|
||||
"schema_version": 3,
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"alias": "legacy_data",
|
||||
"name": "Legacy data contract",
|
||||
"description": "",
|
||||
"elements": {
|
||||
"00000000-0000-0000-0000-000000000002": {
|
||||
"id": "00000000-0000-0000-0000-000000000002",
|
||||
"alias": "produce",
|
||||
"name": "Produce",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": "Function"
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000003": {
|
||||
"id": "00000000-0000-0000-0000-000000000003",
|
||||
"alias": "consume",
|
||||
"name": "Consume",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": "Function"
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000004": {
|
||||
"id": "00000000-0000-0000-0000-000000000004",
|
||||
"alias": "packet",
|
||||
"name": "Position event",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"ExchangeItem": {
|
||||
"kind": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000005": {
|
||||
"id": "00000000-0000-0000-0000-000000000005",
|
||||
"alias": "coordinate",
|
||||
"name": "Coordinate",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"DataType": {
|
||||
"definition": "Record"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000006": {
|
||||
"id": "00000000-0000-0000-0000-000000000006",
|
||||
"alias": "latitude",
|
||||
"name": "Latitude",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-000000000005",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"DataField": {
|
||||
"type_ref": "Real",
|
||||
"minimum": 1,
|
||||
"maximum": 1,
|
||||
"unit": "deg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000007": {
|
||||
"id": "00000000-0000-0000-0000-000000000007",
|
||||
"alias": "origin",
|
||||
"name": "Origin",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-000000000004",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"DataField": {
|
||||
"type_ref": {
|
||||
"Named": "00000000-0000-0000-0000-000000000005"
|
||||
},
|
||||
"minimum": 1,
|
||||
"maximum": 1,
|
||||
"unit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000008": {
|
||||
"id": "00000000-0000-0000-0000-000000000008",
|
||||
"alias": "destination",
|
||||
"name": "Destination",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-000000000004",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"DataField": {
|
||||
"type_ref": {
|
||||
"Named": "00000000-0000-0000-0000-000000000005"
|
||||
},
|
||||
"minimum": 1,
|
||||
"maximum": 1,
|
||||
"unit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000009": {
|
||||
"id": "00000000-0000-0000-0000-000000000009",
|
||||
"alias": "sender_contract",
|
||||
"name": "Sender contract",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Interface": {
|
||||
"items": [
|
||||
"00000000-0000-0000-0000-000000000004"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-00000000000a": {
|
||||
"id": "00000000-0000-0000-0000-00000000000a",
|
||||
"alias": "receiver_contract",
|
||||
"name": "Receiver contract",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Interface": {
|
||||
"items": [
|
||||
"00000000-0000-0000-0000-000000000004"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-00000000000b": {
|
||||
"id": "00000000-0000-0000-0000-00000000000b",
|
||||
"alias": "output",
|
||||
"name": "Output",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-000000000002",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Port": {
|
||||
"contract": {
|
||||
"FunctionOutput": {
|
||||
"outgoing_items": [
|
||||
"00000000-0000-0000-0000-000000000004"
|
||||
]
|
||||
}
|
||||
},
|
||||
"legacy": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-00000000000c": {
|
||||
"id": "00000000-0000-0000-0000-00000000000c",
|
||||
"alias": "input",
|
||||
"name": "Input",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-000000000003",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Port": {
|
||||
"contract": {
|
||||
"FunctionInput": {
|
||||
"incoming_items": [
|
||||
"00000000-0000-0000-0000-000000000004"
|
||||
]
|
||||
}
|
||||
},
|
||||
"legacy": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-00000000000e": {
|
||||
"id": "00000000-0000-0000-0000-00000000000e",
|
||||
"alias": "scenario",
|
||||
"name": "Position scenario",
|
||||
"description": "",
|
||||
"parent": null,
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Scenario": {
|
||||
"viewpoint": "Functional",
|
||||
"context": null,
|
||||
"precondition": "",
|
||||
"postcondition": "",
|
||||
"steps": [
|
||||
"00000000-0000-0000-0000-000000000011"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-00000000000f": {
|
||||
"id": "00000000-0000-0000-0000-00000000000f",
|
||||
"alias": "sender",
|
||||
"name": "Sender",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-00000000000e",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Lifeline": {
|
||||
"represented": "00000000-0000-0000-0000-000000000002"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000010": {
|
||||
"id": "00000000-0000-0000-0000-000000000010",
|
||||
"alias": "receiver",
|
||||
"name": "Receiver",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-00000000000e",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Lifeline": {
|
||||
"represented": "00000000-0000-0000-0000-000000000003"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000011": {
|
||||
"id": "00000000-0000-0000-0000-000000000011",
|
||||
"alias": "message",
|
||||
"name": "Position",
|
||||
"description": "",
|
||||
"parent": "00000000-0000-0000-0000-00000000000e",
|
||||
"body": {
|
||||
"Sa": {
|
||||
"Shared": {
|
||||
"Occurrence": {
|
||||
"body": {
|
||||
"Exchange": {
|
||||
"exchange": "00000000-0000-0000-0000-00000000000d",
|
||||
"sender": "00000000-0000-0000-0000-00000000000f",
|
||||
"receiver": "00000000-0000-0000-0000-000000000010",
|
||||
"message_kind": "Asynchronous",
|
||||
"reply_to": null,
|
||||
"values": [
|
||||
{
|
||||
"field": "00000000-0000-0000-0000-000000000006",
|
||||
"expression": "navigation.latitude"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"precondition": "",
|
||||
"postcondition": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"00000000-0000-0000-0000-00000000000d": {
|
||||
"id": "00000000-0000-0000-0000-00000000000d",
|
||||
"alias": "position_exchange",
|
||||
"name": "Position exchange",
|
||||
"description": "",
|
||||
"body": {
|
||||
"FunctionalExchange": {
|
||||
"source": "00000000-0000-0000-0000-000000000002",
|
||||
"target": "00000000-0000-0000-0000-000000000003",
|
||||
"source_port": "00000000-0000-0000-0000-00000000000b",
|
||||
"target_port": "00000000-0000-0000-0000-00000000000c",
|
||||
"items": [
|
||||
"00000000-0000-0000-0000-000000000004"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagrams": {},
|
||||
"artifact_links": {},
|
||||
"traces": {}
|
||||
}
|
||||
@@ -168,20 +168,6 @@ project mission "Harbor safety — 港 🌊" {
|
||||
.find(|e| e.alias == "event_record")
|
||||
.unwrap()
|
||||
.id;
|
||||
let associated = p
|
||||
.add_element(Kind::DataType, Layer::Sa, "Associated record", None)
|
||||
.unwrap();
|
||||
if let Some(SharedBody::DataType { definition, .. }) =
|
||||
p.elements.get_mut(&associated).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Record { supertype: None };
|
||||
}
|
||||
p.relate(
|
||||
RelationKind::DataAssociation,
|
||||
"Related record",
|
||||
record,
|
||||
associated,
|
||||
);
|
||||
for (index, type_ref) in [
|
||||
TypeRef::Boolean,
|
||||
TypeRef::Integer,
|
||||
@@ -204,30 +190,18 @@ project mission "Harbor safety — 港 🌊" {
|
||||
minimum: 1,
|
||||
maximum: Some(5),
|
||||
unit: "unit".into(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
}
|
||||
for definition in [
|
||||
DataTypeDefinition::Boolean,
|
||||
DataTypeDefinition::Integer {
|
||||
minimum: None,
|
||||
maximum: None,
|
||||
},
|
||||
DataTypeDefinition::String {
|
||||
minimum_length: 0,
|
||||
maximum_length: None,
|
||||
},
|
||||
DataTypeDefinition::Integer,
|
||||
DataTypeDefinition::String,
|
||||
] {
|
||||
let id = p
|
||||
.add_element(Kind::DataType, Layer::Sa, "Primitive type", None)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
||||
definition,
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
};
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() =
|
||||
SharedBody::DataType { definition };
|
||||
}
|
||||
for kind in [
|
||||
ExchangeItemKind::Material,
|
||||
@@ -242,11 +216,7 @@ project mission "Harbor safety — 港 🌊" {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind,
|
||||
mechanism: ExchangeMechanism::Unspecified,
|
||||
supertype: None,
|
||||
};
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem { kind };
|
||||
}
|
||||
let scenario = p
|
||||
.elements
|
||||
@@ -491,7 +461,7 @@ fn authoring_defaults_and_forward_references_remain_strict() {
|
||||
#[test]
|
||||
fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() {
|
||||
for (source, expected) in [
|
||||
("arcadia 5; project mission \"x\" {}", "Unsupported schema"),
|
||||
("arcadia 4; project mission \"x\" {}", "Unsupported schema"),
|
||||
(
|
||||
"arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }",
|
||||
"Unknown field",
|
||||
@@ -795,27 +765,9 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
assert_eq!(from_source, from_json);
|
||||
let original: serde_json::Value = serde_json::from_slice(old_json).unwrap();
|
||||
let mut retained = serde_json::to_value(&from_json).unwrap();
|
||||
assert_eq!(retained["schema_version"], 4);
|
||||
assert_eq!(retained["schema_version"], 3);
|
||||
retained["schema_version"] = 2.into();
|
||||
let allocations: Vec<_> = from_json
|
||||
.relations
|
||||
.values()
|
||||
.filter_map(|relation| {
|
||||
if let RelationBody::InterfaceItemAllocation {
|
||||
source,
|
||||
target,
|
||||
order,
|
||||
..
|
||||
} = relation.body
|
||||
{
|
||||
Some((source, target, order))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for element in retained["elements"].as_object_mut().unwrap().values_mut() {
|
||||
let element_id: Id = element["id"].as_str().unwrap().parse().unwrap();
|
||||
for layer in ["Oa", "Sa"] {
|
||||
let Some(shared) = element["body"]
|
||||
.get_mut(layer)
|
||||
@@ -827,46 +779,6 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
assert_eq!(port["contract"], "Legacy");
|
||||
*port = port["legacy"].clone();
|
||||
}
|
||||
if let Some(item) = shared
|
||||
.get_mut("ExchangeItem")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
if item["mechanism"] == "Event" {
|
||||
item.insert("kind".into(), "Event".into());
|
||||
}
|
||||
item.remove("mechanism");
|
||||
item.remove("supertype");
|
||||
}
|
||||
if let Some(kind) = shared
|
||||
.get_mut("DataType")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
kind.remove("default");
|
||||
kind.remove("unit");
|
||||
let definition = &kind["definition"];
|
||||
if let Some(name) = ["Integer", "Real", "String", "Record"]
|
||||
.into_iter()
|
||||
.find(|name| definition.get(name).is_some())
|
||||
{
|
||||
kind.insert("definition".into(), name.into());
|
||||
}
|
||||
}
|
||||
if let Some(field) = shared
|
||||
.get_mut("DataField")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
for key in ["direction", "storage", "default"] {
|
||||
field.remove(key);
|
||||
}
|
||||
}
|
||||
if let Some(interface) = shared.get_mut("Interface") {
|
||||
let mut entries: Vec<_> = allocations
|
||||
.iter()
|
||||
.filter(|(source, _, _)| *source == element_id)
|
||||
.collect();
|
||||
entries.sort_by_key(|(_, _, order)| *order);
|
||||
*interface = serde_json::json!({"items":entries.into_iter().map(|(_,target,_)|target).collect::<Vec<_>>()});
|
||||
}
|
||||
if let Some(scenario) = shared
|
||||
.get_mut("Scenario")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
@@ -885,10 +797,6 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
}
|
||||
}
|
||||
}
|
||||
retained["relations"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.retain(|_, relation| relation["body"].get("InterfaceItemAllocation").is_none());
|
||||
for relation in retained["relations"].as_object_mut().unwrap().values_mut() {
|
||||
if let Some(boundary) = relation["body"].get_mut("BoundaryExchange") {
|
||||
assert_eq!(boundary["oriented"], true);
|
||||
@@ -918,7 +826,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
);
|
||||
assert_eq!(
|
||||
from_json.relations.len(),
|
||||
original["relations"].as_object().unwrap().len() + allocations.len()
|
||||
original["relations"].as_object().unwrap().len()
|
||||
);
|
||||
assert_eq!(
|
||||
from_json.diagrams.len(),
|
||||
@@ -991,7 +899,7 @@ fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics()
|
||||
assert_eq!(from_text, from_json);
|
||||
assert_eq!(read(text), from_text);
|
||||
assert_eq!(persistence::decode(json).unwrap(), from_json);
|
||||
assert_eq!(from_text.schema_version, 4);
|
||||
assert_eq!(from_text.schema_version, 3);
|
||||
assert!(!from_text.elements.values().any(|e| e.layer() == Layer::Sa));
|
||||
let raw: serde_json::Value = serde_json::from_slice(json).unwrap();
|
||||
for (id, record) in raw["elements"].as_object().unwrap() {
|
||||
@@ -1192,7 +1100,6 @@ fn complete_normative_examples_are_accepted_by_the_real_parser() {
|
||||
}
|
||||
if code.trim_start().starts_with("arcadia 2;")
|
||||
|| code.trim_start().starts_with("arcadia 3;")
|
||||
|| code.trim_start().starts_with("arcadia 4;")
|
||||
{
|
||||
read(&code);
|
||||
examples += 1;
|
||||
|
||||
@@ -39,7 +39,7 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
|
||||
assert_eq!(decoded.project, from_source.project);
|
||||
assert_eq!(decoded.migration, from_source.migration);
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (2, 4));
|
||||
assert_eq!((report.from_schema, report.to_schema), (2, 3));
|
||||
assert_eq!(report.preserved_ports.len(), 4);
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
assert_eq!(report.orientation_defaults.len(), 1);
|
||||
@@ -56,7 +56,6 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
|
||||
+ old["traces"].as_object().unwrap().len()
|
||||
+ old["artifact_links"].as_object().unwrap().len()
|
||||
+ 1
|
||||
+ report.generated_interface_allocations.len()
|
||||
);
|
||||
let mut current = serde_json::to_value(&decoded.project).unwrap();
|
||||
for diagram in current["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
@@ -144,7 +143,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() {
|
||||
}
|
||||
}
|
||||
let canonical = language::format(&first.project);
|
||||
assert!(canonical.starts_with("arcadia 4;"));
|
||||
assert!(canonical.starts_with("arcadia 3;"));
|
||||
let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap();
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(reopened.project, first.project);
|
||||
@@ -224,13 +223,7 @@ fn migration_admission_counts_generated_objects_records_and_references() {
|
||||
_ => 0,
|
||||
})
|
||||
})
|
||||
.sum::<usize>()
|
||||
+ decode(&old)
|
||||
.migration
|
||||
.unwrap()
|
||||
.generated_interface_allocations
|
||||
.len()
|
||||
* 2;
|
||||
.sum::<usize>();
|
||||
for limits in [
|
||||
ParseLimits {
|
||||
max_graph_objects: graph + 1,
|
||||
@@ -251,7 +244,7 @@ fn migration_admission_counts_generated_objects_records_and_references() {
|
||||
));
|
||||
}
|
||||
let accepted = ParseLimits {
|
||||
max_graph_objects: migrated.elements.len() + migrated.relations.len(),
|
||||
max_graph_objects: graph + 2,
|
||||
max_records: records,
|
||||
max_step_references: reference_count,
|
||||
..ParseLimits::UNLIMITED
|
||||
@@ -274,7 +267,7 @@ fn old_contract_is_validated_before_new_shapes_and_v1_flows_through_v2() {
|
||||
assert!(error.contains("V2") && error.contains(&fe), "{error}");
|
||||
let v1 = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json");
|
||||
let result = persistence::decode_with_report(v1).unwrap();
|
||||
assert_eq!(result.project.schema_version, 4);
|
||||
assert_eq!(result.project.schema_version, 3);
|
||||
assert_eq!(result.migration.unwrap().from_schema, 1);
|
||||
assert_eq!(persistence::decode(v1).unwrap(), result.project);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use oa_core::{language, persistence, *};
|
||||
|
||||
const SOURCE: &str = r#"arcadia 4;
|
||||
const SOURCE: &str = r#"arcadia 3;
|
||||
project typed_ports "Typed ports" {
|
||||
sa_system producer "Producer" {}
|
||||
sa_actor consumer "Consumer" {}
|
||||
@@ -9,8 +9,7 @@ project typed_ports "Typed ports" {
|
||||
allocation sends "Produces" { source @producer; target @send; }
|
||||
allocation receives "Consumes" { source @consumer; target @receive; }
|
||||
sa_exchange_item data "Data" {}
|
||||
sa_interface service "Service" {}
|
||||
interface_item_allocation service_data "Service data" { source @service; target @data; }
|
||||
sa_interface service "Service" { items [@data]; }
|
||||
sa_port output "Output" { parent @send; contract { FunctionOutput { outgoing_items [@data]; }; }; }
|
||||
sa_port input "Input" { parent @receive; contract { FunctionInput { incoming_items [@data]; }; }; }
|
||||
sa_port provider "Provider port" { parent @producer; contract { Component { kind Standard; orientation Unset; }; }; }
|
||||
@@ -27,7 +26,7 @@ project typed_ports "Typed ports" {
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
|
||||
fn source3_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
|
||||
let decoded = language::parse_with_report(SOURCE).unwrap();
|
||||
assert!(decoded.migration.is_none());
|
||||
let canonical = language::format(&decoded.project);
|
||||
@@ -70,7 +69,7 @@ fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source4_requires_explicit_port_contract_and_fe_endpoints_without_generation() {
|
||||
fn source3_requires_explicit_port_contract_and_fe_endpoints_without_generation() {
|
||||
for (remove, expected) in [
|
||||
("source_port @output; ", "source_port"),
|
||||
("target_port @input; ", "target_port"),
|
||||
@@ -91,7 +90,7 @@ fn source4_requires_explicit_port_contract_and_fe_endpoints_without_generation()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source4_rejects_mixed_component_kinds_and_nested_reference_typos() {
|
||||
fn source3_rejects_mixed_component_kinds_and_nested_reference_typos() {
|
||||
let mixed = SOURCE.replace(
|
||||
"parent @consumer; contract { Component { kind Standard; orientation Unset;",
|
||||
"parent @consumer; contract { Component { kind Flow; orientation Unset;",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,228 +0,0 @@
|
||||
use oa_core::{language, persistence, *};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const SOURCE3: &str = include_str!("fixtures/v3-data-contract.arcadia");
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(n)
|
||||
}
|
||||
|
||||
// Frozen JSON uses the same authored schema3 contract as the source fixture;
|
||||
// this helper never serializes current defaults under version3.
|
||||
fn original3() -> Value {
|
||||
serde_json::from_str(include_str!("fixtures/v3-data-contract.json")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships() {
|
||||
let old = original3();
|
||||
let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
|
||||
let source = language::parse_with_report(SOURCE3).unwrap();
|
||||
assert_eq!(decoded.project, source.project);
|
||||
assert_eq!(decoded.migration, source.migration);
|
||||
let project = &decoded.project;
|
||||
let report = decoded.migration.as_ref().unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (3, 4));
|
||||
assert_eq!(project.identities().len(), 19);
|
||||
assert!(report.preserved_ports.is_empty());
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
assert_eq!(report.generated_interface_allocations.len(), 2);
|
||||
for allocation in &report.generated_interface_allocations {
|
||||
assert_eq!(
|
||||
allocation.id,
|
||||
Id::new_v5(
|
||||
&id(1),
|
||||
format!(
|
||||
"v2-interface-item:{}:{}",
|
||||
allocation.source, allocation.target
|
||||
)
|
||||
.as_bytes()
|
||||
)
|
||||
);
|
||||
assert_eq!(allocation.target, id(4));
|
||||
assert_eq!(allocation.order, 0);
|
||||
assert!(matches!(
|
||||
project.relations[&allocation.id].body,
|
||||
RelationBody::InterfaceItemAllocation {
|
||||
transmission: TransmissionProtocol::Unspecified,
|
||||
acquisition: AcquisitionProtocol::Unspecified,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
let now = serde_json::to_value(project).unwrap();
|
||||
for port in [id(11), id(12)] {
|
||||
assert_eq!(
|
||||
now["elements"][port.to_string()],
|
||||
old["elements"][port.to_string()]
|
||||
);
|
||||
}
|
||||
assert!(matches!(
|
||||
project.elements[&id(4)].shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Event,
|
||||
supertype: None
|
||||
})
|
||||
));
|
||||
assert_eq!(report.unresolved_message_values.len(), 1);
|
||||
assert!(
|
||||
validation::validate(project)
|
||||
.iter()
|
||||
.any(|issue| issue.code == "message_value_ambiguous_path")
|
||||
);
|
||||
let canonical = language::format(project);
|
||||
assert!(canonical.starts_with("arcadia 4;"));
|
||||
let reopened = language::parse_with_report(&canonical).unwrap();
|
||||
assert_eq!(&reopened.project, project);
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(canonical, language::format(&reopened.project));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v3_changed_payloads_reject_schema4_fields_and_new_constructor_names() {
|
||||
let mut old = original3();
|
||||
old["elements"][id(4).to_string()]["body"]["Sa"]["Shared"]["ExchangeItem"]["mechanism"] =
|
||||
json!("Flow");
|
||||
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
|
||||
let mut old = original3();
|
||||
old["elements"][id(5).to_string()]["body"]["Sa"]["Shared"]["DataType"]["definition"] =
|
||||
json!({"Record":{"supertype":null}});
|
||||
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
|
||||
let mut old = original3();
|
||||
old["elements"][id(17).to_string()]["body"]["Sa"]["Shared"]["Occurrence"]["body"]["Exchange"]
|
||||
["values"][0]["path"] = json!([]);
|
||||
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
|
||||
let source = SOURCE3.replace("kind Event;", "kind Information; mechanism Flow;");
|
||||
assert!(language::parse(&source).is_err());
|
||||
let source = SOURCE3.replace(
|
||||
"functional_exchange position_exchange",
|
||||
"interface_item_allocation position_exchange",
|
||||
);
|
||||
assert!(language::parse(&source).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_allocation_id_collision_is_located_without_overwriting_any_object() {
|
||||
let mut old = original3();
|
||||
let collision = Id::new_v5(
|
||||
&id(1),
|
||||
format!("v2-interface-item:{}:{}", id(9), id(4)).as_bytes(),
|
||||
);
|
||||
old["artifact_links"][collision.to_string()] = json!({"id":collision,"alias":"retained_artifact","name":"Retain","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null});
|
||||
let error = persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap_err();
|
||||
match error {
|
||||
persistence::FileError::Model { issues, .. } => {
|
||||
assert_eq!(issues[0].code, "identity_collision");
|
||||
assert_eq!(issues[0].element, Some(id(9)));
|
||||
assert_eq!(issues[0].field.as_deref(), Some("items"));
|
||||
}
|
||||
other => panic!("unexpected error {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_legacy_nested_value_is_qualified_and_physical_nature_is_not_guessed() {
|
||||
let source = SOURCE3.replace("kind Event;", "kind Material;").replace(
|
||||
"type_ref { Named @coordinate; }; }\n }",
|
||||
"type_ref String; }\n }",
|
||||
);
|
||||
let decoded = language::parse_with_report(&source).unwrap();
|
||||
assert!(matches!(
|
||||
decoded.project.elements[&id(4)].shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Material,
|
||||
mechanism: ExchangeMechanism::Unspecified,
|
||||
..
|
||||
})
|
||||
));
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = decoded.project.elements[&id(17)].shared()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(values[0].path, vec![id(7)]);
|
||||
assert_eq!(values[0].expression, "navigation.latitude");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() {
|
||||
let v2 =
|
||||
persistence::decode_with_report(include_bytes!("fixtures/v2-coastal-rescue.json")).unwrap();
|
||||
let text =
|
||||
language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap();
|
||||
assert_eq!(v2.project, text.project);
|
||||
assert_eq!(v2.migration, text.migration);
|
||||
assert_eq!(v2.migration.unwrap().to_schema, 4);
|
||||
let v1 = language::parse_with_report(
|
||||
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v1.project.schema_version, 4);
|
||||
assert_eq!(v1.migration.unwrap().from_schema, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocation_alias_collisions_are_stable_and_duplicate_legacy_payload_fields_reject() {
|
||||
let mut old = original3();
|
||||
let first = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
|
||||
let alias = first.migration.unwrap().generated_interface_allocations[0]
|
||||
.alias
|
||||
.clone();
|
||||
old["artifact_links"][id(100).to_string()] = json!({"id":id(100),"alias":alias,"name":"Existing alias","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null});
|
||||
let bytes = serde_json::to_vec(&old).unwrap();
|
||||
let migrated = persistence::decode_with_report(&bytes).unwrap();
|
||||
let generated = &migrated
|
||||
.migration
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.generated_interface_allocations[0];
|
||||
assert_ne!(generated.alias, alias);
|
||||
assert!(generated.alias.starts_with(&alias));
|
||||
assert_eq!(
|
||||
migrated.project,
|
||||
persistence::decode_with_report(&bytes).unwrap().project
|
||||
);
|
||||
let duplicate = include_str!("fixtures/v3-data-contract.json").replace(
|
||||
"\"kind\": \"Event\"",
|
||||
"\"kind\": \"Event\", \"kind\": \"Material\"",
|
||||
);
|
||||
assert!(
|
||||
persistence::decode(duplicate.as_bytes())
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Duplicate field 'kind'")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_legacy_record_paths_remain_ambiguous_instead_of_becoming_one_argument() {
|
||||
let mut old = original3();
|
||||
old["elements"][id(8).to_string()]["body"]["Sa"]["Shared"]["DataField"]["type_ref"] =
|
||||
json!("String");
|
||||
old["elements"][id(100).to_string()] = json!({"id":id(100),"alias":"next_coordinate","name":"Next coordinate","description":"","parent":id(5),"body":{"Sa":{"Shared":{"DataField":{"type_ref":{"Named":id(5)},"minimum":0,"maximum":1,"unit":""}}}}});
|
||||
let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
decoded
|
||||
.migration
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.unresolved_message_values
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = decoded.project.elements[&id(17)].shared()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert!(values[0].path.is_empty());
|
||||
assert!(
|
||||
validation::validate(&decoded.project)
|
||||
.iter()
|
||||
.any(|issue| issue.code == "message_value_ambiguous_path")
|
||||
);
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
use oa_core::{language, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(n)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_identity() {
|
||||
let original = include_str!("fixtures/v3-data-contract.arcadia");
|
||||
let header = original.find(" sa_function").unwrap();
|
||||
let close = original.rfind('}').unwrap();
|
||||
let entry = format!(
|
||||
"{} // Contracts stay in their original module.\n include \"contracts.arcadia\";\n}}\n",
|
||||
&original[..header]
|
||||
);
|
||||
let fragment = original[header..close].replace(
|
||||
"items [@packet];",
|
||||
"items [@ // interface contract decision\npacket];",
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), entry),
|
||||
("contracts.arcadia".into(), fragment),
|
||||
]),
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle.files.len(), 2);
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 4;"));
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";"));
|
||||
for text in [
|
||||
"// Contracts stay in their original module.",
|
||||
"// Reused record parameters retain their own identities.",
|
||||
"// interface contract decision",
|
||||
] {
|
||||
assert!(
|
||||
compiled
|
||||
.bundle
|
||||
.files
|
||||
.values()
|
||||
.any(|source| source.contains(text)),
|
||||
"Lost {text}"
|
||||
);
|
||||
}
|
||||
assert_eq!(compiled.index.lexical_owners[&id(6)], id(5));
|
||||
assert_eq!(compiled.index.lexical_owners[&id(7)], id(4));
|
||||
assert_eq!(compiled.index.lexical_owners[&id(8)], id(4));
|
||||
assert_eq!(compiled.index.lexical_owners[&id(17)], id(14));
|
||||
let report = compiled.decoded.migration.as_ref().unwrap();
|
||||
assert_eq!(report.from_schema, 3);
|
||||
for allocation in &report.generated_interface_allocations {
|
||||
assert_eq!(
|
||||
compiled.index.declarations[&allocation.id].path,
|
||||
"contracts.arcadia"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
compiled
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.code == "message_value_ambiguous_path")
|
||||
);
|
||||
let second = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(second.bundle, compiled.bundle);
|
||||
assert_eq!(second.decoded.project, compiled.decoded.project);
|
||||
assert!(second.decoded.migration.is_none());
|
||||
let mut renamed = compiled.decoded.project.clone();
|
||||
renamed.elements.get_mut(&id(6)).unwrap().name = "Latitude degrees".into();
|
||||
let updated = update_bundle(&compiled, &renamed, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(updated.index.lexical_owners, compiled.index.lexical_owners);
|
||||
assert!(updated.bundle.files["contracts.arcadia"].contains("Latitude degrees"));
|
||||
assert!(
|
||||
bundle.files["project.arcadia"].contains("arcadia 3;"),
|
||||
"Migration does not mutate the caller's draft"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema4_typed_values_supertype_paths_and_protocols_roundtrip() {
|
||||
let source = r#"arcadia 4; project data_contract "Data contract" {
|
||||
sa_data_type bounded "Bounded" { definition { Integer { minimum -9223372036854775808; maximum 9223372036854775807; }; }; default { Integer -9223372036854775808; }; unit "m"; }
|
||||
sa_data_type label_type "Label" { definition { String { minimum_length 1; maximum_length 4; }; }; default { String "航海"; }; }
|
||||
sa_data_type base "Base" { definition { Record { supertype none; }; }; }
|
||||
sa_data_type derived "Derived" { definition { Record { supertype @base; }; }; }
|
||||
sa_exchange_item operation "Operation" { mechanism Operation;
|
||||
sa_data_field value "Value" { type_ref { Named @bounded; }; direction In; storage Value; default [{ Integer 42; }]; }
|
||||
}
|
||||
sa_interface contract "Contract" { supertype none; }
|
||||
interface_item_allocation member "Operation member" { source @contract; target @operation; transmission Synchronous; acquisition Accept; order 3; }
|
||||
data_association association "Related records" { source @base; target @derived; source_role "parent"; target_role "children"; source_minimum 0; source_maximum 1; target_minimum 0; target_maximum none; source_navigable true; target_navigable true; }
|
||||
}"#;
|
||||
let parsed = language::parse(source).unwrap();
|
||||
let formatted = language::format(&parsed);
|
||||
assert!(formatted.contains("supertype @base"));
|
||||
assert!(formatted.contains("transmission Synchronous"));
|
||||
assert_eq!(language::parse(&formatted).unwrap(), parsed);
|
||||
let compiled = compile_bundle(&format_bundle(&parsed), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, parsed);
|
||||
assert!(compiled.decoded.migration.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_ids_are_stabilized_before_allocation_identity_generation() {
|
||||
let bundle=SourceBundle{entry:"project.arcadia".into(),files:BTreeMap::from([("project.arcadia".into(),"arcadia 3; project old_model \"Old\" { sa_exchange_item event_item \"Event\" { kind Event; } sa_interface contract \"Contract\" { items [@event_item]; } }".into())])};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
let report = compiled.decoded.migration.as_ref().unwrap();
|
||||
let allocation = &report.generated_interface_allocations[0];
|
||||
assert_eq!(
|
||||
allocation.id,
|
||||
Id::new_v5(
|
||||
&compiled.decoded.project.id,
|
||||
format!(
|
||||
"v2-interface-item:{}:{}",
|
||||
allocation.source, allocation.target
|
||||
)
|
||||
.as_bytes()
|
||||
)
|
||||
);
|
||||
let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.decoded.project, compiled.decoded.project);
|
||||
assert_eq!(reopened.bundle, compiled.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inferring_a_legacy_path_keeps_rationale_inside_its_argument() {
|
||||
let source = include_str!("fixtures/v3-data-contract.arcadia")
|
||||
.replace(
|
||||
"type_ref { Named @coordinate; }; }\n }",
|
||||
"type_ref String; }\n }",
|
||||
)
|
||||
.replace(
|
||||
"field @latitude; expression",
|
||||
"field @latitude; // original argument rationale\nexpression",
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
let source = &compiled.bundle.files["project.arcadia"];
|
||||
let argument = &source[source.find("values [{").unwrap()..];
|
||||
let argument = &argument[..argument.find('}').unwrap()];
|
||||
assert!(
|
||||
argument.contains("// original argument rationale"),
|
||||
"{source}"
|
||||
);
|
||||
assert!(argument.contains("path [@origin]"), "{source}");
|
||||
assert_eq!(compiled.index.lexical_owners[&id(17)], id(14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_value_reordering_and_path_reidentification_keep_each_arguments_comments() {
|
||||
let mut project = language::parse(include_str!("fixtures/v3-data-contract.arcadia")).unwrap();
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project.elements.get_mut(&id(17)).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*values = vec![
|
||||
MessageValue {
|
||||
path: vec![id(7)],
|
||||
field: id(6),
|
||||
expression: "origin value".into(),
|
||||
},
|
||||
MessageValue {
|
||||
path: vec![id(8)],
|
||||
field: id(6),
|
||||
expression: "destination value".into(),
|
||||
},
|
||||
];
|
||||
let mut bundle = format_bundle(&project);
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = source
|
||||
.replace(
|
||||
"expression \"origin value\";",
|
||||
"expression \"origin value\"; // origin argument rationale",
|
||||
)
|
||||
.replace(
|
||||
"expression \"destination value\";",
|
||||
"expression \"destination value\"; // destination argument rationale",
|
||||
);
|
||||
}
|
||||
let current = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project.elements.get_mut(&id(17)).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
values.reverse();
|
||||
let renamed = Id::from_u128(100);
|
||||
let mut identities: BTreeMap<_, _> = project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.map(|(id, _)| (id, id))
|
||||
.collect();
|
||||
identities.insert(id(7), renamed);
|
||||
reidentify(&mut project, id(7), renamed).unwrap();
|
||||
project.elements.get_mut(&renamed).unwrap().alias = "revised_origin".into();
|
||||
let result =
|
||||
update_bundle_with_identity_map(¤t, &project, &identities, ProjectLimits::default())
|
||||
.unwrap();
|
||||
let source = &result.bundle.files[&result.index.declarations[&id(17)].path];
|
||||
let destination = source.find("// destination argument rationale").unwrap();
|
||||
let origin = source.find("// origin argument rationale").unwrap();
|
||||
assert!(
|
||||
destination < origin,
|
||||
"Distinct uses of the same terminal field must reorder as complete argument annotations: {source}"
|
||||
);
|
||||
assert!(source[origin..].contains("path [@revised_origin]"));
|
||||
assert_eq!(result.decoded.project, project);
|
||||
}
|
||||
@@ -447,12 +447,11 @@ fn standard_interface_roles_do_not_follow_message_direction_and_kind_mixing_is_r
|
||||
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
|
||||
let (source, target) = p.relations[&boundary].body.endpoints();
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Service");
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
let requirement = p.relate(
|
||||
RelationKind::InterfaceRequirement,
|
||||
"Caller requires service",
|
||||
@@ -500,12 +499,11 @@ fn legacy_split_preserves_original_identity_prose_interface_artifact_constraint_
|
||||
let (mut p, a, b, item, forward) = fixture();
|
||||
let (old, _) = endpoints(&p, forward);
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original interface");
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
p.elements.get_mut(&old).unwrap().description = "Original bidirectional rationale".into();
|
||||
*p.elements.get_mut(&old).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
@@ -661,12 +659,11 @@ fn directed_legacy_resolution_retains_unclassified_interface_and_role_identity()
|
||||
let (mut p, _, _, item, fe) = fixture();
|
||||
let output = endpoints(&p, fe).0;
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Original service");
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
|
||||
@@ -94,9 +94,7 @@ fn fixture(layer: Layer) -> Fixture {
|
||||
.add_element(Kind::DataType, layer, "Request record", None)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype: None },
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
definition: DataTypeDefinition::Record,
|
||||
};
|
||||
let field = p
|
||||
.add_element(Kind::DataField, layer, "Requested level", Some(record))
|
||||
@@ -162,7 +160,6 @@ fn fixture(layer: Layer) -> Fixture {
|
||||
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
||||
{
|
||||
values.push(MessageValue {
|
||||
path: vec![payload],
|
||||
field,
|
||||
expression: "requested_level".into(),
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ fn setup() -> tempfile::TempDir {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; }\n",
|
||||
"arcadia 3; project p \"P\" { include \"parts/a.arcadia\"; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::create_dir(directory.path().join("parts")).unwrap();
|
||||
@@ -59,7 +59,7 @@ fn loading_keeps_original_no_uuid_bytes_and_saving_stabilizes_exactly_once() {
|
||||
fn invalid_and_missing_includes_keep_editable_text_and_located_diagnostics() {
|
||||
let directory = setup();
|
||||
let entry = directory.path().join("project.arcadia");
|
||||
fs::write(&entry, "arcadia 4; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
|
||||
fs::write(&entry, "arcadia 3; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
|
||||
fs::write(
|
||||
directory.path().join("parts/a.arcadia"),
|
||||
"oa_activity a \"A\" { invalid",
|
||||
@@ -102,7 +102,7 @@ fn discovery_never_refreshes_drafts_and_preserves_detached_buffers() {
|
||||
let overlays = BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project p \"P\" { include \"new.arcadia\"; }".into(),
|
||||
"arcadia 3; project p \"P\" { include \"new.arcadia\"; }".into(),
|
||||
),
|
||||
(
|
||||
"parts/a.arcadia".into(),
|
||||
@@ -176,7 +176,7 @@ fn new_destinations_require_explicit_missing_expectations_and_reject_collisions(
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }".into(),
|
||||
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }".into(),
|
||||
),
|
||||
("child.arcadia".into(), "oa_activity a \"A\" {}".into()),
|
||||
]),
|
||||
@@ -256,7 +256,7 @@ fn physical_hard_link_aliases_are_rejected() {
|
||||
.unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
@@ -286,7 +286,7 @@ fn symlinks_to_outside_or_inside_project_are_rejected() {
|
||||
std::os::unix::fs::symlink(&destination, &link).unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"alias.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"alias.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
let loaded = load_project(
|
||||
|
||||
@@ -6,10 +6,10 @@ fn source() -> CompiledSourceProject {
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 4; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
|
||||
("project.arcadia".into(), "arcadia 3; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
|
||||
("team/first.arcadia".into(), "// Rationale for first\nsa_exchange_item first \"First\" { description \"First rationale\"; }\n".into()),
|
||||
("team/second.arcadia".into(), "// Rationale for second\nsa_exchange_item second \"Second\" {}\n".into()),
|
||||
("contracts.arcadia".into(), "sa_event contract \"Contract\" { body { Signal { items [\n @first // first item rationale\n ,\n @second // second item rationale\n ]; }; }; }\n".into()),
|
||||
("contracts.arcadia".into(), "sa_interface contract \"Contract\" { items [\n @first // first item rationale\n ,\n @second // second item rationale\n ]; }\n".into()),
|
||||
]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
@@ -91,9 +91,8 @@ fn identity_swap_and_reorder_keep_both_declarations_and_item_comments_with_origi
|
||||
reidentify(&mut candidate, second, first).unwrap();
|
||||
reidentify(&mut candidate, temporary, second).unwrap();
|
||||
let contract = find(&candidate, "contract");
|
||||
let Some(SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
}) = candidate.elements.get_mut(&contract).unwrap().shared_mut()
|
||||
let Some(SharedBody::Interface { items }) =
|
||||
candidate.elements.get_mut(&contract).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("interface")
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ fn compile(a: &str, b: &str) -> CompiledSourceProject {
|
||||
compile_bundle(&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 4; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
|
||||
("project.arcadia".into(), "arcadia 3; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
|
||||
("a.arcadia".into(), a.into()), ("b.arcadia".into(), b.into()),
|
||||
]),
|
||||
}, ProjectLimits::default()).unwrap()
|
||||
|
||||
@@ -30,7 +30,7 @@ fn id(project: &Project, alias: &str) -> Id {
|
||||
#[test]
|
||||
fn route_edits_keep_diagram_file_ownership_and_comments_with_edge_identity() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
"arcadia 3; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"work.arcadia",
|
||||
@@ -95,7 +95,7 @@ diagram review "Review" {
|
||||
#[test]
|
||||
fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
|
||||
let mut input = bundle(
|
||||
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
"arcadia 3; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"work.arcadia",
|
||||
@@ -127,7 +127,7 @@ fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
|
||||
|
||||
fn typed() -> SourceBundle {
|
||||
bundle(
|
||||
"arcadia 4; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
||||
"arcadia 3; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
||||
&[
|
||||
(
|
||||
"exchanges.arcadia",
|
||||
@@ -194,7 +194,7 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
|
||||
#[test]
|
||||
fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"sa_function first \"First\" {}\nsa_function second \"Second\" {}\n",
|
||||
@@ -228,7 +228,7 @@ fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
|
||||
#[test]
|
||||
fn source_errors_keep_exact_fragment_unicode_positions() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
|
||||
&[(
|
||||
"child.arcadia",
|
||||
"// Пример\nsa_function child \"航海\" { parent @missing; }",
|
||||
@@ -274,7 +274,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
),
|
||||
] {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[("a.arcadia", left), ("b.arcadia", right)],
|
||||
);
|
||||
let error = compile_bundle(&input, ProjectLimits::default())
|
||||
@@ -285,7 +285,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
assert_eq!(error.related[0].path, "a.arcadia");
|
||||
}
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
|
||||
&[("child.arcadia", "oa_activity p \"Collision\" {}")],
|
||||
);
|
||||
let error = compile_bundle(&input, ProjectLimits::default())
|
||||
@@ -297,7 +297,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
#[test]
|
||||
fn includes_are_once_only_and_cycles_missing_and_unreachable_files_are_located() {
|
||||
let mut input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
||||
&[
|
||||
("sub/a.arcadia", "include \"../shared.arcadia\";"),
|
||||
("sub/b.arcadia", "include \"../shared.arcadia\";"),
|
||||
@@ -356,7 +356,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
||||
);
|
||||
assert!(resolve_include("../bad.arcadia", "a.arcadia").is_err());
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" {}",
|
||||
"arcadia 3; project p \"P\" {}",
|
||||
&[("A.arcadia", ""), ("a.arcadia", "")],
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -370,7 +370,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
||||
#[test]
|
||||
fn project_admission_is_aggregate_and_precedes_global_resolution() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[
|
||||
("a.arcadia", "oa_activity one \"One\" { parent @missing; }"),
|
||||
("b.arcadia", "oa_activity two \"Two\" {}"),
|
||||
@@ -591,7 +591,7 @@ fn failed_update_preserves_candidate_and_rejects_stale_index() {
|
||||
|
||||
#[test]
|
||||
fn single_string_and_legacy_apis_remain_explicit_and_filesystem_free() {
|
||||
let source = "arcadia 4; project p \"P\" { include \"missing.arcadia\"; }";
|
||||
let source = "arcadia 3; project p \"P\" { include \"missing.arcadia\"; }";
|
||||
assert!(
|
||||
language::parse(source).unwrap_err()[0]
|
||||
.message
|
||||
@@ -681,7 +681,7 @@ fn json_bundle_rejects_duplicate_file_keys_and_unknown_fields() {
|
||||
#[test]
|
||||
fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_indexed() {
|
||||
let input = bundle(
|
||||
"arcadia 4;project p\"P\"{include \"child.arcadia\";}",
|
||||
"arcadia 3;project p\"P\"{include \"child.arcadia\";}",
|
||||
&[(
|
||||
"child.arcadia",
|
||||
"oa_activity a\"A\"{}diagram d\"D\"{}node n{diagram @d;element @a;}",
|
||||
@@ -703,7 +703,7 @@ fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_ind
|
||||
#[test]
|
||||
fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_aggregate() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
||||
&[("a.arcadia", "")],
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -720,7 +720,7 @@ fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_agg
|
||||
"project_limit"
|
||||
);
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"a.arcadia",
|
||||
@@ -763,7 +763,7 @@ fn include_inspection_reuses_strict_entry_fragment_syntax_and_real_spans() {
|
||||
&source[span.start..span.end],
|
||||
"include \"../shared.arcadia\";"
|
||||
);
|
||||
let entry = "arcadia 4; project p \"P\" { include \"sub/work.arcadia\"; }";
|
||||
let entry = "arcadia 3; project p \"P\" { include \"sub/work.arcadia\"; }";
|
||||
assert_eq!(
|
||||
inspect_includes("project.arcadia", entry, true, ProjectLimits::default()).unwrap()[0]
|
||||
.target,
|
||||
@@ -829,7 +829,7 @@ fn comments(source: &str) -> Vec<String> {
|
||||
|
||||
#[test]
|
||||
fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_spelling() {
|
||||
let entry = "arcadia 4;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
||||
let entry = "arcadia 3;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
||||
let source = "oa_state_machine machine \"Machine\" {}\r\noa_region region_one \"Region\" { parent @machine; }\r\noa_state first \"First\" { parent @region_one; }\r\noa_state second \"Second\" { parent @region_one; }\r\nstate_transition route // route title\r\n \"Original transition\" {\r\n\tguard // feasibility\r\n\t \"old guard\"; // retain explanation\r\n\tdescription \"Keep\\u0020spelling\";\r\n\tsource @ // endpoint proof\r\n first; target @second;\r\n}\r\n";
|
||||
let current = compile(&bundle(entry, &[("behavior.arcadia", source)]));
|
||||
let mut project = current.decoded.project.clone();
|
||||
@@ -864,7 +864,7 @@ fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_s
|
||||
#[test]
|
||||
fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after_at() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity root_work // root rationale\n \"Root\\u0020name\" { description \"root_work is prose\"; }\noa_activity child \"Child\" { parent @ // parent proof\n root_work; }\n",
|
||||
@@ -887,7 +887,7 @@ fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after
|
||||
#[test]
|
||||
fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity work \"Work\" {\r\n\t// owner rationale\r\n\tparent none ; // preserve spacing\r\n}\r\n",
|
||||
@@ -913,7 +913,7 @@ fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields
|
||||
#[test]
|
||||
fn kind_change_removes_field_syntax_but_keeps_its_comments_and_other_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"item.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"item.arcadia\"; }",
|
||||
&[(
|
||||
"item.arcadia",
|
||||
"oa_exchange_item item \"Item\" {\n description \"Keep\\u0020this\" ; // description note\n kind // classification rationale\n Information ;\n parent none;\n}\n",
|
||||
@@ -953,7 +953,7 @@ fn significant_words(source: &str) -> Vec<String> {
|
||||
#[test]
|
||||
fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comments() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"event.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"event.arcadia\"; }",
|
||||
&[(
|
||||
"event.arcadia",
|
||||
"oa_event timer \"Timer\" { body { Time // time rationale\n { expression // expression rationale\n \"before\" ; kind After; // clock rationale\n }; }; }\n",
|
||||
@@ -986,7 +986,7 @@ fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comment
|
||||
#[test]
|
||||
fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"ports.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"ports.arcadia\"; }",
|
||||
&[(
|
||||
"ports.arcadia",
|
||||
"sa_function work \"Work\" {}\nsa_port input \"Input\" { parent @work; contract Legacy; // contract rationale\n legacy { direction In; // direction rationale\n interface none; // interface rationale\n }; }\n",
|
||||
@@ -1017,10 +1017,10 @@ fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
||||
#[test]
|
||||
fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_identity() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }; }; }\n",
|
||||
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
@@ -1029,9 +1029,8 @@ fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_ident
|
||||
let b = id(&project, "b");
|
||||
let service = id(&project, "service");
|
||||
project.elements.get_mut(&a).unwrap().alias = "renamed_a".into();
|
||||
if let Some(SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
}) = project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
if let Some(SharedBody::Interface { items }) =
|
||||
project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
{
|
||||
*items = vec![b, a];
|
||||
} else {
|
||||
@@ -1052,10 +1051,10 @@ fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_ident
|
||||
#[test]
|
||||
fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }; }; }\n",
|
||||
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
@@ -1064,9 +1063,8 @@ fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations()
|
||||
let b = id(&project, "b");
|
||||
let service = id(&project, "service");
|
||||
project.elements.remove(&a);
|
||||
if let Some(SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
}) = project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
if let Some(SharedBody::Interface { items }) =
|
||||
project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
{
|
||||
*items = vec![b];
|
||||
} else {
|
||||
@@ -1104,7 +1102,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
||||
}; }; }
|
||||
"#;
|
||||
let current = compile(&bundle(
|
||||
"arcadia 4; project p \"P\" { include \"conversation.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"conversation.arcadia\"; }",
|
||||
&[("conversation.arcadia", source)],
|
||||
));
|
||||
let mut project = current.decoded.project.clone();
|
||||
@@ -1153,7 +1151,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
||||
#[test]
|
||||
fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"oa_data_type values \"Values\" { definition { Enumeration { literals [ // enumeration rationale\n ]; }; }; }\n",
|
||||
@@ -1162,7 +1160,7 @@ fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotation
|
||||
let empty = compile(&input);
|
||||
let mut project = empty.decoded.project.clone();
|
||||
let values = id(&project, "values");
|
||||
if let Some(SharedBody::DataType { definition, .. }) =
|
||||
if let Some(SharedBody::DataType { definition }) =
|
||||
project.elements.get_mut(&values).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Enumeration {
|
||||
@@ -1187,7 +1185,7 @@ fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotation
|
||||
.replace("\"Beta\"", "\"Beta\" // beta explanation\n"),
|
||||
);
|
||||
let annotated = compile(&annotated);
|
||||
if let Some(SharedBody::DataType { definition, .. }) =
|
||||
if let Some(SharedBody::DataType { definition }) =
|
||||
project.elements.get_mut(&values).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Enumeration {
|
||||
|
||||
@@ -328,12 +328,11 @@ fn items_ports_interfaces_and_exchange_allocation_are_typed() {
|
||||
let f2 = add(&mut p, Kind::Function, Layer::Sa, "Receive", None);
|
||||
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Message", None);
|
||||
let interface = add(&mut p, Kind::Interface, Layer::Sa, "Link interface", None);
|
||||
p.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Interface payload",
|
||||
interface,
|
||||
item,
|
||||
);
|
||||
*p.elements
|
||||
.get_mut(&interface)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Interface { items: vec![item] };
|
||||
let port = |p: &mut Project, name: &str, parent, direction| {
|
||||
let id = Id::new_v4();
|
||||
let contract = if p.elements[&parent].kind() == Kind::Function {
|
||||
@@ -741,9 +740,6 @@ fn reidentifying_data_types_and_function_ports_updates_every_typed_reference() {
|
||||
minimum: 0,
|
||||
maximum: None,
|
||||
unit: "items".into(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None);
|
||||
let other = add(&mut p, Kind::Function, Layer::Sa, "Other", None);
|
||||
@@ -808,40 +804,8 @@ fn every_demo_identity_can_be_remapped_and_deleted_without_dangling_references()
|
||||
"Reidentification must preserve all values and order"
|
||||
);
|
||||
if id != p.id {
|
||||
let deleted = if p
|
||||
.relations
|
||||
.get(&id)
|
||||
.is_some_and(|r| r.kind() == RelationKind::InterfaceItemAllocation)
|
||||
{
|
||||
// An explicitly empty interface cannot silently accept the
|
||||
// still-carried payload. Removing its definition needs a
|
||||
// reviewed edit of that dependent contract in the same batch.
|
||||
let mut session = Session::new(p.clone());
|
||||
assert!(
|
||||
session
|
||||
.execute(Command::Delete(id))
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("port_interface_items")
|
||||
);
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(session.revision(), 0);
|
||||
let item = p.relations[&id].target();
|
||||
let mut commands = vec![Command::Delete(id)];
|
||||
commands.extend(
|
||||
p.relations
|
||||
.values()
|
||||
.filter(|r| {
|
||||
r.kind() == RelationKind::BoundaryExchange
|
||||
&& r.body.items().contains(&item)
|
||||
})
|
||||
.map(|r| Command::Delete(r.id)),
|
||||
);
|
||||
execute_batch(&p, &commands).unwrap()
|
||||
} else {
|
||||
execute_batch(&p, &[Command::Delete(id)])
|
||||
.unwrap_or_else(|err| panic!("Delete {id}: {err}"))
|
||||
};
|
||||
let deleted = execute_batch(&p, &[Command::Delete(id)])
|
||||
.unwrap_or_else(|err| panic!("Delete {id}: {err}"));
|
||||
accepted(&deleted);
|
||||
}
|
||||
}
|
||||
@@ -926,15 +890,13 @@ fn demo_has_both_layers_complete_contracts_and_no_checks() {
|
||||
assert!(p.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Material,
|
||||
..
|
||||
kind: ExchangeItemKind::Material
|
||||
})
|
||||
)));
|
||||
assert!(p.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Energy,
|
||||
..
|
||||
kind: ExchangeItemKind::Energy
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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 supported older-schema migration.
|
||||
// Format is chosen by HTTP. The shared decoder owns explicit schema-1 and schema-2 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, "migration": stored.migration
|
||||
"source_project": stored.source_project
|
||||
}),
|
||||
))
|
||||
})
|
||||
@@ -947,16 +947,14 @@ 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();
|
||||
let migration = if !diagnostics.iter().any(|issue| issue.severity == "error") {
|
||||
PreparedProject::from_bundle(source_project)?.migration().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((diagnostics, migration))
|
||||
if !diagnostics.iter().any(|issue| issue.severity == "error") {
|
||||
PreparedProject::from_bundle(source_project)?;
|
||||
}
|
||||
Ok(diagnostics)
|
||||
});
|
||||
let (diagnostics, migration) = result.unwrap_or_else(|error| (candidate_issues(error), None));
|
||||
let diagnostics = result.unwrap_or_else(candidate_issues);
|
||||
let valid = !diagnostics.iter().any(|issue| issue.severity == "error");
|
||||
return Ok(response(stored, StatusCode::OK, json!({"revision":stored.revision,"valid":valid,"issues":diagnostics,"migration":migration})));
|
||||
return Ok(response(stored, StatusCode::OK, json!({"revision":stored.revision,"valid":valid,"issues":diagnostics,"migration":null})));
|
||||
}
|
||||
_ => return Err(ApiError::bad_request("Validation requires one matching format/payload: json with model, arcadia with source, or source-project with source_project.")),
|
||||
};
|
||||
|
||||
@@ -128,10 +128,9 @@ 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,
|
||||
migration: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,10 +173,6 @@ 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
|
||||
@@ -334,12 +329,7 @@ impl Store {
|
||||
let prepared = if let Some(bundle) = envelope.source_project {
|
||||
let prepared = PreparedProject::from_bundle(bundle.clone())
|
||||
.map_err(|error| corrupt(error.detail))?;
|
||||
// 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
|
||||
{
|
||||
if prepared.bundle() != &bundle || prepared.model() != &decoded.project {
|
||||
return Err(corrupt(
|
||||
"Stored source and model disagree, or source lacks stable identities."
|
||||
.into(),
|
||||
@@ -357,7 +347,7 @@ impl Store {
|
||||
revision: envelope.revision,
|
||||
model: decoded.project,
|
||||
source_project: prepared.compiled.bundle,
|
||||
migration: decoded.migration.or(prepared.migration),
|
||||
migration: decoded.migration,
|
||||
};
|
||||
if stored.resource_id != filename_id || stored.revision == 0 {
|
||||
return Err(corrupt(
|
||||
|
||||
@@ -8,8 +8,6 @@ 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"]
|
||||
@@ -1931,7 +1929,7 @@ async fn legacy_json_import_and_model_reidentification_keep_the_service_address(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_migration_reports_and_invalid_typed_transaction_preserve_http_state() {
|
||||
async fn schema3_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();
|
||||
@@ -1947,7 +1945,7 @@ async fn current_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"], oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(summary["schema_version"], 3);
|
||||
assert_eq!(
|
||||
summary["migration"],
|
||||
serde_json::to_value(&expected.migration).unwrap()
|
||||
@@ -1960,7 +1958,7 @@ async fn current_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"], oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(envelope["model"]["schema_version"], 3);
|
||||
let validation = server
|
||||
.client
|
||||
.post(server.url(&format!("/projects/{id}/validation")))
|
||||
@@ -2002,7 +2000,7 @@ async fn current_migration_reports_and_invalid_typed_transaction_preserve_http_s
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(source.starts_with("arcadia 4;"));
|
||||
assert!(source.starts_with("arcadia 3;"));
|
||||
assert_eq!(oa_core::language::parse(&source).unwrap(), stored.model);
|
||||
let no_op = server
|
||||
.client
|
||||
@@ -2051,7 +2049,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, oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(migrated.model.schema_version, 3);
|
||||
assert_eq!(
|
||||
migrated
|
||||
.migration
|
||||
@@ -2076,7 +2074,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"], oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(saved["model"]["schema_version"], 3);
|
||||
assert!(saved.get("migration").is_none());
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
//! 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 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()),
|
||||
("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()),
|
||||
("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\" {}\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()),
|
||||
("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()),
|
||||
("work.arcadia".into(), "// Operational rationale\r\noa_activity assess \"Assess assistance\" {}\r\n".into()),
|
||||
]),
|
||||
};
|
||||
|
||||
+8
-32
@@ -784,8 +784,8 @@ candidate validation reports the same diagnostic in its 200 response.
|
||||
Parsing, validation and query processing use snapshot workers, so this work
|
||||
does not hold the catalog lock or serialize unrelated model preparation.</p>
|
||||
<p>Import accepts schema-1 JSON through the shared explicit migration adapter,
|
||||
as well as schema-2, schema-3 and current schema-4 JSON. Source import uses the shared language parser
|
||||
and its supported migrations. Export uses schema 4 / arcadia 4. Native JSON
|
||||
as well as schema-2 native JSON. Source import uses the shared language parser
|
||||
and its supported migrations. Export uses schema 3 / arcadia 3. Native JSON
|
||||
replacement accepts supported older schemas through the same explicit migration.
|
||||
Only documented query parameters are accepted. In particular, appending
|
||||
<span class="docutils literal"><span class="pre">?revision=...</span></span> to model/source export is an error, not historical retrieval.</p>
|
||||
@@ -796,7 +796,7 @@ Only documented query parameters are accepted. In particular, appending
|
||||
<pre class="literal-block">{
|
||||
"entry": "rescue.arcadia",
|
||||
"files": {
|
||||
"rescue.arcadia": "arcadia 4; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n",
|
||||
"rescue.arcadia": "arcadia 3; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n",
|
||||
"oa/work.arcadia": "// Engineering rationale\noa_activity assess \"Assess assistance\" {}\n"
|
||||
}
|
||||
}</pre>
|
||||
@@ -807,9 +807,8 @@ targets or another hosted resource. All submitted files must be reachable;
|
||||
missing files, cycles, duplicate keys, case collisions and escaping paths are
|
||||
rejected. The bundle permits at most 256 files, 32 include edges along a path,
|
||||
and 1,024 include edges in total. Model and source-byte budgets apply across
|
||||
the complete bundle, not separately per file. Current bundle syntax is source4; source3 bundles migrate explicitly while
|
||||
preserving their files/includes/comments. V1/V2 single-file migration remains
|
||||
available through interchange import.</p>
|
||||
the complete bundle, not separately per file. Bundle syntax is source3;
|
||||
single-file legacy migration remains available through interchange import.</p>
|
||||
<p>Compilation inserts UUIDs into headers that omit them. Import returns 201 with
|
||||
Location/ETag; GET source-project retrieves the stabilized accepted bundle.
|
||||
PUT returns <span class="docutils literal">revision</span>, <span class="docutils literal">changed</span>, located <span class="docutils literal">issues</span> and that accepted
|
||||
@@ -966,11 +965,8 @@ does not execute conditions or simulate a process.</p>
|
||||
store <span class="docutils literal">message_kind</span>, optional correlated-call <span class="docutils literal">reply_to</span> and contextual
|
||||
<span class="docutils literal">values</span>. Message kinds are Asynchronous, Call, Reply, Create, Delete, or
|
||||
<span class="docutils literal"><span class="pre">{"Timer":{"expression":"after</span> 5 <span class="pre">s"}}</span></span>. Each value record contains a
|
||||
carried DataField UUID in <span class="docutils literal">field</span>, enclosing field UUID list <span class="docutils literal">path</span> (default
|
||||
[]) and textual <span class="docutils literal">expression</span>. Identity is (path, field), allowing repeated uses
|
||||
of a Record. Operation Call/Reply direction checks use the direct parameter;
|
||||
Reference storage stops nested payload traversal. Expressions do not change the
|
||||
shared exchange item's type or evaluate server-side.</p>
|
||||
carried DataField UUID in <span class="docutils literal">field</span> and its textual <span class="docutils literal">expression</span>; expressions
|
||||
do not change the shared exchange item's type or evaluate server-side.</p>
|
||||
<p><span class="docutils literal">ScenarioUse</span> contains a referenced <span class="docutils literal">scenario</span> and one-to-one lifeline
|
||||
<span class="docutils literal">bindings</span> records with <span class="docutils literal">referenced</span> and <span class="docutils literal">local</span> UUIDs. The core validates
|
||||
viewpoint/layer compatibility, call/reply order and reversal, lifetime order,
|
||||
@@ -982,26 +978,6 @@ exports make those compatibility defaults explicit. This preserves original V2
|
||||
models while the release schema is being integrated; it does not silently
|
||||
reinterpret old mixed lifelines as a strict new viewpoint.</p>
|
||||
</section>
|
||||
<section id="data-and-interface-contracts">
|
||||
<h2>Data and interface contracts</h2>
|
||||
<p>Schema4 uses typed bounded DataTypeDefinition and DataValue constructors, field
|
||||
storage/defaults/directions, and single specialization. Exact JSON shapes are in
|
||||
<span class="docutils literal">openapi.json</span> and semantic rules and runnable source in <span class="docutils literal">language.rst</span> section 4.5.
|
||||
Interface membership is an identified InterfaceItemAllocation relation with
|
||||
independent order/transmission/acquisition. DataAssociation relates Record types
|
||||
with named, navigable, multiplicity-bearing ends; it does not add carried fields.</p>
|
||||
<p>Use ordinary relation queries, transactions and artifact links, for example
|
||||
<span class="docutils literal">GET <span class="pre">/api/v1/projects/{id}/relations?kind=InterfaceItemAllocation</span></span>. The service
|
||||
and desktop use the same inheritance/path/default validation. Invalid bounds,
|
||||
defaults, directions, cycles or contract-breaking deletions reject atomically,
|
||||
preserving the accepted source/model, revision and stored bytes. Correct a related
|
||||
contract in the same transaction when removing a used interface allocation.</p>
|
||||
<p>Explicit schema1/2/3 migration preserves prior identities and typed ports. Reports
|
||||
add semantic defaults, generated interface allocations and unresolved message
|
||||
values. Old source3 bundles retain files/includes/comments during upgrade; all
|
||||
current exports use schema4/source4. Reading an old envelope does not write it or
|
||||
advance its ETag. Canonical exports omit transient migration reports.</p>
|
||||
</section>
|
||||
<section id="validation">
|
||||
<h2>Validation</h2>
|
||||
<p>GET returns current-model diagnostics. POST accepts either:</p>
|
||||
@@ -1009,7 +985,7 @@ advance its ETag. Canonical exports omit transient migration reports.</p>
|
||||
<p>or:</p>
|
||||
<pre class="literal-block">{"format":"arcadia", "source":"complete source document"}</pre>
|
||||
<p>or:</p>
|
||||
<pre class="literal-block">{"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 4; project rescue \"Rescue\" {}"}}}</pre>
|
||||
<pre class="literal-block">{"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 3; project rescue \"Rescue\" {}"}}}</pre>
|
||||
<p>A well-formed validation request returns 200 with <span class="docutils literal">valid</span> and <span class="docutils literal">issues</span> even
|
||||
if its candidate is invalid, including a model record that fails typed decoding.
|
||||
Malformed request JSON or an invalid format/payload envelope is a 400.
|
||||
|
||||
+8
-35
@@ -146,8 +146,8 @@ Parsing, validation and query processing use snapshot workers, so this work
|
||||
does not hold the catalog lock or serialize unrelated model preparation.
|
||||
|
||||
Import accepts schema-1 JSON through the shared explicit migration adapter,
|
||||
as well as schema-2, schema-3 and current schema-4 JSON. Source import uses the shared language parser
|
||||
and its supported migrations. Export uses schema 4 / arcadia 4. Native JSON
|
||||
as well as schema-2 native JSON. Source import uses the shared language parser
|
||||
and its supported migrations. Export uses schema 3 / arcadia 3. Native JSON
|
||||
replacement accepts supported older schemas through the same explicit migration.
|
||||
Only documented query parameters are accepted. In particular, appending
|
||||
``?revision=...`` to model/source export is an error, not historical retrieval.
|
||||
@@ -160,7 +160,7 @@ A source bundle contains ``entry`` and a map of ``files``. For example::
|
||||
{
|
||||
"entry": "rescue.arcadia",
|
||||
"files": {
|
||||
"rescue.arcadia": "arcadia 4; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n",
|
||||
"rescue.arcadia": "arcadia 3; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n",
|
||||
"oa/work.arcadia": "// Engineering rationale\noa_activity assess \"Assess assistance\" {}\n"
|
||||
}
|
||||
}
|
||||
@@ -172,9 +172,8 @@ targets or another hosted resource. All submitted files must be reachable;
|
||||
missing files, cycles, duplicate keys, case collisions and escaping paths are
|
||||
rejected. The bundle permits at most 256 files, 32 include edges along a path,
|
||||
and 1,024 include edges in total. Model and source-byte budgets apply across
|
||||
the complete bundle, not separately per file. Current bundle syntax is source4; source3 bundles migrate explicitly while
|
||||
preserving their files/includes/comments. V1/V2 single-file migration remains
|
||||
available through interchange import.
|
||||
the complete bundle, not separately per file. Bundle syntax is source3;
|
||||
single-file legacy migration remains available through interchange import.
|
||||
|
||||
Compilation inserts UUIDs into headers that omit them. Import returns 201 with
|
||||
Location/ETag; GET source-project retrieves the stabilized accepted bundle.
|
||||
@@ -356,11 +355,8 @@ Scenario bodies store an explicit ``viewpoint``. Occurrence Exchange bodies
|
||||
store ``message_kind``, optional correlated-call ``reply_to`` and contextual
|
||||
``values``. Message kinds are Asynchronous, Call, Reply, Create, Delete, or
|
||||
``{"Timer":{"expression":"after 5 s"}}``. Each value record contains a
|
||||
carried DataField UUID in ``field``, enclosing field UUID list ``path`` (default
|
||||
[]) and textual ``expression``. Identity is (path, field), allowing repeated uses
|
||||
of a Record. Operation Call/Reply direction checks use the direct parameter;
|
||||
Reference storage stops nested payload traversal. Expressions do not change the
|
||||
shared exchange item's type or evaluate server-side.
|
||||
carried DataField UUID in ``field`` and its textual ``expression``; expressions
|
||||
do not change the shared exchange item's type or evaluate server-side.
|
||||
|
||||
``ScenarioUse`` contains a referenced ``scenario`` and one-to-one lifeline
|
||||
``bindings`` records with ``referenced`` and ``local`` UUIDs. The core validates
|
||||
@@ -374,29 +370,6 @@ exports make those compatibility defaults explicit. This preserves original V2
|
||||
models while the release schema is being integrated; it does not silently
|
||||
reinterpret old mixed lifelines as a strict new viewpoint.
|
||||
|
||||
Data and interface contracts
|
||||
----------------------------
|
||||
|
||||
Schema4 uses typed bounded DataTypeDefinition and DataValue constructors, field
|
||||
storage/defaults/directions, and single specialization. Exact JSON shapes are in
|
||||
``openapi.json`` and semantic rules and runnable source in ``language.rst`` section 4.5.
|
||||
Interface membership is an identified InterfaceItemAllocation relation with
|
||||
independent order/transmission/acquisition. DataAssociation relates Record types
|
||||
with named, navigable, multiplicity-bearing ends; it does not add carried fields.
|
||||
|
||||
Use ordinary relation queries, transactions and artifact links, for example
|
||||
``GET /api/v1/projects/{id}/relations?kind=InterfaceItemAllocation``. The service
|
||||
and desktop use the same inheritance/path/default validation. Invalid bounds,
|
||||
defaults, directions, cycles or contract-breaking deletions reject atomically,
|
||||
preserving the accepted source/model, revision and stored bytes. Correct a related
|
||||
contract in the same transaction when removing a used interface allocation.
|
||||
|
||||
Explicit schema1/2/3 migration preserves prior identities and typed ports. Reports
|
||||
add semantic defaults, generated interface allocations and unresolved message
|
||||
values. Old source3 bundles retain files/includes/comments during upgrade; all
|
||||
current exports use schema4/source4. Reading an old envelope does not write it or
|
||||
advance its ETag. Canonical exports omit transient migration reports.
|
||||
|
||||
Validation
|
||||
----------
|
||||
|
||||
@@ -410,7 +383,7 @@ or::
|
||||
|
||||
or::
|
||||
|
||||
{"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 4; project rescue \"Rescue\" {}"}}}
|
||||
{"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 3; project rescue \"Rescue\" {}"}}}
|
||||
|
||||
A well-formed validation request returns 200 with ``valid`` and ``issues`` even
|
||||
if its candidate is invalid, including a model record that fails typed decoding.
|
||||
|
||||
+42
-215
@@ -623,7 +623,7 @@ a { text-decoration: none; }
|
||||
<h1 class="title">Arcadia Studio OA / SA Language and Editing Contract</h1>
|
||||
<dl class="docinfo simple">
|
||||
<dt class="version">Version<span class="colon">:</span></dt>
|
||||
<dd class="version">4</dd>
|
||||
<dd class="version">3</dd>
|
||||
<dt class="status">Status<span class="colon">:</span></dt>
|
||||
<dd class="status">Normative current implementation contract; first-release integration</dd>
|
||||
<dt class="core-implementation">Core implementation<span class="colon">:</span></dt>
|
||||
@@ -637,13 +637,14 @@ a { text-decoration: none; }
|
||||
<h2>1. Scope and conformance</h2>
|
||||
<p>This specification defines the shared Operational Analysis (OA) and System
|
||||
Analysis (SA) model, its textual language, graphical editing obligations,
|
||||
validation, persistence, and explicit migration from versions 1, 2 and 3. MUST, MUST NOT,
|
||||
validation, persistence, and explicit migration from versions 1 and 2. MUST, MUST NOT,
|
||||
and MAY indicate requirements, prohibitions, and permitted choices.</p>
|
||||
<p>Schema 4 adds bounded reusable data types, typed scalar defaults, exchange
|
||||
mechanisms, qualified message values, specialization, identified interface
|
||||
allocations and reference associations. It retains schema 3 typed ports and
|
||||
state views. Explicit older readers preserve existing meanings and identities.
|
||||
This contract does not by itself establish native acceptance or release completion.</p>
|
||||
<p>Schema 3 introduces typed function and component ports, required functional
|
||||
exchange endpoints, independently oriented boundary exchanges, and explicit
|
||||
PortAllocation relations. First-release scenario/state additions remain in
|
||||
scope; this schema change does not declare native acceptance or the wider
|
||||
release complete. The preserved schema-2 reader validates the original
|
||||
contract before migration.</p>
|
||||
<p>OA describes stakeholders, operational work and needs. SA describes the
|
||||
selected system boundary, external actors, functions, services and expected
|
||||
behavior. SA permits at most one System of Interest; its absence is an
|
||||
@@ -661,7 +662,7 @@ Actual verification evidence is separate from this contract.</p>
|
||||
</section>
|
||||
<section id="one-authoritative-model">
|
||||
<h2>2. One authoritative model</h2>
|
||||
<p>A Project stores <span class="docutils literal">schema_version</span> (4), <span class="docutils literal">id</span>, <span class="docutils literal">alias</span>, <span class="docutils literal">name</span>,
|
||||
<p>A Project stores <span class="docutils literal">schema_version</span> (3), <span class="docutils literal">id</span>, <span class="docutils literal">alias</span>, <span class="docutils literal">name</span>,
|
||||
<span class="docutils literal">description</span>, and maps named <span class="docutils literal">elements</span>, <span class="docutils literal">relations</span>, <span class="docutils literal">diagrams</span>,
|
||||
<span class="docutils literal">artifact_links</span>, and <span class="docutils literal">traces</span>. Each map is keyed by the UUID contained in
|
||||
its record. Duplicate map keys and mismatching keys/identities are errors.</p>
|
||||
@@ -721,20 +722,20 @@ lists all additional fields. <span class="docutils literal">ref</span> means <sp
|
||||
<td><p><span class="docutils literal">[]</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_exchange_item</span>, <span class="docutils literal">sa_exchange_item</span></p></td>
|
||||
<td><p><span class="docutils literal">kind</span>: Information, Material, Energy; <span class="docutils literal">mechanism</span>: ExchangeMechanism; <span class="docutils literal">supertype</span>: item ref?</p></td>
|
||||
<td><p>Information; Unspecified; none</p></td>
|
||||
<td><p><span class="docutils literal">kind</span>: Information, Material, Energy, Event</p></td>
|
||||
<td><p>Information</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_data_type</span>, <span class="docutils literal">sa_data_type</span></p></td>
|
||||
<td><p><span class="docutils literal">definition</span>: DataTypeDefinition; <span class="docutils literal">default</span>: DataValue?; <span class="docutils literal">unit</span>: string</p></td>
|
||||
<td><p>String with minimum_length 0 / maximum_length none; none; <span class="docutils literal">""</span></p></td>
|
||||
<td><p><span class="docutils literal">definition</span>: Boolean, Integer, Real, String, Record, or Enumeration constructor</p></td>
|
||||
<td><p>String</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_data_field</span>, <span class="docutils literal">sa_data_field</span></p></td>
|
||||
<td><p><span class="docutils literal">type_ref</span>; <span class="docutils literal">minimum</span>: u32; <span class="docutils literal">maximum</span>: optional u32; <span class="docutils literal">unit</span>: string; <span class="docutils literal">direction</span>: ParameterDirection; <span class="docutils literal">storage</span>: FieldStorage; <span class="docutils literal">default</span>: DataValue list?</p></td>
|
||||
<td><p>String; 1; 1; <span class="docutils literal">""</span>; Unspecified; Value; none</p></td>
|
||||
<td><p><span class="docutils literal">type_ref</span>; <span class="docutils literal">minimum</span>: u32; <span class="docutils literal">maximum</span>: optional u32; <span class="docutils literal">unit</span>: string</p></td>
|
||||
<td><p>String; 1; 1; <span class="docutils literal">""</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_interface</span>, <span class="docutils literal">sa_interface</span></p></td>
|
||||
<td><p><span class="docutils literal">supertype</span>: interface ref?</p></td>
|
||||
<td><p>none; membership is an identified interface_item_allocation relation</p></td>
|
||||
<td><p><span class="docutils literal">items</span>: exchange-item refs</p></td>
|
||||
<td><p><span class="docutils literal">[]</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_port</span>, <span class="docutils literal">sa_port</span></p></td>
|
||||
<td><p><span class="docutils literal">contract</span>: required PortContract; <span class="docutils literal">legacy</span>: optional LegacyPortContract</p></td>
|
||||
@@ -882,14 +883,6 @@ references. Their kind is the declaration keyword:</p>
|
||||
<td><p>Same-layer participant → leaf activity/function</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">interface_item_allocation</span></p></td>
|
||||
<td><p>Same-layer Interface → ExchangeItem</p></td>
|
||||
<td><p><span class="docutils literal">order</span> u32 / 0; <span class="docutils literal">transmission</span> TransmissionProtocol / Unspecified; <span class="docutils literal">acquisition</span> AcquisitionProtocol / Unspecified</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">data_association</span></p></td>
|
||||
<td><p>Same-layer Record DataType → Record DataType</p></td>
|
||||
<td><p><span class="docutils literal">source_role</span>, <span class="docutils literal">target_role</span> / ""; <span class="docutils literal">source_minimum</span>, <span class="docutils literal">target_minimum</span> / 0; <span class="docutils literal">source_maximum</span>, <span class="docutils literal">target_maximum</span> / none; <span class="docutils literal">source_navigable</span>, <span class="docutils literal">target_navigable</span> / false</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">interaction</span></p></td>
|
||||
<td><p>OA activity → OA activity</p></td>
|
||||
<td><p><span class="docutils literal">items</span> refs / []</p></td>
|
||||
@@ -970,8 +963,7 @@ references. Their kind is the declaration keyword:</p>
|
||||
</table>
|
||||
<p>Relation endpoints and carried ExchangeItems stay in the same layer. Parallel
|
||||
exchanges with distinct identities are permitted. Self-relations are invalid
|
||||
except for a state transition that returns to its source state or an explicit
|
||||
DataAssociation between two ends of the same Record type.</p>
|
||||
except for a state transition that returns to its source state.</p>
|
||||
<p>Direct Allocation has at most one performer per leaf. Role sharing is a
|
||||
different relationship: several OA participants can realize a Role. A direct
|
||||
allocation inconsistent with its role's realized performers produces an
|
||||
@@ -1019,13 +1011,12 @@ not create endpoints. Graphical/command FE creation plans ports atomically,
|
||||
using unique compatible typed-port reuse or deterministic new endpoints.
|
||||
Port deletion cascades to incident FEs; FE deletion retains independent ports.</p>
|
||||
<p>For example:</p>
|
||||
<pre class="literal-block">arcadia 4;
|
||||
<pre class="literal-block">arcadia 3;
|
||||
project ports_example "Typed function contract" {
|
||||
sa_function send "Send" {}
|
||||
sa_function receive "Receive" {}
|
||||
sa_exchange_item payload "Payload" {}
|
||||
sa_interface old_service "Preserved service" {}
|
||||
interface_item_allocation service_payload "Service payload" { source @old_service; target @payload; }
|
||||
sa_interface old_service "Preserved service" { items [@payload]; }
|
||||
sa_port output "Output" {
|
||||
parent @send;
|
||||
contract { FunctionOutput { outgoing_items [@payload]; }; };
|
||||
@@ -1134,19 +1125,11 @@ local to its sender lifeline, has no global exchange or values, and has either
|
||||
no receiver or the same receiver as sender. Its nonempty expression describes
|
||||
the delay/deadline; an empty expression warns. Conditions and expressions are
|
||||
stored engineering annotations, not an executable simulation language.</p>
|
||||
<p>Each MessageValue is <span class="docutils literal">{ path [@parameter]; field @data_field; expression "value expression"; }</span>.
|
||||
Its identity is the pair (path, field), so two uses of one Record can have
|
||||
different values for the same terminal field. Path contains enclosing DataField
|
||||
references, excluding the terminal field; direct parameters use <span class="docutils literal">[]</span>. Every
|
||||
hop MUST follow Value storage through a named Record's effective fields. Reference
|
||||
storage stops traversal. Operation Call accepts In/InOut and Reply accepts
|
||||
Out/InOut/Return, interpreted from the provider's perspective. Nested field paths
|
||||
inherit their direct parameter's direction. Unspecified direction warns.
|
||||
At most one annotation for a pair is allowed. Omitted path defaults to <span class="docutils literal">[]</span>;
|
||||
an ambiguous preserved legacy nested annotation warns <span class="docutils literal">message_value_ambiguous_path</span>
|
||||
until an explicit path is selected. New graphical choices always identify a path.
|
||||
Values never overwrite types or items. Expression text is descriptive, preserved
|
||||
exactly and not evaluated; an empty expression warns.</p>
|
||||
<p>Each MessageValue is <span class="docutils literal">{ field @data_field; expression "value expression"; }</span>.
|
||||
The field MUST be a DataField carried by the message exchange, including fields
|
||||
inside named nested records. At most one annotation for a given field is
|
||||
allowed in one message. Values do not overwrite the referenced type or global
|
||||
exchange item. Expression text is preserved exactly; an empty expression warns.</p>
|
||||
<p>ScenarioUse references an existing same-layer scenario and maps its lifelines
|
||||
with <span class="docutils literal">bindings [ { referenced @callee_line; local @caller_line; } ];</span>.
|
||||
Each binding is one-to-one and references a direct lifeline of the appropriate
|
||||
@@ -1297,137 +1280,6 @@ DeepHistory remembers nested configuration. Fallback applies when no resumable
|
||||
history exists. History is per Region, including top-level Regions in this
|
||||
Studio profile; no running history state is persisted or synthesized.</p>
|
||||
</section>
|
||||
<section id="data-contracts-specialization-and-interface-allocation">
|
||||
<h3>4.5. Data contracts, specialization and interface allocation</h3>
|
||||
<p>ExchangeItem <span class="docutils literal">kind</span> describes nature: Information, Material or Energy.
|
||||
<span class="docutils literal">mechanism</span> is Unspecified, Flow, Event, Operation or SharedData. Unknown
|
||||
mechanisms remain explicit Unspecified drafts with a warning. Material/Energy
|
||||
with Event/Operation/SharedData warns for engineering review; it is not silently
|
||||
reclassified. Empty Event notifications and void Operations are valid. An empty
|
||||
carried/allocated Flow, SharedData or Unspecified item warns.</p>
|
||||
<p>An Operation's directly owned DataFields are parameters. <span class="docutils literal">direction</span> is
|
||||
Unspecified, In, Out, InOut or Return, from the provider's perspective. At most
|
||||
one effective parameter is Return. Unspecified parameters warn; non-operation
|
||||
fields MUST use Unspecified. Nested record fields inherit direction from their
|
||||
direct parameter when used in message paths, rather than storing context on a
|
||||
shared type. <span class="docutils literal">storage</span> is Value or Reference. Reference requires a named Record,
|
||||
has no explicit default, and does not convey ownership or deletion of the type.</p>
|
||||
<p>DataTypeDefinition has exactly the following constructors:</p>
|
||||
<table>
|
||||
<caption>Reusable definitions</caption>
|
||||
<thead>
|
||||
<tr><th class="head"><p>Constructor</p></th>
|
||||
<th class="head"><p>Required fields</p></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><p>Boolean</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Integer</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: optional signed 64-bit integers</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Real</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: optional finite IEEE-754 binary64 values</p></td>
|
||||
</tr>
|
||||
<tr><td><p>String</p></td>
|
||||
<td><p><span class="docutils literal">minimum_length</span>: u32; <span class="docutils literal">maximum_length</span>: optional u32</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Enumeration</p></td>
|
||||
<td><p><span class="docutils literal">literals</span>: unique nonempty, case-sensitive strings</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Record</p></td>
|
||||
<td><p><span class="docutils literal">supertype</span>: same-layer Record ref?</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Bounds are inclusive and ordered; an absent bound means unbounded within the
|
||||
represented numeric domain. Integer comparisons MUST NOT convert to binary64.
|
||||
NaN, infinities, overflow and reversed bounds reject. String length counts Unicode
|
||||
scalar values, not UTF-8 bytes or grapheme clusters. Enum defaults match exact
|
||||
literals. Empty enum definitions remain incomplete drafts with a warning.</p>
|
||||
<p>DataValue is a tagged scalar: <span class="docutils literal">{ Boolean true; }</span>, <span class="docutils literal">{ Integer 42; }</span>,
|
||||
<span class="docutils literal">{ Real 1.5; }</span>, <span class="docutils literal">{ String "value"; }</span> or <span class="docutils literal">{ Enumeration "Ready"; }</span>.
|
||||
There is no implicit numeric or string coercion. A type default is one compatible
|
||||
scalar within its bounds. Record type defaults and record/object instance literals
|
||||
are unsupported. A field default is absent (<span class="docutils literal">none</span>) or a list of compatible
|
||||
scalars with length inside its multiplicity. <span class="docutils literal">[]</span> explicitly supplies an empty
|
||||
collection and requires minimum 0, including Value Record fields; a nonempty
|
||||
Record literal rejects. Reference fields MUST use <span class="docutils literal">none</span>. A one-valued scalar
|
||||
field uses a one-value list. A named type default supplies an individual element's
|
||||
default, not a collection or an implicit instance of a mandatory field.</p>
|
||||
<p>Numeric types may carry unit text. Empty field units inherit the named type unit;
|
||||
an explicit different unit warns when that type has a unit. Units on nonnumeric
|
||||
fields/types warn and remain preserved. Unit strings never convert values or
|
||||
establish dimensional compatibility. Expressions are not executed.</p>
|
||||
<p>Record, ExchangeItem and Interface each permit one same-family, same-layer
|
||||
supertype. Specialization is acyclic and separate from containment. Effective
|
||||
fields are base fields followed by local fields, preserving original field UUIDs
|
||||
and owners. Local names MUST NOT shadow inherited names. Item specialization
|
||||
preserves base nature and any specified mechanism; an Unspecified base mechanism
|
||||
may be refined. Deleting a derived record never deletes its base. Mandatory
|
||||
Value-field cycles warn <span class="docutils literal">record_no_finite_value</span>; optional or Reference cycles
|
||||
are valid without that warning. Traversal is cycle guarded and bounded; a payload
|
||||
path budget failure is an explicit diagnostic, not silent truncation.</p>
|
||||
<p>InterfaceItemAllocation is an ordinary identified relation from Interface to
|
||||
ExchangeItem. It stores <span class="docutils literal">order</span> and independent transmission/acquisition
|
||||
protocols. Transmission is Unspecified, Synchronous, Asynchronous, Unicast,
|
||||
Multicast or Broadcast; acquisition is Unspecified, Read or Accept. No protocol
|
||||
combination is inferred from mechanism. Effective allocations are base-first,
|
||||
then local ordered by (order, alias, UUID). A local item already allocated locally
|
||||
or inherited is an error. Removing an allocation keeps its item and other
|
||||
allocations; if live boundary compatibility would break, the transaction MUST
|
||||
repair the affected contract explicitly or reject atomically. Artifacts can
|
||||
reference the allocation identity independently of the global item.</p>
|
||||
<p>An exchange carrying a specialized item satisfies a declaration of its base,
|
||||
never the reverse. Standard provided/required compatibility uses explicit
|
||||
interface ancestry. Identity/ancestry, not matching names, defines conformance.
|
||||
The current typed function/component port contracts remain unchanged.</p>
|
||||
<p>DataAssociation connects Record types without adding fields or transitively
|
||||
carrying payload. Each named end has role, multiplicity and navigability. Source
|
||||
multiplicity counts source instances per target instance and vice versa; it is
|
||||
not a count of model elements. An end's navigable flag permits navigation toward
|
||||
that end. Self-associations are valid; if both ends are navigable they require
|
||||
distinct nonempty roles. Empty roles and duplicate role contracts warn, whereas
|
||||
invalid ranges reject. Composition/shared aggregation, multiple inheritance,
|
||||
redefinition, instance literals and arbitrary-precision decimal arithmetic are
|
||||
outside this bounded contract.</p>
|
||||
<p>This complete data contract example is also exercised through the REST API:</p>
|
||||
<pre class="code text literal-block"><code>arcadia 4;
|
||||
project engineering_data "Engineering data" {
|
||||
sa_data_type voltage "Voltage" {
|
||||
definition { Real { minimum 0.0; maximum 48.0; }; };
|
||||
default { Real 24.0; }; unit "V";
|
||||
}
|
||||
sa_data_type coordinate "Coordinate" {
|
||||
definition { Record { supertype none; }; };
|
||||
sa_data_field latitude "Latitude" { type_ref Real; }
|
||||
}
|
||||
sa_data_type located "Located asset" {
|
||||
definition { Record { supertype @coordinate; }; };
|
||||
sa_data_field supply "Supply" { type_ref { Named @voltage; }; }
|
||||
}
|
||||
sa_exchange_item navigate "Navigate" {
|
||||
mechanism Operation;
|
||||
sa_data_field origin "Origin" { type_ref { Named @coordinate; }; direction In; }
|
||||
sa_data_field destination "Destination" { type_ref { Named @coordinate; }; direction In; }
|
||||
sa_data_field accepted "Accepted" { type_ref Boolean; direction Return; }
|
||||
}
|
||||
sa_interface navigation "Navigation" {}
|
||||
sa_interface monitoring "Monitoring" {}
|
||||
interface_item_allocation nav_operation "Navigation operation" {
|
||||
source @navigation; target @navigate; transmission Synchronous; acquisition Accept;
|
||||
}
|
||||
interface_item_allocation monitor_operation "Monitoring operation" {
|
||||
source @monitoring; target @navigate; transmission Asynchronous; acquisition Read;
|
||||
}
|
||||
data_association location_link "Location relationship" {
|
||||
source @coordinate; target @located;
|
||||
source_role "location"; target_role "assets";
|
||||
source_maximum 1;
|
||||
}
|
||||
}</code></pre>
|
||||
</section>
|
||||
</section>
|
||||
<section id="diagrams-artifacts-and-cross-layer-traceability">
|
||||
<h2>5. Diagrams, artifacts, and cross-layer traceability</h2>
|
||||
@@ -1456,7 +1308,7 @@ remove or relocate incompatible placements in the same transaction. Context
|
||||
errors use <span class="docutils literal">state_diagram_context</span>; incompatible placements use
|
||||
<span class="docutils literal">state_diagram_member</span>. Neither case silently hides invalid members.</p>
|
||||
<p>This complete source example places one state from an existing machine:</p>
|
||||
<pre class="code text literal-block"><code>arcadia 4;
|
||||
<pre class="code text literal-block"><code>arcadia 3;
|
||||
project operating "Operating modes" {
|
||||
oa_entity crew "Crew" {}
|
||||
oa_state_machine modes "Modes" { parent @crew; }
|
||||
@@ -1665,7 +1517,7 @@ spellings can be used in alias positions because references begin with <span cla
|
||||
<span class="docutils literal"><span class="pre">element-kind</span></span> and <span class="docutils literal"><span class="pre">relation-kind</span></span> are exactly the corresponding declaration
|
||||
keywords listed above. Typed field
|
||||
sets and enum constructors are closed by sections 3–5.</p>
|
||||
<pre class="code text literal-block"><code>document = "arcadia" "4" ";" project
|
||||
<pre class="code text literal-block"><code>document = "arcadia" "3" ";" project
|
||||
project = "project" alias string identity? "{" project-entry* "}"
|
||||
project-entry = "description" string ";" | declaration
|
||||
declaration = element
|
||||
@@ -1702,16 +1554,16 @@ Ordinary string fields require quoted strings and cannot contain an unquoted
|
||||
<span class="docutils literal">@alias</span> reference. Enum fields require unquoted named values or their typed
|
||||
constructor records. Typed values MUST NOT nest more than 64 levels; malformed
|
||||
deeply nested drafts produce a diagnostic instead of exhausting the parser stack.</p>
|
||||
<section id="modular-source-projects">
|
||||
<h3>6.1. Modular source projects</h3>
|
||||
<section id="modular-source3-projects">
|
||||
<h3>6.1. Modular source3 projects</h3>
|
||||
<p>The pure <span class="docutils literal">source_project</span> core API compiles a virtual source bundle, containing
|
||||
<span class="docutils literal">entry</span> and a <span class="docutils literal">files</span> map from logical paths to UTF-8 strings. Its entry is a
|
||||
root-level <span class="docutils literal">.arcadia</span> file with the source4 project header above. A project entry
|
||||
root-level <span class="docutils literal">.arcadia</span> file with the source3 project header above. A project entry
|
||||
MAY also be <span class="docutils literal">include string ";"</span>. An included fragment consists only of ordinary
|
||||
declarations and include statements; it MUST NOT contain another version header,
|
||||
project wrapper, or project description. Includes MUST occur at document level,
|
||||
never inside an element body or typed field value. For example:</p>
|
||||
<pre class="literal-block">arcadia 4;
|
||||
<pre class="literal-block">arcadia 3;
|
||||
project rescue "Rescue" id "00000000-0000-4000-8000-000000000001" {
|
||||
description "Operational model";
|
||||
include "oa/participants.arcadia";
|
||||
@@ -1769,12 +1621,9 @@ the source index. Columns remain one-based Unicode scalars within the real file;
|
||||
spans never refer to concatenated or synthetic source.</p>
|
||||
<p>Single-string parse/lint APIs remain filesystem-free and reject include
|
||||
statements with an explicit source-bundle requirement. V1/V2 single-file imports
|
||||
continue through explicit migration. Modular projects accept source3 and source4;
|
||||
source3 is upgraded as one project, preserving file paths, includes, nested
|
||||
ownership and comments through targeted token edits. Generated allocation
|
||||
declarations stay in their interface's existing file. Individual include fragments
|
||||
are not independently migrated. Missing UUIDs stabilize before migration so new
|
||||
allocation identities are deterministic on subsequent compilation.</p>
|
||||
continue through their existing migration readers and reports. Modular projects
|
||||
require source3; migration is performed before modular export, not independently
|
||||
on included fragments. Modular source does not change the semantic JSON schema.</p>
|
||||
</section>
|
||||
<section id="nested-element-ownership">
|
||||
<h3>6.2. Nested element ownership</h3>
|
||||
@@ -1978,31 +1827,9 @@ explicitly before Apply succeeds.</p>
|
||||
<h2>9. Persistence and explicit migration</h2>
|
||||
<p>Loading detects JSON from its first non-whitespace <span class="docutils literal">{</span>; other input is parsed
|
||||
as Arcadia source regardless of filename. Saving an <span class="docutils literal">.arcadia</span> suffix uses
|
||||
the accepted V4 source bundle (<span class="docutils literal">arcadia 4</span>); other suffixes use V4 JSON. Both forms preserve the same
|
||||
the accepted V3 source bundle (<span class="docutils literal">arcadia 3</span>); other suffixes use V3 JSON. Both forms preserve the same
|
||||
complete model. JSON rejects unknown fields throughout typed payloads,
|
||||
duplicate maps, unsupported versions, and structural errors.</p>
|
||||
<p>The schema-3 reader freezes the changed data payloads and old constructor catalog;
|
||||
schema-4 fields/constructors MUST NOT be silently accepted as schema 3. Existing
|
||||
typed PortContract records remain unchanged. Information/Material/Energy retain
|
||||
their nature with Unspecified mechanism; old Event becomes Information with Event
|
||||
mechanism. Old bounds/defaults/supertypes are absent, String length is 0..unbounded,
|
||||
and fields retain type/multiplicity/unit with Unspecified direction, Value storage
|
||||
and no explicit default. No physical mechanism or direction is guessed.</p>
|
||||
<p>Each old Interface.items entry generates one InterfaceItemAllocation with its
|
||||
original zero-based order and Unspecified protocols. UUID v5 uses the project UUID
|
||||
namespace and name <span class="docutils literal"><span class="pre">v2-interface-item:<interface-uuid>:<item-uuid></span></span> consistently
|
||||
for schema-2 and schema-3 inputs. Aliases are deterministic and collision-free;
|
||||
collision with any existing identity is an error rather than an overwrite. Existing
|
||||
interface/item identities remain intact. Old nested message values acquire a path
|
||||
only when exactly one valid path exists. Ambiguous paths, including recursive
|
||||
reuse, retain the old annotation with a located warning rather than guessing.</p>
|
||||
<p>Migration reports additionally contain <span class="docutils literal">semantic_defaults</span> (object, field,
|
||||
description), <span class="docutils literal">generated_interface_allocations</span> (id, source, target, alias,
|
||||
order) and <span class="docutils literal">unresolved_message_values</span> (occurrence, field, reason). These new
|
||||
report arrays default empty when reading older responses. Version 1 normalizes
|
||||
through V2, then the typed-port V3 adapter and this shared V4 data upgrade. Original
|
||||
V2 and real V3 fixtures remain unchanged compatibility inputs. Current exports
|
||||
always use version 4; reading/migrating alone never saves or changes revisions.</p>
|
||||
<p>Native source Save All checks the original contents of observed dependencies
|
||||
before staging writes. Existing target files are replaced individually and the
|
||||
entry is installed last. A transaction journal records originals and intended
|
||||
@@ -2026,7 +1853,7 @@ defaults without changing any prior identity, name, reference, order, geometry,
|
||||
artifact or trace field. The original V2 golden source/JSON in
|
||||
<span class="docutils literal"><span class="pre">crates/oa-core/tests/fixtures</span></span> MUST NOT be regenerated from the newer demo;
|
||||
they verify that every old persistent field survives. Canonical output writes
|
||||
the added defaults through the explicit schema-3 and schema-4 migrations.</p>
|
||||
the added defaults through the explicit schema-3 migration.</p>
|
||||
<p>The V2 adapter preserves every original identity, alias, prose, parent,
|
||||
placement, trace and artifact. Generic ports become Legacy with their original
|
||||
direction/interface record. Existing FE endpoint IDs remain unchanged. For
|
||||
@@ -2043,13 +1870,13 @@ IDs, required resolutions, generated endpoint IDs/aliases/owners, and boundary
|
||||
orientation defaults. REST import, replacement, candidate validation and
|
||||
project snapshots surface the same report. Opening an old server envelope
|
||||
preserves its resource ID/revision and bytes; the next explicit edit writes
|
||||
schema 4. Model/source exports contain only the canonical model. Migration
|
||||
schema 3. Model/source exports contain only the canonical model. Migration
|
||||
reports are transient response metadata, not persisted model facts.</p>
|
||||
<p>Admission checks account for generated objects and their references before
|
||||
construction. Both the old V2 contract and the resulting V4 graph are
|
||||
construction. Both the old V2 contract and the resulting V3 graph are
|
||||
validated. Migration itself does not save or increase a stored revision.
|
||||
Canonical second saves are stable. V1 normalization produces the frozen V2
|
||||
model and then uses these same V2-to-3-to-4 stages.</p>
|
||||
model and then uses this same V2-to-3 stage.</p>
|
||||
<p>Save validates a snapshot, writes a sibling temporary file, flushes it, and
|
||||
atomically replaces the destination. Failed serialization or validation must
|
||||
not truncate a previous file. External concurrent-writer conflict resolution
|
||||
@@ -2065,20 +1892,20 @@ step becomes a distinct occurrence element owned by its process. Generated
|
||||
diagram, node, and occurrence identities use UUID v5 with the old Project ID
|
||||
as namespace and deterministic role/index names. If such an identity collides,
|
||||
a deterministic suffix is tried. Reimporting the same fully identified V1
|
||||
input therefore yields the same V4 result, including repeated occurrences.
|
||||
input therefore yields the same V3 result, including repeated occurrences.
|
||||
No System or SA content is invented during an OA-only migration.</p>
|
||||
<p>V1 source aliases are retained when valid in V2. JSON imports generate the old
|
||||
canonical aliases deterministically. Any alias newly reserved in V2 is changed
|
||||
by a deterministic suffix; aliases were not persistent V1 semantic data.
|
||||
A project alias and aliases for new records are generated without colliding
|
||||
with retained aliases. The existing bundled V1 text and JSON migrate to equal
|
||||
V4 projects. Future versions remain errors until a separate migration exists.</p>
|
||||
V3 projects. Future versions remain errors until a separate migration exists.</p>
|
||||
</section>
|
||||
<section id="complete-authoring-example">
|
||||
<h2>10. Complete authoring example</h2>
|
||||
<p>This document intentionally omits IDs for new authoring. Apply and Save write
|
||||
the assigned UUIDs explicitly.</p>
|
||||
<pre class="code text literal-block"><code>arcadia 4;
|
||||
<pre class="code text literal-block"><code>arcadia 3;
|
||||
project harbor "Harbor assistance" {
|
||||
oa_entity control "Harbor control" {}
|
||||
oa_activity assess "Assess assistance" {}
|
||||
|
||||
+41
-219
@@ -1,7 +1,7 @@
|
||||
Arcadia Studio OA / SA Language and Editing Contract
|
||||
====================================================
|
||||
|
||||
:Version: 4
|
||||
:Version: 3
|
||||
:Status: Normative current implementation contract; first-release integration
|
||||
:Core implementation: ``crates/oa-core``
|
||||
:Text suffix: ``.arcadia``
|
||||
@@ -11,14 +11,15 @@ Arcadia Studio OA / SA Language and Editing Contract
|
||||
|
||||
This specification defines the shared Operational Analysis (OA) and System
|
||||
Analysis (SA) model, its textual language, graphical editing obligations,
|
||||
validation, persistence, and explicit migration from versions 1, 2 and 3. MUST, MUST NOT,
|
||||
validation, persistence, and explicit migration from versions 1 and 2. MUST, MUST NOT,
|
||||
and MAY indicate requirements, prohibitions, and permitted choices.
|
||||
|
||||
Schema 4 adds bounded reusable data types, typed scalar defaults, exchange
|
||||
mechanisms, qualified message values, specialization, identified interface
|
||||
allocations and reference associations. It retains schema 3 typed ports and
|
||||
state views. Explicit older readers preserve existing meanings and identities.
|
||||
This contract does not by itself establish native acceptance or release completion.
|
||||
Schema 3 introduces typed function and component ports, required functional
|
||||
exchange endpoints, independently oriented boundary exchanges, and explicit
|
||||
PortAllocation relations. First-release scenario/state additions remain in
|
||||
scope; this schema change does not declare native acceptance or the wider
|
||||
release complete. The preserved schema-2 reader validates the original
|
||||
contract before migration.
|
||||
|
||||
OA describes stakeholders, operational work and needs. SA describes the
|
||||
selected system boundary, external actors, functions, services and expected
|
||||
@@ -39,7 +40,7 @@ Actual verification evidence is separate from this contract.
|
||||
2. One authoritative model
|
||||
--------------------------
|
||||
|
||||
A Project stores ``schema_version`` (4), ``id``, ``alias``, ``name``,
|
||||
A Project stores ``schema_version`` (3), ``id``, ``alias``, ``name``,
|
||||
``description``, and maps named ``elements``, ``relations``, ``diagrams``,
|
||||
``artifact_links``, and ``traces``. Each map is keyed by the UUID contained in
|
||||
its record. Duplicate map keys and mismatching keys/identities are errors.
|
||||
@@ -95,17 +96,17 @@ lists all additional fields. ``ref`` means ``@alias``; ``ref?`` also accepts
|
||||
- ``steps``: occurrence refs
|
||||
- ``[]``
|
||||
* - ``oa_exchange_item``, ``sa_exchange_item``
|
||||
- ``kind``: Information, Material, Energy; ``mechanism``: ExchangeMechanism; ``supertype``: item ref?
|
||||
- Information; Unspecified; none
|
||||
- ``kind``: Information, Material, Energy, Event
|
||||
- Information
|
||||
* - ``oa_data_type``, ``sa_data_type``
|
||||
- ``definition``: DataTypeDefinition; ``default``: DataValue?; ``unit``: string
|
||||
- String with minimum_length 0 / maximum_length none; none; ``""``
|
||||
- ``definition``: Boolean, Integer, Real, String, Record, or Enumeration constructor
|
||||
- String
|
||||
* - ``oa_data_field``, ``sa_data_field``
|
||||
- ``type_ref``; ``minimum``: u32; ``maximum``: optional u32; ``unit``: string; ``direction``: ParameterDirection; ``storage``: FieldStorage; ``default``: DataValue list?
|
||||
- String; 1; 1; ``""``; Unspecified; Value; none
|
||||
- ``type_ref``; ``minimum``: u32; ``maximum``: optional u32; ``unit``: string
|
||||
- String; 1; 1; ``""``
|
||||
* - ``oa_interface``, ``sa_interface``
|
||||
- ``supertype``: interface ref?
|
||||
- none; membership is an identified interface_item_allocation relation
|
||||
- ``items``: exchange-item refs
|
||||
- ``[]``
|
||||
* - ``oa_port``, ``sa_port``
|
||||
- ``contract``: required PortContract; ``legacy``: optional LegacyPortContract
|
||||
- No implicit contract; ``legacy`` defaults none. OA admits Legacy only.
|
||||
@@ -229,12 +230,6 @@ references. Their kind is the declaration keyword:
|
||||
* - ``allocation``
|
||||
- Same-layer participant → leaf activity/function
|
||||
- None
|
||||
* - ``interface_item_allocation``
|
||||
- Same-layer Interface → ExchangeItem
|
||||
- ``order`` u32 / 0; ``transmission`` TransmissionProtocol / Unspecified; ``acquisition`` AcquisitionProtocol / Unspecified
|
||||
* - ``data_association``
|
||||
- Same-layer Record DataType → Record DataType
|
||||
- ``source_role``, ``target_role`` / ""; ``source_minimum``, ``target_minimum`` / 0; ``source_maximum``, ``target_maximum`` / none; ``source_navigable``, ``target_navigable`` / false
|
||||
* - ``interaction``
|
||||
- OA activity → OA activity
|
||||
- ``items`` refs / []
|
||||
@@ -295,8 +290,7 @@ references. Their kind is the declaration keyword:
|
||||
|
||||
Relation endpoints and carried ExchangeItems stay in the same layer. Parallel
|
||||
exchanges with distinct identities are permitted. Self-relations are invalid
|
||||
except for a state transition that returns to its source state or an explicit
|
||||
DataAssociation between two ends of the same Record type.
|
||||
except for a state transition that returns to its source state.
|
||||
|
||||
Direct Allocation has at most one performer per leaf. Role sharing is a
|
||||
different relationship: several OA participants can realize a Role. A direct
|
||||
@@ -353,13 +347,12 @@ Port deletion cascades to incident FEs; FE deletion retains independent ports.
|
||||
|
||||
For example::
|
||||
|
||||
arcadia 4;
|
||||
arcadia 3;
|
||||
project ports_example "Typed function contract" {
|
||||
sa_function send "Send" {}
|
||||
sa_function receive "Receive" {}
|
||||
sa_exchange_item payload "Payload" {}
|
||||
sa_interface old_service "Preserved service" {}
|
||||
interface_item_allocation service_payload "Service payload" { source @old_service; target @payload; }
|
||||
sa_interface old_service "Preserved service" { items [@payload]; }
|
||||
sa_port output "Output" {
|
||||
parent @send;
|
||||
contract { FunctionOutput { outgoing_items [@payload]; }; };
|
||||
@@ -481,19 +474,11 @@ no receiver or the same receiver as sender. Its nonempty expression describes
|
||||
the delay/deadline; an empty expression warns. Conditions and expressions are
|
||||
stored engineering annotations, not an executable simulation language.
|
||||
|
||||
Each MessageValue is ``{ path [@parameter]; field @data_field; expression "value expression"; }``.
|
||||
Its identity is the pair (path, field), so two uses of one Record can have
|
||||
different values for the same terminal field. Path contains enclosing DataField
|
||||
references, excluding the terminal field; direct parameters use ``[]``. Every
|
||||
hop MUST follow Value storage through a named Record's effective fields. Reference
|
||||
storage stops traversal. Operation Call accepts In/InOut and Reply accepts
|
||||
Out/InOut/Return, interpreted from the provider's perspective. Nested field paths
|
||||
inherit their direct parameter's direction. Unspecified direction warns.
|
||||
At most one annotation for a pair is allowed. Omitted path defaults to ``[]``;
|
||||
an ambiguous preserved legacy nested annotation warns ``message_value_ambiguous_path``
|
||||
until an explicit path is selected. New graphical choices always identify a path.
|
||||
Values never overwrite types or items. Expression text is descriptive, preserved
|
||||
exactly and not evaluated; an empty expression warns.
|
||||
Each MessageValue is ``{ field @data_field; expression "value expression"; }``.
|
||||
The field MUST be a DataField carried by the message exchange, including fields
|
||||
inside named nested records. At most one annotation for a given field is
|
||||
allowed in one message. Values do not overwrite the referenced type or global
|
||||
exchange item. Expression text is preserved exactly; an empty expression warns.
|
||||
|
||||
ScenarioUse references an existing same-layer scenario and maps its lifelines
|
||||
with ``bindings [ { referenced @callee_line; local @caller_line; } ];``.
|
||||
@@ -635,141 +620,6 @@ DeepHistory remembers nested configuration. Fallback applies when no resumable
|
||||
history exists. History is per Region, including top-level Regions in this
|
||||
Studio profile; no running history state is persisted or synthesized.
|
||||
|
||||
4.5. Data contracts, specialization and interface allocation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ExchangeItem ``kind`` describes nature: Information, Material or Energy.
|
||||
``mechanism`` is Unspecified, Flow, Event, Operation or SharedData. Unknown
|
||||
mechanisms remain explicit Unspecified drafts with a warning. Material/Energy
|
||||
with Event/Operation/SharedData warns for engineering review; it is not silently
|
||||
reclassified. Empty Event notifications and void Operations are valid. An empty
|
||||
carried/allocated Flow, SharedData or Unspecified item warns.
|
||||
|
||||
An Operation's directly owned DataFields are parameters. ``direction`` is
|
||||
Unspecified, In, Out, InOut or Return, from the provider's perspective. At most
|
||||
one effective parameter is Return. Unspecified parameters warn; non-operation
|
||||
fields MUST use Unspecified. Nested record fields inherit direction from their
|
||||
direct parameter when used in message paths, rather than storing context on a
|
||||
shared type. ``storage`` is Value or Reference. Reference requires a named Record,
|
||||
has no explicit default, and does not convey ownership or deletion of the type.
|
||||
|
||||
DataTypeDefinition has exactly the following constructors:
|
||||
|
||||
.. list-table:: Reusable definitions
|
||||
:header-rows: 1
|
||||
|
||||
* - Constructor
|
||||
- Required fields
|
||||
* - Boolean
|
||||
- None
|
||||
* - Integer
|
||||
- ``minimum``, ``maximum``: optional signed 64-bit integers
|
||||
* - Real
|
||||
- ``minimum``, ``maximum``: optional finite IEEE-754 binary64 values
|
||||
* - String
|
||||
- ``minimum_length``: u32; ``maximum_length``: optional u32
|
||||
* - Enumeration
|
||||
- ``literals``: unique nonempty, case-sensitive strings
|
||||
* - Record
|
||||
- ``supertype``: same-layer Record ref?
|
||||
|
||||
Bounds are inclusive and ordered; an absent bound means unbounded within the
|
||||
represented numeric domain. Integer comparisons MUST NOT convert to binary64.
|
||||
NaN, infinities, overflow and reversed bounds reject. String length counts Unicode
|
||||
scalar values, not UTF-8 bytes or grapheme clusters. Enum defaults match exact
|
||||
literals. Empty enum definitions remain incomplete drafts with a warning.
|
||||
|
||||
DataValue is a tagged scalar: ``{ Boolean true; }``, ``{ Integer 42; }``,
|
||||
``{ Real 1.5; }``, ``{ String "value"; }`` or ``{ Enumeration "Ready"; }``.
|
||||
There is no implicit numeric or string coercion. A type default is one compatible
|
||||
scalar within its bounds. Record type defaults and record/object instance literals
|
||||
are unsupported. A field default is absent (``none``) or a list of compatible
|
||||
scalars with length inside its multiplicity. ``[]`` explicitly supplies an empty
|
||||
collection and requires minimum 0, including Value Record fields; a nonempty
|
||||
Record literal rejects. Reference fields MUST use ``none``. A one-valued scalar
|
||||
field uses a one-value list. A named type default supplies an individual element's
|
||||
default, not a collection or an implicit instance of a mandatory field.
|
||||
|
||||
Numeric types may carry unit text. Empty field units inherit the named type unit;
|
||||
an explicit different unit warns when that type has a unit. Units on nonnumeric
|
||||
fields/types warn and remain preserved. Unit strings never convert values or
|
||||
establish dimensional compatibility. Expressions are not executed.
|
||||
|
||||
Record, ExchangeItem and Interface each permit one same-family, same-layer
|
||||
supertype. Specialization is acyclic and separate from containment. Effective
|
||||
fields are base fields followed by local fields, preserving original field UUIDs
|
||||
and owners. Local names MUST NOT shadow inherited names. Item specialization
|
||||
preserves base nature and any specified mechanism; an Unspecified base mechanism
|
||||
may be refined. Deleting a derived record never deletes its base. Mandatory
|
||||
Value-field cycles warn ``record_no_finite_value``; optional or Reference cycles
|
||||
are valid without that warning. Traversal is cycle guarded and bounded; a payload
|
||||
path budget failure is an explicit diagnostic, not silent truncation.
|
||||
|
||||
InterfaceItemAllocation is an ordinary identified relation from Interface to
|
||||
ExchangeItem. It stores ``order`` and independent transmission/acquisition
|
||||
protocols. Transmission is Unspecified, Synchronous, Asynchronous, Unicast,
|
||||
Multicast or Broadcast; acquisition is Unspecified, Read or Accept. No protocol
|
||||
combination is inferred from mechanism. Effective allocations are base-first,
|
||||
then local ordered by (order, alias, UUID). A local item already allocated locally
|
||||
or inherited is an error. Removing an allocation keeps its item and other
|
||||
allocations; if live boundary compatibility would break, the transaction MUST
|
||||
repair the affected contract explicitly or reject atomically. Artifacts can
|
||||
reference the allocation identity independently of the global item.
|
||||
|
||||
An exchange carrying a specialized item satisfies a declaration of its base,
|
||||
never the reverse. Standard provided/required compatibility uses explicit
|
||||
interface ancestry. Identity/ancestry, not matching names, defines conformance.
|
||||
The current typed function/component port contracts remain unchanged.
|
||||
|
||||
DataAssociation connects Record types without adding fields or transitively
|
||||
carrying payload. Each named end has role, multiplicity and navigability. Source
|
||||
multiplicity counts source instances per target instance and vice versa; it is
|
||||
not a count of model elements. An end's navigable flag permits navigation toward
|
||||
that end. Self-associations are valid; if both ends are navigable they require
|
||||
distinct nonempty roles. Empty roles and duplicate role contracts warn, whereas
|
||||
invalid ranges reject. Composition/shared aggregation, multiple inheritance,
|
||||
redefinition, instance literals and arbitrary-precision decimal arithmetic are
|
||||
outside this bounded contract.
|
||||
|
||||
This complete data contract example is also exercised through the REST API:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
arcadia 4;
|
||||
project engineering_data "Engineering data" {
|
||||
sa_data_type voltage "Voltage" {
|
||||
definition { Real { minimum 0.0; maximum 48.0; }; };
|
||||
default { Real 24.0; }; unit "V";
|
||||
}
|
||||
sa_data_type coordinate "Coordinate" {
|
||||
definition { Record { supertype none; }; };
|
||||
sa_data_field latitude "Latitude" { type_ref Real; }
|
||||
}
|
||||
sa_data_type located "Located asset" {
|
||||
definition { Record { supertype @coordinate; }; };
|
||||
sa_data_field supply "Supply" { type_ref { Named @voltage; }; }
|
||||
}
|
||||
sa_exchange_item navigate "Navigate" {
|
||||
mechanism Operation;
|
||||
sa_data_field origin "Origin" { type_ref { Named @coordinate; }; direction In; }
|
||||
sa_data_field destination "Destination" { type_ref { Named @coordinate; }; direction In; }
|
||||
sa_data_field accepted "Accepted" { type_ref Boolean; direction Return; }
|
||||
}
|
||||
sa_interface navigation "Navigation" {}
|
||||
sa_interface monitoring "Monitoring" {}
|
||||
interface_item_allocation nav_operation "Navigation operation" {
|
||||
source @navigation; target @navigate; transmission Synchronous; acquisition Accept;
|
||||
}
|
||||
interface_item_allocation monitor_operation "Monitoring operation" {
|
||||
source @monitoring; target @navigate; transmission Asynchronous; acquisition Read;
|
||||
}
|
||||
data_association location_link "Location relationship" {
|
||||
source @coordinate; target @located;
|
||||
source_role "location"; target_role "assets";
|
||||
source_maximum 1;
|
||||
}
|
||||
}
|
||||
|
||||
5. Diagrams, artifacts, and cross-layer traceability
|
||||
----------------------------------------------------
|
||||
|
||||
@@ -804,7 +654,7 @@ This complete source example places one state from an existing machine:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
arcadia 4;
|
||||
arcadia 3;
|
||||
project operating "Operating modes" {
|
||||
oa_entity crew "Crew" {}
|
||||
oa_state_machine modes "Modes" { parent @crew; }
|
||||
@@ -1025,7 +875,7 @@ sets and enum constructors are closed by sections 3–5.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
document = "arcadia" "4" ";" project
|
||||
document = "arcadia" "3" ";" project
|
||||
project = "project" alias string identity? "{" project-entry* "}"
|
||||
project-entry = "description" string ";" | declaration
|
||||
declaration = element
|
||||
@@ -1067,18 +917,18 @@ Ordinary string fields require quoted strings and cannot contain an unquoted
|
||||
constructor records. Typed values MUST NOT nest more than 64 levels; malformed
|
||||
deeply nested drafts produce a diagnostic instead of exhausting the parser stack.
|
||||
|
||||
6.1. Modular source projects
|
||||
6.1. Modular source3 projects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The pure ``source_project`` core API compiles a virtual source bundle, containing
|
||||
``entry`` and a ``files`` map from logical paths to UTF-8 strings. Its entry is a
|
||||
root-level ``.arcadia`` file with the source4 project header above. A project entry
|
||||
root-level ``.arcadia`` file with the source3 project header above. A project entry
|
||||
MAY also be ``include string ";"``. An included fragment consists only of ordinary
|
||||
declarations and include statements; it MUST NOT contain another version header,
|
||||
project wrapper, or project description. Includes MUST occur at document level,
|
||||
never inside an element body or typed field value. For example::
|
||||
|
||||
arcadia 4;
|
||||
arcadia 3;
|
||||
project rescue "Rescue" id "00000000-0000-4000-8000-000000000001" {
|
||||
description "Operational model";
|
||||
include "oa/participants.arcadia";
|
||||
@@ -1144,12 +994,9 @@ spans never refer to concatenated or synthetic source.
|
||||
|
||||
Single-string parse/lint APIs remain filesystem-free and reject include
|
||||
statements with an explicit source-bundle requirement. V1/V2 single-file imports
|
||||
continue through explicit migration. Modular projects accept source3 and source4;
|
||||
source3 is upgraded as one project, preserving file paths, includes, nested
|
||||
ownership and comments through targeted token edits. Generated allocation
|
||||
declarations stay in their interface's existing file. Individual include fragments
|
||||
are not independently migrated. Missing UUIDs stabilize before migration so new
|
||||
allocation identities are deterministic on subsequent compilation.
|
||||
continue through their existing migration readers and reports. Modular projects
|
||||
require source3; migration is performed before modular export, not independently
|
||||
on included fragments. Modular source does not change the semantic JSON schema.
|
||||
|
||||
6.2. Nested element ownership
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1381,35 +1228,10 @@ explicitly before Apply succeeds.
|
||||
|
||||
Loading detects JSON from its first non-whitespace ``{``; other input is parsed
|
||||
as Arcadia source regardless of filename. Saving an ``.arcadia`` suffix uses
|
||||
the accepted V4 source bundle (``arcadia 4``); other suffixes use V4 JSON. Both forms preserve the same
|
||||
the accepted V3 source bundle (``arcadia 3``); other suffixes use V3 JSON. Both forms preserve the same
|
||||
complete model. JSON rejects unknown fields throughout typed payloads,
|
||||
duplicate maps, unsupported versions, and structural errors.
|
||||
|
||||
The schema-3 reader freezes the changed data payloads and old constructor catalog;
|
||||
schema-4 fields/constructors MUST NOT be silently accepted as schema 3. Existing
|
||||
typed PortContract records remain unchanged. Information/Material/Energy retain
|
||||
their nature with Unspecified mechanism; old Event becomes Information with Event
|
||||
mechanism. Old bounds/defaults/supertypes are absent, String length is 0..unbounded,
|
||||
and fields retain type/multiplicity/unit with Unspecified direction, Value storage
|
||||
and no explicit default. No physical mechanism or direction is guessed.
|
||||
|
||||
Each old Interface.items entry generates one InterfaceItemAllocation with its
|
||||
original zero-based order and Unspecified protocols. UUID v5 uses the project UUID
|
||||
namespace and name ``v2-interface-item:<interface-uuid>:<item-uuid>`` consistently
|
||||
for schema-2 and schema-3 inputs. Aliases are deterministic and collision-free;
|
||||
collision with any existing identity is an error rather than an overwrite. Existing
|
||||
interface/item identities remain intact. Old nested message values acquire a path
|
||||
only when exactly one valid path exists. Ambiguous paths, including recursive
|
||||
reuse, retain the old annotation with a located warning rather than guessing.
|
||||
|
||||
Migration reports additionally contain ``semantic_defaults`` (object, field,
|
||||
description), ``generated_interface_allocations`` (id, source, target, alias,
|
||||
order) and ``unresolved_message_values`` (occurrence, field, reason). These new
|
||||
report arrays default empty when reading older responses. Version 1 normalizes
|
||||
through V2, then the typed-port V3 adapter and this shared V4 data upgrade. Original
|
||||
V2 and real V3 fixtures remain unchanged compatibility inputs. Current exports
|
||||
always use version 4; reading/migrating alone never saves or changes revisions.
|
||||
|
||||
Native source Save All checks the original contents of observed dependencies
|
||||
before staging writes. Existing target files are replaced individually and the
|
||||
entry is installed last. A transaction journal records originals and intended
|
||||
@@ -1434,7 +1256,7 @@ defaults without changing any prior identity, name, reference, order, geometry,
|
||||
artifact or trace field. The original V2 golden source/JSON in
|
||||
``crates/oa-core/tests/fixtures`` MUST NOT be regenerated from the newer demo;
|
||||
they verify that every old persistent field survives. Canonical output writes
|
||||
the added defaults through the explicit schema-3 and schema-4 migrations.
|
||||
the added defaults through the explicit schema-3 migration.
|
||||
|
||||
The V2 adapter preserves every original identity, alias, prose, parent,
|
||||
placement, trace and artifact. Generic ports become Legacy with their original
|
||||
@@ -1453,14 +1275,14 @@ IDs, required resolutions, generated endpoint IDs/aliases/owners, and boundary
|
||||
orientation defaults. REST import, replacement, candidate validation and
|
||||
project snapshots surface the same report. Opening an old server envelope
|
||||
preserves its resource ID/revision and bytes; the next explicit edit writes
|
||||
schema 4. Model/source exports contain only the canonical model. Migration
|
||||
schema 3. Model/source exports contain only the canonical model. Migration
|
||||
reports are transient response metadata, not persisted model facts.
|
||||
|
||||
Admission checks account for generated objects and their references before
|
||||
construction. Both the old V2 contract and the resulting V4 graph are
|
||||
construction. Both the old V2 contract and the resulting V3 graph are
|
||||
validated. Migration itself does not save or increase a stored revision.
|
||||
Canonical second saves are stable. V1 normalization produces the frozen V2
|
||||
model and then uses these same V2-to-3-to-4 stages.
|
||||
model and then uses this same V2-to-3 stage.
|
||||
|
||||
Save validates a snapshot, writes a sibling temporary file, flushes it, and
|
||||
atomically replaces the destination. Failed serialization or validation must
|
||||
@@ -1479,7 +1301,7 @@ step becomes a distinct occurrence element owned by its process. Generated
|
||||
diagram, node, and occurrence identities use UUID v5 with the old Project ID
|
||||
as namespace and deterministic role/index names. If such an identity collides,
|
||||
a deterministic suffix is tried. Reimporting the same fully identified V1
|
||||
input therefore yields the same V4 result, including repeated occurrences.
|
||||
input therefore yields the same V3 result, including repeated occurrences.
|
||||
No System or SA content is invented during an OA-only migration.
|
||||
|
||||
V1 source aliases are retained when valid in V2. JSON imports generate the old
|
||||
@@ -1487,7 +1309,7 @@ canonical aliases deterministically. Any alias newly reserved in V2 is changed
|
||||
by a deterministic suffix; aliases were not persistent V1 semantic data.
|
||||
A project alias and aliases for new records are generated without colliding
|
||||
with retained aliases. The existing bundled V1 text and JSON migrate to equal
|
||||
V4 projects. Future versions remain errors until a separate migration exists.
|
||||
V3 projects. Future versions remain errors until a separate migration exists.
|
||||
|
||||
10. Complete authoring example
|
||||
------------------------------
|
||||
@@ -1497,7 +1319,7 @@ the assigned UUIDs explicitly.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
arcadia 4;
|
||||
arcadia 3;
|
||||
project harbor "Harbor assistance" {
|
||||
oa_entity control "Harbor control" {}
|
||||
oa_activity assess "Assess assistance" {}
|
||||
|
||||
+63
-652
@@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "Arcadia Studio model API",
|
||||
"version": "1.0.0",
|
||||
"description": "Current schema-4 snapshots and conditional atomic writes. Request body limit: 8 MiB; at most 200 nested commands; no retained revisions, request receipts, automatic retries or persistent transition plans. See docs/api.rst. Fixed store capacity: 128 models / 256 MiB serialized envelopes. Stateless transition preview/apply. Artifact URI/revision strings remain opaque core fields. Model admission before structural validation: 5,000 graph objects, 20,000 total records and 50,000 ordered references. Preparation uses snapshots outside the catalog lock; final commits recheck the exact ETag. Each accepted envelope couples model and authored source under one revision. Source-only edits advance the shared ETag. Source bundles resolve logical paths only, preserve authored files/comments, and have a stabilized JSON cap of 8 MiB, 256 files, 32 include edges along a path and 1,024 include edges overall."
|
||||
"description": "Current schema-3 snapshots and conditional atomic writes. Request body limit: 8 MiB; at most 200 nested commands; no retained revisions, request receipts, automatic retries or persistent transition plans. See docs/api.rst. Fixed store capacity: 128 models / 256 MiB serialized envelopes. Stateless transition preview/apply. Artifact URI/revision strings remain opaque core fields. Model admission before structural validation: 5,000 graph objects, 20,000 total records and 50,000 ordered references. Preparation uses snapshots outside the catalog lock; final commits recheck the exact ETag. Each accepted envelope couples model and authored source under one revision. Source-only edits advance the shared ETag. Source bundles resolve logical paths only, preserve authored files/comments, and have a stabilized JSON cap of 8 MiB, 256 files, 32 include edges along a path and 1,024 include edges overall."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -389,7 +389,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Schema-4 native JSON is described below. Schema-1, schema-2 and schema-3 JSON/source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Reports list preserved Legacy contracts, generated FE endpoint IDs/aliases and boundary orientation defaults. Export emits schema 4 / arcadia 4. See docs/language.rst."
|
||||
"description": "Schema-3 native JSON is described below. Schema-1 and schema-2 JSON/source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Reports list preserved Legacy contracts, generated FE endpoint IDs/aliases and boundary orientation defaults. Export emits schema 3 / arcadia 3. See docs/language.rst."
|
||||
}
|
||||
},
|
||||
"/api/v1/projects/{id}": {
|
||||
@@ -1902,9 +1902,7 @@
|
||||
"CapabilityGeneralization",
|
||||
"MissionInvolvement",
|
||||
"SequenceLink",
|
||||
"PortAllocation",
|
||||
"InterfaceItemAllocation",
|
||||
"DataAssociation"
|
||||
"PortAllocation"
|
||||
]
|
||||
},
|
||||
"Position": {
|
||||
@@ -2006,185 +2004,40 @@
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Boolean"
|
||||
"Boolean",
|
||||
"Integer",
|
||||
"Real",
|
||||
"String",
|
||||
"Record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Integer": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minimum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": -9223372036854775808,
|
||||
"maximum": 9223372036854775807
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"Enumeration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"literals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"maximum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": -9223372036854775808,
|
||||
"maximum": 9223372036854775807
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": [
|
||||
"literals"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"minimum",
|
||||
"maximum"
|
||||
"Enumeration"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Integer"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Real": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minimum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Finite IEEE-754 binary64; no coercion from typed Integer values."
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maximum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Finite IEEE-754 binary64; no coercion from typed Integer values."
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"minimum",
|
||||
"maximum"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Real"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"String": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minimum_length": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"maximum_length": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"minimum_length",
|
||||
"maximum_length"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"String"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Enumeration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"literals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"literals"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Enumeration"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Record": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"supertype": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"supertype"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Record"
|
||||
],
|
||||
"additionalProperties": false
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2549,27 +2402,17 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"$ref": "#/components/schemas/ExchangeItemKind"
|
||||
},
|
||||
"mechanism": {
|
||||
"$ref": "#/components/schemas/ExchangeMechanism"
|
||||
},
|
||||
"supertype": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Information",
|
||||
"Material",
|
||||
"Energy",
|
||||
"Event"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"mechanism",
|
||||
"supertype"
|
||||
"kind"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -2587,25 +2430,10 @@
|
||||
"properties": {
|
||||
"definition": {
|
||||
"$ref": "#/components/schemas/DataTypeDefinition"
|
||||
},
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DataValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"definition",
|
||||
"default",
|
||||
"unit"
|
||||
"definition"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -2626,15 +2454,13 @@
|
||||
},
|
||||
"minimum": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
"minimum": 0
|
||||
},
|
||||
"maximum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -2643,35 +2469,13 @@
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"direction": {
|
||||
"$ref": "#/components/schemas/ParameterDirection"
|
||||
},
|
||||
"storage": {
|
||||
"$ref": "#/components/schemas/FieldStorage"
|
||||
},
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DataValue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type_ref",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"unit",
|
||||
"direction",
|
||||
"storage",
|
||||
"default"
|
||||
"unit"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -2687,20 +2491,16 @@
|
||||
"Interface": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"supertype": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"supertype"
|
||||
"items"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -3781,128 +3581,6 @@
|
||||
"PortAllocation"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"InterfaceItemAllocation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"transmission": {
|
||||
"$ref": "#/components/schemas/TransmissionProtocol"
|
||||
},
|
||||
"acquisition": {
|
||||
"$ref": "#/components/schemas/AcquisitionProtocol"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"source",
|
||||
"target",
|
||||
"order",
|
||||
"transmission",
|
||||
"acquisition"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"InterfaceItemAllocation"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"DataAssociation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"source_role": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_role": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_minimum": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"source_maximum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"target_minimum": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"target_maximum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source_navigable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"target_navigable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"source",
|
||||
"target",
|
||||
"source_role",
|
||||
"target_role",
|
||||
"source_minimum",
|
||||
"source_maximum",
|
||||
"target_minimum",
|
||||
"target_maximum",
|
||||
"source_navigable",
|
||||
"target_navigable"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"DataAssociation"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3939,12 +3617,7 @@
|
||||
},
|
||||
"PortAnchorSide": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Left",
|
||||
"Right",
|
||||
"Top",
|
||||
"Bottom"
|
||||
],
|
||||
"enum": ["Left", "Right", "Top", "Bottom"],
|
||||
"description": "Presentation side of the owner placement; independent of semantic port direction."
|
||||
},
|
||||
"PortAnchor": {
|
||||
@@ -3965,11 +3638,7 @@
|
||||
"description": "Finite binary32 fraction of the complete owner edge, measured from top on Left/Right and from left on Top/Bottom."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"port",
|
||||
"side",
|
||||
"offset"
|
||||
],
|
||||
"required": ["port", "side", "offset"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NodePlacement": {
|
||||
@@ -4287,7 +3956,7 @@
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "integer",
|
||||
"const": 4
|
||||
"const": 3
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
@@ -4401,7 +4070,7 @@
|
||||
"source_project"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "Accepted coupled model/source snapshot. Compiling source_project reproduces model; revision changes once if either representation changes. Older model-only or schema3 coupled envelopes migrate in memory without a write or revision change; both representations must independently migrate to the same graph."
|
||||
"description": "Accepted coupled model/source snapshot. Compiling source_project reproduces model; revision changes once if either representation changes. Old model-only envelopes acquire source in memory without a write or revision change."
|
||||
},
|
||||
"ProjectSummary": {
|
||||
"type": "object",
|
||||
@@ -5145,29 +4814,15 @@
|
||||
"type": "object",
|
||||
"description": "Upsert manual port position by port identity on an existing owner placement, preserving unrelated entries. Port must be a direct child of the placement's semantic element.",
|
||||
"properties": {
|
||||
"diagram": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"node": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"anchor": {
|
||||
"$ref": "#/components/schemas/PortAnchor"
|
||||
}
|
||||
"diagram": { "type": "string", "format": "uuid" },
|
||||
"node": { "type": "string", "format": "uuid" },
|
||||
"anchor": { "$ref": "#/components/schemas/PortAnchor" }
|
||||
},
|
||||
"required": [
|
||||
"diagram",
|
||||
"node",
|
||||
"anchor"
|
||||
],
|
||||
"required": ["diagram", "node", "anchor"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SetPortAnchor"
|
||||
],
|
||||
"required": ["SetPortAnchor"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
@@ -5177,30 +4832,15 @@
|
||||
"type": "object",
|
||||
"description": "Restore automatic positioning for an existing direct child Port. No-op when that valid owner placement has no manual anchor for the port; wrong or stale ownership is rejected.",
|
||||
"properties": {
|
||||
"diagram": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"node": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"port": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
"diagram": { "type": "string", "format": "uuid" },
|
||||
"node": { "type": "string", "format": "uuid" },
|
||||
"port": { "type": "string", "format": "uuid" }
|
||||
},
|
||||
"required": [
|
||||
"diagram",
|
||||
"node",
|
||||
"port"
|
||||
],
|
||||
"required": ["diagram", "node", "port"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"RemovePortAnchor"
|
||||
],
|
||||
"required": ["RemovePortAnchor"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
@@ -6169,14 +5809,6 @@
|
||||
"MessageValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"field": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -6189,8 +5821,7 @@
|
||||
"field",
|
||||
"expression"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "Identity is (path, field). Path lists enclosing DataField UUIDs, excluding the terminal field. Value storage traverses named Records; Reference storage stops traversal. Operation Call accepts In/InOut and Reply accepts Out/InOut/Return. Unspecified direction and retained ambiguous legacy empty paths warn. Expressions are descriptive, never executed."
|
||||
"additionalProperties": false
|
||||
},
|
||||
"LifelineBinding": {
|
||||
"type": "object",
|
||||
@@ -6452,13 +6083,12 @@
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
2
|
||||
]
|
||||
},
|
||||
"to_schema": {
|
||||
"type": "integer",
|
||||
"const": 4
|
||||
"const": 3
|
||||
},
|
||||
"preserved_ports": {
|
||||
"type": "array",
|
||||
@@ -6549,91 +6179,6 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"semantic_defaults": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"object": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"object",
|
||||
"field",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"generated_interface_allocations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"source",
|
||||
"target",
|
||||
"alias",
|
||||
"order"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"unresolved_message_values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"occurrence": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"field": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"occurrence",
|
||||
"field",
|
||||
"reason"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6645,11 +6190,11 @@
|
||||
"required_resolutions"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "Transient observations from explicit V1/V2/V3 to V4 migration. New report lists default to empty when reading older responses. Reading an old envelope neither saves nor changes its revision. Allocation UUIDv5 uses project namespace and v2-interface-item:<interface>:<item>."
|
||||
"description": "Transient observations from explicit V1→V2→V3 or V2→V3 migration. No save or revision increase occurs merely by reading an old envelope."
|
||||
},
|
||||
"SourceBundle": {
|
||||
"type": "object",
|
||||
"description": "Complete source4 workspace (source3 migrates explicitly). The entry is a root filename. Files use canonical portable slash-separated relative .arcadia paths and all must be reachable. Includes resolve only against this map, never the service filesystem. Up to 256 files, 32 include edges along a path and 1,024 include edges overall. Serialized bundle JSON is at most 8 MiB, checked again after missing UUIDs are inserted. GET returns exact accepted stable text.",
|
||||
"description": "Complete source3 workspace. The entry is a root filename. Files use canonical portable slash-separated relative .arcadia paths and all must be reachable. Includes resolve only against this map, never the service filesystem. Up to 256 files, 32 include edges along a path and 1,024 include edges overall. Serialized bundle JSON is at most 8 MiB, checked again after missing UUIDs are inserted. GET returns exact accepted stable text.",
|
||||
"properties": {
|
||||
"entry": {
|
||||
"type": "string"
|
||||
@@ -6722,149 +6267,15 @@
|
||||
},
|
||||
"source_project": {
|
||||
"$ref": "#/components/schemas/SourceBundle"
|
||||
},
|
||||
"migration": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/MigrationReport"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"revision",
|
||||
"changed",
|
||||
"issues",
|
||||
"source_project",
|
||||
"migration"
|
||||
"source_project"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExchangeItemKind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Information",
|
||||
"Material",
|
||||
"Energy"
|
||||
]
|
||||
},
|
||||
"ExchangeMechanism": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Unspecified",
|
||||
"Flow",
|
||||
"Event",
|
||||
"Operation",
|
||||
"SharedData"
|
||||
]
|
||||
},
|
||||
"ParameterDirection": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Unspecified",
|
||||
"In",
|
||||
"Out",
|
||||
"InOut",
|
||||
"Return"
|
||||
]
|
||||
},
|
||||
"FieldStorage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Value",
|
||||
"Reference"
|
||||
]
|
||||
},
|
||||
"TransmissionProtocol": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Unspecified",
|
||||
"Synchronous",
|
||||
"Asynchronous",
|
||||
"Unicast",
|
||||
"Multicast",
|
||||
"Broadcast"
|
||||
]
|
||||
},
|
||||
"AcquisitionProtocol": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Unspecified",
|
||||
"Read",
|
||||
"Accept"
|
||||
]
|
||||
},
|
||||
"DataValue": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Boolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Boolean"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Integer": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": -9223372036854775808,
|
||||
"maximum": 9223372036854775807
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Integer"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Real": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Finite IEEE-754 binary64; no coercion from typed Integer values."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Real"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"String": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"String"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Enumeration": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Enumeration"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# C: finite typed facet reviews — executable comparison
|
||||
|
||||
Status: isolated proof, pending root freeze. Production remains at `959b83c78fcaa82b0184aade81a0508b47eb1e41`; B1 evidence is commit `8ac9f464eaf0ec813ee3ecf903404891ae2a3841`. No R6/schema/source contract changed.
|
||||
|
||||
## Result and proposed shape
|
||||
|
||||
**11 tests pass: the original 3 B1 tests plus 8 C tests.** C stores reviewed facts directly, without an own-record carrier, reviewed mask, order shadow, observation version or recursive value tree. `OwnRecord` is reused only for transient source/target candidates and existing fixture helpers. The exact types/adapter are in `src/typed_reviews.rs`; assertions are in `src/typed_reviews_tests.rs`.
|
||||
|
||||
```rust
|
||||
enum Observed<T> { NotApplicable, Value(T) }
|
||||
struct Reviewed<T> { oa: Observed<T>, sa: Observed<T> }
|
||||
struct KeyPair<K> { oa: Option<K>, sa: Option<K> }
|
||||
struct ValueKey { path: Vec<Id>, field: Id }
|
||||
struct Baseline { facets: Vec<FacetReview> }
|
||||
```
|
||||
|
||||
`FacetReview` is self-tagged: Data family leaves carry `Reviewed<Option<i64>>`, `Reviewed<Option<f64>>`, `Reviewed<Option<DataValue>>`, etc.; the tag identifies the facet. Other proof leaves are field minimum, state guard, mapped trigger/step membership, step order, atomic keyed annotations and atomic keyed bindings. Existing `MessageValue`, `LifelineBinding`, `DataValue` and `DataTypeDefinition` are reused. `T` is a concrete closed type in each variant, not an arbitrary recursively nested value.
|
||||
|
||||
An absent entry means Unreviewed. `Value(None)` means an applicable optional slot has no value (for example an unbounded Real minimum). `NotApplicable` means a selected constructor branch does not exist on that side. A mixed review keeping an SA Integer while acknowledging an OA Real records Real-minimum OA=`Value(None)`, SA=`NotApplicable`; Integer-maximum records the reverse applicability. A both-side-inapplicable leaf is rejected.
|
||||
|
||||
Recommendation remains **C for the full contract**, because facts are explicit and independent. This proof does **not** show fewer lines of implementation: see the measured cost below. B1 remains viable at restricted granularity; C removes its storage-overlap invariants rather than eliminating exhaustive adapter work.
|
||||
|
||||
## What the executable proof establishes
|
||||
|
||||
| Case | Observed assertions |
|
||||
| --- | --- |
|
||||
| Bounds/unit and field minimum | Keep acknowledges only selected facts; unselected maximum still drifts. Take maximum retains target unit/minimum. Field minimum retains target maximum. Repeat application is unchanged. |
|
||||
| Guard/triggers after deletion | Deleted source UUID remains in a typed historical key. Removal unlinks the mapped trigger but leaves its target object; extra target trigger and local guard survive. |
|
||||
| Ordered steps | Accepting membership leaves the order fact byte-for-byte unchanged. Explicit final order must contain all surviving target members exactly once. Target-only step object survives unchanged. |
|
||||
| Qualified annotation rows | Two paths with the same terminal latitude UUID remain separate. Updating/removing the first row does not acknowledge or overwrite the second row's expression. Removal acknowledges the whole selected row. |
|
||||
| Binding rows | Key is the referenced lifeline; changing `local` updates that row through frozen mapping. Other binding remains unchanged. Missing local mapping rejects. |
|
||||
| Mixed constructor group | Keep can acknowledge Real/Integer as intentionally different constructors, including correct applicable/not-applicable leaves. Take changes definition+default while preserving unit. A later explicit target switch can reconsider a previous Keep. Retained Record child blocks an incompatible switch atomically. |
|
||||
| Prepared induced changes | A real inherited-field removal causes core `prepare_command` to prune annotations. C compares the actual prepared project with the reviewed expected project and rejects these unreviewed changes. Project and baseline inputs remain unchanged. |
|
||||
| Adversarial admission | Exact duplicates, keys colliding on either side, mismatched frozen key mapping, changed pairing against old baseline, wrong-family/both-side-NA Keep, unchanged-constructor acknowledgement, invalid historical row/key combinations, empty key pairs and nonfinite values reject. Collision permutations both reject. Returned baseline is revalidated. |
|
||||
|
||||
Constructor grouping copies the definition and default in this subset; accepting a switch retires old definition facts and records the union of both resulting constructor branches. Unit stays independent. An unchanged constructor cannot be used as a shortcut to acknowledge changed bounds/defaults. Constructor and descendant decisions in one request reject as overlapping. Membership operations precede order regardless of request order.
|
||||
|
||||
Pair remapping is deliberately conservative: a new KeyPair colliding with an old baseline pair rejects with an explicit review-invalidation requirement. This probe does not implement the subsequent mapping-reset workflow or agreement-based many-to-one merges. It cannot silently accumulate a baseline that fails on its next application.
|
||||
|
||||
## Measured implementation cost
|
||||
|
||||
`measure-code.ps1` counts nonblank, non-comment rustfmt lines. B1 is read from its committed version, not a moving file. These are code-size measurements, not a normalized complexity or speed benchmark.
|
||||
|
||||
| Portion | B1 | C | Difference |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Public types/requests | 69 | 118 | +49 |
|
||||
| Adapter/admission/application | 256 | 778 | +522 |
|
||||
| Tests | 512 | 1,035 | +523 |
|
||||
|
||||
Both use the same 52 lines of basic own-record/list helpers; C does not call B1's carrier copy/compare/apply logic. C covers 18 selector cases versus B1's 9, including both keyed row kinds and all DataType constructor branches. C also adds admission and prepared-project checks absent from B1. Therefore attributing the entire difference to representation would be misleading. C's repeated capture/tag/write matches are real maintenance cost; one finite catalog should supply selector/type metadata during implementation, without introducing a general metamodel framework.
|
||||
|
||||
## Full coverage and minimal granularity recommendation
|
||||
|
||||
| Family | Facets required by the current model |
|
||||
| --- | --- |
|
||||
| Headers | Name, description, element parent. No incidental identity/alias/layer edits. |
|
||||
| OA/SA primitive bodies | No payload; Process/FunctionalChain step membership and order. |
|
||||
| ExchangeItem / Interface | Nature, mechanism, supertype; interface supertype. Allocations remain real relation records. |
|
||||
| DataType | Constructor, integer/real bounds, string lengths, enum literal membership, Record supertype, default, unit. |
|
||||
| DataField | Whole TypeRef, multiplicity bounds, unit, direction, storage, whole optional default vector. No unstable per-index default selection. |
|
||||
| Port | Contract constructor; input/output item membership; component kind/orientation; legacy direction/interface. Existing typed/Legacy resolution and split semantics remain authoritative. |
|
||||
| Scenario / Lifeline | Viewpoint, context, pre/postconditions, step membership/order; represented reference. |
|
||||
| Occurrence envelope | Constructor and separate common pre/postconditions. |
|
||||
| Exchange occurrence | Exchange, sender, receiver, message kind/Timer expression, reply correlation, annotations keyed by full `(path,field)`. |
|
||||
| Remaining occurrences | Action/action-lifeline; control kind; sequence reference; scenario reference/bindings; state/state-lifeline; fragment operator/branches; branch guard/ordered occurrences. |
|
||||
| Event / State | Event constructor and Signal items/Time kind+expression/Change expression; StateKind structural change and entry/exit/do prose. Machine/Region have no own payload. |
|
||||
| Every relation | Source and target endpoints, plus name/description. |
|
||||
| Relation payloads | Interface allocation order/protocols; association roles/multiplicities/navigation; exchange items/ports/orientation; capability-extend condition; sequence guard; transition kind/trigger+effect sets and three prose fields; StateAction phase. All other relation kinds are endpoint-only. |
|
||||
| External membership/artifacts | Direct children; incident relation identity+endpoint role; attached artifact membership and name/description/owner/relation/URI/revision/fragment. |
|
||||
|
||||
Atomic annotation and binding rows are the smallest useful contract: key changes are explicit remove/add, while all other rows remain independent. A separate expression-versus-presence checkpoint is unnecessary at this granularity. If such controls are later required, C can add separate typed facts without a second carrier state.
|
||||
|
||||
Steps/branch occurrences are ordered. Trigger alternatives, bindings and annotations must not accidentally acquire execution ordering semantics from vector position. Enum literals need string-keyed membership to preserve target-only literals; standalone whole-list `TakeSource` is explicitly rejected by this probe, pending that finite selector. A constructor conversion that would overwrite retained target enum literals also requires an explicit candidate. Field default vectors remain atomic.
|
||||
|
||||
Constructor scopes own discriminator and introduced/removed variant fields. Dependent changes outside that scope require explicit resolutions. In particular, changing Occurrence's constructor must not acknowledge its outer pre/postconditions; Call→Timer must expose exchange/reply/value effects; StateKind is semantic even though it changes `Element::kind()`. Prepared induced changes must be reviewed for every representation.
|
||||
|
||||
Current Trace endpoints/properties do not accept artifacts. Prefer attached artifact review facts owned by the semantic trace instead of silently widening trace compatibility. Missing artifact versus present artifact with `revision=None` needs distinct typed observations.
|
||||
|
||||
## Reuse the existing source decoder, with typed context
|
||||
|
||||
Baseline JSON roundtrips, including deleted historical UUIDs and mixed constructor observations. All accepted live candidates roundtrip through current Arcadia 4 source. **C baseline source syntax is not implemented or proved.**
|
||||
|
||||
The concrete reuse route is:
|
||||
|
||||
1. `language.rs::Parser::value_inner`: recognize a distinct historical `uuid "..."` AST literal. Do not convert it to an ordinary quoted string too early.
|
||||
2. `Ast::resolve`: accept an explicit slot context supplied by the finite facet catalog. Historical reference slots accept UUID literals; live reference slots keep `@alias` requirements. A new context must be entered only through the semantic-baseline subtree of a Trace declaration.
|
||||
3. Keep `resolve_declarations` and its existing `serde_json::from_value::<Trace>`/body decoding as the typed authority. Do not duplicate the live body factory/default merge logic for historical facts.
|
||||
4. `write_fields`/`write_value`: use the same slot metadata to emit historical UUIDs without current alias lookup. Generic `Reviewed<T>{oa,sa}` cannot pass through current field-name-only rules: `oa`/`sa` are currently classified as references even when T is a unit string. Renaming keys alone does not distinguish reference/string/enum types.
|
||||
5. `edit_positions`/`EditKind` and `source_project.rs::item_key`: retain full UUID token spans, historical identity remapping, and full annotation/binding keys for comment ownership.
|
||||
6. Extend finite historical validation/admission counts and explicit remap visitors; exclude baseline IDs from live dangling-reference and deletion traversal. Historical carrier/facet validation must not fetch old objects or impose cross-facet live range invariants.
|
||||
|
||||
This needs explicit schema/source migration and OpenAPI descriptions before acceptance. No source4 or data4 changes were made here.
|
||||
|
||||
## Commands, evidence, and stopping point
|
||||
|
||||
From `D:/CodexBuilds/Capella2-transition-probe`:
|
||||
|
||||
```powershell
|
||||
cargo test --manifest-path experiments/r3-facet-carrier/Cargo.toml --locked --offline --config target/r3-probe/cargo.toml --target-dir D:/CodexBuilds/Capella2-transition-probe-target -- --nocapture 2>&1 | Tee-Object -FilePath target/r3-probe/c-tests.log; exit $LASTEXITCODE
|
||||
```
|
||||
|
||||
Exit 0: **11 passed, 0 failed**, no compiler warnings. The vendor config is unchanged from the B1 report. `& experiments/r3-facet-carrier/measure-code.ps1` reproduces counts. Logs, `code-counts.json` and generated `evidence/c-*-baseline.json` / `c-*-candidate.arcadia` remain under `target/r3-probe/`.
|
||||
|
||||
Still outside this proof: complete facet catalog; reverse direction; multiple target records; historical identity remapping; mapping-reset and agreeing many-to-one resolution; unambiguous automatic ordering; source baseline parser/formatter; OpenAPI/migration; size budgets for every new fact; and production baseline+undo/revision/ETag/disk atomicity. String literal/row keys and historical UUID admission need full adversarial coverage at implementation time. No claim of full R3 implementation or acceptance is made.
|
||||
|
||||
Stop here for the root's freeze decision. No native UI or other checkout/target was used.
|
||||
Generated
-396
@@ -1,396 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "oa-core"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "r3-facet-carrier-probe"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"oa-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "r3-facet-carrier-probe"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
oa-core = { path = "../../crates/oa-core" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -1,118 +0,0 @@
|
||||
# R3 facet carrier experiment
|
||||
|
||||
Status: isolated design/probe, not R3 implementation or an accepted persistence contract.
|
||||
|
||||
Follow-up: [C-REPORT.md](C-REPORT.md) contains the executable self-tagged typed-facet comparison, current recommendation, additional adversarial cases and measured code costs. This document preserves the earlier B0/B1 analysis.
|
||||
|
||||
Base: `959b83c78fcaa82b0184aade81a0508b47eb1e41`. Worktree: `D:/CodexBuilds/Capella2-transition-probe`, branch `codex/transition-probe`. The standalone crate depends on the unchanged local `oa-core`. It changes no accepted data4 shape or production code.
|
||||
|
||||
## Finding
|
||||
|
||||
Prefer **B: existing typed own-record carriers plus finite facet selectors** for further review. A carrier holds independently acknowledged values, not a coherent historical model. This removes the main reason to add immutable observation histories/cursors. One narrow extension is justified by a demonstrated overlap: store a separately acknowledged order vector for each ordered collection.
|
||||
|
||||
This is a proposal to investigate further. The probe covers three concrete cases, not every model constructor, mapping direction, source baseline format, or transition command lifecycle.
|
||||
|
||||
| Design | Benefits | Cost or failure demonstrated |
|
||||
| --- | --- | --- |
|
||||
| A: `SemanticPath(Vec<String>)` + recursive `SemanticValue` | Each leaf has independent storage; arbitrary nested values can be presented uniformly | Adds a second public model of numbers, enums, records, lists, references, and absence. Every read/write still needs an exhaustive typed adapter and closed path catalog. Wrong path/value combinations become runtime errors. Constructor groups, mapping, collection deltas, and validation remain necessary. Its flexibility supplies no demonstrated benefit for the three cases. |
|
||||
| B0: one typed carrier per side + reviewed selectors | Reuses `ElementBody`, `RelationBody`, serde validation of shapes, and existing payload types; handles partial scalar review and historical references | **Insufficient for independently acknowledged membership and order.** A new step appended to the carrier can falsely acknowledge its pending placement. |
|
||||
| B1: B0 + typed `OrderCheckpoint` | Preserves the older reviewed order while independently changing member presence; passes the counterexample | Duplicates only an order's identity vector. Other overlapping projections must be identified before adoption. |
|
||||
| Immutable observation pairs + facet cursors | Preserves actual whole-record observations and independently selects review epochs | Adds observation IDs/indexes, retention, compaction, duplicate snapshots, and cursor validation. Mixed-time carrier values are legitimate, so the tested scalar cases do not justify this machinery. Order needs only its own typed checkpoint. Rejected for now, not proved universally unnecessary. |
|
||||
|
||||
A is not intrinsically incorrect. The rejection is unnecessary public representation and validation cost for a closed typed model, not a claim that it cannot implement the semantics.
|
||||
|
||||
## Smallest public shape under test
|
||||
|
||||
The exact compiled types are in `src/lib.rs`. They deliberately leave current prose `TransitionField::{Name,Description}`, `FieldDecision::Value(String)`, and `TracedProperties` unchanged. A production extension could add `TraceBaseline.semantic: Option<SemanticBaseline>` and `TransitionField::Semantic(Facet)`; old absent semantic baselines remain Unreviewed.
|
||||
|
||||
```rust
|
||||
enum OwnRecord {
|
||||
Element { parent: Option<Id>, body: ElementBody },
|
||||
Relation { body: RelationBody },
|
||||
}
|
||||
struct MemberPair { oa: Option<Id>, sa: Option<Id> }
|
||||
struct SemanticBaseline {
|
||||
oa: OwnRecord,
|
||||
sa: OwnRecord,
|
||||
reviewed: Vec<Facet>,
|
||||
orders: Vec<OrderCheckpoint>,
|
||||
}
|
||||
struct OrderCheckpoint {
|
||||
collection: OrderedCollection,
|
||||
oa: Vec<Id>,
|
||||
sa: Vec<Id>,
|
||||
}
|
||||
enum OrderedCollection { Steps } // finite catalog to extend
|
||||
enum Facet {
|
||||
IntegerMinimum, IntegerMaximum, DataUnit, FieldMinimum,
|
||||
StateGuard, Trigger(MemberPair), Step(MemberPair), StepsOrder,
|
||||
DataConstructor,
|
||||
}
|
||||
enum Decision { TakeSource, KeepTarget, Candidate }
|
||||
struct Resolution { facet: Facet, decision: Decision }
|
||||
struct Review {
|
||||
source: Id,
|
||||
target: Id,
|
||||
candidate: Option<OwnRecord>,
|
||||
resolutions: Vec<Resolution>,
|
||||
}
|
||||
```
|
||||
|
||||
`Facet` is the closed proof subset, not a proposed complete catalog. A final catalog needs typed enum branches for element/relation scalar fields, parent/endpoints, reference members, order, and constructor scopes. It must not accept string paths. Message annotations need their existing `(path, field)` member identity; binding keys and other non-UUID members likewise need a finite typed key variant. `MemberPair` uses fixed OA/SA coordinates so reverse transition does not rename persistent selectors. At least one side must exist; conflicting pairs must be rejected. Historical pairs permit removal deltas even after a source object and its ordinary trace have been deleted.
|
||||
|
||||
All carrier types derive `PartialEq`; bodies can contain finite floats. Runtime admission must reject duplicate selectors, invalid selector/body combinations, non-finite values and excessive sizes. It must **not** impose live graph existence or cross-facet invariants on historical carriers. A baseline with independently acknowledged minimum=120 and maximum=100 is valid history of those two facets even though that combination was never a valid live range. Unreviewed slots are carrier storage, not evidence of acknowledgement.
|
||||
|
||||
`DataConstructor` is a grouped decision: the probe copies definition and default together. Production drift should compare constructor discriminants separately from descendant values; accepting a switch must refresh the complete new constructor group and retire old descendant selectors. That full catalog/acknowledgement expansion is not implemented by this subset. Constructor scope does not include unrelated unit/prose or silently delete target children.
|
||||
|
||||
A typed candidate is supplied once per target record, not a recursive merged value per leaf. The adapter selects only acknowledged facets. The probe reconstructs the expected final record and rejects a supplied candidate that changes any unselected slot. Identity/alias are absent; the existing body layer/kind must remain consistent with the explicit mapping. Full model validation still decides whether all accepted choices can coexist.
|
||||
|
||||
Preview can carry `source_before`, `source_now`, `target_before`, `target_now`, and `candidate` as `OwnRecord`, plus finite changed-facet rows. The GUI reads typed selected slots from these carriers. Historical `before` carriers must be labeled as per-facet reviewed values, never as a single past project state. Existing prose `FieldChange` can stay unchanged; a sibling semantic review payload avoids replacing its strings with a new value language.
|
||||
|
||||
## Executed cases
|
||||
|
||||
1. **Bounded data and field multiplicity.** Source integer 0..100 m becomes 120..200 cm; target remains 0..100 ft. Keep minimum and unit acknowledges those facets only. The source carrier intentionally becomes 120..100, while maximum still reports drift. Taking maximum produces target 0..200 ft. A second application is unchanged. Taking a field minimum preserves its independently larger target maximum. Replacing a target Record with Integer is rejected atomically while its target-only field remains; applying an Integer-minimum selector across Record/Integer constructors also rejects.
|
||||
2. **State guard and triggers.** Delete the old OA Event; source then points to a new Event and changes guard. Target has an extra trigger and its own guard. Keep guard, take old-trigger removal and new-trigger addition. The historical `MemberPair` still identifies the removed source reference. Result preserves target-only trigger and guard, unlinks the old mapped target trigger, and leaves the target Event object alive. Reapplication is unchanged. Baseline JSON still roundtrips the deleted UUID.
|
||||
3. **Ordered steps.** Source reverses A/B while target inserts independent step X. Take-source ordering reports that an explicit final order is needed. Candidate `[B,A]` rejects because X is missing; `[B,X,A]` passes and X's record stays byte-for-byte equal. Duplicate resolutions and a hidden parent edit reject. The same test proves the B0 counterexample: starting from `[A,B]`, independently acknowledging appended C changes the carrier to `[A,B,C]`, falsely hiding unreviewed order. B1 keeps order checkpoint `[A,B]` and reports the pending change.
|
||||
|
||||
The merge functions are intentionally small OA→SA experiments. They use real `prepare_command(Command::Batch(...))` validation before returning either the project or updated baseline, so the rejected constructor leaves both inputs intact. The baseline is outside the real Project; undo/redo, revision, ETag, fingerprint and on-disk atomic baseline integration are not claimed proved.
|
||||
|
||||
## Source and JSON parity implication
|
||||
|
||||
The tests prove serde JSON roundtrip for the new carrier and for each live candidate. Each live candidate also roundtrips through the existing `language::format` / `language::parse` typed Arcadia 4 implementation. No public arbitrary value tree or escaped JSON blob is persisted by this proposal.
|
||||
|
||||
**They do not prove persisted R3 baseline source parity.** The source language currently resolves live body reference fields through `@alias` and rejects a quoted UUID there (`language.rs`, `Ast::resolve`). A negative test transplants the deleted-Event historical body into a live declaration: its formatted source is rejected. This demonstrates the need for an explicit historical context; inventing a live stub or alias would alter semantics.
|
||||
|
||||
The smallest integration route is to reuse the existing typed body serde decoder/constructor catalog inside a new baseline-carrier source context. In that context, references should use a dedicated literal such as `uuid "..."`, resolve directly to `Id`, and bypass live-reference existence checks. Context is essential: a live `@alias` must not become valid after its object disappears, and historical carrier fields must not be pruned by ordinary deletion. The same historical visitor must explicitly remap UUIDs during real identity remapping, including member pairs and order vectors. A generic field-name-only decoder cannot distinguish these meanings.
|
||||
|
||||
The new wrapper/cursor-free types still require source constructor registration, historical token ownership/comment behavior, OpenAPI declarations, resource budgets, and an explicit schema/source migration. Accepted data4 stays unchanged in this experiment. Whether all current typed decoder logic can be reused without duplication needs a targeted follow-up inspection; this report does not assume that work is already solved.
|
||||
|
||||
## Open questions before adoption
|
||||
|
||||
- Exhaustive scalar/reference/constructor coverage and overlap audit. The proof's `DataConstructor` comparison is a group-copy shortcut; production needs separate discriminant drift and complete descendant acknowledgement. Other composite fields may need similar finite split storage, not a new arbitrary history mechanism by default.
|
||||
- Per-member edits to annotated records, bindings and repeated occurrence contributions. Stable identity is not always a single UUID. Ownership and incident-relation membership plus artifact facets need explicit typed storage outside the own body.
|
||||
- Automatic ordering when anchors are genuinely unambiguous. The probe conservatively requires an explicit order; it does not implement the contract's allowed automatic insertions.
|
||||
- New/changed mappings, many-to-one conflict agreement, reverse direction, deleted mapped targets, historic remapping, missing/ambiguous reference counterparts, and validation of client-supplied masks and member pairs.
|
||||
- Coherent constructor transitions with target-only dependents: one blocking example passed, but successful transitions and every nested constructor group still need coverage.
|
||||
- Canonical processing of membership before order and constructor groups before descendants, independent of request array order. The bounded probe consumes resolutions in request order.
|
||||
- Drift caused by shared referenced records, child membership rollups, bounds on historical membership, migration from old prose-only baselines, and production atomic baseline/history/source persistence.
|
||||
|
||||
## Reproduce and evidence
|
||||
|
||||
Run from the worktree in PowerShell. The standalone lockfile is included; the vendor configuration is generated locally and points read-only to the pre-existing vendor directory. The target belongs only to this experiment.
|
||||
|
||||
```powershell
|
||||
cargo test --manifest-path experiments/r3-facet-carrier/Cargo.toml --locked --offline --config target/r3-probe/cargo.toml --target-dir D:/CodexBuilds/Capella2-transition-probe-target -- --nocapture
|
||||
```
|
||||
|
||||
Result: **3 passed, 0 failed**. The final captured command additionally pipes output through `Tee-Object -FilePath target/r3-probe/tests.log` and exits with `$LASTEXITCODE`.
|
||||
|
||||
`target/r3-probe/cargo.toml`:
|
||||
|
||||
```toml
|
||||
[source.crates-io]
|
||||
replace-with = "vendored-sources"
|
||||
[source.vendored-sources]
|
||||
directory = "D:/CodexBuilds/Capella2-01a06e2e/vendor"
|
||||
```
|
||||
|
||||
Evidence is under `target/r3-probe/`: `tests.log`, and generated `evidence/*-baseline.json` / `*-candidate.arcadia` for mixed range, deleted trigger and target-only step. Generated IDs differ between runs; the asserted behavior does not depend on them. No native UI was used, no package installed, and no other checkout or build target was changed.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Run from the isolated worktree root. Counts rustfmt lines, not complexity.
|
||||
$r3B = @(git show 8ac9f46:experiments/r3-facet-carrier/src/lib.rs)
|
||||
$r3C = @(Get-Content -LiteralPath experiments/r3-facet-carrier/src/typed_reviews.rs)
|
||||
$r3Tests = @(Get-Content -LiteralPath experiments/r3-facet-carrier/src/typed_reviews_tests.rs)
|
||||
function Count-R3Lines($Lines) {
|
||||
@($Lines | Where-Object { $_.Trim().Length -gt 0 -and -not $_.Trim().StartsWith('//') }).Count
|
||||
}
|
||||
function Find-R3Line($Lines, $Prefix) {
|
||||
for ($r3Index = 0; $r3Index -lt $Lines.Length; $r3Index++) {
|
||||
if ($Lines[$r3Index].StartsWith($Prefix)) { return $r3Index }
|
||||
}
|
||||
throw "Missing section marker: $Prefix"
|
||||
}
|
||||
$r3BOwn = Find-R3Line $r3B 'pub fn own('
|
||||
$r3BCopy = Find-R3Line $r3B 'fn copy('
|
||||
$r3BTests = Find-R3Line $r3B '#[cfg(test)]'
|
||||
$r3CAdapter = Find-R3Line $r3C '// ADAPTER:'
|
||||
$r3CTestStart = Find-R3Line $r3C '#[cfg(test)]'
|
||||
[ordered]@{
|
||||
method = 'Nonblank non-comment rustfmt lines; excludes test module registration'
|
||||
b_public = Count-R3Lines $r3B[5..($r3BOwn-1)]
|
||||
shared_record_helpers = Count-R3Lines $r3B[$r3BOwn..($r3BCopy-1)]
|
||||
b_adapter = Count-R3Lines $r3B[$r3BCopy..($r3BTests-1)]
|
||||
b_tests = Count-R3Lines $r3B[($r3BTests+1)..($r3B.Length-1)]
|
||||
c_public = Count-R3Lines $r3C[5..($r3CAdapter-1)]
|
||||
c_adapter = Count-R3Lines $r3C[$r3CAdapter..($r3CTestStart-1)]
|
||||
c_tests = Count-R3Lines $r3Tests
|
||||
} | ConvertTo-Json
|
||||
@@ -1,934 +0,0 @@
|
||||
//! Throwaway R3 experiment. Nothing in oa-core depends on this crate.
|
||||
//! A carrier stores independently reviewed facets; it is NOT a past valid model.
|
||||
use oa_core::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub mod typed_reviews;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum OwnRecord {
|
||||
Element {
|
||||
parent: Option<Id>,
|
||||
body: ElementBody,
|
||||
},
|
||||
Relation {
|
||||
body: RelationBody,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MemberPair {
|
||||
pub oa: Option<Id>,
|
||||
pub sa: Option<Id>,
|
||||
}
|
||||
|
||||
/// Deliberately finite proof subset, not the final exhaustive R3 catalog.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum Facet {
|
||||
IntegerMinimum,
|
||||
IntegerMaximum,
|
||||
DataUnit,
|
||||
FieldMinimum,
|
||||
StateGuard,
|
||||
Trigger(MemberPair),
|
||||
Step(MemberPair),
|
||||
StepsOrder,
|
||||
/// Definition AND default form one explicit constructor replacement group.
|
||||
DataConstructor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SemanticBaseline {
|
||||
pub oa: OwnRecord,
|
||||
pub sa: OwnRecord,
|
||||
pub reviewed: Vec<Facet>,
|
||||
pub orders: Vec<OrderCheckpoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OrderedCollection {
|
||||
Steps,
|
||||
}
|
||||
|
||||
/// Order overlaps membership in the live Vec, so it needs a separate review value.
|
||||
/// No observation IDs, history versions, or generic value representation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OrderCheckpoint {
|
||||
pub collection: OrderedCollection,
|
||||
pub oa: Vec<Id>,
|
||||
pub sa: Vec<Id>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Decision {
|
||||
TakeSource,
|
||||
KeepTarget,
|
||||
Candidate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Resolution {
|
||||
pub facet: Facet,
|
||||
pub decision: Decision,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Review {
|
||||
pub source: Id,
|
||||
pub target: Id,
|
||||
pub candidate: Option<OwnRecord>,
|
||||
pub resolutions: Vec<Resolution>,
|
||||
}
|
||||
|
||||
pub fn own(p: &Project, id: Id) -> OwnRecord {
|
||||
if let Some(e) = p.elements.get(&id) {
|
||||
OwnRecord::Element {
|
||||
parent: e.parent,
|
||||
body: e.body.clone(),
|
||||
}
|
||||
} else {
|
||||
OwnRecord::Relation {
|
||||
body: p.relations[&id].body.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn shared(record: &OwnRecord) -> Result<&SharedBody, String> {
|
||||
match record {
|
||||
OwnRecord::Element { body, .. } => body.shared().ok_or("Not a shared body".into()),
|
||||
_ => Err("Not an element".into()),
|
||||
}
|
||||
}
|
||||
fn shared_mut(record: &mut OwnRecord) -> Result<&mut SharedBody, String> {
|
||||
match record {
|
||||
OwnRecord::Element { body, .. } => body.shared_mut().ok_or("Not a shared body".into()),
|
||||
_ => Err("Not an element".into()),
|
||||
}
|
||||
}
|
||||
fn list(record: &OwnRecord, trigger: bool) -> Result<&Vec<Id>, String> {
|
||||
match (record, trigger) {
|
||||
(
|
||||
OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { triggers, .. },
|
||||
},
|
||||
true,
|
||||
) => Ok(triggers),
|
||||
(OwnRecord::Element { body, .. }, false) => {
|
||||
body.steps().ok_or("Not an ordered sequence".into())
|
||||
}
|
||||
_ => Err("Wrong member selector".into()),
|
||||
}
|
||||
}
|
||||
fn list_mut(record: &mut OwnRecord, trigger: bool) -> Result<&mut Vec<Id>, String> {
|
||||
match (record, trigger) {
|
||||
(
|
||||
OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { triggers, .. },
|
||||
},
|
||||
true,
|
||||
) => Ok(triggers),
|
||||
(OwnRecord::Element { body, .. }, false) => {
|
||||
body.steps_mut().ok_or("Not an ordered sequence".into())
|
||||
}
|
||||
_ => Err("Wrong member selector".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy same-coordinate facet; mapped reference proposals use copy_member below.
|
||||
fn copy(dst: &mut OwnRecord, src: &OwnRecord, facet: &Facet, oa: bool) -> Result<(), String> {
|
||||
match facet {
|
||||
Facet::IntegerMinimum | Facet::IntegerMaximum => {
|
||||
let SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Integer { minimum, maximum },
|
||||
..
|
||||
} = shared(src)?
|
||||
else {
|
||||
return Err("Explicit constructor decision required".into());
|
||||
};
|
||||
let (minimum, maximum) = (*minimum, *maximum);
|
||||
let SharedBody::DataType {
|
||||
definition:
|
||||
DataTypeDefinition::Integer {
|
||||
minimum: dst_min,
|
||||
maximum: dst_max,
|
||||
},
|
||||
..
|
||||
} = shared_mut(dst)?
|
||||
else {
|
||||
return Err("Explicit constructor decision required".into());
|
||||
};
|
||||
if *facet == Facet::IntegerMinimum {
|
||||
*dst_min = minimum;
|
||||
} else {
|
||||
*dst_max = maximum;
|
||||
}
|
||||
}
|
||||
Facet::DataUnit => {
|
||||
let SharedBody::DataType { unit, .. } = shared(src)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
let unit = unit.clone();
|
||||
let SharedBody::DataType { unit: dst_unit, .. } = shared_mut(dst)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*dst_unit = unit;
|
||||
}
|
||||
Facet::FieldMinimum => {
|
||||
let SharedBody::DataField { minimum, .. } = shared(src)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
let minimum = *minimum;
|
||||
let SharedBody::DataField {
|
||||
minimum: dst_min, ..
|
||||
} = shared_mut(dst)?
|
||||
else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*dst_min = minimum;
|
||||
}
|
||||
Facet::StateGuard => {
|
||||
let OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { guard, .. },
|
||||
} = src
|
||||
else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
let OwnRecord::Relation {
|
||||
body:
|
||||
RelationBody::StateTransition {
|
||||
guard: dst_guard, ..
|
||||
},
|
||||
} = dst
|
||||
else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*dst_guard = guard.clone();
|
||||
}
|
||||
Facet::Trigger(pair) | Facet::Step(pair) => {
|
||||
let id = if oa { pair.oa } else { pair.sa };
|
||||
copy_member(dst, src, matches!(facet, Facet::Trigger(_)), id, id)?;
|
||||
}
|
||||
Facet::StepsOrder => *list_mut(dst, false)? = list(src, false)?.clone(),
|
||||
Facet::DataConstructor => {
|
||||
let SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
} = shared(src)?
|
||||
else {
|
||||
return Err("Wrong constructor scope".into());
|
||||
};
|
||||
let (definition, default) = (definition.clone(), default.clone());
|
||||
let SharedBody::DataType {
|
||||
definition: dst_definition,
|
||||
default: dst_default,
|
||||
..
|
||||
} = shared_mut(dst)?
|
||||
else {
|
||||
return Err("Wrong constructor scope".into());
|
||||
};
|
||||
*dst_definition = definition;
|
||||
*dst_default = default;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn copy_member(
|
||||
dst: &mut OwnRecord,
|
||||
src: &OwnRecord,
|
||||
trigger: bool,
|
||||
src_id: Option<Id>,
|
||||
dst_id: Option<Id>,
|
||||
) -> Result<(), String> {
|
||||
if src_id.is_none() && dst_id.is_none() {
|
||||
return Err("Empty member identity".into());
|
||||
}
|
||||
let present =
|
||||
src_id.is_some_and(|id| list(src, trigger).is_ok_and(|items| items.contains(&id)));
|
||||
let Some(dst_id) = dst_id else {
|
||||
return if present {
|
||||
Err("Missing counterpart mapping".into())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
};
|
||||
let items = list_mut(dst, trigger)?;
|
||||
if present && !items.contains(&dst_id) {
|
||||
items.push(dst_id);
|
||||
}
|
||||
if !present {
|
||||
items.retain(|id| *id != dst_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn equal(a: &OwnRecord, b: &OwnRecord, facet: &Facet, oa: bool) -> Result<bool, String> {
|
||||
let mut projected = a.clone();
|
||||
copy(&mut projected, b, facet, oa)?;
|
||||
Ok(projected == *a)
|
||||
}
|
||||
pub fn reviewed_changed(
|
||||
base: &SemanticBaseline,
|
||||
now: &OwnRecord,
|
||||
facet: &Facet,
|
||||
oa: bool,
|
||||
) -> Result<Option<bool>, String> {
|
||||
if !base.reviewed.contains(facet) {
|
||||
return Ok(None);
|
||||
}
|
||||
if *facet == Facet::StepsOrder {
|
||||
let checkpoint = base
|
||||
.orders
|
||||
.iter()
|
||||
.find(|o| o.collection == OrderedCollection::Steps)
|
||||
.ok_or("Missing order checkpoint")?;
|
||||
return Ok(Some(
|
||||
list(now, false)? != if oa { &checkpoint.oa } else { &checkpoint.sa },
|
||||
));
|
||||
}
|
||||
equal(if oa { &base.oa } else { &base.sa }, now, facet, oa).map(|same| Some(!same))
|
||||
}
|
||||
|
||||
/// One validated core candidate, then baseline assignment. No partial commit.
|
||||
/// OA→SA only in this bounded experiment.
|
||||
pub fn apply(
|
||||
p: &Project,
|
||||
base: &SemanticBaseline,
|
||||
review: &Review,
|
||||
) -> Result<(Project, SemanticBaseline), String> {
|
||||
let source = own(p, review.source);
|
||||
let current = own(p, review.target);
|
||||
let mut target = current.clone();
|
||||
let mut acknowledged = base.clone();
|
||||
let mut seen = Vec::new();
|
||||
if review
|
||||
.resolutions
|
||||
.iter()
|
||||
.any(|r| r.facet == Facet::DataConstructor)
|
||||
&& review
|
||||
.resolutions
|
||||
.iter()
|
||||
.any(|r| matches!(r.facet, Facet::IntegerMinimum | Facet::IntegerMaximum))
|
||||
{
|
||||
return Err("Constructor and descendant facets overlap".into());
|
||||
}
|
||||
for resolution in &review.resolutions {
|
||||
if seen.contains(&resolution.facet) {
|
||||
return Err("Duplicate facet".into());
|
||||
}
|
||||
seen.push(resolution.facet.clone());
|
||||
match resolution.decision {
|
||||
Decision::KeepTarget => {}
|
||||
Decision::TakeSource => match &resolution.facet {
|
||||
Facet::Trigger(pair) | Facet::Step(pair) => copy_member(
|
||||
&mut target,
|
||||
&source,
|
||||
matches!(resolution.facet, Facet::Trigger(_)),
|
||||
pair.oa,
|
||||
pair.sa,
|
||||
)?,
|
||||
Facet::StepsOrder => {
|
||||
return Err("This probe requires an explicit final order".into());
|
||||
}
|
||||
facet => copy(&mut target, &source, facet, false)?,
|
||||
},
|
||||
Decision::Candidate => {
|
||||
let candidate = review.candidate.as_ref().ok_or("Missing candidate")?;
|
||||
if resolution.facet == Facet::StepsOrder {
|
||||
let mut expected = list(&target, false)?.clone();
|
||||
let mut proposed = list(candidate, false)?.clone();
|
||||
expected.sort();
|
||||
proposed.sort();
|
||||
if expected != proposed || proposed.windows(2).any(|p| p[0] == p[1]) {
|
||||
return Err(
|
||||
"Final order must contain every surviving target member exactly once"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
copy(&mut target, candidate, &resolution.facet, false)?;
|
||||
}
|
||||
}
|
||||
if resolution.facet == Facet::DataConstructor {
|
||||
acknowledged.reviewed.retain(|f| {
|
||||
!matches!(
|
||||
f,
|
||||
Facet::IntegerMinimum | Facet::IntegerMaximum | Facet::DataConstructor
|
||||
)
|
||||
});
|
||||
}
|
||||
if resolution.facet == Facet::StepsOrder {
|
||||
acknowledged
|
||||
.orders
|
||||
.retain(|o| o.collection != OrderedCollection::Steps);
|
||||
acknowledged.orders.push(OrderCheckpoint {
|
||||
collection: OrderedCollection::Steps,
|
||||
oa: list(&source, false)?.clone(),
|
||||
sa: list(&target, false)?.clone(),
|
||||
});
|
||||
} else {
|
||||
copy(&mut acknowledged.oa, &source, &resolution.facet, true)?;
|
||||
copy(&mut acknowledged.sa, &target, &resolution.facet, false)?;
|
||||
}
|
||||
if !acknowledged.reviewed.contains(&resolution.facet) {
|
||||
acknowledged.reviewed.push(resolution.facet.clone());
|
||||
}
|
||||
}
|
||||
// Explicit candidate cannot smuggle changes to any unreviewed facet.
|
||||
if let Some(candidate) = &review.candidate {
|
||||
if *candidate != target {
|
||||
return Err("Candidate changes an unselected facet".into());
|
||||
}
|
||||
}
|
||||
let command = match target {
|
||||
OwnRecord::Element { parent, body } => {
|
||||
let mut e = p.elements[&review.target].clone();
|
||||
e.parent = parent;
|
||||
e.body = body;
|
||||
Command::EditElement(review.target, e)
|
||||
}
|
||||
OwnRecord::Relation { body } => {
|
||||
let mut r = p.relations[&review.target].clone();
|
||||
r.body = body;
|
||||
Command::EditRelation(review.target, r)
|
||||
}
|
||||
};
|
||||
let prepared = prepare_command(p, &Command::Batch(vec![command])).map_err(|e| e.to_string())?;
|
||||
Ok((prepared.project, acknowledged))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
pub(crate) fn add(
|
||||
p: &mut Project,
|
||||
kind: Kind,
|
||||
layer: Layer,
|
||||
name: &str,
|
||||
parent: Option<Id>,
|
||||
) -> Id {
|
||||
p.add_element(kind, layer, name, parent).unwrap()
|
||||
}
|
||||
pub(crate) fn set(p: &mut Project, id: Id, body: SharedBody) {
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body;
|
||||
}
|
||||
pub(crate) fn valid(p: &Project) {
|
||||
let errors: Vec<_> = validation::validate(p)
|
||||
.into_iter()
|
||||
.filter(|i| i.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
}
|
||||
fn baseline(p: &Project, oa: Id, sa: Id, reviewed: Vec<Facet>) -> SemanticBaseline {
|
||||
let orders = if reviewed.contains(&Facet::StepsOrder) {
|
||||
vec![OrderCheckpoint {
|
||||
collection: OrderedCollection::Steps,
|
||||
oa: list(&own(p, oa), false).unwrap().clone(),
|
||||
sa: list(&own(p, sa), false).unwrap().clone(),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
SemanticBaseline {
|
||||
oa: own(p, oa),
|
||||
sa: own(p, sa),
|
||||
reviewed,
|
||||
orders,
|
||||
}
|
||||
}
|
||||
fn resolution(facet: Facet, decision: Decision) -> Resolution {
|
||||
Resolution { facet, decision }
|
||||
}
|
||||
fn roundtrip(p: &Project, base: &SemanticBaseline) {
|
||||
let json = serde_json::to_string(base).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<SemanticBaseline>(&json).unwrap(),
|
||||
*base
|
||||
);
|
||||
assert_eq!(language::parse(&language::format(p)).unwrap(), *p);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Project>(&serde_json::to_string(p).unwrap()).unwrap(),
|
||||
*p
|
||||
);
|
||||
}
|
||||
fn evidence(name: &str, p: &Project, base: &SemanticBaseline) {
|
||||
let folder =
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/r3-probe/evidence");
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
std::fs::write(
|
||||
folder.join(format!("{name}-baseline.json")),
|
||||
serde_json::to_string_pretty(base).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
folder.join(format!("{name}-candidate.arcadia")),
|
||||
language::format(p),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
pub(crate) fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody {
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Integer {
|
||||
minimum: Some(minimum),
|
||||
maximum: Some(maximum),
|
||||
},
|
||||
default: None,
|
||||
unit: unit.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_data_partial_keep_take_and_constructor_atomicity() {
|
||||
let mut p = Project::empty();
|
||||
let oa = add(&mut p, Kind::DataType, Layer::Oa, "OA bounded", None);
|
||||
let sa = add(&mut p, Kind::DataType, Layer::Sa, "SA bounded", None);
|
||||
set(&mut p, oa, integer(0, 100, "m"));
|
||||
set(&mut p, sa, integer(0, 100, "ft"));
|
||||
let base = baseline(
|
||||
&p,
|
||||
oa,
|
||||
sa,
|
||||
vec![
|
||||
Facet::IntegerMinimum,
|
||||
Facet::IntegerMaximum,
|
||||
Facet::DataUnit,
|
||||
],
|
||||
);
|
||||
set(&mut p, oa, integer(120, 200, "cm"));
|
||||
let review = Review {
|
||||
source: oa,
|
||||
target: sa,
|
||||
candidate: None,
|
||||
resolutions: vec![
|
||||
resolution(Facet::IntegerMinimum, Decision::KeepTarget),
|
||||
resolution(Facet::DataUnit, Decision::KeepTarget),
|
||||
],
|
||||
};
|
||||
let (kept, mixed) = apply(&p, &base, &review).unwrap();
|
||||
assert_eq!(kept, p);
|
||||
// OA baseline now min=120,max=100. This is intentional: fields were reviewed independently.
|
||||
assert!(matches!(
|
||||
shared(&mixed.oa).unwrap(),
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Integer {
|
||||
minimum: Some(120),
|
||||
maximum: Some(100)
|
||||
},
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(
|
||||
reviewed_changed(&mixed, &own(&p, oa), &Facet::IntegerMinimum, true).unwrap(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
reviewed_changed(&mixed, &own(&p, oa), &Facet::IntegerMaximum, true).unwrap(),
|
||||
Some(true)
|
||||
);
|
||||
let take = Review {
|
||||
source: oa,
|
||||
target: sa,
|
||||
candidate: None,
|
||||
resolutions: vec![resolution(Facet::IntegerMaximum, Decision::TakeSource)],
|
||||
};
|
||||
let (p, merged) = apply(&p, &mixed, &take).unwrap();
|
||||
assert!(
|
||||
matches!(shared(&own(&p, sa)).unwrap(), SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(0), maximum: Some(200) }, unit, .. } if unit == "ft")
|
||||
);
|
||||
let (again, same) = apply(&p, &merged, &take).unwrap();
|
||||
assert_eq!(again, p);
|
||||
assert_eq!(same, merged);
|
||||
roundtrip(&p, &mixed);
|
||||
evidence("mixed-range", &p, &mixed);
|
||||
|
||||
let mut fields = p;
|
||||
let owner_oa = add(&mut fields, Kind::DataType, Layer::Oa, "OA record", None);
|
||||
let owner_sa = add(&mut fields, Kind::DataType, Layer::Sa, "SA record", None);
|
||||
for id in [owner_oa, owner_sa] {
|
||||
set(
|
||||
&mut fields,
|
||||
id,
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype: None },
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let field_oa = add(
|
||||
&mut fields,
|
||||
Kind::DataField,
|
||||
Layer::Oa,
|
||||
"OA field",
|
||||
Some(owner_oa),
|
||||
);
|
||||
let field_sa = add(
|
||||
&mut fields,
|
||||
Kind::DataField,
|
||||
Layer::Sa,
|
||||
"SA field",
|
||||
Some(owner_sa),
|
||||
);
|
||||
for (id, max) in [(field_oa, 5), (field_sa, 9)] {
|
||||
set(
|
||||
&mut fields,
|
||||
id,
|
||||
SharedBody::DataField {
|
||||
type_ref: TypeRef::Named(if id == field_oa { oa } else { sa }),
|
||||
minimum: 0,
|
||||
maximum: Some(max),
|
||||
unit: String::new(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
let field_base = baseline(&fields, field_oa, field_sa, vec![Facet::FieldMinimum]);
|
||||
if let Some(SharedBody::DataField { minimum, .. }) =
|
||||
fields.elements.get_mut(&field_oa).unwrap().shared_mut()
|
||||
{
|
||||
*minimum = 2;
|
||||
}
|
||||
let (fields, field_base) = apply(
|
||||
&fields,
|
||||
&field_base,
|
||||
&Review {
|
||||
source: field_oa,
|
||||
target: field_sa,
|
||||
candidate: None,
|
||||
resolutions: vec![resolution(Facet::FieldMinimum, Decision::TakeSource)],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
fields.elements[&field_sa].shared(),
|
||||
Some(SharedBody::DataField {
|
||||
minimum: 2,
|
||||
maximum: Some(9),
|
||||
..
|
||||
})
|
||||
));
|
||||
roundtrip(&fields, &field_base);
|
||||
|
||||
let mut switched = fields.clone();
|
||||
let constructor_base = baseline(&switched, owner_oa, owner_sa, vec![]);
|
||||
// Remove only the source's child; the target-only field remains real independent work.
|
||||
switched = prepare_command(&switched, &Command::Delete(field_oa))
|
||||
.unwrap()
|
||||
.project;
|
||||
set(&mut switched, owner_oa, integer(0, 10, ""));
|
||||
valid(&switched);
|
||||
let before = switched.clone();
|
||||
let before_base = constructor_base.clone();
|
||||
let result = apply(
|
||||
&switched,
|
||||
&constructor_base,
|
||||
&Review {
|
||||
source: owner_oa,
|
||||
target: owner_sa,
|
||||
candidate: None,
|
||||
resolutions: vec![resolution(Facet::DataConstructor, Decision::TakeSource)],
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"target Record child blocks primitive constructor switch"
|
||||
);
|
||||
assert_eq!(switched, before);
|
||||
assert_eq!(constructor_base, before_base);
|
||||
let forbidden = apply(
|
||||
&switched,
|
||||
&constructor_base,
|
||||
&Review {
|
||||
source: owner_oa,
|
||||
target: owner_sa,
|
||||
candidate: None,
|
||||
resolutions: vec![resolution(Facet::IntegerMinimum, Decision::TakeSource)],
|
||||
},
|
||||
);
|
||||
assert!(forbidden.unwrap_err().contains("constructor"));
|
||||
println!(
|
||||
"bounded data: mixed historical min/max permitted; unselected drift retained; target max/unit preserved; constructor candidate rejected atomically"
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn state_side(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) {
|
||||
let owner = add(
|
||||
p,
|
||||
if layer == Layer::Oa {
|
||||
Kind::Entity
|
||||
} else {
|
||||
Kind::System
|
||||
},
|
||||
layer,
|
||||
&format!("{prefix} owner"),
|
||||
None,
|
||||
);
|
||||
let machine = add(
|
||||
p,
|
||||
Kind::StateMachine,
|
||||
layer,
|
||||
&format!("{prefix} machine"),
|
||||
Some(owner),
|
||||
);
|
||||
let region = add(
|
||||
p,
|
||||
Kind::Region,
|
||||
layer,
|
||||
&format!("{prefix} region"),
|
||||
Some(machine),
|
||||
);
|
||||
let a = add(p, Kind::State, layer, &format!("{prefix} A"), Some(region));
|
||||
let b = add(p, Kind::State, layer, &format!("{prefix} B"), Some(region));
|
||||
let old = add(p, Kind::Event, layer, &format!("{prefix} old event"), None);
|
||||
let new = add(p, Kind::Event, layer, &format!("{prefix} new event"), None);
|
||||
let relation = p.relate(
|
||||
RelationKind::StateTransition,
|
||||
&format!("{prefix} transition"),
|
||||
a,
|
||||
b,
|
||||
);
|
||||
if let RelationBody::StateTransition {
|
||||
triggers, guard, ..
|
||||
} = &mut p.relations.get_mut(&relation).unwrap().body
|
||||
{
|
||||
*triggers = vec![old];
|
||||
*guard = "ready".into();
|
||||
}
|
||||
(relation, old, new)
|
||||
}
|
||||
#[test]
|
||||
fn state_guard_and_trigger_delta_retain_deleted_historical_identity() {
|
||||
let mut p = Project::empty();
|
||||
let (oa, old_oa, new_oa) = state_side(&mut p, Layer::Oa, "OA");
|
||||
let (sa, old_sa, new_sa) = state_side(&mut p, Layer::Sa, "SA");
|
||||
let extra = add(&mut p, Kind::Event, Layer::Sa, "Target only", None);
|
||||
let old = MemberPair {
|
||||
oa: Some(old_oa),
|
||||
sa: Some(old_sa),
|
||||
};
|
||||
let new = MemberPair {
|
||||
oa: Some(new_oa),
|
||||
sa: Some(new_sa),
|
||||
};
|
||||
let base = baseline(
|
||||
&p,
|
||||
oa,
|
||||
sa,
|
||||
vec![Facet::StateGuard, Facet::Trigger(old), Facet::Trigger(new)],
|
||||
);
|
||||
p = prepare_command(&p, &Command::Delete(old_oa))
|
||||
.unwrap()
|
||||
.project;
|
||||
if let RelationBody::StateTransition {
|
||||
triggers, guard, ..
|
||||
} = &mut p.relations.get_mut(&oa).unwrap().body
|
||||
{
|
||||
*triggers = vec![new_oa];
|
||||
*guard = "armed".into();
|
||||
}
|
||||
if let RelationBody::StateTransition {
|
||||
triggers, guard, ..
|
||||
} = &mut p.relations.get_mut(&sa).unwrap().body
|
||||
{
|
||||
triggers.push(extra);
|
||||
*guard = "local inhibit clear".into();
|
||||
}
|
||||
valid(&p);
|
||||
assert!(!p.elements.contains_key(&old_oa));
|
||||
assert!(list(&base.oa, true).unwrap().contains(&old_oa));
|
||||
// Current source uses live @alias semantics. A historical body needs a
|
||||
// distinct source context, not transplantation into a live declaration.
|
||||
let mut historical_as_live = p.clone();
|
||||
if let OwnRecord::Relation { body } = &base.oa {
|
||||
historical_as_live.relations.get_mut(&oa).unwrap().body = body.clone();
|
||||
}
|
||||
let rejected = language::parse(&language::format(&historical_as_live));
|
||||
assert!(
|
||||
rejected.is_err(),
|
||||
"deleted historical UUID is not a live alias"
|
||||
);
|
||||
let review = Review {
|
||||
source: oa,
|
||||
target: sa,
|
||||
candidate: None,
|
||||
resolutions: vec![
|
||||
resolution(Facet::StateGuard, Decision::KeepTarget),
|
||||
resolution(Facet::Trigger(old), Decision::TakeSource),
|
||||
resolution(Facet::Trigger(new), Decision::TakeSource),
|
||||
],
|
||||
};
|
||||
let (p, acknowledged) = apply(&p, &base, &review).unwrap();
|
||||
assert_eq!(list(&own(&p, sa), true).unwrap(), &vec![extra, new_sa]);
|
||||
assert!(p.elements.contains_key(&old_sa));
|
||||
assert!(
|
||||
matches!(&p.relations[&sa].body, RelationBody::StateTransition { guard, .. } if guard == "local inhibit clear")
|
||||
);
|
||||
roundtrip(&p, &base);
|
||||
roundtrip(&p, &acknowledged);
|
||||
evidence("deleted-trigger", &p, &base);
|
||||
let (again, same) = apply(&p, &acknowledged, &review).unwrap();
|
||||
assert_eq!(again, p);
|
||||
assert_eq!(same, acknowledged);
|
||||
println!(
|
||||
"state: old source Event deleted; historical member pair still removes its target link, preserves target object and extra trigger, keeps guard"
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn sequence(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) {
|
||||
let sequence = add(
|
||||
p,
|
||||
if layer == Layer::Oa {
|
||||
Kind::Process
|
||||
} else {
|
||||
Kind::FunctionalChain
|
||||
},
|
||||
layer,
|
||||
prefix,
|
||||
None,
|
||||
);
|
||||
let a = add(
|
||||
p,
|
||||
Kind::Occurrence,
|
||||
layer,
|
||||
&format!("{prefix} A"),
|
||||
Some(sequence),
|
||||
);
|
||||
let b = add(
|
||||
p,
|
||||
Kind::Occurrence,
|
||||
layer,
|
||||
&format!("{prefix} B"),
|
||||
Some(sequence),
|
||||
);
|
||||
for id in [a, b] {
|
||||
set(
|
||||
p,
|
||||
id,
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Control {
|
||||
kind: ControlKind::Choice,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
*p.elements.get_mut(&sequence).unwrap().steps_mut().unwrap() = vec![a, b];
|
||||
(sequence, a, b)
|
||||
}
|
||||
#[test]
|
||||
fn ordered_steps_preserve_target_only_members_and_reject_hidden_edits() {
|
||||
let mut p = Project::empty();
|
||||
let (oa, oa_a, oa_b) = sequence(&mut p, Layer::Oa, "OA flow");
|
||||
let (sa, sa_a, sa_b) = sequence(&mut p, Layer::Sa, "SA flow");
|
||||
let base = baseline(&p, oa, sa, vec![Facet::StepsOrder]);
|
||||
// Counterexample: a bare carrier loses pending placement review after
|
||||
// independently acknowledging append membership. The order checkpoint
|
||||
// keeps the exact older order and therefore retains the drift.
|
||||
let oa_c = Id::new_v4();
|
||||
let sa_c = Id::new_v4();
|
||||
let mut source_with_append = own(&p, oa);
|
||||
list_mut(&mut source_with_append, false).unwrap().push(oa_c);
|
||||
let mut membership_only = base.clone();
|
||||
copy(
|
||||
&mut membership_only.oa,
|
||||
&source_with_append,
|
||||
&Facet::Step(MemberPair {
|
||||
oa: Some(oa_c),
|
||||
sa: Some(sa_c),
|
||||
}),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
membership_only.oa, source_with_append,
|
||||
"bare carrier would falsely report no order drift"
|
||||
);
|
||||
assert_eq!(
|
||||
reviewed_changed(
|
||||
&membership_only,
|
||||
&source_with_append,
|
||||
&Facet::StepsOrder,
|
||||
true
|
||||
)
|
||||
.unwrap(),
|
||||
Some(true)
|
||||
);
|
||||
let extra = add(
|
||||
&mut p,
|
||||
Kind::Occurrence,
|
||||
Layer::Sa,
|
||||
"Target only step",
|
||||
Some(sa),
|
||||
);
|
||||
set(
|
||||
&mut p,
|
||||
extra,
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Control {
|
||||
kind: ControlKind::Merge,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
},
|
||||
);
|
||||
*p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![sa_a, extra, sa_b];
|
||||
*p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![oa_b, oa_a];
|
||||
valid(&p);
|
||||
let take = Review {
|
||||
source: oa,
|
||||
target: sa,
|
||||
candidate: None,
|
||||
resolutions: vec![resolution(Facet::StepsOrder, Decision::TakeSource)],
|
||||
};
|
||||
assert!(
|
||||
apply(&p, &base, &take)
|
||||
.unwrap_err()
|
||||
.contains("explicit final order")
|
||||
);
|
||||
let mut candidate = own(&p, sa);
|
||||
*list_mut(&mut candidate, false).unwrap() = vec![sa_b, sa_a];
|
||||
let mut review = Review {
|
||||
candidate: Some(candidate),
|
||||
resolutions: vec![resolution(Facet::StepsOrder, Decision::Candidate)],
|
||||
..take
|
||||
};
|
||||
assert!(
|
||||
apply(&p, &base, &review)
|
||||
.unwrap_err()
|
||||
.contains("every surviving")
|
||||
);
|
||||
*list_mut(review.candidate.as_mut().unwrap(), false).unwrap() = vec![sa_b, extra, sa_a];
|
||||
let target_only = p.elements[&extra].clone();
|
||||
let (p, acknowledged) = apply(&p, &base, &review).unwrap();
|
||||
assert_eq!(list(&own(&p, sa), false).unwrap(), &vec![sa_b, extra, sa_a]);
|
||||
assert_eq!(p.elements[&extra], target_only);
|
||||
roundtrip(&p, &acknowledged);
|
||||
evidence("target-only-step", &p, &acknowledged);
|
||||
let mut duplicate = review.clone();
|
||||
duplicate.resolutions.push(duplicate.resolutions[0].clone());
|
||||
assert!(
|
||||
apply(&p, &acknowledged, &duplicate)
|
||||
.unwrap_err()
|
||||
.contains("Duplicate")
|
||||
);
|
||||
let mut hidden = review;
|
||||
if let Some(OwnRecord::Element { parent, .. }) = &mut hidden.candidate {
|
||||
*parent = Some(extra);
|
||||
}
|
||||
assert!(
|
||||
apply(&p, &acknowledged, &hidden)
|
||||
.unwrap_err()
|
||||
.contains("unselected")
|
||||
);
|
||||
println!(
|
||||
"steps: ambiguous source reorder demands explicit candidate; missing target-only member and unselected parent edits reject"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,909 +0,0 @@
|
||||
//! C: closed typed reviewed facts. Whole records are transient candidates only.
|
||||
use crate::{Decision, OwnRecord, list, list_mut, own, shared, shared_mut};
|
||||
use oa_core::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// PUBLIC TYPES: persisted facts have no carrier, review mask or shadow order.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum Observed<T> {
|
||||
NotApplicable,
|
||||
Value(T),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Reviewed<T> {
|
||||
pub oa: Observed<T>,
|
||||
pub sa: Observed<T>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct KeyPair<K> {
|
||||
pub oa: Option<K>,
|
||||
pub sa: Option<K>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ValueKey {
|
||||
pub path: Vec<Id>,
|
||||
pub field: Id,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataConstructor {
|
||||
Boolean,
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration,
|
||||
Record,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataFacet {
|
||||
Constructor,
|
||||
IntegerMinimum,
|
||||
IntegerMaximum,
|
||||
RealMinimum,
|
||||
RealMaximum,
|
||||
MinimumLength,
|
||||
MaximumLength,
|
||||
Literals,
|
||||
Supertype,
|
||||
Default,
|
||||
Unit,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum DataReview {
|
||||
Constructor(Reviewed<DataConstructor>),
|
||||
IntegerMinimum(Reviewed<Option<i64>>),
|
||||
IntegerMaximum(Reviewed<Option<i64>>),
|
||||
RealMinimum(Reviewed<Option<f64>>),
|
||||
RealMaximum(Reviewed<Option<f64>>),
|
||||
MinimumLength(Reviewed<u32>),
|
||||
MaximumLength(Reviewed<Option<u32>>),
|
||||
Literals(Reviewed<Vec<String>>),
|
||||
Supertype(Reviewed<Option<Id>>),
|
||||
Default(Reviewed<Option<DataValue>>),
|
||||
Unit(Reviewed<String>),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum Selector {
|
||||
Data(DataFacet),
|
||||
FieldMinimum,
|
||||
StateGuard,
|
||||
Trigger(KeyPair<Id>),
|
||||
Step(KeyPair<Id>),
|
||||
StepOrder,
|
||||
Annotation(KeyPair<ValueKey>),
|
||||
Binding(KeyPair<Id>),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum FacetReview {
|
||||
Data(DataReview),
|
||||
FieldMinimum(Reviewed<u32>),
|
||||
StateGuard(Reviewed<String>),
|
||||
Trigger {
|
||||
keys: KeyPair<Id>,
|
||||
values: Reviewed<bool>,
|
||||
},
|
||||
Step {
|
||||
keys: KeyPair<Id>,
|
||||
values: Reviewed<bool>,
|
||||
},
|
||||
StepOrder(Reviewed<Vec<Id>>),
|
||||
Annotation {
|
||||
keys: KeyPair<ValueKey>,
|
||||
values: Reviewed<Option<MessageValue>>,
|
||||
},
|
||||
Binding {
|
||||
keys: KeyPair<Id>,
|
||||
values: Reviewed<Option<LifelineBinding>>,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Baseline {
|
||||
pub facets: Vec<FacetReview>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Resolution {
|
||||
pub facet: Selector,
|
||||
pub decision: Decision,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Review {
|
||||
pub source: Id,
|
||||
pub target: Id,
|
||||
pub candidate: Option<OwnRecord>,
|
||||
pub resolutions: Vec<Resolution>,
|
||||
pub references: BTreeMap<Id, Id>,
|
||||
}
|
||||
|
||||
// ADAPTER: exhaustive typed capture, comparison, and application for this subset.
|
||||
fn observed<T>(v: Option<T>) -> Observed<T> {
|
||||
v.map_or(Observed::NotApplicable, Observed::Value)
|
||||
}
|
||||
fn pair<T>(a: &OwnRecord, b: &OwnRecord, f: impl Fn(&OwnRecord) -> Option<T>) -> Reviewed<T> {
|
||||
Reviewed {
|
||||
oa: observed(f(a)),
|
||||
sa: observed(f(b)),
|
||||
}
|
||||
}
|
||||
fn data(r: &OwnRecord) -> Option<(&DataTypeDefinition, &Option<DataValue>, &String)> {
|
||||
match shared(r).ok()? {
|
||||
SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
unit,
|
||||
} => Some((definition, default, unit)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn constructor(d: &DataTypeDefinition) -> DataConstructor {
|
||||
match d {
|
||||
DataTypeDefinition::Boolean => DataConstructor::Boolean,
|
||||
DataTypeDefinition::Integer { .. } => DataConstructor::Integer,
|
||||
DataTypeDefinition::Real { .. } => DataConstructor::Real,
|
||||
DataTypeDefinition::String { .. } => DataConstructor::String,
|
||||
DataTypeDefinition::Enumeration { .. } => DataConstructor::Enumeration,
|
||||
DataTypeDefinition::Record { .. } => DataConstructor::Record,
|
||||
}
|
||||
}
|
||||
fn annotation(r: &OwnRecord, key: &Option<ValueKey>) -> Option<Option<MessageValue>> {
|
||||
let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
} = shared(r).ok()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Some(key.as_ref().and_then(|key| {
|
||||
values
|
||||
.iter()
|
||||
.find(|v| v.path == key.path && v.field == key.field)
|
||||
.cloned()
|
||||
}))
|
||||
}
|
||||
fn binding(r: &OwnRecord, key: Option<Id>) -> Option<Option<LifelineBinding>> {
|
||||
let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::ScenarioUse { bindings, .. },
|
||||
..
|
||||
} = shared(r).ok()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Some(key.and_then(|key| bindings.iter().find(|v| v.referenced == key).cloned()))
|
||||
}
|
||||
impl DataReview {
|
||||
fn facet(&self) -> DataFacet {
|
||||
match self {
|
||||
Self::Constructor(_) => DataFacet::Constructor,
|
||||
Self::IntegerMinimum(_) => DataFacet::IntegerMinimum,
|
||||
Self::IntegerMaximum(_) => DataFacet::IntegerMaximum,
|
||||
Self::RealMinimum(_) => DataFacet::RealMinimum,
|
||||
Self::RealMaximum(_) => DataFacet::RealMaximum,
|
||||
Self::MinimumLength(_) => DataFacet::MinimumLength,
|
||||
Self::MaximumLength(_) => DataFacet::MaximumLength,
|
||||
Self::Literals(_) => DataFacet::Literals,
|
||||
Self::Supertype(_) => DataFacet::Supertype,
|
||||
Self::Default(_) => DataFacet::Default,
|
||||
Self::Unit(_) => DataFacet::Unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FacetReview {
|
||||
pub fn selector(&self) -> Selector {
|
||||
match self {
|
||||
Self::Data(v) => Selector::Data(v.facet()),
|
||||
Self::FieldMinimum(_) => Selector::FieldMinimum,
|
||||
Self::StateGuard(_) => Selector::StateGuard,
|
||||
Self::Trigger { keys, .. } => Selector::Trigger(keys.clone()),
|
||||
Self::Step { keys, .. } => Selector::Step(keys.clone()),
|
||||
Self::StepOrder(_) => Selector::StepOrder,
|
||||
Self::Annotation { keys, .. } => Selector::Annotation(keys.clone()),
|
||||
Self::Binding { keys, .. } => Selector::Binding(keys.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn capture(a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> FacetReview {
|
||||
match selector {
|
||||
Selector::Data(f) => FacetReview::Data(match f {
|
||||
DataFacet::Constructor => {
|
||||
DataReview::Constructor(pair(a, b, |r| Some(constructor(data(r)?.0))))
|
||||
}
|
||||
DataFacet::IntegerMinimum => DataReview::IntegerMinimum(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Integer { minimum, .. } = data(r)?.0 {
|
||||
Some(*minimum)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::IntegerMaximum => DataReview::IntegerMaximum(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Integer { maximum, .. } = data(r)?.0 {
|
||||
Some(*maximum)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::RealMinimum => DataReview::RealMinimum(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Real { minimum, .. } = data(r)?.0 {
|
||||
Some(*minimum)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::RealMaximum => DataReview::RealMaximum(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Real { maximum, .. } = data(r)?.0 {
|
||||
Some(*maximum)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::MinimumLength => DataReview::MinimumLength(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::String { minimum_length, .. } = data(r)?.0 {
|
||||
Some(*minimum_length)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::MaximumLength => DataReview::MaximumLength(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::String { maximum_length, .. } = data(r)?.0 {
|
||||
Some(*maximum_length)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::Literals => DataReview::Literals(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Enumeration { literals } = data(r)?.0 {
|
||||
Some(literals.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::Supertype => DataReview::Supertype(pair(a, b, |r| {
|
||||
if let DataTypeDefinition::Record { supertype } = data(r)?.0 {
|
||||
Some(*supertype)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
DataFacet::Default => DataReview::Default(pair(a, b, |r| Some(data(r)?.1.clone()))),
|
||||
DataFacet::Unit => DataReview::Unit(pair(a, b, |r| Some(data(r)?.2.clone()))),
|
||||
}),
|
||||
Selector::FieldMinimum => FacetReview::FieldMinimum(pair(a, b, |r| {
|
||||
if let SharedBody::DataField { minimum, .. } = shared(r).ok()? {
|
||||
Some(*minimum)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
Selector::StateGuard => FacetReview::StateGuard(pair(a, b, |r| {
|
||||
if let OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { guard, .. },
|
||||
} = r
|
||||
{
|
||||
Some(guard.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
Selector::Trigger(keys) | Selector::Step(keys) => {
|
||||
let triggers = matches!(selector, Selector::Trigger(_));
|
||||
let values = Reviewed {
|
||||
oa: observed(
|
||||
list(a, triggers)
|
||||
.ok()
|
||||
.map(|v| keys.oa.is_some_and(|id| v.contains(&id))),
|
||||
),
|
||||
sa: observed(
|
||||
list(b, triggers)
|
||||
.ok()
|
||||
.map(|v| keys.sa.is_some_and(|id| v.contains(&id))),
|
||||
),
|
||||
};
|
||||
if triggers {
|
||||
FacetReview::Trigger {
|
||||
keys: keys.clone(),
|
||||
values,
|
||||
}
|
||||
} else {
|
||||
FacetReview::Step {
|
||||
keys: keys.clone(),
|
||||
values,
|
||||
}
|
||||
}
|
||||
}
|
||||
Selector::StepOrder => FacetReview::StepOrder(pair(a, b, |r| list(r, false).ok().cloned())),
|
||||
Selector::Annotation(keys) => FacetReview::Annotation {
|
||||
keys: keys.clone(),
|
||||
values: Reviewed {
|
||||
oa: observed(annotation(a, &keys.oa)),
|
||||
sa: observed(annotation(b, &keys.sa)),
|
||||
},
|
||||
},
|
||||
Selector::Binding(keys) => FacetReview::Binding {
|
||||
keys: keys.clone(),
|
||||
values: Reviewed {
|
||||
oa: observed(binding(a, keys.oa)),
|
||||
sa: observed(binding(b, keys.sa)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
impl Baseline {
|
||||
pub fn capture(
|
||||
p: &Project,
|
||||
source: Id,
|
||||
target: Id,
|
||||
selectors: &[Selector],
|
||||
) -> Result<Self, String> {
|
||||
unique(selectors)?;
|
||||
let result = Self {
|
||||
facets: selectors
|
||||
.iter()
|
||||
.map(|f| capture(&own(p, source), &own(p, target), f))
|
||||
.collect(),
|
||||
};
|
||||
result.validate()?;
|
||||
Ok(result)
|
||||
}
|
||||
pub fn changed(&self, a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> Option<bool> {
|
||||
self.facets
|
||||
.iter()
|
||||
.find(|v| v.selector() == *selector)
|
||||
.map(|v| *v != capture(a, b, selector))
|
||||
}
|
||||
fn upsert(&mut self, review: FacetReview) {
|
||||
self.facets.retain(|v| v.selector() != review.selector());
|
||||
self.facets.push(review);
|
||||
}
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
unique(
|
||||
&self
|
||||
.facets
|
||||
.iter()
|
||||
.map(FacetReview::selector)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
for fact in &self.facets {
|
||||
if !has_applicable_value(fact) {
|
||||
return Err("Facet is inapplicable on both sides".into());
|
||||
}
|
||||
match fact {
|
||||
FacetReview::Annotation { keys, values } => {
|
||||
if keys.oa.is_none() && keys.sa.is_none() {
|
||||
return Err("Empty keyed facet".into());
|
||||
}
|
||||
for (key, value) in [(&keys.oa, &values.oa), (&keys.sa, &values.sa)] {
|
||||
if let Observed::Value(Some(value)) = value
|
||||
&& key.as_ref()
|
||||
!= Some(&ValueKey {
|
||||
path: value.path.clone(),
|
||||
field: value.field,
|
||||
})
|
||||
{
|
||||
return Err("Historical annotation payload disagrees with key".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
FacetReview::Binding { keys, values } => {
|
||||
if keys.oa.is_none() && keys.sa.is_none() {
|
||||
return Err("Empty keyed facet".into());
|
||||
}
|
||||
for (key, value) in [(&keys.oa, &values.oa), (&keys.sa, &values.sa)] {
|
||||
if let Observed::Value(Some(value)) = value
|
||||
&& *key != Some(value.referenced)
|
||||
{
|
||||
return Err("Historical binding payload disagrees with key".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
FacetReview::Trigger { keys, .. } | FacetReview::Step { keys, .. }
|
||||
if keys.oa.is_none() && keys.sa.is_none() =>
|
||||
{
|
||||
return Err("Empty keyed facet".into());
|
||||
}
|
||||
FacetReview::Data(DataReview::RealMinimum(v) | DataReview::RealMaximum(v)) => {
|
||||
for v in [&v.oa, &v.sa] {
|
||||
if let Observed::Value(Some(v)) = v
|
||||
&& !v.is_finite()
|
||||
{
|
||||
return Err("Historical real must be finite".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
FacetReview::Data(DataReview::Default(v)) => {
|
||||
for v in [&v.oa, &v.sa] {
|
||||
if let Observed::Value(Some(DataValue::Real(v))) = v
|
||||
&& !v.is_finite()
|
||||
{
|
||||
return Err("Historical real default must be finite".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn has_applicable_value(fact: &FacetReview) -> bool {
|
||||
fn any<T>(v: &Reviewed<T>) -> bool {
|
||||
matches!(v.oa, Observed::Value(_)) || matches!(v.sa, Observed::Value(_))
|
||||
}
|
||||
match fact {
|
||||
FacetReview::Data(v) => match v {
|
||||
DataReview::Constructor(v) => any(v),
|
||||
DataReview::IntegerMinimum(v) | DataReview::IntegerMaximum(v) => any(v),
|
||||
DataReview::RealMinimum(v) | DataReview::RealMaximum(v) => any(v),
|
||||
DataReview::MinimumLength(v) => any(v),
|
||||
DataReview::MaximumLength(v) => any(v),
|
||||
DataReview::Literals(v) => any(v),
|
||||
DataReview::Supertype(v) => any(v),
|
||||
DataReview::Default(v) => any(v),
|
||||
DataReview::Unit(v) => any(v),
|
||||
},
|
||||
FacetReview::FieldMinimum(v) => any(v),
|
||||
FacetReview::StateGuard(v) => any(v),
|
||||
FacetReview::Trigger { values, .. } | FacetReview::Step { values, .. } => any(values),
|
||||
FacetReview::StepOrder(v) => any(v),
|
||||
FacetReview::Annotation { values, .. } => any(values),
|
||||
FacetReview::Binding { values, .. } => any(values),
|
||||
}
|
||||
}
|
||||
fn unique(selectors: &[Selector]) -> Result<(), String> {
|
||||
fn overlap<K: PartialEq>(a: &KeyPair<K>, b: &KeyPair<K>) -> bool {
|
||||
a.oa.as_ref().is_some_and(|k| b.oa.as_ref() == Some(k))
|
||||
|| a.sa.as_ref().is_some_and(|k| b.sa.as_ref() == Some(k))
|
||||
}
|
||||
for (index, selector) in selectors.iter().enumerate() {
|
||||
if selectors[..index].iter().any(|old| {
|
||||
old == selector
|
||||
|| match (old, selector) {
|
||||
(Selector::Trigger(a), Selector::Trigger(b))
|
||||
| (Selector::Step(a), Selector::Step(b))
|
||||
| (Selector::Binding(a), Selector::Binding(b)) => overlap(a, b),
|
||||
(Selector::Annotation(a), Selector::Annotation(b)) => overlap(a, b),
|
||||
_ => false,
|
||||
}
|
||||
}) {
|
||||
return Err("Duplicate facet or conflicting keyed slot".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn applicable(a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> bool {
|
||||
[a, b].into_iter().all(|r| match selector {
|
||||
Selector::Data(_) => data(r).is_some(),
|
||||
Selector::FieldMinimum => matches!(shared(r), Ok(SharedBody::DataField { .. })),
|
||||
Selector::StateGuard | Selector::Trigger(_) => matches!(
|
||||
r,
|
||||
OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { .. }
|
||||
}
|
||||
),
|
||||
Selector::Step(_) | Selector::StepOrder => list(r, false).is_ok(),
|
||||
Selector::Annotation(_) => annotation(r, &None).is_some(),
|
||||
Selector::Binding(_) => binding(r, None).is_some(),
|
||||
})
|
||||
}
|
||||
fn check_keys(selector: &Selector, refs: &BTreeMap<Id, Id>) -> Result<(), String> {
|
||||
fn ids(keys: &KeyPair<Id>, refs: &BTreeMap<Id, Id>) -> Result<(), String> {
|
||||
match (keys.oa, keys.sa) {
|
||||
(None, None) => Err("Empty keyed facet".into()),
|
||||
(Some(a), Some(b)) if refs.get(&a) != Some(&b) => {
|
||||
Err("Key pair disagrees with frozen reference mapping".into())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
match selector {
|
||||
Selector::Trigger(k) | Selector::Step(k) | Selector::Binding(k) => ids(k, refs),
|
||||
Selector::Annotation(k) => match (&k.oa, &k.sa) {
|
||||
(None, None) => Err("Empty keyed facet".into()),
|
||||
(Some(a), Some(b)) => {
|
||||
if a.path.len() != b.path.len() {
|
||||
return Err("Annotation path mapping differs".into());
|
||||
}
|
||||
for (oa, sa) in a
|
||||
.path
|
||||
.iter()
|
||||
.chain([&a.field])
|
||||
.zip(b.path.iter().chain([&b.field]))
|
||||
{
|
||||
ids(
|
||||
&KeyPair {
|
||||
oa: Some(*oa),
|
||||
sa: Some(*sa),
|
||||
},
|
||||
refs,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
},
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
fn chosen<T: Clone>(v: &Reviewed<T>, source: bool) -> Result<T, String> {
|
||||
match if source { &v.oa } else { &v.sa } {
|
||||
Observed::Value(v) => Ok(v.clone()),
|
||||
Observed::NotApplicable => {
|
||||
Err("Facet is not applicable; explicit constructor review required".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
fn definition_mut(r: &mut OwnRecord) -> Result<&mut DataTypeDefinition, String> {
|
||||
match shared_mut(r)? {
|
||||
SharedBody::DataType { definition, .. } => Ok(definition),
|
||||
_ => Err("Wrong data constructor scope".into()),
|
||||
}
|
||||
}
|
||||
fn map_ref(id: Id, refs: &BTreeMap<Id, Id>) -> Result<Id, String> {
|
||||
refs.get(&id)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("Missing reference mapping for {id}"))
|
||||
}
|
||||
fn write(
|
||||
target: &mut OwnRecord,
|
||||
fact: &FacetReview,
|
||||
source: bool,
|
||||
refs: &BTreeMap<Id, Id>,
|
||||
) -> Result<(), String> {
|
||||
macro_rules! bound {
|
||||
($v:expr, $variant:ident, $field:ident) => {{
|
||||
let value = chosen($v, source)?;
|
||||
let DataTypeDefinition::$variant { $field, .. } = definition_mut(target)? else {
|
||||
return Err("Explicit constructor review required".into());
|
||||
};
|
||||
*$field = value;
|
||||
}};
|
||||
}
|
||||
match fact {
|
||||
FacetReview::Data(v) => match v {
|
||||
DataReview::IntegerMinimum(v) => bound!(v, Integer, minimum),
|
||||
DataReview::IntegerMaximum(v) => bound!(v, Integer, maximum),
|
||||
DataReview::RealMinimum(v) => bound!(v, Real, minimum),
|
||||
DataReview::RealMaximum(v) => bound!(v, Real, maximum),
|
||||
DataReview::MinimumLength(v) => bound!(v, String, minimum_length),
|
||||
DataReview::MaximumLength(v) => bound!(v, String, maximum_length),
|
||||
DataReview::Literals(v) => bound!(v, Enumeration, literals),
|
||||
DataReview::Supertype(v) => {
|
||||
let mut value = chosen(v, source)?;
|
||||
if source {
|
||||
value = value.map(|id| map_ref(id, refs)).transpose()?;
|
||||
}
|
||||
let DataTypeDefinition::Record { supertype } = definition_mut(target)? else {
|
||||
return Err("Explicit constructor review required".into());
|
||||
};
|
||||
*supertype = value;
|
||||
}
|
||||
DataReview::Default(v) => {
|
||||
let SharedBody::DataType { default, .. } = shared_mut(target)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*default = chosen(v, source)?;
|
||||
}
|
||||
DataReview::Unit(v) => {
|
||||
let SharedBody::DataType { unit, .. } = shared_mut(target)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*unit = chosen(v, source)?;
|
||||
}
|
||||
DataReview::Constructor(_) => return Err("Constructor is a grouped operation".into()),
|
||||
},
|
||||
FacetReview::FieldMinimum(v) => {
|
||||
let SharedBody::DataField { minimum, .. } = shared_mut(target)? else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*minimum = chosen(v, source)?;
|
||||
}
|
||||
FacetReview::StateGuard(v) => {
|
||||
let OwnRecord::Relation {
|
||||
body: RelationBody::StateTransition { guard, .. },
|
||||
} = target
|
||||
else {
|
||||
return Err("Wrong facet".into());
|
||||
};
|
||||
*guard = chosen(v, source)?;
|
||||
}
|
||||
FacetReview::Trigger { keys, values } | FacetReview::Step { keys, values } => {
|
||||
let present = chosen(values, source)?;
|
||||
let Some(id) = keys.sa else {
|
||||
return if present {
|
||||
Err("Missing member mapping".into())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
};
|
||||
let items = list_mut(target, matches!(fact, FacetReview::Trigger { .. }))?;
|
||||
if present && !items.contains(&id) {
|
||||
items.push(id);
|
||||
} else if !present {
|
||||
items.retain(|v| *v != id);
|
||||
}
|
||||
}
|
||||
FacetReview::StepOrder(values) => {
|
||||
if source {
|
||||
return Err("Explicit final order required by this probe".into());
|
||||
}
|
||||
let value = chosen(values, false)?;
|
||||
let mut expected = list(target, false)?.clone();
|
||||
let mut proposed = value.clone();
|
||||
expected.sort();
|
||||
proposed.sort();
|
||||
if expected != proposed || proposed.windows(2).any(|p| p[0] == p[1]) {
|
||||
return Err("Final order must contain every surviving member exactly once".into());
|
||||
}
|
||||
*list_mut(target, false)? = value;
|
||||
}
|
||||
FacetReview::Annotation { keys, values } => {
|
||||
let mut value = chosen(values, source)?;
|
||||
let Some(key) = &keys.sa else {
|
||||
return if value.is_some() {
|
||||
Err("Missing annotation mapping".into())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
};
|
||||
if let Some(value) = &mut value {
|
||||
value.path = key.path.clone();
|
||||
value.field = key.field;
|
||||
}
|
||||
let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
} = shared_mut(target)?
|
||||
else {
|
||||
return Err("Wrong annotation scope".into());
|
||||
};
|
||||
if let Some(index) = values
|
||||
.iter()
|
||||
.position(|v| v.path == key.path && v.field == key.field)
|
||||
{
|
||||
if let Some(value) = value {
|
||||
values[index] = value;
|
||||
} else {
|
||||
values.remove(index);
|
||||
}
|
||||
} else if let Some(value) = value {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
FacetReview::Binding { keys, values } => {
|
||||
let mut value = chosen(values, source)?;
|
||||
let Some(key) = keys.sa else {
|
||||
return if value.is_some() {
|
||||
Err("Missing binding mapping".into())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
};
|
||||
if let Some(value) = &mut value {
|
||||
value.referenced = key;
|
||||
if source {
|
||||
value.local = map_ref(value.local, refs)?;
|
||||
}
|
||||
}
|
||||
let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::ScenarioUse { bindings, .. },
|
||||
..
|
||||
} = shared_mut(target)?
|
||||
else {
|
||||
return Err("Wrong binding scope".into());
|
||||
};
|
||||
if let Some(index) = bindings.iter().position(|v| v.referenced == key) {
|
||||
if let Some(value) = value {
|
||||
bindings[index] = value;
|
||||
} else {
|
||||
bindings.remove(index);
|
||||
}
|
||||
} else if let Some(value) = value {
|
||||
bindings.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn in_constructor(selector: &Selector) -> bool {
|
||||
matches!(selector, Selector::Data(f) if *f != DataFacet::Unit)
|
||||
}
|
||||
fn constructor_group(a: &OwnRecord, b: &OwnRecord) -> Vec<FacetReview> {
|
||||
use DataFacet::*;
|
||||
let mut facets = vec![Constructor, Default];
|
||||
for r in [a, b] {
|
||||
if let Some((definition, ..)) = data(r) {
|
||||
for f in match definition {
|
||||
DataTypeDefinition::Integer { .. } => vec![IntegerMinimum, IntegerMaximum],
|
||||
DataTypeDefinition::Real { .. } => vec![RealMinimum, RealMaximum],
|
||||
DataTypeDefinition::String { .. } => vec![MinimumLength, MaximumLength],
|
||||
DataTypeDefinition::Enumeration { .. } => vec![Literals],
|
||||
DataTypeDefinition::Record { .. } => vec![Supertype],
|
||||
DataTypeDefinition::Boolean => vec![],
|
||||
} {
|
||||
if !facets.contains(&f) {
|
||||
facets.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
facets
|
||||
.into_iter()
|
||||
.map(|f| capture(a, b, &Selector::Data(f)))
|
||||
.collect()
|
||||
}
|
||||
pub fn apply(p: &Project, base: &Baseline, review: &Review) -> Result<(Project, Baseline), String> {
|
||||
base.validate()?;
|
||||
unique(
|
||||
&review
|
||||
.resolutions
|
||||
.iter()
|
||||
.map(|r| r.facet.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
let switch = review
|
||||
.resolutions
|
||||
.iter()
|
||||
.any(|r| r.facet == Selector::Data(DataFacet::Constructor));
|
||||
if switch
|
||||
&& review
|
||||
.resolutions
|
||||
.iter()
|
||||
.any(|r| in_constructor(&r.facet) && r.facet != Selector::Data(DataFacet::Constructor))
|
||||
{
|
||||
return Err("Overlapping constructor and descendant decisions".into());
|
||||
}
|
||||
let source = own(p, review.source);
|
||||
let current = own(p, review.target);
|
||||
let mut target = current.clone();
|
||||
let constructor_selector = Selector::Data(DataFacet::Constructor);
|
||||
for r in &review.resolutions {
|
||||
if !applicable(&source, ¤t, &r.facet) {
|
||||
return Err("Facet does not belong to this record family".into());
|
||||
}
|
||||
check_keys(&r.facet, &review.references)?;
|
||||
if !has_applicable_value(&capture(&source, ¤t, &r.facet)) {
|
||||
return Err("Facet is inapplicable on both sides".into());
|
||||
}
|
||||
for previous in &base.facets {
|
||||
if previous.selector() != r.facet
|
||||
&& unique(&[previous.selector(), r.facet.clone()]).is_err()
|
||||
{
|
||||
return Err(
|
||||
"Mapping changed; explicitly invalidate the affected previous facet review"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if r.facet == Selector::Data(DataFacet::Literals) && r.decision == Decision::TakeSource {
|
||||
return Err("String-keyed literal deltas are outside this probe; no whole-list TakeSource overwrite".into());
|
||||
}
|
||||
}
|
||||
if switch && base.changed(&source, ¤t, &constructor_selector) == Some(false) {
|
||||
let decision = review
|
||||
.resolutions
|
||||
.iter()
|
||||
.find(|r| r.facet == constructor_selector)
|
||||
.unwrap()
|
||||
.decision;
|
||||
let proposed = match decision {
|
||||
Decision::TakeSource => Some(&source),
|
||||
Decision::Candidate => review.candidate.as_ref(),
|
||||
Decision::KeepTarget => None,
|
||||
};
|
||||
let actual_target_switch = proposed
|
||||
.and_then(data)
|
||||
.zip(data(¤t))
|
||||
.is_some_and(|(new, old)| constructor(new.0) != constructor(old.0));
|
||||
if !actual_target_switch {
|
||||
return Err("No constructor change; review descendant facets individually".into());
|
||||
}
|
||||
}
|
||||
if !switch
|
||||
&& base.changed(&source, ¤t, &constructor_selector) == Some(true)
|
||||
&& review.resolutions.iter().any(|r| in_constructor(&r.facet))
|
||||
{
|
||||
return Err("Explicit constructor review required".into());
|
||||
}
|
||||
let mut resolutions = review.resolutions.iter().collect::<Vec<_>>();
|
||||
resolutions.sort_by_key(|r| {
|
||||
if r.facet == constructor_selector {
|
||||
0
|
||||
} else if r.facet == Selector::StepOrder {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
});
|
||||
for r in &resolutions {
|
||||
if r.decision == Decision::KeepTarget {
|
||||
continue;
|
||||
}
|
||||
let from_source = r.decision == Decision::TakeSource;
|
||||
let supplied = if from_source {
|
||||
&source
|
||||
} else {
|
||||
review.candidate.as_ref().ok_or("Missing candidate")?
|
||||
};
|
||||
if r.facet == constructor_selector {
|
||||
let (definition, default, _) = data(supplied).ok_or("Wrong constructor group")?;
|
||||
let mut definition = definition.clone();
|
||||
if from_source
|
||||
&& let DataTypeDefinition::Enumeration { literals } = &definition
|
||||
&& let Some((DataTypeDefinition::Enumeration { literals: old }, ..)) = data(&target)
|
||||
&& old.iter().any(|v| !literals.contains(v))
|
||||
{
|
||||
return Err(
|
||||
"Constructor candidate must explicitly review target-only enum literals".into(),
|
||||
);
|
||||
}
|
||||
if from_source
|
||||
&& let DataTypeDefinition::Record {
|
||||
supertype: Some(id),
|
||||
} = &mut definition
|
||||
{
|
||||
*id = map_ref(*id, &review.references)?;
|
||||
}
|
||||
let SharedBody::DataType {
|
||||
definition: to_definition,
|
||||
default: to_default,
|
||||
..
|
||||
} = shared_mut(&mut target)?
|
||||
else {
|
||||
return Err("Wrong constructor group".into());
|
||||
};
|
||||
*to_definition = definition;
|
||||
*to_default = default.clone();
|
||||
} else {
|
||||
let fact = capture(&source, supplied, &r.facet);
|
||||
write(&mut target, &fact, from_source, &review.references)?;
|
||||
}
|
||||
}
|
||||
if review.candidate.as_ref().is_some_and(|v| *v != target) {
|
||||
return Err("Candidate changes an unselected facet".into());
|
||||
}
|
||||
let mut expected = p.clone();
|
||||
let command = match target {
|
||||
OwnRecord::Element { parent, body } => {
|
||||
let e = expected.elements.get_mut(&review.target).unwrap();
|
||||
e.parent = parent;
|
||||
e.body = body;
|
||||
Command::EditElement(review.target, e.clone())
|
||||
}
|
||||
OwnRecord::Relation { body } => {
|
||||
let r = expected.relations.get_mut(&review.target).unwrap();
|
||||
r.body = body;
|
||||
Command::EditRelation(review.target, r.clone())
|
||||
}
|
||||
};
|
||||
let prepared = prepare_command(p, &Command::Batch(vec![command])).map_err(|e| e.to_string())?;
|
||||
if prepared.project != expected {
|
||||
return Err("Prepared candidate induces unreviewed changes outside selected facets".into());
|
||||
}
|
||||
let target = own(&prepared.project, review.target);
|
||||
let mut acknowledged = base.clone();
|
||||
for r in resolutions {
|
||||
if r.facet == constructor_selector {
|
||||
acknowledged
|
||||
.facets
|
||||
.retain(|f| !in_constructor(&f.selector()));
|
||||
for f in constructor_group(&source, &target) {
|
||||
acknowledged.upsert(f);
|
||||
}
|
||||
} else {
|
||||
acknowledged.upsert(capture(&source, &target, &r.facet));
|
||||
}
|
||||
}
|
||||
acknowledged.validate()?;
|
||||
Ok((prepared.project, acknowledged))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "typed_reviews_tests.rs"]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
+32
-133
@@ -1,139 +1,38 @@
|
||||
# First release — current engineering state
|
||||
# First release continuation pointer — 2026-09-06
|
||||
|
||||
## Objective and phase
|
||||
The goal is active and incomplete. This C: checkout remains a continuation pointer
|
||||
with older production code. Do substantial work in the active integration checkout,
|
||||
not here; read its STATE, PLAN and TASK_LEDGER before continuing:
|
||||
|
||||
The first-release goal remains active and incomplete. Deliver usable Rust/Iced
|
||||
Operational Analysis and System Analysis/System Architecture, equivalent graphical,
|
||||
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.
|
||||
PLAN.md retains the entire release scope; no subsystem acceptance completes it.
|
||||
D:/CodexBuilds/Capella2-data-integration (codex/data-contract-integration).
|
||||
Current production cf04ff1, root native/evidence checkpoint follows it.
|
||||
Linux peer D:/CodexBuilds/Capella2-data-linux-review, b5db730.
|
||||
Authoritative objective: attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md.
|
||||
|
||||
Direct state-machine editing is accepted through production316dc55. The main
|
||||
checkout was fast-forwarded to acceptance a8f451e. R6 data/interface contracts are now
|
||||
being implemented in a separate checkout; they are not accepted or released.
|
||||
Meaningful implementation and runtime progress continues; no external blocker exists.
|
||||
R6 contracts, direct behavior, source5/core/REST/scalar and closed constructor GUI
|
||||
are integrated. Full Windows767/Linux770 workspace tests, strict checks and release
|
||||
builds passed before newest desktop constructor/help slice; its platform gates run.
|
||||
REST live184schemas/19typed values/9 malformed rejections passed earlier. No remote
|
||||
service was deployed. Native Windows hosted publish, exact includes/comments,
|
||||
graph/text Save, invalid-draft rejection, API artifact review/Reload/Undo/Redo,
|
||||
revision conflicts, offline recovery, local copy and catalog Open were exercised.
|
||||
That trial closed0/empty stderr. Source/history/ports/routing/explorer/state and
|
||||
basic advanced behavior have scoped native Windows/Linux evidence in active ledgers.
|
||||
|
||||
## Accepted foundations
|
||||
Current high-impact issue: installed invalid local repair source retained a previous
|
||||
hosted handle, allowing repaired source to save to the old resource. Actual-App/HTTP
|
||||
red reproduction exists; HOST owns minimal fix and fresh reviewer. A separate stale
|
||||
empty catalog UX fix follows. Native verification is required after both fixes.
|
||||
GUI worker owns independent reset/member/literal decisions in SEM; other keyed/order/
|
||||
artifact/multi-source decisions remain. Whole release coverage, advanced native cases,
|
||||
Home/recovery/help and final cross-disciplinary acceptance/packaging are unfinished.
|
||||
|
||||
- Core typed OA/SA semantics, validated atomic commands and ordinary history are
|
||||
shared by three Rust crates. Typed ports, explicit unresolved migration and
|
||||
original Capella observations: release-port-{contract,core,migration}.md and
|
||||
release-capella-workflow-benchmark.md.
|
||||
- Explorer/port discovery d364d3c: Add port on participants, grouped roots, focused
|
||||
children, breadcrumbs, contextual paging/search and full-depth outline. Actual
|
||||
Windows creation,251-root/12-level trials,1100x700 and Linux reopen/font fixes:
|
||||
release-explorer-usability.md.
|
||||
- Routing23535b4, viewport366ce44, labels65fa9ab: authored bends/labels, group
|
||||
movement, fold restoration, Undo/Redo, save/reopen and SVG, including minimum-size
|
||||
native trials. Dense full solve measured119ms; arbitrary crossings remain possible.
|
||||
See release-routing-verification.md and release-routing-linux.md.
|
||||
- Source4e8575b: nested lexical ownership/includes, provenance-aware token writes,
|
||||
exact accepted source/model history, retained drafts, Save All/recovery and coupled
|
||||
hosted source/model revisions. Windows/Linux native and actual REST restart,
|
||||
stale ETag, comment-only edit and cross-file reparent passed. Full422 Windows /
|
||||
425 Linux tests at that checkpoint. See release-source-integration.md.
|
||||
- Direct drawing fef2e0d plus tooltip ad7feb5: OA Interaction, SA Functional and
|
||||
Boundary Exchange by palette click-click or drag; compatible highlights, automatic
|
||||
function ports, four-side draggable anchors, contextual menus and source folders.
|
||||
Independent pointer/tooltip findings fixed and native retested. Windows193 desktop
|
||||
and Linux189+4 focused tests followed earlier full485/488 suites. Scoped native
|
||||
Windows/Linux acceptance: release-port-drawing.md and release-context-menus.md.
|
||||
Root is sole native operator; authorization persists. Isolated loopback test server
|
||||
20984 at127.0.0.1:8787 uses Temp/capella2-hosted-native-20260906/store, resource
|
||||
d28b2fc9-fbbd-456b-8ba8-a1195d1c8f33 revision6. No desktop trial is currently active.
|
||||
Original Capella remains available; re-inventory native windows before input.
|
||||
|
||||
## Newly accepted state workspace
|
||||
|
||||
Production316dc55 includes typed machine views, shared persisted geometry, atomic
|
||||
region/symbol/state creation, group movement, direct transitions and reconnection,
|
||||
Internal rows, Local edges, typed detail forms, folding, placement and navigation.
|
||||
Architecture and Behavior view selections are separate. Drafts survive rejected
|
||||
source/model commits; passive camera events never apply a form.
|
||||
|
||||
Independent/native findings fixed: covered GPU shapes, pseudo-label/edge collision,
|
||||
selection-relayout drag displacement, missing typed form access, folded-content
|
||||
access, wrong-view placement Apply, and camera/tool leakage on machine navigation.
|
||||
An actual owner/view change resets gesture state and fits; returning to the same
|
||||
view preserves camera/tool. Exact evidence and limitations: release-state-editing.md.
|
||||
|
||||
Verification: full551 Windows /554 Linux workspace tests and strict workspace
|
||||
Clippy at e0b2210. Final316dc55:241 desktop tests, strict all-target desktop Clippy,
|
||||
and desktop builds on each platform; five new navigation tests included. Two guide
|
||||
tests passed after help changes. Source/reST/API state contracts and actual HTTP
|
||||
rejection/revision/source/restart tests passed earlier in the integration.
|
||||
|
||||
Root and independent Windows native trials covered1440x900 and1100x700 creation,
|
||||
nested movement, first-drag transition, endpoint/menu reconnect, invalid target,
|
||||
Escape, timed Internal, Local/EntryPoint, placement Apply, Undo/Redo and save/reopen.
|
||||
Linux native covered1440x900 creation, first transition/move, Undo/Redo, fold and
|
||||
saved collapsed reopen. Final316dc55 native on both platforms proved same-view
|
||||
camera/tool preservation and different-machine Fit/Select reset. Logs are empty.
|
||||
|
||||
## Active R6 implementation and next actions
|
||||
|
||||
Checkout D:/CodexBuilds/Capella2-data-integration; branch
|
||||
codex/data-contract-integration, base5ce3eef+plan2bf4c54+navigation9118218.
|
||||
Dedicated target D:/CodexBuilds/Capella2-data-target. Contract freeze is schema/source4
|
||||
with full data payload and dictionary slices together; existing typed ports remain.
|
||||
See release-data-implementation.md and release-data-contract.md in that checkout.
|
||||
|
||||
Core is committed0b58107; source/migration1230ab8. Root API/docs are integrated
|
||||
on the same working tree; desktop remains in flight and R6 is not accepted.
|
||||
Core15 R6 +28workflow tests passed (final allocation-arrow suppression arrived
|
||||
afterward and is queued for rerun). Earlier13ports/14scenarios and115 source/lib
|
||||
tests passed; strict core Clippy preceded the last geometry/performance fixes.
|
||||
|
||||
Root41 server tests pass, including3 new 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
|
||||
rejected legitimate old source/model pairs; both root fixes passed retest.
|
||||
Strict reST HTML generation and81 OpenAPI schema definitions passed. Actual frozen
|
||||
server32033FFE imported the published example; its complete Project/StoredProject
|
||||
responses passed served/current JSON Schema validation and obsolete shapes rejected.
|
||||
Evidence: api-r6-contract and r6-server-tests-after.log under the release directory.
|
||||
|
||||
Independent core review found association transition closure pulling unrelated
|
||||
types and missing empty-enum warning; both corrected and focused tests passed.
|
||||
Repeated per-ancestor full scans were simplified to one scan per helper call.
|
||||
Explicit debug inheritance timings250/500/1000:147/664/2739ms. Optimized timing is
|
||||
still required; no optimized performance claim yet.
|
||||
|
||||
Current ownership: drawing_correctness owns desktop except data_view.rs;
|
||||
state_architecture_review now owns only data_view.rs bounded focused-dictionary
|
||||
correction. Its independent review found unbounded inherited rows/picklists,
|
||||
unreachable recursive paths and loss of scenario values on same-phase style or
|
||||
same-exchange reselection. Drawing worker fixes recursive drill-down/draft retention.
|
||||
data_core now verifies Linux in a separate data-linux-review checkout/target.
|
||||
Root owns API/OpenAPI/reST, integration and native input. No concurrent file writers.
|
||||
|
||||
1. State evidence is committed and main fast-forwarded to a8f451e; DATA received
|
||||
the same evidence in c5276b1. Finish the3 concrete desktop review fixes and
|
||||
optimized inheritance measurement, then native data-authoring acceptance.
|
||||
2. Finish R6 core/source/desktop, integrate actual REST/schema/reST, independent
|
||||
semantic/correctness/UX review, Windows/Linux checks and real data authoring.
|
||||
3. Implement R3 typed semantic drift/reconciliation after R6 freezes. Current
|
||||
baselines/reconciliation only cover prose. Prefer historical typed-body snapshots
|
||||
and finite proposals; avoid a duplicate recursive semantic schema without evidence.
|
||||
4. Complete direct scenario/process gestures, persistent coverage/filters,
|
||||
Home/recent/recovery, hosted desktop ETag workflows and offline guidance.
|
||||
5. Fresh final requirements/domain/correctness/native UX/architecture review; full
|
||||
Windows/Linux/MSRV checks, real runtime, reproducible docs and release packaging.
|
||||
|
||||
## Environment and native ownership
|
||||
|
||||
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
|
||||
trial721472 also closed normally, exec72680 exit0. Independent Windows3672536 is
|
||||
saved at1100x700. Older root drawing windows are saved. Re-inventory before input.
|
||||
Protected user7210312/6228978 were absent; do not reset/save/close if they return.
|
||||
|
||||
Evidence: D:/CodexBuilds/Capella2-release1-01a06e2e. State source checkout/target:
|
||||
D:/CodexBuilds/Capella2-state-integration and -target. Linux source/target:
|
||||
D:/CodexBuilds/Capella2-state-linux-review and -target. Use distinct Cargo targets
|
||||
per checkout; shared-cache results were previously caught and excluded.
|
||||
|
||||
Windows: cargo test --locked --workspace; cargo clippy --locked --workspace
|
||||
--all-targets -- -D warnings; cargo build --locked -p oa-desktop -p oa-server.
|
||||
WSL Ubuntu24.04 uses target/linux-cargo.toml and Mesa software Vulkan for native
|
||||
WSLg. No remote push or usage reset authorized. No broad recursive target deletion.
|
||||
Linux is WSL Ubuntu-24.04, not the separate unconfigured Ubuntu distro. Optional
|
||||
X11 needs libxkbcommon-x11-0 and the user's sudo password; the user already has the
|
||||
exact apt command. Wayland/Windows/REST work is not blocked. Do not bypass auth.
|
||||
No remote push, usage reset or broad target cleanup authorized.
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# R6 data and interface contract proposal
|
||||
|
||||
Implementation freeze (2026-09-06): `release-data-implementation.md` supersedes
|
||||
the earlier version-number and generic-port proposals below. Current contract is
|
||||
schema/source4 with explicit1/2/3 migration; existing typed PortContract remains
|
||||
authoritative. Value Record default `[]` is allowed at minimum0; Reference fields
|
||||
have no explicit default. See docs/language.rst4.5 for the implemented specification.
|
||||
|
||||
Status: read-only design, 2026-09-05. No implementation, test pass, or native Studio acceptance is claimed here. This is the recommended integration contract for the lead to freeze before the R6 worker changes exhaustive enums. It extends the current typed model and transaction path; it does not introduce an instance database, expression evaluator, units engine, or plugin metamodel.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# R6 implementation freeze — schema/source 4
|
||||
|
||||
Status: active implementation in isolated D:/CodexBuilds/Capella2-data-integration.
|
||||
State editing is accepted in main a8f451e; DATA includes its navigation fix and evidence.
|
||||
The full first-release goal remains incomplete.
|
||||
|
||||
## Outcome and contract
|
||||
|
||||
Implement the complete data/interface contract in release-data-contract.md before
|
||||
accepting schema/source4 artifacts: nature/mechanism, bounded scalar types/defaults,
|
||||
field direction/storage, qualified scenario value paths, single specialization,
|
||||
identified interface-item allocations/protocols and binary reference associations.
|
||||
Use its exact types and validation rules. Existing PortContract and legacy
|
||||
resolution/split behavior are authoritative; the old R6 InOut paragraph is superseded
|
||||
by release-port-contract.md. R6 never creates a new InOut functional contract.
|
||||
|
||||
Retain three crates, Project and validated atomic Command/Document boundaries.
|
||||
Use data.rs shared helpers for effective fields, item ancestry, inherited interface
|
||||
allocations, carried field paths and value validation. Reference visitors own live
|
||||
references; no second model or mutation API. Inherited rows expose original identity.
|
||||
Scenario paths disambiguate reused records and stop at Reference storage.
|
||||
|
||||
## Sequence and ownership
|
||||
|
||||
1. Core owner freezes public shapes/helpers, implements typed data validation,
|
||||
reference/removal/transition maintenance and focused meaningful regressions.
|
||||
2. Source/migration owner preserves V1/V2/current3 inputs through an explicit4
|
||||
reader; updates parser/source-project migration without flattening nesting,
|
||||
includes or comments. Frozen strict DTOs only for changed old payloads plus a
|
||||
closed old constructor catalog avoid cloning the whole metamodel.
|
||||
3. Desktop owner integrates typed forms, path pickers, readable nested dictionary
|
||||
and interface-allocation rows. Root integrates OpenAPI/spec and actual REST.
|
||||
4. Full tests/strict checks, source/JSON/HTTP roundtrips, invalid/stale transactions,
|
||||
Windows/Linux native authoring, compact UI and fresh independent review.
|
||||
|
||||
Separate files per owner; communicate model/helper signatures before dependent edits.
|
||||
Use a dedicated target for this worktree. No simultaneous Cargo mutations.
|
||||
Do not publish or accept intermediate4 files until both payload and dictionary
|
||||
slices are complete. R3 semantic reconciliation follows this shape/reference freeze.
|
||||
|
||||
## Required evidence
|
||||
|
||||
- Physical flow and Operation parameters use bounded reusable types and defaults.
|
||||
- Two parameters using one Record retain distinct value paths through calls/replies,
|
||||
reidentify/delete, graphical/source edits and initial transitions.
|
||||
- Two interfaces allocate one item with independent protocols/artifact links; removing
|
||||
one allocation retains the item and the other allocation, and Undo restores it.
|
||||
- Specialization/inherited fields and associations preserve ownership and identity;
|
||||
invalid cycles/defaults/paths/ranges reject atomically, incomplete drafts warn.
|
||||
- V1/V2/current3 migration preserves identities, typed ports, comments, includes and
|
||||
nested ownership; deterministic allocation IDs and unresolved legacy annotations
|
||||
have explicit diagnostics. GUI/source/REST save/reopen agree.
|
||||
|
||||
Alternatives: silent changes under schema3 rejected because existing saved3 projects
|
||||
are real compatibility inputs. Whole-model frozen DTO/validator duplication rejected
|
||||
in favor of strict changed-payload compatibility adapters. Generic JSON diff or
|
||||
opaque body replacement is not the R3 solution. Instance literals, unit conversion,
|
||||
expression execution and multiple inheritance remain explicitly outside this
|
||||
bounded contract; the release requirements for categories/filters are separate.
|
||||
Reference in New Issue
Block a user