Arcadia Studio OA / SA Language and Editing Contract
- Version:
- 3
- Status:
- Normative current implementation contract; first-release integration
- Core implementation:
crates/oa-core
- Text suffix:
.arcadia
1. Scope and conformance
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 and 2. MUST, MUST NOT, and MAY indicate requirements, prohibitions, and permitted choices.
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 behavior. SA permits at most one System of Interest; its absence is an incomplete-model warning when other SA elements exist. The SA System MUST NOT contain internal components. Functional decomposition is independent of that structural boundary. Logical Architecture, Physical Architecture, deployment, Capella file interchange, and executable behavioral simulation are outside this specification. This independent language does not claim complete Arcadia, Eclipse Capella, or SysML conformance.
An implementation conforms only when each supported field and relationship is reachable in both graphical and textual editing, preserves identity through save/open and undo, and uses the same structural validation. Merely displaying a name or storing an opaque payload does not establish graphical support. Actual verification evidence is separate from this contract.
3. Element declarations and typed fields
Each element declaration starts with its layer and kind, for example oa_activity or sa_function. Every element body accepts description (default "") and parent (default none for a document-level declaration, or its lexical element owner when nested; section 6.2). The table lists all additional fields. ref means @alias; ref? also accepts none. Lists preserve order. Unknown or duplicate fields are errors.
Declarations |
Additional fields |
Defaults |
|---|---|---|
oa_entity, oa_actor, oa_role, oa_activity, oa_capability, oa_mission |
None |
Empty body |
sa_system, sa_actor, sa_function, sa_capability, sa_mission |
None |
Empty body |
oa_process, sa_functional_chain |
steps: occurrence refs |
[] |
oa_exchange_item, sa_exchange_item |
kind: Information, Material, Energy, Event |
Information |
oa_data_type, sa_data_type |
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 |
String; 1; 1; "" |
oa_interface, sa_interface |
items: exchange-item refs |
[] |
oa_port, sa_port |
contract: required PortContract; legacy: optional LegacyPortContract |
No implicit contract; legacy defaults none. OA admits Legacy only. |
oa_scenario, sa_scenario |
viewpoint: ScenarioViewpoint; context: ref?; precondition, postcondition: strings; steps: occurrence refs |
MixedLegacy for omitted schema-2 source; none; ""; ""; [] |
oa_lifeline, sa_lifeline |
represented: participant, work, or role ref? |
none |
oa_occurrence, sa_occurrence |
body: occurrence constructor; precondition, postcondition: strings |
Exchange with none references, Asynchronous message kind and no values; two empty strings |
oa_state_machine, sa_state_machine, oa_region, sa_region |
None |
Empty body |
oa_state, sa_state, oa_mode, sa_mode, oa_initial, sa_initial, oa_final, sa_final |
kind: StateKind; entry, exit, do_activity: strings |
Kind selected by declaration; three empty strings |
oa_choice, oa_junction, oa_fork, oa_join, oa_shallow_history, oa_deep_history, oa_entry_point, oa_exit_point, oa_terminate and corresponding sa_ declarations |
Same State body; StateKind is Choice, Junction, Fork, Join, ShallowHistory, DeepHistory, EntryPoint, ExitPoint or Terminate |
Kind selected by declaration; three empty strings |
oa_event, sa_event |
body: Signal, Time or Change constructor |
Signal with items [] |
oa_constraint, sa_constraint |
expression: string; constrained: semantic record refs |
""; [] |
The explicit state kind field MUST agree with its declaration keyword. u32 is an integer from 0 through 4294967295. A DataField's maximum MUST be at least its minimum; none means unbounded. Primitive TypeRefs are Boolean, Integer, Real, and String. A named TypeRef uses a DataType reference. An Enumeration contains unique string literals. For example:
definition { Enumeration { literals ["Clear", "Fog"]; }; };
type_ref { Named @weather_classification; };
These are typed constructors with named fields, not embedded JSON documents. Constructor field names and enum spellings are case-sensitive. When replacing a constructor, all of its fields MUST be supplied; the containing declaration defaults do not silently fill a different constructor's missing fields. During schema-2 compatibility, the new Exchange fields message_kind, reply_to and values are exceptions: omitted values hydrate as Asynchronous, none and [] respectively. Canonical output writes them explicitly. This preserves older schema-2 source/JSON without inventing call correlation or value annotations. It is not a rule for an unspecified future schema.
Constructor |
Required fields |
|---|---|
Exchange |
exchange: exchange relation ref?; sender, receiver: lifeline refs?; message_kind: MessageKind; reply_to: occurrence ref?; values: MessageValue records |
Action |
action: activity/function ref?; lifeline: ref? |
Control |
kind: Choice, Merge, Fork, Join, Loop |
SequenceReference |
sequence: process/functional-chain ref? |
ScenarioUse |
scenario: scenario ref?; bindings: LifelineBinding records |
State |
state: state/mode ref?; lifeline: ref? |
Fragment |
operator: Alternative, Parallel, Loop, Optional; branches: branch occurrence refs |
Branch |
guard: string; occurrences: occurrence refs |
For example, body { Action { action @assess; lifeline @operator_line; }; }; selects an Action occurrence. Processes and functional chains contain Exchange, Action, Control and SequenceReference occurrences. Process/chain Actions are contextual work contributions and MUST have lifeline none. A Control or SequenceReference MUST belong directly to a Process or FunctionalChain, not a Scenario. Scenarios contain Exchange, Action, State, Fragment, Branch and ScenarioUse occurrences according to their enclosing ownership rules. Each listed occurrence belongs to its enclosing sequence or branch through parent. A Branch belongs to a Fragment; the Fragment's branches list references the branches it owns.
An exchange and an occurrence of that exchange have different identities. Repeating an exchange requires distinct occurrence records. Repeating one occurrence identity within an ordered list is invalid. Conditions, guards, entry/exit/do text, and constraint expressions are descriptive strings; this version does not parse or execute them as a programming language.
4. Containment, allocation, and relationships
Containment MUST be acyclic and stay within one layer. Entities may contain entities, actors, and roles. Actors are not structural containers. Activities contain activities; functions contain functions. Capability grouping uses same-layer capability parents; it does not mean inheritance or inclusion. Operational and system missions are root semantic records; they do not own capabilities by containment. MissionCapability expresses that association. DataFields belong to ExchangeItems or Record DataTypes. Preserved generic ports belong to participants or work. Lifelines belong to scenarios. State-machine ownership uses participant → machine → region → state/mode; a state or mode can contain a nested region. All other state vertices belong to Regions as well. Events are root semantic records and do not acquire a machine owner when referenced.
DataFields, ports, lifelines, occurrences, Regions and all state vertices require an owner. An unowned StateMachine can be saved as incomplete work and produces a warning. The System cannot own internal components. The present Studio profile does not decompose SA Actors.
All relations have a header identity, alias, display name, optional description (default ""), and required source and target references. Their kind is the declaration keyword:
Declaration |
Endpoint meaning |
Additional fields / defaults |
|---|---|---|
allocation |
Same-layer participant → leaf activity/function |
None |
interaction |
OA activity → OA activity |
items refs / [] |
functional_exchange |
SA function → SA function |
Required source_port, target_port refs; items / [] |
boundary_exchange |
Different participant owners; typed component ports have matching kinds |
oriented bool / true; items refs / [] |
port_allocation |
SA Component port → SA FunctionInput/FunctionOutput port |
Requires the function's actual work Allocation to that component |
capability_involvement |
Participant, activity/function, or OA role → capability |
None |
chain_involvement |
Process/functional chain → capability |
None |
role_realization |
OA entity/actor → OA role |
None |
role_assignment |
OA role → leaf OA activity |
None |
mission_capability |
OA mission → OA capability, or SA mission → SA capability |
None |
mission_involvement |
OA entity/actor → OA mission, or SA System/Actor → SA mission |
None |
capability_include |
Including capability → included capability in one layer |
None |
capability_extend |
Extending capability → extended capability in one layer |
condition string / "" |
capability_generalization |
Specific capability → general capability in one layer |
None |
sequence_link |
Preceding contribution → following contribution in one process/chain |
guard string / "" |
state_transition |
State vertices in one machine, subject to section 4.4 |
trigger, guard, effect strings / ""; transition_kind External; triggers, effects reference lists / [] |
availability |
Work, participant, capability, or process/chain → state/mode |
None |
exchange_allocation |
Boundary-exchange relation → SA functional-exchange relation |
None |
interface_provision, interface_requirement |
Participant/port → provided/required interface |
None |
state_action |
State/mode → activity/function |
phase: Entry, Do, Exit / Do |
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.
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 advisory warning; it does not prevent saving the model. Shared roles do not imply a unique performer. Allocation of a non-leaf work element is invalid. A computed graphical summary of its allocated leaves MUST NOT create an extra semantic allocation.
Typed function ports belong directly to SA Functions. FunctionOutput declares outgoing_items; FunctionInput declares incoming_items. The lists contain distinct same-layer ExchangeItem references. A functional exchange MUST reference output/input ports owned by its respective functions, and each carried item MUST fit both declarations. Empty declarations accept no items. Item compatibility is identity equality; future specialization is not inferred from names. Port declarations remain independent of incident exchanges after creation.
A Component contract belongs to an SA System or SystemActor and stores kind (Standard or Flow) and orientation (Unset, In, Out, InOut). Standard requires Unset. Flow Unset is an incomplete draft, not InOut. Typed component connections require matching kinds: Standard/Flow mismatch is an error. Standard connections normally use oriented false; directional Flow connections use true. Boundary orientation is separate from endpoint kind and orientation. Oriented Flow endpoints permit Out at source and In at target; unoriented Flow endpoints must permit at least one communication direction.
Provided/required interfaces are separate identified relations. A Standard partner satisfies a requirement by providing the identical interface; stored source/target order does not infer service roles. Component carried items must fit the declared interface-item union when explicit declarations exist. An explicitly empty interface is not a wildcard. Missing declarations and unsatisfied required interfaces produce located completeness warnings.
PortAllocation relates a Component port to a typed function port only when that function has an actual Allocation to the component owner. Standard may allocate either function direction; Flow directions must agree. A typed ExchangeAllocation requires both corresponding PortAllocations and compatible carried items. Unoriented boundary exchanges allow reversed correspondence. Visual allocation nesting is never evidence of a stored allocation relation.
Legacy requires legacy { direction ...; interface ...; }. It preserves generic direction/interface data and emits a located compatibility warning. It is an explicit compatibility constructor in JSON/source; ordinary creation pickers do not offer it. Resolution to a typed SA contract retains this record without treating it as another active constraint. OA generic ports remain Legacy; OA Interactions stay activity-to-activity without function ports. Deleting a referenced legacy interface clears that optional live reference.
A source-3 functional exchange MUST specify both port aliases; the parser does 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.
For example:
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" { items [@payload]; }
sa_port output "Output" {
parent @send;
contract { FunctionOutput { outgoing_items [@payload]; }; };
}
sa_port input "Input" {
parent @receive;
contract { FunctionInput { incoming_items [@payload]; }; };
}
functional_exchange message "Message" {
source @send; target @receive;
source_port @output; target_port @input; items [@payload];
}
sa_port preserved "Unresolved original" {
parent @send; contract Legacy;
legacy { direction InOut; interface @old_service; };
}
An exclusive region has at most one initial state. Missing initial state is a completeness warning. Initial states have no incoming transitions and at most one outgoing transition; their outgoing transition has no trigger/guard. Final states have no outgoing transitions. Mixing State and Mode entries is permitted by this Studio profile. StateAction links provide typed work references alongside the descriptive entry/exit/do strings.
4.1. Capabilities and missions
Include means the including capability requires the included capability's behavior. Extend means the source capability adds conditional behavior to the target; its condition records when that behavior applies. Generalization means a specific source capability specializes a general target capability. Each relationship stays within OA capabilities or within SA capabilities. Include and Generalization graphs MUST each be acyclic, including transitive cycles. Extend does not introduce an additional cycle restriction; the ordinary no-self rule still applies. An empty Extend condition is an engineering warning so an incomplete capability map can be authored and saved.
MissionCapability and MissionInvolvement express need and stakeholder/system participation. They MUST NOT create a capability parent, a performer allocation, or a cross-layer realization. These relationships remain distinct when the same objects appear in a diagram or share names.
4.2. Contextual process and chain topology
A Process or FunctionalChain owns contribution occurrences with stable identities. steps is their persistent membership and presentation order; an identity MUST occur at most once in that list. A work contribution points to an existing same-layer Activity or Function. Reusing the same work or exchange requires separate occurrence identities and MUST NOT duplicate the referenced engineering element. A SequenceReference points to another same-layer Process or FunctionalChain of the owner's kind. Its target may be none while the draft is incomplete. Direct and transitive recursive sequence reuse are errors.
SequenceLinks connect distinct occurrences directly owned by the same process or chain. Their source is the predecessor and target is the successor. A link from work to an exchange requires that work to be the referenced exchange's source; an exchange-to-work link requires the target work to be its endpoint. Two adjacent exchange contributions require the first exchange's target to match the second exchange's source. A mismatch is a structural error. Other links express contextual precedence without manufacturing a global exchange.
When links or work/control/reuse contributions exist, topology is explicit. Reordering steps MUST NOT add, remove or reorder SequenceLinks. The editor MUST retain the difference between alternative Choice/Merge and parallel Fork/Join contributions. Choice/Fork expect one incoming and at least two outgoing links; Merge/Join expect at least two incoming and one outgoing link. Loop expects at least one incoming and two outgoing links for repetition and exit. These cardinalities are completeness warnings, not barriers to building an incomplete flow. Choice and Loop outgoing paths with empty guards warn; an explicit else guard is permitted. Guards remain descriptive strings.
Validation warns about missing/ambiguous entry and exit, isolated or unreachable contributions, contributions with no reachable exit, and splits/merges that lack explicit controls. Topology cycles are permitted for loops; the recursive SequenceReference prohibition is a different rule. A legacy exchange-only sequence without explicit links retains its ordered linear meaning and adjacent-exchange gap diagnostics. Implementations MUST NOT silently invent topology links when loading or reordering such a sequence.
4.3. Typed scenarios and reusable behavior
ScenarioViewpoint is one of OperationalActivity (OA activity lifelines), ParticipantInteraction (OA entity/actor/role lifelines), Functional (SA function lifelines), BoundaryInterface (SA System/Actor lifelines), or MixedLegacy. The first two are OA viewpoints; the next two are SA viewpoints. Lifeline references MUST agree with that perspective. OperationalActivity messages use Interactions; Functional messages use FunctionalExchanges. Participant views may use their layer's work exchanges through effective performer allocation, or participant boundary exchanges through the relevant ports. Corresponding lifelines and endpoints MUST agree; a viewpoint is not a label that suppresses reference checks.
Omitted schema-2 viewpoints hydrate as MixedLegacy in both source and JSON. This preserves pre-extension mixed work/participant scenes without discarding their lifelines or silently assigning a stricter meaning. MixedLegacy emits a review warning. New graphical factories explicitly select OperationalActivity or Functional by layer. Authors can select a strict viewpoint after reviewing the legacy scene; canonical source then records that choice.
MessageKind is Asynchronous, Call, Reply, Create, Delete, or { Timer { expression "after 5 s"; }; }. A process/chain exchange contribution MUST remain Asynchronous with no reply correlation. Call/reply, creation/deletion and timer semantics belong to scenarios. Reply's reply_to references a Call occurrence in the same scenario. The call MUST precede the reply on a guaranteed common sequence/enclosing branch path. Reply lifelines reverse the call's sender/receiver; it may reuse the call's exchange in reverse or use an explicit reverse-direction exchange. A second reply to the same call on that guaranteed path is an error. Missing reply correlation is a draft warning; non-Reply messages MUST NOT have reply_to.
Create identifies the receiver lifeline's creation. An earlier guaranteed use of that lifeline is an error. Delete identifies its destruction; a later guaranteed use is an error. Ordering checks respect branch ownership; they MUST NOT flatten alternative branches into a false unconditional order. A Timer is 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.
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; } ];. Each binding is one-to-one and references a direct lifeline of the appropriate scenario. Bound lifelines represent the same semantic object, or an explicitly realized operational role. Strict viewpoints MUST agree; MixedLegacy retains the compatibility case until reviewed. Missing bindings produce warnings. Direct/transitive recursive scenario use is invalid; repetition uses a Loop fragment instead. ScenarioUse owns neither its referenced scenario nor its lifelines; deleting a use does not delete the reusable definition.
4.4. State vertices, events and transition contracts
StateKind comprises State, Mode, Initial, Final, Choice, Junction, Fork, Join, ShallowHistory, DeepHistory, EntryPoint, ExitPoint and Terminate. Every vertex belongs to a Region. Only State and Mode are stable states: only these may own nested Regions, typed StateAction links, availability and scenario state observations. Multiple sibling Regions describe orthogonal configurations. EntryPoint and ExitPoint retain Region ownership; their boundary is that Region's owning State/Mode or StateMachine. Their graphical symbols belong on that boundary. This is Studio's explicit containment encoding.
Events are ordinary same-layer semantic elements, independent of diagram placements. EventBody has exactly these constructors:
Constructor |
Fields and meaning |
|---|---|
Signal |
items is a unique list of same-layer ExchangeItem references; empty is valid |
Time |
kind is After or At; expression is descriptive time text |
Change |
expression describes the Boolean condition becoming true |
After measures relative time from activation of a transition's source state; At describes an absolute instant. Empty Time/Change expressions are located completeness warnings. The application does not execute expressions, fire timers or transport signals. For example:
sa_event timeout "Recovery timeout" {
body { Time { kind After; expression "30 s"; }; };
}
sa_event recovered "Pressure recovered" {
body { Change { expression "pressure > 4 && status == \"Ready\""; }; };
}
StateTransition's triggers references Events or layer-appropriate work (OA Activity / SA Function). Referenced work need not have the machine owner's performer: external work can cause the observed transition. Multiple triggers are alternatives. effects references work describing effects, without executing it. Duplicate, missing, wrong-layer or wrong-kind references are errors. The existing trigger, guard and effect strings remain authored prose; they MUST NOT be renamed, parsed as UUIDs or inferred by name.
With typed triggers, trigger adds descriptive information. With no typed triggers and nonblank trigger prose, the transition has an unresolved descriptive trigger and emits a warning. Both empty denotes completion from a stable state or transient routing from a pseudostate. Deleting the final typed trigger when prose is empty MUST leave an explicit descriptive reminder; it MUST NOT silently create a completion transition. Guards/effect prose remain unchanged. Signal payload references and typed effect references are pruned when their targets are removed.
TransitionKind is External, Local or Internal. External leaves/re-enters the affected configuration; a stable self-transition includes exit and entry behavior. Internal requires the same stable source and target, preserves nested configuration, and does not imply entry/exit. Local requires a composite stable source and proper descendant target, preserving the source activation; an EntryPoint also routes locally into its boundary. Local self/outside targets are invalid. Other pseudostates route externally. Endpoints MUST share a layer and StateMachine; crossing nested Regions within that machine is not generally forbidden. A pseudostate self-transition is invalid.
A simple state's completion means entry/do behavior is complete. A composite completes when its active Regions complete. Final completes its containing Region; Terminate ends the containing machine's activation. These are modeling meanings, not evidence of a running state-machine engine.
Missing connections/guards and unfinished initial/history paths produce warnings, so an engineer can insert vertices before wiring them. Contradictory upper cardinalities, forbidden directions/kinds and invalid references reject.
Vertex |
Structural restrictions |
Incomplete-model warnings |
|---|---|---|
Initial |
At most one per Region; no incoming; at most one outgoing; no outgoing trigger or guard |
Missing initial/default entry or outgoing |
Final / Terminate |
No outgoing or nested Region |
No incoming |
Choice |
At most one incoming; outgoing transient alternatives |
Missing incoming, fewer than two outgoing, missing guard or else |
Junction |
Transient routing, no implicit concurrency |
Missing side or ambiguous unguarded alternatives |
Fork |
At most one incoming; outgoing stable targets in distinct orthogonal Regions |
Missing incoming or fewer than two outgoing |
Join |
At most one outgoing; incoming stable sources in distinct orthogonal Regions |
Fewer than two incoming or missing outgoing |
ShallowHistory |
At most one per Region; at most one fallback to a direct stable child of that Region; no fallback trigger/guard |
Missing fallback/default entry |
DeepHistory |
At most one per Region; at most one fallback to a stable descendant of that Region; no fallback trigger/guard |
Missing fallback/default entry |
EntryPoint |
Incoming from outside boundary, outgoing Local into it; at most one outgoing per destination Region |
Missing outgoing or incomplete affected-Region entry |
ExitPoint |
Incoming from inside boundary; at most one outgoing External to outside |
Missing input/output; top-level exit is an exit endpoint, not implicit termination |
Fork outputs and Join inputs MUST NOT carry guards or typed/descriptive triggers. Their distinct orthogonal Regions are the first Regions beneath the nearest common composite or machine, not merely each endpoint's immediate parent. Choice/Junction routing expresses conditional alternatives. Retained pseudostate entry/exit/do prose produces a warning and remains editable; it does not become a typed state behavior. StateAction remains the maintained reference for entry/do/exit work on stable states.
Other pseudostate outgoing segments MUST NOT introduce event/work triggers or trigger prose. A Choice has at most one literal else guard; the tool does not prove arbitrary guard expressions disjoint or exhaustive. Choice expresses a dynamic decision after incoming effects; Junction expresses a compound path selected by guards before traversing it. ShallowHistory remembers the immediate state and uses default entry for deeper configuration; 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.
5. Diagrams, artifacts, and cross-layer traceability
diagram has name, identity, alias, description, layer (Oa by default), projection (Overview by default), and routes (empty list by default). The string projections are Overview, Decomposition, Allocation, FunctionalDataflow, Capabilities, and Traceability. StateMachine is a payload constructor with one required machine reference. Traceability diagrams can show both layers; other diagrams show their chosen layer.
Allocation is an architecture view: it shows participants and work, nests allocated work under its performer, and displays interactions, functional exchanges and boundary exchanges. FunctionalDataflow shows the work hierarchy and its exchanges. Capabilities shows participants, roles, missions and capabilities with their involvement, exploitation, include, extend and generalization links. Decomposition and Overview retain general containment; Overview also exposes all relationships. These string projections hide inapplicable placements without deleting them or changing semantic records.
A StateMachine projection MUST reference an existing StateMachine in the diagram's layer. Its placements MUST refer to that machine, its Regions, or its state vertices, including nested Regions and vertices. Events, other machines and their members, and unrelated semantic elements are not valid placements in this view. Empty and partial state views are valid; neither creating nor reading a view synthesizes semantic elements or placements. Changing an existing diagram to StateMachine, or changing its machine, MUST remove or relocate incompatible placements in the same transaction. Context errors use state_diagram_context; incompatible placements use state_diagram_member. Neither case silently hides invalid members.
This complete source example places one state from an existing machine:
arcadia 3;
project operating "Operating modes" {
oa_entity crew "Crew" {}
oa_state_machine modes "Modes" { parent @crew; }
oa_region main "Main region" { parent @modes; }
oa_state ready "Ready" { parent @main; }
diagram mode_view "Operating modes" {
layer Oa;
projection { StateMachine { machine @modes; }; };
}
node ready_view { diagram @mode_view; element @ready; }
}
The JSON projection is {"StateMachine":{"machine":"11111111-1111-4111-8111-111111111111"}}, where the UUID is the referenced machine's identity. A bare "StateMachine", a missing or null machine, an invalid UUID, or additional payload fields is malformed. Existing projections retain their string representation, for example "Overview" in JSON and projection Overview; in source.
Ports are boundary anchors of visible owners, with their existing semantic identity. An explicitly placed port is not rendered as a separate large node. Functional exchanges use their declared source/target ports; boundary exchanges attach to their participant ports even if those ports have no separate placement. Collapsing an owner hides its interior; projected links may terminate at a visible ancestor without creating a new port or exchange.
node has an alias and identity but no display-name string. Required fields are diagram and element references. Optional fields are position (record with x/y, default 0/0), size (width/height, default 224/94), and collapsed (false), and port_anchors (empty list). Node identity is independent of its semantic element. Coordinates are absolute diagram coordinates. Placement does not alter parent, allocation, or realization. Hiding an element from a diagram must remain distinguishable from deleting it from the model. Changing a node's diagram preserves the placement identity, alias, coordinates, size, collapse state, manual port anchors, and attached artifacts. The destination must accept the element's layer, satisfy StateMachine membership when applicable, and must not already contain another placement of that element.
Each port_anchors entry is a PortAnchor record with required port reference, side (Left, Right, Top, or Bottom), and offset (finite binary32 from 0 through 1). It has no separate identity or alias. The port MUST be an existing Port directly owned by the placement's element. A port MAY have one manual anchor per placement; different diagrams MAY position it independently. List order is preserved but has no positional meaning.
offset measures a fraction of the complete projected owner edge, from top on Left/Right or from left on Top/Bottom. Zero and one denote its corners. The side is presentation data and MUST NOT change the port's contract, orientation, ownership, allocations, or connected exchanges. Absent anchors use automatic positioning. Empty lists are omitted in canonical JSON/source; earlier schema-3 input without this field hydrates as [].
node actor_view {
diagram @architecture; element @vessel;
port_anchors [{ port @communication; side Top; offset 0.25; }];
}
SetPortAnchor { diagram, node, anchor } upserts by port identity and retains unrelated entries. RemovePortAnchor { diagram, node, port } restores automatic positioning; it is a no-op only for an existing direct child port of that placement. Unknown fields, duplicate port references, non-port or foreign-owner references, and invalid offsets are structural errors. Each anchor counts as one record against ParseLimits.max_records.
These commands use the same atomic history, conditional REST transactions, and source writeback as other presentation edits. Source references resolve across files, and anchor comments follow port identity. Reidentifying a port updates anchor references; deleting it removes its anchors. Reparenting MUST repair old-owner anchors in the same transaction, for example by removing them before the Reparent command. Moving, resizing, arranging, or folding an owner retains its authored sides and fractions. Frozen schema-1/schema-2 readers do not accept port_anchors; their explicit migration supplies an empty list.
Each routes entry is an EdgeRoute record with a required edge reference to an existing Relation or Trace, waypoints (default []), and label (default none). Route records have no separate UUID or alias. An edge occurs at most once in a diagram's route list; different diagrams MAY store independent routes for the same edge. List order and explicit empty entries are retained by source and JSON round trips, although list order has no routing meaning. Edges need not currently be visible in their diagram.
routes [
{ edge @report_exchange;
waypoints [{ x 420; y 260; }, { x 690; y 260; }];
label { fraction 0.55; offset { x 0; y -16; }; };
}
];
Waypoints are pinned absolute diagram positions in source-to-target traversal order; at most 128 are allowed per edge. Computed endpoints and automatic elbows MUST NOT be persisted as authored waypoints. A label record requires both fraction (finite binary32 from 0 through 1, measured along the computed polyline's arc length) and offset (a relative x/y diagram displacement). All position/displacement components obey the coordinate bounds in section 2. An absent label uses automatic placement. Unknown fields, missing required nested fields, duplicate route edges, non-edge references and invalid numbers are structural errors. A pinned point that obstructs routing remains stored; geometric routing difficulty is reported separately from structural validity. Each EdgeRoute counts as one record against ParseLimits.max_records; pinned points are bounded by the per-route limit and aggregate input byte limits.
SetEdgeRoute { diagram, route } upserts an entry by edge identity, retaining the order and contents of unrelated entries. RemoveEdgeRoute { diagram, edge } removes it; removing an absent entry is a no-op for an existing diagram and edge. The latter is the Reset routing operation. Either command participates in the same atomic transactions, validation and undo as other model edits. Deleting a relation or trace removes its routes in every diagram; reidentifying it updates route references without changing geometry. Folding, hiding, changing projection or relocating placements MUST NOT erase routing metadata. Group movement translates pins when both actual endpoint owners move by the same delta; label offsets remain relative. Raw EditNode and source position edits are exact record edits and do not implicitly translate pins.
This is an additive schema-3 field. Earlier schema-3 JSON/source that omits routes loads as an empty list. Frozen schema-1 and schema-2 readers do not accept this new field; migration creates empty route lists. Source projects keep routes in their owning diagram declaration/file, resolve edge @alias across files, and retain route-entry comments by referenced edge identity when entries are reordered. The existing REST transaction endpoint accepts the same externally tagged command records with UUID references and conditional ETags; no separate routing endpoint or UI-only routing model exists.
artifact has name, identity, alias, description, required owner ref, relation (References, Satisfies, Verifies, Implements, DerivedFrom; default References), uri (default empty), revision (optional string), and fragment (optional string). The two optional strings default to none. URI must be nonempty, have no whitespace/backslash/control characters, and use valid percent escapes. The owner is another existing model object. Validation MUST NOT fetch the URI. Revision and fragment are retained verbatim.
trace has name, identity, alias, description, required oa and sa endpoint references, kind (default Refinement), and baseline (default none). Endpoints are canonical: SA realizes OA, regardless of which side initiated creation. Trace does not change endpoint identity or containment. Multiple distinct counterparts are permitted; duplicate endpoint/kind triples are invalid.
Kind |
OA → SA endpoint types |
|---|---|
Refinement |
Activity → Function |
Participant |
Entity/Actor → System/Actor |
Capability |
Operational Capability → System Capability |
Mission |
Operational Mission → System Mission |
Exchange |
Interaction relation → FunctionalExchange relation |
Sequence |
Process → FunctionalChain, or paired occurrence contributions directly owned by Process / FunctionalChain |
Item |
Same shared kind: ExchangeItem, DataType, DataField, Interface, or Port |
Scenario |
Same shared kind: Scenario, Lifeline, or Occurrence |
State |
Same shared kind: StateMachine, Region, any StateKind vertex, or Event |
Manual |
Engineer-defined relationship between OA/SA elements or relations |
A nonempty baseline is { oa { name "..."; description "..."; }; sa { name "..."; description "..."; }; }. It records last-reviewed names and descriptions, not a full structural snapshot. Implementations MUST NOT present it as structural synchronization or a lossless inverse transition. Explicit counterpart creation, linking, comparison, and reviewed property update may start in either layer. Ordinary edits MUST NOT automatically overwrite the counterpart. Deletion MUST NOT silently cascade across layers.
Operational Capability normally creates a System Capability. Operational Mission and System Mission are default counterparts in both directions and use Mission traces. An explicit Operational Capability ↔ System Mission mapping remains supported with a Manual trace; a uniquely linked such counterpart is reused instead of silently creating a different mapping. Required/subtree process and chain transitions include their contextual links, owned contributions and referenced dependencies. New process/chain contribution mappings use Sequence traces; older Scenario-kind occurrence mappings remain accepted. The current name/description baseline does not reconcile later topology changes.
Name and description are compared independently against each endpoint's last-reviewed baseline. An unchanged source MUST NOT overwrite an accepted different target value. A source-only change proposes an update; different changes on both sides require an explicit decision to take the source, keep the target, or enter a merged value. An explicit decision records both current values as reviewed, even when they differ. A target-only edit is preserved without advancing that field's baseline, so it remains eligible to propagate in the reverse direction. Equal current values can be recorded as converged.
6. Text grammar and resolution
The input is UTF-8 without a byte-order mark. LF, CR, space, and tab separate tokens. // starts a comment through the next LF or end of input. Quoted strings use JSON string escaping, including \n, \r, \t, \\, and Unicode escapes. Literal unescaped newlines inside strings are invalid. Numbers use JSON decimal/exponent syntax; NaN, infinity, hexadecimal numbers, leading plus signs, and leading zeroes are invalid.
Aliases match [A-Za-z_][A-Za-z0-9_]*. The following aliases are reserved: arcadia, project, id, oa, sa, element, relation, diagram, node, artifact, trace, body, alias, name, description, parent, from, to, entity, actor, activity, capability, process, allocation, interaction, contribution, realization, position, steps, true, false, null. Other field/constructor spellings can be used in alias positions because references begin with @.
The grammar below uses * for repetition and ? for optional syntax. element-kind and relation-kind are exactly the corresponding declaration keywords listed above. Typed field sets and enum constructors are closed by sections 3–5.
document = "arcadia" "3" ";" project
project = "project" alias string identity? "{" project-entry* "}"
project-entry = "description" string ";" | declaration
declaration = element
| (relation-kind | "diagram" | "artifact" | "trace")
alias string identity? fields
| "node" alias identity? fields
element = element-kind alias string identity? element-body
element-body = "{" (field-name value ";" | element)* "}"
identity = "id" uuid-string
fields = "{" (field-name value ";")* "}"
value = string | number | "true" | "false" | "none"
| enum-word | "@" alias | list | fields
list = "[" (value ("," value)*)? "]"
The project description defaults to empty and may appear once. There is no semicolon after a declaration or project closing brace. Every field inside a record ends with a semicolon, including fields whose values are nested records. Trailing list commas are invalid. Fields may appear in any order.
UUID strings use 8-4-4-4-12 hexadecimal spelling, with uppercase or lowercase hexadecimal accepted. Canonical output uses lowercase. Omitting an identity creates a fresh UUID for that parse. After Apply, accepted source includes the generated identity. Authors must keep identity clauses to preserve existing objects across subsequent edits.
All declaration aliases are collected before references resolve, so forward references are legal, including references to nodes and diagrams. Unknown @alias values are errors at their reference occurrence. Quoted UUIDs and bare words cannot substitute for reference-valued fields. Alias resolution produces UUID references; the alias itself remains stored on its declaration. Changing an alias in source requires updating its uses. Display names never resolve references.
Unknown fields, duplicate fields, incompatible constructor payloads, unsupported enum spellings, duplicate aliases/identities, and structural validation errors reject the entire candidate. No declaration silently replaces another. Ordinary string fields require quoted strings and cannot contain an unquoted @alias 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.
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 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 3;
project rescue "Rescue" id "00000000-0000-4000-8000-000000000001" {
description "Operational model";
include "oa/participants.arcadia";
include "oa/activities/activity_respond.arcadia";
include "views/diagram_operational.arcadia";
}
An include path is relative to its containing file. . and .. segments normalize within the virtual project root; escaping that root is an error. Bundle keys MUST already be normalized, use forward slashes and the lowercase .arcadia suffix, and contain no empty path segments. Absolute, drive, UNC, URL, backslash, control-character and non-portable Windows device paths are rejected. Paths ending a segment in dot or space are rejected. Keys that collide after Unicode lowercasing are rejected. References use the exact normalized spelling; filesystems do not participate in resolution.
Every reachable file is compiled once, including through repeated or diamond includes. Include cycles are errors with their chain and including location. Missing files, duplicate JSON file keys and supplied unreachable files are errors. The compiler MUST NOT read local files, network resources, environment variables or the process working directory. The native loader is responsible for translating a directory into this explicit bundle and enforcing physical path ownership; the pure compiler grants no filesystem access.
inspect_includes(path, source, is_entry, limits) supports native discovery before all included documents are available. It uses the same strict entry or fragment syntax parser, applies document byte/record and local include-edge budgets, and returns normalized targets with exact directive spans. Comments and strings that mention include syntax are not directives. A syntax failure anywhere in the document returns located errors and no partial include list; it does not scan arbitrary token strings after failure. Global references and engineering validation are deferred to bundle compilation. A native loader MUST retain the unparsed draft on failure and enforce aggregate bytes/files/depth/edges while loading; this inspection call does not own or inspect a directory.
All files share the existing global alias/UUID namespace. Files add no semantic namespace, implicit parent, allocation or port contract. Forward references work across files. Duplicate aliases and identities report the offending declaration and the previous declaration location, including collisions with the project. Moving a declaration between files MUST preserve the resulting model and UUIDs.
ProjectLimits adds file, include-depth and include-edge budgets to the existing language::ParseLimits. Model bytes, graph objects, records and ordered references are counted across the complete bundle, never reset per file. The default candidate policy is 256 files, depth 32 (entry depth zero), 1,024 include edges and 64 MiB combined UTF-8 source. Other model counts remain caller policy. Repeated include edges count toward the edge budget; file contents count once. The compiler admits input and stabilized output against the same limits.
compile_bundle returns either located errors and no candidate, or a CompiledSourceProject with the validated DecodeResult, stabilized bundle, derived SourceIndex and located engineering warnings. It inserts explicit UUID clauses into headers that omitted them without otherwise reformatting the input. The returned source index and diagnostics address the returned stabilized bytes. Clients MUST retain that bundle after successful Apply; recompiling the original buffers with missing identities would generate fresh identities.
Bundle diagnostics wrap the existing Diagnostic with a logical path and optional related locations. Declaration spans, own-field value spans, lexical owners, declaration bodies, project header/body/description spans and resolved includes are available in the source index. Columns remain one-based Unicode scalars within the real file; 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 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
An element body MAY contain element declarations interleaved with its ordinary fields. Its immediate enclosing element is its lexical owner. For example:
sa_actor harbor "Harbor operator" {
sa_port radio "Radio" {
contract { Component { kind Standard; orientation Unset; }; };
}
}
Nesting expresses semantic Element.parent only. It does not allocate a function to an actor or System, deploy equipment, or add or reorder occurrence references in an owner's explicit steps list. Those relationships retain their explicit records and fields. The same parent-kind, layer and acyclicity constraints apply as for flat source. Only element declarations can nest: relations, diagrams, placements, artifacts, traces and include directives remain at document level. A typed record value contains fields, never element declarations.
If a nested declaration omits parent, it inherits its lexical owner's UUID. An explicit parent MUST reference that owner; another reference or none is an error located at the parent value. At document level, omitted parent means none and explicit parent references retain their existing meaning. Nesting creates no alias namespace. Aliases and UUIDs remain globally unique, and references can target nested declarations before or after their definition or from another included file. Identity stabilization inserts missing UUIDs in nested headers just as it does in document-level headers.
Source positions distinguish each declaration's own fields from its descendants' fields. A declaration's body span encloses its lexical children as well as its own fields. An inherited parent has no synthetic source token or field span. Located diagnostics and editor navigation MUST address real source bytes.
7. Canonical output and source tooling
For every structurally valid Project P, parsing its canonical output MUST reproduce P, and canonicalizing that result MUST be idempotent:
parse(format(P)) == P
format(parse(format(P))) == format(P)
Equality includes all persistent fields, stored aliases, body variants, references, placements, artifacts, trace baselines, and ordered occurrences. Map order is irrelevant. The writer preserves signed coordinate zero and emits decimals that recover the original binary32 value.
Canonical output uses LF, four-space indentation, explicit identities, defaults, and JSON-escaped strings. Elements are emitted in UUID order, then relations; diagrams and their nodes follow, then artifacts, then traces. Fields use deterministic name order. Output ends with LF. Alias values come from the model, never from regenerating display names.
Comments and input whitespace are lexical trivia to the canonical writer. Canonical export does not preserve them. The source-project editor and compiler preserve authored comments and whitespace on Apply while inserting missing identities. Explicit canonical export remains a separate operation. Project update/export distinctions are described below. Camera position, selection, source cursor, and undo history are not stored model fields. Layout, collapse state, and manual port anchors inside NodePlacement are model data.
The pure format_bundle exporter uses the same typed declaration writer as format. It creates a small entry file, layer/category modules, separate activity/function roots and behavior contexts, and separate diagrams with their placements. Ports follow their owner; relations follow their source/context. Module names derive from persistent aliases, not display names or UUID chunks. Declarations are ordered by semantic ancestry and alias, with owners before descendants. Case-colliding generated file names receive deterministic numeric suffixes. When more than 240 distinct modules would be produced, aggregate folders consolidate into category modules to avoid thousands of tiny files. For every valid P, compile_bundle(format_bundle(P)).decoded.project == P when the caller supplies sufficient admission budgets.
update_bundle(compiled, P, limits) is a core source update facility. It checks that the compiled document/index/model snapshot still agrees, preserves untouched document bytes, and recompiles the result before returning it. Flat declarations retain their document-level position and file when reparented. Nested declarations move into their new semantic owner's body, across files if necessary; becoming a root moves them to document level. New children prefer their owner's body. New roots use canonical module rules and add an explicit entry include. Removed declarations do not delete source files. Alias changes also update declarations containing references to that alias. Invalid or stale updates return errors without mutating the input workspace.
A moved declaration carries its contiguous leading comment block and comments following its closing brace on the same line. A blank line separates a comment block from the following declaration; such gap comments stay in the enclosing scope. Comments trailing a preceding field likewise stay with that field. Deletion retains annotations in the nearest surviving original scope and separates them from adjacent declarations with blank lines. Surviving descendants move with an enclosing declaration unless their own semantic parent changes; independent parent and child moves are resolved together. Leading whitespace adjusts to the destination scope; other token spelling remains authored.
Updates MUST NOT canonicalize surviving declarations in full. Header, field and nested value positions are derived from the existing syntax parser. Unchanged fields retain their exact bytes, including field order, omitted defaults, number and string spelling, comments, indentation and line endings. Changed literals and references replace only the required tokens; a reference with comments between @ and its alias retains those comments. A previously omitted default is inserted only if its semantic value changes. New fields use the existing record's indentation and line-ending style without reformatting its other fields.
A constructor tag change retains shared nested fields in place. Removed fields, constructors and declarations remove syntax tokens, retaining their comments and whitespace as annotations in the enclosing record or file. A complete value shape replacement likewise retains the old value's comments beside the new value. The updater does not invent an engineering interpretation for orphaned comments.
List entries use stable referenced UUIDs, message-value field references or lifeline-binding referenced identities when these keys are unique. Literal entries use their literal value. Retained entries carry their authored value text and inter-separator comments when reordered. Trivia after a preceding comma belongs to the following entry; trivia before the next comma belongs to the preceding entry. Separators are reconstructed before trailing line comments so that the result remains valid syntax. Unkeyed or duplicate-key lists use source positions. Removed-entry comments remain as annotations in the list; newly added entries receive formatted syntax. These rules preserve comments without silently attaching new semantic identity to an ambiguous list entry.
Retained declaration UUIDs remain unchanged unless explicitly edited. Source ownership remains unchanged except for the nested relocation rules above. Records match by UUID, not display name or alias. The model-only update_bundle treats an explicit UUID replacement as removal and creation. The desktop instead uses update_bundle_with_identity_map with the surviving-record provenance from prepare_command: explicit identity edits and sequential identity swaps MUST preserve each original declaration's owning file and attached source trivia, unless the same edit changes its nested semantic parent and requires relocation. New records have no original identity mapping. The mapping MUST be injective, refer to existing records and preserve record classes. The result is recompiled, reindexed and checked for equality with the requested model before return. Equal model values do not imply a no-op if command provenance removes or replaces a declaration; the resulting model and source bundle together determine whether the accepted state changed. Source changes needed for undo are workspace history data; callers should retain complete compiled workspace snapshots rather than infer past omission/layout choices from a model-only undo.
Canonical modular export is a distinct, explicit operation and does not promise original comment/layout preservation. The native editor integrates per-file drafts and source-project saves as specified below. The pure compiler performs no filesystem access. REST source-project endpoints exchange the accepted bundle with all its files. One revision and ETag protect the model and source together, including source-only comment edits; stored envelopes contain both. Canonical source endpoints remain explicit model interchange. The precise transport, admission and persistence contract is in api.rst.
parse returns a complete structurally validated candidate or error diagnostics and no model. Engineering warnings alone MUST NOT reject a valid candidate. Parsing may stop at the first syntax/conversion error. Diagnostics contain severity (Error or Warning), a code identifying the diagnostic category, optional object UUID and field path, a message, one-based line and Unicode-scalar column, and a half-open UTF-8 byte Span. Tabs count as one scalar; LF advances the line. Structural errors and engineering warnings point to a relevant declaration or field when available. Clients MUST use severity/code rather than parse the human message; diagnostic wording is not a stable machine API.
parse_limited and decode_limited accept caller admission budgets for input bytes, graph objects, total persistent records and ordered step references. Counts MUST be checked before expensive structural validation; migration budgets include generated records before migration. Exceeding a budget does not mutate the model. Admission limits are host capacity policy, not hidden restrictions on otherwise valid language semantics; ordinary parse has no such model size policy. The REST service's fixed policy is specified in api.rst.
tokenize is tolerant: it covers every input byte with nonoverlapping TokenSpans, including incomplete strings and invalid characters. Token classes are Keyword, Identifier, String, Number, Comment, Punctuation, Whitespace, and Error. @ is punctuation followed by the referenced alias token. lint returns syntax, structural and engineering warning diagnostics for the current draft without adopting a model. A valid unallocated SA function, for example, can report missing System and allocation warnings before Apply. analyze combines tokens, diagnostics, and declaration spans with alias, optional explicit UUID, and declaration kind. A malformed later declaration does not erase earlier declaration spans. None of these APIs mutates a session.
8. Graphical parity and transactional editing
The graphical editor MUST expose all persistent properties through typed controls, including optional references, lists, variant constructors, aliases, IDs, nested parent changes, diagram placements, artifact metadata, and trace baselines. Inspector controls may provide operations unavailable as a canvas gesture. Exact strings, including tabs and line breaks in names, must remain editable through an appropriate escaped-string input when ordinary widgets cannot represent them.
A move into a functional parent, an allocation onto a performer, and a diagram placement move are different transactions. The UI MUST identify which operation will occur. Reparenting work must not silently change its performer. A computed parent summary must remain distinguishable from direct allocation. A collapse operation hides presentation detail without deleting the children.
Applying a reachable source bundle is one model transaction. Graphical edits update its owning declarations through the source-preserving updater. Both paths validate their candidate and model/source equality before adoption; failure preserves the committed model, save baseline, and history. Incomplete but structurally safe references and coverage gaps produce warnings that allow continued authoring and saving.
Source and property drafts contribute to unsaved-work state. Invalid drafts MUST NOT be replaced by generated text or lost on ordinary navigation. Undoing source-buffer edits and undoing committed model changes are distinct operations. Successful source Apply creates one model-history entry. Undo/redo restores the full stored model and exact accepted source bundle, including aliases, trace links, occurrence identity, comments and file ownership. Each open file retains its own unapplied draft across file navigation. Located lint diagnostics refer to the raw draft bytes rather than an identity-stabilized intermediate. Property editing MUST NOT silently discard a pending source draft.
Changing a graphical UUID must atomically update map keys and every reference. Alias changes preserve UUID identity. Deletion must account for owned elements, incident relationships, ordered occurrences, and diagram placements without leaving dangling references. Deleting a sequence does not delete the semantic exchanges it references. Cross-layer counterparts are not owned descendants. Text edits describe a complete candidate and must remove or repair dependencies explicitly before Apply succeeds.
9. Persistence and explicit migration
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 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.
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 writes; explicit recovery restores originals only when current bytes match a known transaction state. Unrelated external contents MUST NOT be overwritten by recovery. Missing entry files and lint updates MUST NOT hide a detected pending-recovery action. This is not cross-process compare-and-swap or atomic multi-file visibility. Save failures retain the accepted model and drafts. Detached dirty buffers MUST be reattached or reverted before source saving; JSON export does not replace the source project's saved baseline or location.
The frozen schema-2 reader retains staged compatibility defaults. Old Scenario bodies without a viewpoint retain MixedLegacy, and old Exchange occurrence bodies without message_kind, reply_to or values retain Asynchronous, none and []. Old StateTransition bodies without transition_kind, triggers or effects retain External, [] and []; existing trigger, guard and effect strings are preserved exactly. Source declaration defaults and JSON compatibility defaults MUST agree; no event or work reference is inferred from legacy prose. This compatibility applies equally to source and JSON. It adds explicit 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 migration.
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 only a missing endpoint it creates an item-bearing FunctionOutput/Input owned by the already specified function, using UUID v5 in the project namespace with v2-fe-port:<fe-id>:source or v2-fe-port:<fe-id>:target. Every identity category is checked for collision; collisions are located errors, never overwrites. New aliases follow the deterministic collision-free alias convention. Every old boundary exchange receives oriented true; no PortAllocation is inferred.
decode_with_report, load_with_report and their limited/source variants return the model plus a migration report containing original unresolved port 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 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 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 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 not truncate a previous file. External concurrent-writer conflict resolution and crash recovery are outside this storage contract.
Version 1 text and JSON are accepted through an explicit adapter. Migration preserves project, element, and relation UUIDs; names and descriptions; relation endpoints; all coordinates; and exact process exchange order and repetition. Allocation and Interaction remain their corresponding V2 relations. Contribution becomes CapabilityInvolvement; the old Process-to-Capability Realization becomes ChainInvolvement, retaining its ID and name.
Legacy positions become nodes in one OA Overview diagram. Each old process 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 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 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 V3 projects. Future versions remain errors until a separate migration exists.
11. Verification and primary references
cargo test -p oa-core --test language --locked verifies typed round-trips, stable aliases, strict syntax/reference failures, Unicode source spans, deterministic V1 migration, occurrence identity, coordinate bits, persistence, and model-history integration. Core validation and session tests cover domain mutation rules; native UI and API verification are separate evidence.
The research record in docs/research-v2-semantics.md distinguishes method requirements from Studio design choices and links to original UI evidence. The following sources inform this language without extending its scope: