35 KiB
Arcadia Studio V2: deployable model service
Research/design proposal, 2026-09-05. This document describes work to implement; it is not evidence that a server, V2 schema, or deployment currently exists. The V2 normative model contract must define the exact layer/kind/alias/transition fields before implementation. API examples below propose wire DTOs over that contract.
Recommendation and current implementation boundary
Add one oa-server workspace crate producing arcadia-server, using Axum/Tokio for HTTP and the same oa-core model, typed commands, canonical language, transition planning, and validation as the desktop. Keep HTTP/authentication/storage code outside oa-core. Use one local SQLite database through rusqlite as the initial deployed store; do not add an ORM, plugin runtime, graph database, message broker, or distributed coordination system.
The inspected code already supplies most of the domain foundation:
| Existing code | Reuse / change needed |
|---|---|
crates/oa-core/src/model.rs |
UUID-addressed ordered element/relation maps, schema 1, closed OA kinds, ordered repeated process steps. V2 owns its new concepts and persistent aliases. |
session.rs, Session::execute |
Clones a candidate, applies one command, validates, then adopts it. Extract a reusable candidate reducer and final validation boundary for atomic batches. Preserve existing desktop undo semantics. |
session.rs, reidentify and Delete |
Maintains graph references. Extend in the core for V2 live references, aliases, and trace records; historical revision anchors remain immutable. |
language.rs |
Strict parsing and deterministic source printing. Schema 1 regenerates aliases and discards comments; V2 persistent aliases must survive through the shared V2 parser/printer and JSON, not a service-only alias map. |
persistence.rs |
Strict candidate loading and sibling-temp-file replacement. Useful for local files/export, but currently stores only Project and has no revisions or retry ledger. |
validation.rs |
Shared warnings/errors. Add stable issue codes and locations; currently Issue has a message and optional object ID, and ModelError is string-only. |
Existing Add/Connect generate identities and use UI-oriented defaults. API create commands need explicit client UUIDs and complete records. Existing step commands silently ignore invalid indices, and deleting an unknown object is a no-op; the API contract should reject unintended missing objects/indices explicitly. Add those rules to shared typed commands rather than implementing a second mutation engine in HTTP handlers.
Why SQLite; viable file alternative
A single StoredProject { resource_id, revision, model: ProjectV2 } envelope can atomically store model, artifacts, and trace links together using the existing temp-file mechanism. There is no inherent need for separate database tables for these model records. This is a good minimal option if the required guarantee is just replacement of the current snapshot.
The service proposed here additionally promises retained revisions, atomic optimistic concurrency checks, durable idempotency receipts, and revision-bound transition previews. SQLite allows those records and the current-head pointer to commit in one transaction. With files, use immutable complete revision envelopes plus an atomically replaced head manifest containing the receipt information; never independently replace a model file and a metadata file and call the pair atomic. The file approach needs explicit orphan recovery, directory-sync/platform semantics, receipt retention, and cross-process locking. SQLite is the recommended tradeoff for the deployable V2 service. Its atomic-commit documentation describes both the guarantees and filesystem assumptions. SQLite atomic commit.
Resource and version contract
- Base URL:
/api/v1. HTTP API version, database migration version, and model schema version are independent. - Each hosted project has an immutable service
resource_idUUID.ProjectV2.idremains a model identity and may be edited using the shared identity command. URLs, receipts, and historical trace anchors useresource_id; changing a model ID never changes its URL. Imported copies can share model IDs, so a model UUID alone is not a global service address. - Generate and persist one
service_idUUID when initializing the store. Qualify historical anchors with it so exported evidence cannot accidentally resolve against an unrelated server. Preserve it during disaster-recovery restore; creating an independent service copy requires an explicit new identity and retains imported anchors as external evidence. - A persisted revision is a positive, monotonically increasing signed-64-bit integer, represented as a decimal string in JSON to avoid JavaScript precision loss. A create starts at
"1". Every changed model/metadata commit increments once; a no-op returnschanged: falsewithout a new revision. - All project views expose the snapshot revision and a quoted project validator, for example
ETag: "p-6f7162cc-094c-4f30-9b70-3f6f355f0c7f-r1". API conditional writes compare that validator with the project's current head inside the storage transaction. Each URI has deterministic representation bytes at a given revision; negotiate any future encoded variants correctly. - Require one exact
If-Matchproject validator on every mutation of an existing project. Missing is428; stale is412; weak tags, lists, and*are rejected by this API. GET supportsIf-None-Match/304. The validator is opaque to clients. HTTP conditional request semantics. - Require
Idempotency-Key: <UUID>for committing requests, including project creation/import/deletion and transition commit. Authenticate first, then look up a successful receipt before checkingIf-Match. The same principal/key and same method, path, body, and precondition replay the recorded result, even when head has advanced; a different request under the same key is409 idempotency_key_reused. - Retain successful receipts, including no-ops, for 30 days; publish this window in OpenAPI. Mutation provenance retains request IDs with revisions indefinitely. After expiry clients must read state before deciding whether to send a new intent.
- JSON rejects unknown fields, invalid enum values, duplicate object-map keys, invalid UUIDs, and unsupported schemas. All model import paths run the same structural validator. Warnings allow incomplete engineering work and are returned separately.
Minimum routes
{p} below always means service resource_id; {r} means an immutable revision. Route-specific limits and all DTOs belong in an OpenAPI 3.1 document shipped with the binary and available at /api/v1/openapi.json.
| Method/path | Contract |
|---|---|
GET /health/live |
200 while the HTTP process can answer; no model details or credentials. |
GET /health/ready |
200 after database/migrations/recovery succeed; 503 when shutting down or storage is unavailable. |
GET /api/v1/openapi.json |
Exact API schemas, supported enum vocabulary, limits, auth and error responses. |
POST /api/v1/projects |
Create an empty V2 model from {"name":"Harbor assistance"}; return 201, Location, project metadata, and ETag. |
GET /api/v1/projects?limit=100&cursor=...&q=harbor |
Paged project summaries, not complete graphs. Exclude tombstones. |
GET /api/v1/projects/{p} |
Metadata: service/model IDs, name, head revision, model schema, counts, created/updated timestamps. |
PATCH /api/v1/projects/{p} |
Convenience rename with strict {"name":"..."} DTO; uses the same transaction service as commands. |
DELETE /api/v1/projects/{p} |
Conditional tombstone, one revision, 204. Hide from normal reads; preserve audit/history. Physical purge is a separate future administrative operation. |
POST /api/v1/projects/import |
Create from raw canonical source or native model JSON, selected by Content-Type. Retain model identities and V2 aliases; assign a new service resource ID. |
GET /api/v1/projects/{p}/model[?revision={r}] |
Complete native ProjectV2 JSON, suitable for lossless model export/import. |
PUT /api/v1/projects/{p}/model |
Conditional full-model JSON replacement, shared schema/validation, one revision. |
GET /api/v1/projects/{p}/source[?revision={r}] |
Canonical UTF-8 Arcadia source; lossless for every persistent V2 model field. |
PUT /api/v1/projects/{p}/source |
Conditional raw UTF-8 source replacement; preserve draft on the client if rejected. |
GET /api/v1/projects/{p}/elements |
Paged filter by layer, kind, alias, q, and optional revision; return complete selected records. |
GET /api/v1/projects/{p}/elements/{id} |
One record, optional revision. |
GET /api/v1/projects/{p}/relations |
Paged filter by kind, source, target, incident_to, optional revision. |
POST /api/v1/projects/{p}/query |
Read-only bounded graph neighborhood query; explicit roots, direction, kinds and depth. No SQL or executable expressions. |
GET /api/v1/projects/{p}/validation[?revision={r}] |
Structural issues and engineering warnings for a stored snapshot. |
POST /api/v1/projects/{p}/validation |
Validate a submitted JSON/source candidate, without changing head/history. |
POST /api/v1/projects/{p}/transactions |
Ordered typed batch; optional dry_run: true; all-or-nothing final validation. |
GET /api/v1/projects/{p}/revisions?after={r}&limit=100 |
Ascending immutable revision summaries, provenance and command summaries; usable as a polling change feed. |
GET /api/v1/projects/{p}/revisions/{r} |
Snapshot envelope, provenance and change summary at one revision. Available for tombstoned projects to authorized clients. |
POST /api/v1/projects/{p}/transition-plans |
Run a core-defined layer transition in preview mode against an exact If-Match revision; return stored plan, generated aliases/IDs, diagnostics, effects, and expiry. Does not change model revision. |
POST /api/v1/projects/{p}/transition-plans/{plan}/commit |
Commit the stored typed plan only against its original base revision; revalidate and create one revision. |
GET /api/v1/projects/{p}/artifacts |
Paged model artifact records, filter by kind and optional revision. |
GET /api/v1/projects/{p}/trace-links |
Paged trace records, filter by artifact/object/predicate/revision. |
GET /api/v1/requests/{key} |
Authenticated receipt lookup for the same principal: resulting project/revision/status. 404 means no committed receipt is currently known, not proof that an accepted request cannot still complete. |
Use application/json for model JSON and text/plain; charset=utf-8 for Arcadia source. Both import and replacement routes reject unsupported media with 415; HTTP does not guess formats from filenames. Candidate validation accepts {"format":"arcadia","source":"..."} or {"format":"json","model":...}. It returns 200 {valid, issues} for a well-formed validation request, including an invalid candidate; a rejected committing request returns 422. Source diagnostics include one-based line/column using the core's documented Unicode-scalar convention.
Model JSON/source exports carry the V2 model, including whatever artifact/trace records the V2 normative schema defines. Service revision history, auth configuration, retry receipts, and pending plans belong to service backup, not model interchange. Until V2 parser/printer coverage exists, do not claim source/JSON parity for added metadata. Replacing from an older schema requires an explicit supported migration; it must not silently discard V2 data.
Create and query examples
POST /api/v1/projects HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: d06fbc2d-f953-4a59-a981-c6a9fc37cc8e
Content-Type: application/json
{"name":"Harbor assistance"}
HTTP/1.1 201 Created
Location: /api/v1/projects/6f7162cc-094c-4f30-9b70-3f6f355f0c7f
ETag: "p-6f7162cc-094c-4f30-9b70-3f6f355f0c7f-r1"
Content-Type: application/json
{
"resource_id":"6f7162cc-094c-4f30-9b70-3f6f355f0c7f",
"model_id":"debe7a6a-5a87-4d77-ad9d-39b7b5c4c608",
"name":"Harbor assistance",
"revision":"1"
}
{
"revision":"3",
"roots":["1b37b9d7-9a95-4a51-8707-010878a82419"],
"direction":"both",
"relation_kinds":["Allocation","Interaction"],
"max_depth":2,
"max_objects":200
}
The query response is {"revision":"3","elements":[...],"relations":[...],"truncated":false}. Root IDs must exist. Traverse with a visited set; preserve distinct parallel relation IDs and process step occurrences. Deterministic UUID ordering, depth at most 4, at most 100 roots, and total returned objects at most 1,000. If the bound is reached, return truncated: true explicitly; never imply a complete graph. All returned relation endpoints must be included; count closure endpoints against the same bound.
List filters are conjunctive. Exact alias matching follows the V2 core's alias rules; q is a documented Unicode-lowercase substring match on display name, with no stemming or regex. Model cursors encode resolved revision, normalized filters, and last UUID; subsequent pages read that immutable revision even if head changes. Reject mismatched cursor/filter combinations. Project catalog pagination uses a retained catalog sequence embedded in its cursor, updated atomically with creates/renames/tombstones, so concurrent catalog changes cannot silently skip records.
Transaction contract and example
Use a closed tagged command DTO (op), versioned with the API. Its initial command families are create/edit/delete/reidentify element or relation, edit project, set/reorder process steps, replace model, and the V2-defined artifact/trace commands. Explicit UUIDs and persistent aliases make multi-object AI output deterministic. Creation fails on an existing identity; editing fails on a missing identity; reference maintenance follows the shared core. replace_model cannot be mixed with other model commands in the initial API, but may carry service provenance.
POST /api/v1/projects/6f7162cc-094c-4f30-9b70-3f6f355f0c7f/transactions HTTP/1.1
Authorization: Bearer <token>
If-Match: "p-6f7162cc-094c-4f30-9b70-3f6f355f0c7f-r1"
Idempotency-Key: 8ac64e31-2655-4120-a3b4-9cb9297d3838
Content-Type: application/json
{
"message":"Describe assessment work",
"origin":{"tool":"engineering-assistant","run_id":"run-0042"},
"commands":[
{
"op":"create_element",
"element":{
"id":"1b37b9d7-9a95-4a51-8707-010878a82419",
"alias":"assess",
"layer":"oa",
"kind":"Activity",
"name":"Assess assistance",
"description":"Determine the assistance needed.",
"position":{"x":360,"y":70},
"steps":[]
}
}
]
}
The alias/layer fields illustrate the proposed V2 record; final field spellings must come from its normative schema. A successful response is 200 with the new ETag and {"revision":"2","changed":true,"created_ids":["1b37b9d7-9a95-4a51-8707-010878a82419"],"issues":[...]}. An unallocated activity is an engineering warning, not a failed transaction. Return affected/deleted IDs and identity remappings where relevant.
Apply the batch to one candidate in order, then run structural validation once. Intermediate states may be incomplete; the final candidate must be valid. A failed command or final validator leaves the current snapshot, history, aliases and traces unchanged. dry_run executes the same reducer and validation, returning effects and the base revision; it neither reserves IDs nor authorizes a later write. Commit always checks head again. Shared desktop Session should call this reducer and keep one undo entry for a batch, rather than the server constructing a chain of independent sessions.
Transition preview stores plan_id, resource, base revision, normalized input, generated IDs/aliases, the exact typed commands, a plan hash, creation principal, and a short expiry (24 hours). Only the creating principal may retrieve/commit it in the first release. Return the core's explicit source/target layer transformation and conflicts; do not let the HTTP layer invent architecture elements. Commit requires the plan's original base ETag and applies the stored commands, not a regenerated plan. A concurrent edit is 412; an expired plan is 410; the client must preview again. Revalidate constraints at commit. An exact committed retry returns its receipt even after preview expiry. Remove expired uncommitted previews during bounded maintenance.
Artifact, AI and automation traceability
Keep artifacts and trace links in ProjectV2 and its common typed command set. The service must not maintain a parallel hidden engineering model. A useful minimum artifact record has UUID, persistent alias if the V2 model requires one, title, kind, media type, immutable content digest, optional external URI, and provenance (tool, version, run ID). The authenticated principal and server timestamp are trusted audit fields; client-supplied generator strings are attributed claims.
A trace identifies an artifact, a typed predicate such as generated_from, documents, or verifies, and a model anchor {service_id, resource_id, revision, object_id}. Permit project-level anchors explicitly. The core defines which predicates are valid; arbitrary extension dictionaries must not replace typed semantics. V2 trace representation must distinguish live references, which follow core identity/deletion rules, from historical anchors, which resolve an immutable snapshot and must not be rewritten when head changes.
For a report generated from revision "3", an illustrative trace payload is:
{
"op":"create_trace_link",
"trace":{
"id":"e092799d-9534-46a8-89be-f38a17fd4ce7",
"artifact_id":"bd733c59-81ae-4859-a983-147114c032cb",
"predicate":"generated_from",
"anchor":{
"service_id":"2767a28b-854f-499a-9a46-7f276f763ad1",
"resource_id":"6f7162cc-094c-4f30-9b70-3f6f355f0c7f",
"revision":"3",
"object_id":"1b37b9d7-9a95-4a51-8707-010878a82419"
}
}
}
The artifact and its traces can be created in the same batch. A documented revision: "result" input may anchor a newly created object to the resulting commit; resolve it to the exact numeric revision before persistence. New local trace commands must resolve every anchor against its nominated snapshot (or the resulting candidate). Keep initial locally created anchors within one hosted project; cross-project linking needs its own transaction/deletion policy later. Current-model navigation may report an object as changed/missing/reidentified while retaining the original historical evidence. Never retarget old evidence silently.
Import is a distinct case: an exported model may retain historical anchors to its original service/resource, while model interchange does not include that service's revision history. Preserve these anchors exactly as external evidence; report unresolved_external_anchor as a warning when their source is unavailable, and never label them locally verified or automatically retarget them to the imported copy. This distinction must exist in the common V2 schema/validator. Lossless model interchange preserves the reference; full evidence-history recovery requires the service backup. A replacement containing a newly forged local anchor to a nonexistent revision/object is still an error. Test import/re-export of traced models explicitly.
The minimum service stores metadata and a digest, not arbitrary binary uploads or generation jobs. Accept documented https/urn artifact URI forms as data and never fetch them automatically. A stored digest is a client-supplied claim unless a trusted producer/upload path verifies content; expose this distinction. Do not execute scripts, dereference local filesystem paths, or dispatch tools from model content. An external automation/AI client queries a pinned revision, generates its output, validates a typed batch, commits with the original precondition, and records provenance. REST plus OpenAPI is sufficient initially; an MCP adapter can later be a thin external client of the same API. A polling client follows revision summaries and deduplicates by resource/revision; no webhook delivery guarantee is claimed.
Storage, concurrency and failure behavior
Use one server-owned directory on a local disk, such as /var/lib/arcadia or %LOCALAPPDATA%/ArcadiaStudio/server; do not default to the OneDrive workspace. Keep models.sqlite3 and SQLite's journal companions together. WAL is a single-host storage mode and does not support network filesystems. SQLite WAL.
Use rusqlite with bundled SQLite for reproducible packaging and its backup feature. One dedicated repository thread owns the connection and services a bounded queue. Tokio handles networking; synchronous parsing, validation, serialization and SQLite work do not block its reactor. This deliberately starts with serialized repository operations for modest engineering models, allowing later profiling before adding a reader pool. Tokio documents separating blocking work, and warns that already-started spawn_blocking work cannot be aborted; a dedicated worker makes the commit lifetime explicit. Rusqlite, Tokio blocking tasks.
Minimal tables are store metadata (including service_id), projects(resource_id, head_revision, tombstoned, created_at), immutable revisions(resource_id, revision, snapshot_json, provenance_json, created_at), request_receipts(principal, key, request_hash, response, expires_at), transition_plans(...), and catalog history/sequence. snapshot_json is the complete V2 model including artifacts/traces; it is authoritative. Do not create separate mutable graph tables in the first implementation. Enforce primary/foreign keys and revision uniqueness; use parameterized SQL, explicit migrations, and fail startup on an unsupported newer database.
Configure and verify journal_mode=WAL, synchronous=FULL, foreign_keys=ON, and a finite busy timeout. SQLite documents FULL-mode WAL commit syncing; durability still depends on a correctly functioning local filesystem/device. Never change to NORMAL/OFF without changing the durability claim. SQLite synchronous settings.
One commit executes the following sequence on the repository worker:
- Begin a write transaction; look up the authenticated idempotency receipt.
- Load current head and check exact
If-Match; reject tombstones except the documented history path. - Build one candidate with the shared reducer; validate core invariants, V2 aliases/traces, referenced historical anchors and configured quotas.
- On a change, insert immutable revision, update head and any catalog entry. Insert the success receipt in the same transaction, also for no-ops. Mark a transition plan consumed where applicable.
- Commit. Only then acknowledge success. If commit outcome is uncertain, close/reopen the connection and inspect the receipt before admitting further dependent work; never publish an uncommitted in-memory head.
Failures before commit roll back. A dropped HTTP connection or response deadline does not revoke an already-started transaction. If a response times out after admission, return 504 outcome_unknown when possible, with the request key, and tell the client to retry the same request/key or inspect its receipt. A 404 receipt while work is in flight is not proof of rollback. Queued operations may be discarded before execution if their caller has gone away; started operations must reach a known commit/rollback boundary. Tests must prove these distinctions.
Retain revisions by default. Tombstones retain evidence and receipts. Enforce a storage quota and report a clear capacity error instead of pruning history that trace links depend on. A later retention feature needs explicit anchor-aware policy. Maintain/checkpoint WAL on the worker. Back up with SQLite's backup API or stop the service cleanly before copying its store; copying a live main database file alone is not a backup procedure. Restore into an empty service directory, run integrity checks, and validate stored models before declaring ready. SQLite backup API.
Deployable defaults and bounded work
Ship these initial defaults as documented configuration, then measure before increasing them:
| Boundary | Initial default |
|---|---|
| Bind | 127.0.0.1:8787; explicit setting required for another interface. |
| Authentication | Bearer token required, including loopback; token file required at startup. No built-in password/account flow. |
| Principals | Small configured set of token identities with read-only or read/write role; all hosted projects share this service authorization boundary initially. |
| Body bytes | 1 MiB ordinary commands; 8 MiB source/model import/replacement/validation; reject compressed request bodies initially. |
| Work admission | 8 active model requests including buffered bodies; repository queue 16; reject overload promptly with 503 and Retry-After. Health routes bypass the model-work queue. |
| Deadlines | 10 seconds to receive body; 30 seconds HTTP response budget; no false promise that an admitted commit was cancelled. Reverse proxy adds header/idle/connection limits. |
| Candidate size | 8 MiB serialized model; 5,000 graph elements + relations total; 50,000 process-step occurrences; 10,000 artifact/trace records; all checked before expensive semantic validation where possible. |
| Strings and operations | Names/aliases at most 1 KiB, descriptions 64 KiB, URI 4 KiB; at most 200 typed commands per transaction. V2 semantic limits may be stricter. |
| Lists / traversal | Default page 100, max 1,000; graph depth 4, roots 100, returned objects 1,000. |
| Store | Configurable 10 GiB managed-data quota initially; refuse commits exceeding quota while retaining read/export access. Account for new revision/receipt bytes and reserve journal/maintenance headroom. |
The existing validator repeatedly scans relations, including allocation checks, so its cost is not uniformly linear. Input count limits must run before it. Measure worst cases at the chosen ceilings and add adjacency/count indexes in the shared validator if needed; do not claim a hard CPU deadline based only on a Tokio timeout.
Apply a global raw body limiter and route-specific extractor limits; Axum's default limit is local to participating extractors and can be bypassed by custom body consumption. Map all rejection paths, including middleware and unknown routes/methods, to the documented API error response. Axum body limits.
For remote deployment, use a TLS reverse proxy and keep the server port private. A non-loopback bind should require an explicit acknowledgement of the proxy/private-network configuration and a valid token configuration; this is a startup configuration rule, not a runtime user approval step. Tokens must be randomly generated high-entropy secrets, scoped to named principals, held in files with restrictive OS permissions, and checked using a constant-time secret comparison; document rotation through configuration reload/restart. Disable CORS by default. If a browser client is later added, configure exact origins and allowed headers rather than wildcard credentials. The native Iced client does not require CORS. Redact authorization headers and model/source bodies from logs; log request ID, principal ID, route template, revision, status and duration. Never put tokens in URLs or returned OpenAPI examples.
Clear errors
Use application/problem+json with type, title, status, detail, instance, and extensions code, request_id, optional current_revision, and issues. Each issue can carry code, severity, object_id, command_index, JSON field path, or source line/column. Do not classify errors by matching English ModelError strings. RFC 9457.
{
"type":"urn:arcadia:problem:revision-conflict",
"title":"Project changed",
"status":412,
"detail":"The submitted revision is no longer current. Reload and review the changes before retrying.",
"instance":"/api/v1/requests/8ac64e31-2655-4120-a3b4-9cb9297d3838",
"code":"revision_conflict",
"current_revision":"3",
"request_id":"8ac64e31-2655-4120-a3b4-9cb9297d3838"
}
Status mapping: 400 malformed envelope/query/UUID; 401 missing/invalid token (with WWW-Authenticate); 403 read-only principal attempting a write; 404 missing resource; 409 duplicate identity or conflicting retry key; 410 expired plan/deleted current project; 412 stale revision; 413 body too large; 415 unsupported media; 422 syntactically decoded but invalid model/command; 428 missing precondition; 503 overload or unavailable storage; 507 configured storage capacity reached; 504 response deadline with potentially unknown admitted-write outcome; 500 unexpected internal failure. A graph root missing from a requested existing snapshot is 422 with its ID. Invalid source syntax in a committing source request is 422 with source diagnostics. Return safe details to clients; filesystem paths and database internals belong only in restricted logs.
Desktop integration
The simplest first integration is an explicit Connect to server / Open hosted project mode using an asynchronous Rust HTTP client inside Iced tasks. Preserve local-file mode and its existing Session; both use the same core records/commands. A hosted tab tracks service URL, resource ID, base revision, acknowledged model, and local drafts. Apply/Save sends one conditional typed transaction or complete parsed replacement. The server is authoritative for a hosted project; the desktop must not open/edit the SQLite database directly.
Keep local optimistic preview if useful, but do not mark it saved until the server acknowledges the revision. On 412, retain the draft and show the current server snapshot plus the user's proposed edit; require an explicit revised intent rather than automatically resending a complete model over newer work. Poll project metadata with If-None-Match while idle or refresh on focus; no WebSocket is needed. Drag updates should coalesce into one commit on release. Local undo can formulate an inverse/replacement against the last acknowledged revision; it cannot rewind other clients' history. A restore is a new revision, not decrementing the service counter.
Do not require a background server for existing local-file users. A managed loopback sidecar can follow later, but it adds binary lifecycle, token-file/port discovery, restart and upgrade responsibilities. Initially let developers/operators start the packaged server explicitly and let the desktop connect. This provides remote service use and a testable integration boundary with less process management.
Run, package and verification plan
The official docs inspected currently report Axum 0.8.9, Tokio 1.53.1 and rusqlite 0.40.2. These are research observations, not pins already tested in this workspace. Select compatible dependency versions in Cargo.lock, enable only needed features, and verify the workspace's declared Rust 1.90 floor or explicitly raise it; current local records show builds with 1.98.1. Axum supports the required routing, state, extractor and Tower middleware model. Axum API, Tokio API, rusqlite API.
Proposed commands, valid only after implementing the crate/configuration:
$env:CARGO_TARGET_DIR = 'D:/CodexBuilds/Capella2-01a06e2e'
cargo build --release --locked -p oa-server
& 'D:/CodexBuilds/Capella2-01a06e2e/release/arcadia-server.exe' --bind 127.0.0.1:8787 --data-dir 'D:/ArcadiaData' --tokens-file 'D:/ArcadiaConfig/tokens.toml'
cargo build --release --locked -p oa-server
./target/release/arcadia-server --bind 127.0.0.1:8787 \
--data-dir /var/lib/arcadia --tokens-file /etc/arcadia/tokens.toml
Package a headless Windows executable and Linux binary/container separately from the GUI. Server compilation must not pull Iced/WGPU/native GUI development packages. Run Linux under a dedicated service identity with a writable data directory and a read-only token/config file; mount persistent local storage for a container and publish only through its TLS proxy. Include configuration examples, OpenAPI, migration/backup/restore instructions, licenses and checksums. No credentials should ship in a release. Use existing build storage on D: rather than consuming the low free space on C:.
On Ctrl+C/SIGTERM, mark unready, stop admission, stop accepting connections, and drain admitted repository work before closing SQLite. Axum provides a graceful-shutdown hook, and Tokio describes signaling and waiting for task completion. Set a documented supervisor grace period (for example 45 seconds), record a timeout, and rely on tested database recovery if a supervisor must terminate a stuck process; never report that unacknowledged writes necessarily failed. Axum graceful shutdown, Tokio graceful shutdown.
Release gates:
- Core parity: graphical/API/source/JSON routes produce equal V2 models, preserve aliases/identities/step occurrences/trace fields, and reject the same structural errors. Batch rollback, deletion, reidentification and transition plans use the common reducer.
- HTTP contract: router tests cover auth, strict DTO rejection, every status above, media types, exact ETags, limits, cursor snapshot consistency, query truncation/endpoint closure and OpenAPI examples. Axum's official testing example uses in-process service requests; add a small real-socket suite for transport behavior. Axum testing example.
- Persistence: restart after creates/edits/artifacts/tombstones; two simultaneous writes with one ETag yield exactly one new revision and one
412; retries before/after restart produce one commit. Inject failure around revision/head/receipt/commit and prove no split state. Test abrupt process termination, recovery, disk-full/permission errors and backup/restore. Do not infer power-loss guarantees from a normal restart test. - Transitions/traces: a preview is side-effect free for the model; stale/expired plans cannot commit; committed retries replay; result anchors resolve; old evidence survives deletion/identity changes; incomplete or nonexistent anchors fail atomically.
- Capacity/failure: body limits without Content-Length, slow bodies, maximum-size adverse graphs, full work queue, request cancellation after admission, graceful shutdown under writes, and storage quota. Record measured memory and latency at the chosen defaults before promising supported scale.
- Deployment and desktop: build/test headless on Windows and Ubuntu; connect Iced, edit, detect an external API edit without losing a draft, export/reimport, and restart the service with data intact. Run fmt, strict Clippy, core/server tests and the existing desktop regressions. Test Rust 1.90 explicitly if it remains the minimum.
This design is intentionally a single-host deployable service for modest models. Multi-tenant per-project permissions, automatic merging, active-active servers, binary object storage, webhooks, Capella interchange and a built-in AI execution runtime need separate concrete requirements and are not implied by the REST API.