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
+374 -25
View File
@@ -3,7 +3,7 @@
"info": {
"title": "Arcadia Studio model API",
"version": "1.0.0",
"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."
"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": [
{
@@ -700,7 +700,8 @@
}
}
}
}
},
"description": "Replace semantics while preserving the accepted authored source layout and comments, using common UUIDs as identity correspondence. Model and source commit together. Use source-project PUT to replace authored files."
}
},
"/api/v1/projects/{id}/source": {
@@ -759,7 +760,8 @@
"default": {
"$ref": "#/components/responses/Problem"
}
}
},
"description": "Canonical single-file interchange view of the current model. Use source-project GET to export exact authored files and comments."
},
"put": {
"operationId": "replaceSource",
@@ -829,7 +831,8 @@
}
}
}
}
},
"description": "Replace semantics from canonical single-file interchange while preserving the existing authored source bundle using common UUIDs. Submitted formatting/comments do not replace the authored workspace. Use source-project PUT for that purpose."
}
},
"/api/v1/projects/{id}/elements": {
@@ -1403,7 +1406,8 @@
}
}
}
}
},
"description": "Validate native model JSON, canonical Arcadia source, or a complete source-project bundle without adoption. Source-project diagnostics refer to submitted file paths and positions. Stabilized source/model byte limits are also checked."
}
},
"/api/v1/projects/{id}/transactions": {
@@ -1475,7 +1479,8 @@
}
}
}
}
},
"description": "Command identity correspondence drives source writeback, including UUID swaps and delete/recreate operations even when final models compare equal. Dry runs return both proposed representations and validate writeback. Failure preserves the accepted pair."
}
},
"/api/v1/projects/{id}/transitions/preview": {
@@ -1540,7 +1545,8 @@
}
}
}
}
},
"description": "Conflict-free previews also validate authored-source writeback and stabilized model/source capacities."
}
},
"/api/v1/projects/{id}/transitions/apply": {
@@ -1607,6 +1613,190 @@
}
}
}
},
"/api/v1/projects/import-source-project": {
"post": {
"operationId": "importSourceProject",
"summary": "Import an authored source bundle as a new hosted resource",
"parameters": [],
"responses": {
"201": {
"description": "Success",
"headers": {
"ETag": {
"schema": {
"type": "string"
},
"description": "Project revision validator when scoped to a project."
},
"X-Request-ID": {
"schema": {
"type": "string",
"format": "uuid"
}
},
"Location": {
"schema": {
"type": "string"
},
"description": "Path to the created hosted resource."
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectSummary"
}
}
}
},
"default": {
"$ref": "#/components/responses/Problem"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SourceBundle"
}
}
}
},
"description": "Compile all logical source files, stabilize missing IDs, and persist the accepted model and bundle together. Importing source3 bundles never reads the service filesystem. Export the accepted bundle after import to retain inserted IDs."
}
},
"/api/v1/projects/{id}/source-project": {
"get": {
"operationId": "getSourceProject",
"summary": "Export the exact accepted authored source bundle",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Hosted resource UUID; distinct from editable model ID.",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "If-None-Match",
"in": "header",
"required": false,
"description": "Return 304 when the current representation's revision matches.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"headers": {
"ETag": {
"schema": {
"type": "string"
},
"description": "Project revision validator when scoped to a project."
},
"X-Request-ID": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SourceBundle"
}
}
}
},
"304": {
"description": "Unchanged; empty body."
},
"default": {
"$ref": "#/components/responses/Problem"
}
},
"description": "Returns entry and all files with stable IDs, original comments, line endings and module layout. Shares the model ETag; comment-only changes advance that revision."
},
"put": {
"operationId": "replaceSourceProject",
"summary": "Conditionally replace the complete authored source bundle",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Hosted resource UUID; distinct from editable model ID.",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "If-Match",
"in": "header",
"required": true,
"description": "One exact strong project ETag. Missing: 428; stale: 412. Weak/list/wildcard unsupported.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"headers": {
"ETag": {
"schema": {
"type": "string"
},
"description": "Project revision validator when scoped to a project."
},
"X-Request-ID": {
"schema": {
"type": "string",
"format": "uuid"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SourceProjectMutationResult"
}
}
}
},
"412": {
"$ref": "#/components/responses/Problem"
},
"428": {
"$ref": "#/components/responses/Problem"
},
"default": {
"$ref": "#/components/responses/Problem"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SourceBundle"
}
}
}
},
"description": "Compile and validate the full submitted bundle; return accepted text with missing UUIDs inserted. Model and source persist together under the exact If-Match precondition. Comment-only or file-layout edits advance revision; an exact pair no-op retains it. Rejected candidates preserve both representations."
}
}
},
"components": {
@@ -1750,7 +1940,10 @@
"description": "Relative diagram-space displacement."
}
},
"required": ["fraction", "offset"],
"required": [
"fraction",
"offset"
],
"additionalProperties": false
},
"EdgeRoute": {
@@ -1764,20 +1957,28 @@
},
"waypoints": {
"type": "array",
"items": { "$ref": "#/components/schemas/Position" },
"items": {
"$ref": "#/components/schemas/Position"
},
"maxItems": 128,
"default": [],
"description": "Absolute diagram positions in traversal order."
},
"label": {
"oneOf": [
{ "$ref": "#/components/schemas/EdgeLabel" },
{ "type": "null" }
{
"$ref": "#/components/schemas/EdgeLabel"
},
{
"type": "null"
}
],
"default": null
}
},
"required": ["edge"],
"required": [
"edge"
],
"additionalProperties": false
},
"Size": {
@@ -3493,7 +3694,9 @@
},
"routes": {
"type": "array",
"items": { "$ref": "#/components/schemas/EdgeRoute" },
"items": {
"$ref": "#/components/schemas/EdgeRoute"
},
"default": [],
"description": "Unique by edge identity within this diagram. Stored order is retained. Each entry counts against the total model record admission limit. Missing routes in earlier schema-3 models default to empty."
}
@@ -3782,14 +3985,19 @@
}
],
"description": "Present after legacy import/reopen; excluded from persisted envelopes. Resource identity and revision are unchanged by reopening."
},
"source_project": {
"$ref": "#/components/schemas/SourceBundle"
}
},
"required": [
"resource_id",
"revision",
"model"
"model",
"source_project"
],
"additionalProperties": false
"additionalProperties": false,
"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",
@@ -3866,13 +4074,27 @@
},
"column": {
"type": "integer",
"minimum": 1
"minimum": 1,
"description": "One-based Unicode scalar column."
},
"code": {
"type": "string"
},
"field": {
"type": "string"
},
"path": {
"type": "string",
"description": "Logical source file path, when a source project supplied the diagnostic."
},
"span": {
"$ref": "#/components/schemas/SourceSpan"
},
"related": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SourceLocation"
}
}
},
"required": [
@@ -4464,14 +4686,24 @@
"type": "object",
"description": "Upsert by edge identity; preserves unrelated route entries and order. Uses existing transaction validation and ETag checks.",
"properties": {
"diagram": { "type": "string", "format": "uuid" },
"route": { "$ref": "#/components/schemas/EdgeRoute" }
"diagram": {
"type": "string",
"format": "uuid"
},
"route": {
"$ref": "#/components/schemas/EdgeRoute"
}
},
"required": ["diagram", "route"],
"required": [
"diagram",
"route"
],
"additionalProperties": false
}
},
"required": ["SetEdgeRoute"],
"required": [
"SetEdgeRoute"
],
"additionalProperties": false
},
{
@@ -4481,14 +4713,25 @@
"type": "object",
"description": "Reset to automatic routing. No-op if this existing diagram has no route for the existing edge.",
"properties": {
"diagram": { "type": "string", "format": "uuid" },
"edge": { "type": "string", "format": "uuid" }
"diagram": {
"type": "string",
"format": "uuid"
},
"edge": {
"type": "string",
"format": "uuid"
}
},
"required": ["diagram", "edge"],
"required": [
"diagram",
"edge"
],
"additionalProperties": false
}
},
"required": ["RemoveEdgeRoute"],
"required": [
"RemoveEdgeRoute"
],
"additionalProperties": false
},
{
@@ -4710,7 +4953,12 @@
}
},
"model": {
"$ref": "#/components/schemas/Project"
"$ref": "#/components/schemas/Project",
"description": "Proposed model, present only for a dry run."
},
"source_project": {
"$ref": "#/components/schemas/SourceBundle",
"description": "Proposed accepted bundle, present only for a dry run. Source writeback is validated before returning a successful candidate."
}
},
"required": [
@@ -4788,6 +5036,22 @@
"source"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"format": {
"const": "source-project"
},
"source_project": {
"$ref": "#/components/schemas/SourceBundle"
}
},
"required": [
"format",
"source_project"
],
"additionalProperties": false
}
]
},
@@ -5818,6 +6082,91 @@
],
"additionalProperties": false,
"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 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"
},
"files": {
"type": "object",
"maxProperties": 256,
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"entry",
"files"
],
"additionalProperties": false
},
"SourceSpan": {
"type": "object",
"description": "Half-open UTF-8 byte offsets [start,end) in the named source file.",
"properties": {
"start": {
"type": "integer",
"minimum": 0
},
"end": {
"type": "integer",
"minimum": 0
}
},
"required": [
"start",
"end"
],
"additionalProperties": false
},
"SourceLocation": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"span": {
"$ref": "#/components/schemas/SourceSpan"
}
},
"required": [
"path",
"span"
],
"additionalProperties": false
},
"SourceProjectMutationResult": {
"type": "object",
"properties": {
"revision": {
"type": "integer",
"minimum": 1,
"description": "Current revision counter; use ETag as the opaque write validator."
},
"changed": {
"type": "boolean"
},
"issues": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Issue"
}
},
"source_project": {
"$ref": "#/components/schemas/SourceBundle"
}
},
"required": [
"revision",
"changed",
"issues",
"source_project"
],
"additionalProperties": false
}
}
}