feat: preserve nested source projects across desktop and REST

This commit is contained in:
2026-09-06 00:17:20 +03:00
parent d364d3c058
commit 4e8575bd11
32 changed files with 4283 additions and 417 deletions
+78 -23
View File
@@ -690,7 +690,9 @@ subnormal coordinates are retained.</p>
<h2>3. Element declarations and typed fields</h2>
<p>Each element declaration starts with its layer and kind, for example
<span class="docutils literal">oa_activity</span> or <span class="docutils literal">sa_function</span>. Every element body accepts
<span class="docutils literal">description</span> (default <span class="docutils literal">&quot;&quot;</span>) and <span class="docutils literal">parent</span> (default <span class="docutils literal">none</span>). The table
<span class="docutils literal">description</span> (default <span class="docutils literal">&quot;&quot;</span>) and <span class="docutils literal">parent</span> (default <span class="docutils literal">none</span> for a
document-level declaration, or its lexical element owner when nested; section
6.2). The table
lists all additional fields. <span class="docutils literal">ref</span> means <span class="docutils literal">&#64;alias</span>; <span class="docutils literal">ref?</span> also accepts
<span class="docutils literal">none</span>. Lists preserve order. Unknown or duplicate fields are errors.</p>
<table>
@@ -1450,14 +1452,18 @@ 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 <span class="docutils literal">&#64;</span>.</p>
<p>The grammar below uses <span class="docutils literal">*</span> for repetition and <span class="docutils literal">?</span> for optional syntax.
<span class="docutils literal">kind</span> is exactly an element/relation keyword listed above. Typed field
<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 = &quot;arcadia&quot; &quot;3&quot; &quot;;&quot; project
project = &quot;project&quot; alias string identity? &quot;{&quot; project-entry* &quot;}&quot;
project-entry = &quot;description&quot; string &quot;;&quot; | declaration
declaration = (kind | &quot;diagram&quot; | &quot;artifact&quot; | &quot;trace&quot;)
declaration = element
| (relation-kind | &quot;diagram&quot; | &quot;artifact&quot; | &quot;trace&quot;)
alias string identity? fields
| &quot;node&quot; alias identity? fields
element = element-kind alias string identity? element-body
element-body = &quot;{&quot; (field-name value &quot;;&quot; | element)* &quot;}&quot;
identity = &quot;id&quot; uuid-string
fields = &quot;{&quot; (field-name value &quot;;&quot;)* &quot;}&quot;
value = string | number | &quot;true&quot; | &quot;false&quot; | &quot;none&quot;
@@ -1469,7 +1475,7 @@ record ends with a semicolon, including fields whose values are nested records.
Trailing list commas are invalid. Fields may appear in any order.</p>
<p>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, canonical text includes the
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.</p>
<p>All declaration aliases are collected before references resolve, so forward
@@ -1493,8 +1499,8 @@ deeply nested drafts produce a diagnostic instead of exhausting the parser stack
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 &quot;;&quot;</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 between declarations,
never inside a typed field value. For example:</p>
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 3;
project rescue &quot;Rescue&quot; id &quot;00000000-0000-4000-8000-000000000001&quot; {
description &quot;Operational model&quot;;
@@ -1514,7 +1520,7 @@ spelling; filesystems do not participate in resolution.</p>
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. A future native loader is responsible
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.</p>
<p><span class="docutils literal">inspect_includes(path, source, is_entry, limits)</span> supports native discovery
@@ -1547,8 +1553,8 @@ 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.</p>
<p>Bundle diagnostics wrap the existing Diagnostic with a logical path and optional
related locations. Declaration spans, top-level field value spans, project
header/body/description spans and resolved include locations are available in
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.</p>
<p>Single-string parse/lint APIs remain filesystem-free and reject include
@@ -1557,6 +1563,36 @@ 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>
<p>An element body MAY contain element declarations interleaved with its ordinary
fields. Its immediate enclosing element is its lexical owner. For example:</p>
<pre class="literal-block">sa_actor harbor &quot;Harbor operator&quot; {
sa_port radio &quot;Radio&quot; {
contract { Component { kind Standard; orientation Unset; }; };
}
}</pre>
<p>Nesting expresses semantic <span class="docutils literal">Element.parent</span> 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 <span class="docutils literal">steps</span> 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.</p>
<p>If a nested declaration omits <span class="docutils literal">parent</span>, it inherits its lexical owner's UUID.
An explicit <span class="docutils literal">parent</span> MUST reference that owner; another reference or <span class="docutils literal">none</span>
is an error located at the parent value. At document level, omitted <span class="docutils literal">parent</span>
means <span class="docutils literal">none</span> 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.</p>
<p>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.</p>
</section>
</section>
<section id="canonical-output-and-source-tooling">
<h2>7. Canonical output and source tooling</h2>
@@ -1574,11 +1610,10 @@ 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.</p>
<p>Comments and input whitespace are lexical trivia to the canonical writer.
Canonical export and the current desktop single-document Apply path do not
preserve them; the editor MUST explain that behavior. This existing path does not
define an acceptable lossless authoring policy for a future project editor.
The pure project compiler in section 6.1 preserves them while inserting missing
identities. Project update/export distinctions are described below.
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 and collapse state inside NodePlacement are model data.</p>
<p>The pure <span class="docutils literal">format_bundle</span> exporter uses the same typed declaration writer as
@@ -1594,12 +1629,24 @@ For every valid P, <span class="docutils literal"><span class="pre">compile_bund
when the caller supplies sufficient admission budgets.</p>
<p><span class="docutils literal">update_bundle(compiled, P, limits)</span> is a core source update facility. It
checks that the compiled document/index/model snapshot still agrees, preserves
existing declaration file ownership and all untouched document bytes, and
recompiles the result before returning it. New children inherit their owner's
file; new roots use canonical module rules and add an explicit entry include.
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.</p>
<p>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.</p>
<p>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
@@ -1623,23 +1670,31 @@ 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.</p>
<p>Retained declaration UUIDs and source ownership remain unchanged. Records match
by UUID, not display name or alias. The model-only <span class="docutils literal">update_bundle</span> treats an
<p>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 <span class="docutils literal">update_bundle</span> treats an
explicit UUID replacement as removal and creation. The desktop instead uses
<span class="docutils literal">update_bundle_with_identity_map</span> with the surviving-record provenance from
<span class="docutils literal">prepare_command</span>: explicit identity edits and sequential identity swaps MUST
preserve each original declaration's owning file and attached source trivia.
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. Source changes needed for undo are workspace history data;
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.</p>
<p>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 endpoints currently exchange a canonical
model; they do not yet exchange the multi-file source bundle.</p>
no filesystem access. REST <span class="docutils literal"><span class="pre">source-project</span></span> 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 <span class="docutils literal">source</span> endpoints remain explicit model interchange. The precise
transport, admission and persistence contract is in <span class="docutils literal">api.rst</span>.</p>
<p><span class="docutils literal">parse</span> 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