@openfairygui/backend 0.2.0-alpha.14 → 0.2.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,9 @@ can clean up stale source files. The default Node filesystem/runtime lives under
39
39
  File-backed `openSession` hydrates primary resource bytes so browser-safe transactions can rename/move
40
40
  assets or add/replace/remove binary resources. `saveSession` writes replacement bytes before it removes
41
41
  stale source files, preserving the prior file when a write fails.
42
+ It also compares the source project with a UAM round trip through `ProjectWriter`; sessions with
43
+ unrepresented persisted properties expose `uamFidelity: 'unsupported'`, and write attempts fail with
44
+ `uam_fidelity_unsupported`. Transactions, saves, and materialization are serialized per session.
42
45
 
43
46
  ## Relationship to other packages
44
47
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_runtime = require("./runtime-DqApG30C.cjs");
2
+ const require_runtime = require("./runtime-C5N7GLsG.cjs");
3
3
  //#region src/storage.ts
4
4
  var StorageFileStat = class {
5
5
  constructor(kind) {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as BACKEND_CONTRACT_VERSION, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BACKEND_COMPATIBILITY_POLICY, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as BACKEND_CAPABILITY_SCHEMA_VERSION, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, et as BackendDiagnostic, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, nt as BackendResponseMeta, o as BackendCapabilities, p as BackendFileHandle, q as SavePartialFailureError, r as BackendArtifactBridgeCapability, rt as BackendStage, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, tt as BackendMessage, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-Jec6FcF5.cjs";
1
+ import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BACKEND_CAPABILITY_SCHEMA_VERSION, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as UamFidelityUnsupportedError, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, et as BACKEND_CONTRACT_VERSION, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, it as BackendStage, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, nt as BackendMessage, o as BackendCapabilities, p as BackendFileHandle, q as SavePartialFailureError, r as BackendArtifactBridgeCapability, rt as BackendResponseMeta, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, tt as BackendDiagnostic, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-B6UjcyIt.cjs";
2
2
  import { FileSystem } from "@openfairygui/core/project-io";
3
3
 
4
4
  //#region src/storage.d.ts
@@ -30,4 +30,4 @@ interface BackendAsyncStorageAdapter {
30
30
  type BackendStorageFileSystem = BackendFileSystem & FileSystem;
31
31
  declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
32
32
  //#endregion
33
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, createBackendStorageFileSystem };
33
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, createBackendStorageFileSystem };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as BACKEND_CONTRACT_VERSION, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BACKEND_COMPATIBILITY_POLICY, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as BACKEND_CAPABILITY_SCHEMA_VERSION, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, et as BackendDiagnostic, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, nt as BackendResponseMeta, o as BackendCapabilities, p as BackendFileHandle, q as SavePartialFailureError, r as BackendArtifactBridgeCapability, rt as BackendStage, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, tt as BackendMessage, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-GyNVxAQ0.mjs";
1
+ import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BACKEND_CAPABILITY_SCHEMA_VERSION, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as UamFidelityUnsupportedError, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, et as BACKEND_CONTRACT_VERSION, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, it as BackendStage, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, nt as BackendMessage, o as BackendCapabilities, p as BackendFileHandle, q as SavePartialFailureError, r as BackendArtifactBridgeCapability, rt as BackendResponseMeta, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, tt as BackendDiagnostic, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-BihiQW-H.mjs";
2
2
  import { FileSystem } from "@openfairygui/core/project-io";
3
3
 
4
4
  //#region src/storage.d.ts
@@ -30,4 +30,4 @@ interface BackendAsyncStorageAdapter {
30
30
  type BackendStorageFileSystem = BackendFileSystem & FileSystem;
31
31
  declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
32
32
  //#endregion
33
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, createBackendStorageFileSystem };
33
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, createBackendStorageFileSystem };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-B9z0n2yF.mjs";
1
+ import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-CxH7aT_v.mjs";
2
2
  //#region src/storage.ts
3
3
  var StorageFileStat = class {
4
4
  constructor(kind) {
package/dist/node.cjs CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_runtime = require("./runtime-DqApG30C.cjs");
24
+ const require_runtime = require("./runtime-C5N7GLsG.cjs");
25
25
  let node_fs_promises = require("node:fs/promises");
26
26
  node_fs_promises = __toESM(node_fs_promises);
27
27
  let node_path = require("node:path");
package/dist/node.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-Jec6FcF5.cjs";
1
+ import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-B6UjcyIt.cjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  declare function createNodeBackendFileSystem(): BackendFileSystem;
package/dist/node.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-GyNVxAQ0.mjs";
1
+ import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-BihiQW-H.mjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  declare function createNodeBackendFileSystem(): BackendFileSystem;
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BackendRuntime } from "./runtime-B9z0n2yF.mjs";
1
+ import { t as BackendRuntime } from "./runtime-CxH7aT_v.mjs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  //#region src/node.ts
@@ -193,6 +193,7 @@ interface BackendSessionSnapshot {
193
193
  revision: number;
194
194
  lastSavedRevision: number;
195
195
  dirty: boolean;
196
+ uamFidelity: 'full' | 'unsupported';
196
197
  lockHeld: boolean;
197
198
  capabilities: BackendCapabilities;
198
199
  }
@@ -257,6 +258,12 @@ interface SavePartialFailureError {
257
258
  failedPaths: string[];
258
259
  diskMayBePartiallyUpdated: true;
259
260
  }
261
+ interface UamFidelityUnsupportedError {
262
+ code: 'uam_fidelity_unsupported';
263
+ message: string;
264
+ sessionId: string;
265
+ canonicalPathKey: string;
266
+ }
260
267
  interface MaterializeValidationFailedError {
261
268
  code: 'materialize_validation_failed';
262
269
  message: string;
@@ -409,7 +416,7 @@ interface RefreshCacheInput {
409
416
  sessionId: string;
410
417
  reason?: 'manual' | 'session_open' | 'after_save';
411
418
  }
412
- type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
419
+ type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
413
420
  interface ApplySessionTransactionInput {
414
421
  sessionId: string;
415
422
  expectedRevision: number;
@@ -475,8 +482,8 @@ declare class BackendRuntime {
475
482
  sessionId: string;
476
483
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
477
484
  applyTransaction(input: ApplySessionTransactionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>>;
478
- saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot | MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
479
- materializeSession(input: MaterializeSessionInput): Promise<BackendResult<MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
485
+ saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot | MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
486
+ materializeSession(input: MaterializeSessionInput): Promise<BackendResult<MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
480
487
  closeSession(input: {
481
488
  sessionId: string;
482
489
  }): Promise<BackendResult<{
@@ -491,4 +498,4 @@ declare class BackendRuntime {
491
498
  refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
492
499
  }
493
500
  //#endregion
494
- export { BACKEND_CONTRACT_VERSION as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BACKEND_COMPATIBILITY_POLICY as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, BACKEND_CAPABILITY_SCHEMA_VERSION as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendDiagnostic as et, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendResponseMeta as nt, BackendCapabilities as o, BackendFileHandle as p, SavePartialFailureError as q, BackendArtifactBridgeCapability as r, BackendStage as rt, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendMessage as tt, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
501
+ export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BACKEND_CAPABILITY_SCHEMA_VERSION as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, UamFidelityUnsupportedError as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BACKEND_CONTRACT_VERSION as et, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendStage as it, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendMessage as nt, BackendCapabilities as o, BackendFileHandle as p, SavePartialFailureError as q, BackendArtifactBridgeCapability as r, BackendResponseMeta as rt, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendDiagnostic as tt, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
@@ -193,6 +193,7 @@ interface BackendSessionSnapshot {
193
193
  revision: number;
194
194
  lastSavedRevision: number;
195
195
  dirty: boolean;
196
+ uamFidelity: 'full' | 'unsupported';
196
197
  lockHeld: boolean;
197
198
  capabilities: BackendCapabilities;
198
199
  }
@@ -257,6 +258,12 @@ interface SavePartialFailureError {
257
258
  failedPaths: string[];
258
259
  diskMayBePartiallyUpdated: true;
259
260
  }
261
+ interface UamFidelityUnsupportedError {
262
+ code: 'uam_fidelity_unsupported';
263
+ message: string;
264
+ sessionId: string;
265
+ canonicalPathKey: string;
266
+ }
260
267
  interface MaterializeValidationFailedError {
261
268
  code: 'materialize_validation_failed';
262
269
  message: string;
@@ -409,7 +416,7 @@ interface RefreshCacheInput {
409
416
  sessionId: string;
410
417
  reason?: 'manual' | 'session_open' | 'after_save';
411
418
  }
412
- type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
419
+ type BackendError = SessionNotFoundError | SessionStaleWriteError | InProcessLockConflictError | AdvisoryLockConflictError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | EventCursorInvalidError | BackendJobNotFoundError | BackendJobNotCancellableError | BackendJobCancelledError | CacheRefreshFailedError | BackendCapabilityUnavailableError | ApplyUamTransactionAppError;
413
420
  interface ApplySessionTransactionInput {
414
421
  sessionId: string;
415
422
  expectedRevision: number;
@@ -475,8 +482,8 @@ declare class BackendRuntime {
475
482
  sessionId: string;
476
483
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
477
484
  applyTransaction(input: ApplySessionTransactionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>>;
478
- saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot | MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
479
- materializeSession(input: MaterializeSessionInput): Promise<BackendResult<MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
485
+ saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot | MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
486
+ materializeSession(input: MaterializeSessionInput): Promise<BackendResult<MaterializeSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | UamFidelityUnsupportedError | MaterializeValidationFailedError | MaterializeWriteFailedError | PathPolicyViolationError | InProcessLockConflictError | BackendCapabilityUnavailableError>>;
480
487
  closeSession(input: {
481
488
  sessionId: string;
482
489
  }): Promise<BackendResult<{
@@ -491,4 +498,4 @@ declare class BackendRuntime {
491
498
  refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
492
499
  }
493
500
  //#endregion
494
- export { BACKEND_CONTRACT_VERSION as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BACKEND_COMPATIBILITY_POLICY as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, BACKEND_CAPABILITY_SCHEMA_VERSION as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendDiagnostic as et, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendResponseMeta as nt, BackendCapabilities as o, BackendFileHandle as p, SavePartialFailureError as q, BackendArtifactBridgeCapability as r, BackendStage as rt, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendMessage as tt, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
501
+ export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BACKEND_CAPABILITY_SCHEMA_VERSION as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, UamFidelityUnsupportedError as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BACKEND_CONTRACT_VERSION as et, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendStage as it, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendMessage as nt, BackendCapabilities as o, BackendFileHandle as p, SavePartialFailureError as q, BackendArtifactBridgeCapability as r, BackendResponseMeta as rt, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendDiagnostic as tt, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
@@ -180,6 +180,7 @@ function toSessionSnapshot(session, capabilities) {
180
180
  revision: session.revision,
181
181
  lastSavedRevision: session.lastSavedRevision,
182
182
  dirty: session.dirty,
183
+ uamFidelity: session.uamFidelity,
183
184
  lockHeld: session.lockHeld,
184
185
  capabilities: cloneCapabilitiesSnapshot(capabilities)
185
186
  };
@@ -309,6 +310,14 @@ function createCapabilityUnavailableError$1(message) {
309
310
  requiredAdapter: "BackendFileSystem"
310
311
  };
311
312
  }
313
+ function createUamFidelityUnsupportedError(session) {
314
+ return {
315
+ code: "uam_fidelity_unsupported",
316
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
317
+ sessionId: session.sessionId,
318
+ canonicalPathKey: session.canonicalPathKey
319
+ };
320
+ }
312
321
  function validationDiagnostics(sessionProject) {
313
322
  return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
314
323
  code: "materialize_validation_failed",
@@ -340,12 +349,32 @@ function storageCanonicalTarget(input) {
340
349
  };
341
350
  }
342
351
  var AuthoringService = class {
352
+ sessionOperations = /* @__PURE__ */ new Map();
343
353
  constructor(context, cacheService, eventService) {
344
354
  this.context = context;
345
355
  this.cacheService = cacheService;
346
356
  this.eventService = eventService;
347
357
  }
358
+ async runSessionExclusive(sessionId, operation) {
359
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
360
+ let release = () => void 0;
361
+ const current = new Promise((resolve) => {
362
+ release = resolve;
363
+ });
364
+ const tail = previous.then(() => current);
365
+ this.sessionOperations.set(sessionId, tail);
366
+ await previous;
367
+ try {
368
+ return await operation();
369
+ } finally {
370
+ release();
371
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
372
+ }
373
+ }
348
374
  async applyTransaction(input) {
375
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
376
+ }
377
+ async applyTransactionExclusive(input) {
349
378
  const startedAt = Date.now();
350
379
  const session = this.context.sessions.get(input.sessionId);
351
380
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -412,6 +441,9 @@ var AuthoringService = class {
412
441
  mode: "fullProject",
413
442
  reason: "force_save"
414
443
  });
444
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
445
+ }
446
+ async saveSessionExclusive(input) {
415
447
  const startedAt = Date.now();
416
448
  const session = this.context.sessions.get(input.sessionId);
417
449
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -433,6 +465,10 @@ var AuthoringService = class {
433
465
  sessionId: session.sessionId,
434
466
  revision: session.revision
435
467
  });
468
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
469
+ sessionId: session.sessionId,
470
+ revision: session.revision
471
+ });
436
472
  const committedPaths = [];
437
473
  const failedPaths = [];
438
474
  this.eventService.emit({
@@ -491,6 +527,9 @@ var AuthoringService = class {
491
527
  }
492
528
  }
493
529
  async materializeSession(input) {
530
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
531
+ }
532
+ async materializeSessionExclusive(input) {
494
533
  const startedAt = Date.now();
495
534
  const session = this.context.sessions.get(input.sessionId);
496
535
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -523,6 +562,10 @@ var AuthoringService = class {
523
562
  revision: session.revision
524
563
  });
525
564
  }
565
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
566
+ sessionId: session.sessionId,
567
+ revision: session.revision
568
+ });
526
569
  const diagnostics = validationDiagnostics(session.project);
527
570
  if (diagnostics.length > 0) return failure("authoring", startedAt, {
528
571
  code: "materialize_validation_failed",
@@ -1101,6 +1144,68 @@ function createProjectReaderFileSystem(fileSystem) {
1101
1144
  }
1102
1145
  };
1103
1146
  }
1147
+ function createCaptureFileSystem(files) {
1148
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1149
+ return {
1150
+ async readFile(filePath) {
1151
+ const value = files.get(normalize(filePath));
1152
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1153
+ return value;
1154
+ },
1155
+ async readFileRaw(filePath) {
1156
+ const value = files.get(normalize(filePath));
1157
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1158
+ return value.slice();
1159
+ },
1160
+ async writeFile(filePath, content) {
1161
+ files.set(normalize(filePath), content);
1162
+ },
1163
+ async writeFileRaw(filePath, data) {
1164
+ files.set(normalize(filePath), data.slice());
1165
+ },
1166
+ async mkdir() {},
1167
+ async readdir() {
1168
+ return [];
1169
+ },
1170
+ async exists(filePath) {
1171
+ return files.has(normalize(filePath));
1172
+ },
1173
+ join(...paths) {
1174
+ return normalize(paths.filter(Boolean).join("/"));
1175
+ },
1176
+ dirname(filePath) {
1177
+ const normalized = normalize(filePath);
1178
+ const separator = normalized.lastIndexOf("/");
1179
+ return separator < 0 ? "" : normalized.slice(0, separator);
1180
+ },
1181
+ async unlink(filePath) {
1182
+ files.delete(normalize(filePath));
1183
+ }
1184
+ };
1185
+ }
1186
+ function capturedFilesEqual(left, right) {
1187
+ if (left.size !== right.size) return false;
1188
+ for (const [filePath, leftValue] of left) {
1189
+ const rightValue = right.get(filePath);
1190
+ if (typeof leftValue === "string") {
1191
+ if (leftValue !== rightValue) return false;
1192
+ continue;
1193
+ }
1194
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1195
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1196
+ }
1197
+ return true;
1198
+ }
1199
+ async function hasFullUamFidelity(document, project) {
1200
+ const sourceFiles = /* @__PURE__ */ new Map();
1201
+ const materializedFiles = /* @__PURE__ */ new Map();
1202
+ try {
1203
+ await Promise.all([new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, "Project.fairy"), new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(materializedFiles)).write((0, _openfairygui_core_uam.materializeUamProject)(project), "Project.fairy")]);
1204
+ } catch {
1205
+ return false;
1206
+ }
1207
+ return capturedFilesEqual(sourceFiles, materializedFiles);
1208
+ }
1104
1209
  var RuntimeService = class {
1105
1210
  constructor(context, cacheService, eventService, jobService) {
1106
1211
  this.context = context;
@@ -1135,7 +1240,8 @@ var RuntimeService = class {
1135
1240
  canonicalPathKey
1136
1241
  }));
1137
1242
  await advisoryLock.close();
1138
- const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
1243
+ const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1244
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
1139
1245
  const sessionId = randomId();
1140
1246
  const session = {
1141
1247
  sessionId,
@@ -1145,6 +1251,7 @@ var RuntimeService = class {
1145
1251
  lockFilePath,
1146
1252
  fileSystem,
1147
1253
  project,
1254
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
1148
1255
  revision: 0,
1149
1256
  lastSavedRevision: 0,
1150
1257
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
@@ -1203,6 +1310,7 @@ var RuntimeService = class {
1203
1310
  lockFilePath: "",
1204
1311
  fileSystem: storage?.fileSystem,
1205
1312
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1313
+ uamFidelity: "full",
1206
1314
  revision: 0,
1207
1315
  lastSavedRevision: 0,
1208
1316
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
@@ -179,6 +179,7 @@ function toSessionSnapshot(session, capabilities) {
179
179
  revision: session.revision,
180
180
  lastSavedRevision: session.lastSavedRevision,
181
181
  dirty: session.dirty,
182
+ uamFidelity: session.uamFidelity,
182
183
  lockHeld: session.lockHeld,
183
184
  capabilities: cloneCapabilitiesSnapshot(capabilities)
184
185
  };
@@ -308,6 +309,14 @@ function createCapabilityUnavailableError$1(message) {
308
309
  requiredAdapter: "BackendFileSystem"
309
310
  };
310
311
  }
312
+ function createUamFidelityUnsupportedError(session) {
313
+ return {
314
+ code: "uam_fidelity_unsupported",
315
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
316
+ sessionId: session.sessionId,
317
+ canonicalPathKey: session.canonicalPathKey
318
+ };
319
+ }
311
320
  function validationDiagnostics(sessionProject) {
312
321
  return validateUamProject(sessionProject).map((issue) => ({
313
322
  code: "materialize_validation_failed",
@@ -339,12 +348,32 @@ function storageCanonicalTarget(input) {
339
348
  };
340
349
  }
341
350
  var AuthoringService = class {
351
+ sessionOperations = /* @__PURE__ */ new Map();
342
352
  constructor(context, cacheService, eventService) {
343
353
  this.context = context;
344
354
  this.cacheService = cacheService;
345
355
  this.eventService = eventService;
346
356
  }
357
+ async runSessionExclusive(sessionId, operation) {
358
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
359
+ let release = () => void 0;
360
+ const current = new Promise((resolve) => {
361
+ release = resolve;
362
+ });
363
+ const tail = previous.then(() => current);
364
+ this.sessionOperations.set(sessionId, tail);
365
+ await previous;
366
+ try {
367
+ return await operation();
368
+ } finally {
369
+ release();
370
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
371
+ }
372
+ }
347
373
  async applyTransaction(input) {
374
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
375
+ }
376
+ async applyTransactionExclusive(input) {
348
377
  const startedAt = Date.now();
349
378
  const session = this.context.sessions.get(input.sessionId);
350
379
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -411,6 +440,9 @@ var AuthoringService = class {
411
440
  mode: "fullProject",
412
441
  reason: "force_save"
413
442
  });
443
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
444
+ }
445
+ async saveSessionExclusive(input) {
414
446
  const startedAt = Date.now();
415
447
  const session = this.context.sessions.get(input.sessionId);
416
448
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -432,6 +464,10 @@ var AuthoringService = class {
432
464
  sessionId: session.sessionId,
433
465
  revision: session.revision
434
466
  });
467
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
468
+ sessionId: session.sessionId,
469
+ revision: session.revision
470
+ });
435
471
  const committedPaths = [];
436
472
  const failedPaths = [];
437
473
  this.eventService.emit({
@@ -490,6 +526,9 @@ var AuthoringService = class {
490
526
  }
491
527
  }
492
528
  async materializeSession(input) {
529
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
530
+ }
531
+ async materializeSessionExclusive(input) {
493
532
  const startedAt = Date.now();
494
533
  const session = this.context.sessions.get(input.sessionId);
495
534
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -522,6 +561,10 @@ var AuthoringService = class {
522
561
  revision: session.revision
523
562
  });
524
563
  }
564
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
565
+ sessionId: session.sessionId,
566
+ revision: session.revision
567
+ });
525
568
  const diagnostics = validationDiagnostics(session.project);
526
569
  if (diagnostics.length > 0) return failure("authoring", startedAt, {
527
570
  code: "materialize_validation_failed",
@@ -1100,6 +1143,68 @@ function createProjectReaderFileSystem(fileSystem) {
1100
1143
  }
1101
1144
  };
1102
1145
  }
1146
+ function createCaptureFileSystem(files) {
1147
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1148
+ return {
1149
+ async readFile(filePath) {
1150
+ const value = files.get(normalize(filePath));
1151
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1152
+ return value;
1153
+ },
1154
+ async readFileRaw(filePath) {
1155
+ const value = files.get(normalize(filePath));
1156
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1157
+ return value.slice();
1158
+ },
1159
+ async writeFile(filePath, content) {
1160
+ files.set(normalize(filePath), content);
1161
+ },
1162
+ async writeFileRaw(filePath, data) {
1163
+ files.set(normalize(filePath), data.slice());
1164
+ },
1165
+ async mkdir() {},
1166
+ async readdir() {
1167
+ return [];
1168
+ },
1169
+ async exists(filePath) {
1170
+ return files.has(normalize(filePath));
1171
+ },
1172
+ join(...paths) {
1173
+ return normalize(paths.filter(Boolean).join("/"));
1174
+ },
1175
+ dirname(filePath) {
1176
+ const normalized = normalize(filePath);
1177
+ const separator = normalized.lastIndexOf("/");
1178
+ return separator < 0 ? "" : normalized.slice(0, separator);
1179
+ },
1180
+ async unlink(filePath) {
1181
+ files.delete(normalize(filePath));
1182
+ }
1183
+ };
1184
+ }
1185
+ function capturedFilesEqual(left, right) {
1186
+ if (left.size !== right.size) return false;
1187
+ for (const [filePath, leftValue] of left) {
1188
+ const rightValue = right.get(filePath);
1189
+ if (typeof leftValue === "string") {
1190
+ if (leftValue !== rightValue) return false;
1191
+ continue;
1192
+ }
1193
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1194
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1195
+ }
1196
+ return true;
1197
+ }
1198
+ async function hasFullUamFidelity(document, project) {
1199
+ const sourceFiles = /* @__PURE__ */ new Map();
1200
+ const materializedFiles = /* @__PURE__ */ new Map();
1201
+ try {
1202
+ await Promise.all([new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, "Project.fairy"), new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(materializeUamProject(project), "Project.fairy")]);
1203
+ } catch {
1204
+ return false;
1205
+ }
1206
+ return capturedFilesEqual(sourceFiles, materializedFiles);
1207
+ }
1103
1208
  var RuntimeService = class {
1104
1209
  constructor(context, cacheService, eventService, jobService) {
1105
1210
  this.context = context;
@@ -1134,7 +1239,8 @@ var RuntimeService = class {
1134
1239
  canonicalPathKey
1135
1240
  }));
1136
1241
  await advisoryLock.close();
1137
- const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
1242
+ const document = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1243
+ const project = liftDocumentToUamProject(document);
1138
1244
  const sessionId = randomId();
1139
1245
  const session = {
1140
1246
  sessionId,
@@ -1144,6 +1250,7 @@ var RuntimeService = class {
1144
1250
  lockFilePath,
1145
1251
  fileSystem,
1146
1252
  project,
1253
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
1147
1254
  revision: 0,
1148
1255
  lastSavedRevision: 0,
1149
1256
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
@@ -1202,6 +1309,7 @@ var RuntimeService = class {
1202
1309
  lockFilePath: "",
1203
1310
  fileSystem: storage?.fileSystem,
1204
1311
  project: normalizeUamProject(input.project),
1312
+ uamFidelity: "full",
1205
1313
  revision: 0,
1206
1314
  lastSavedRevision: 0,
1207
1315
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/backend",
3
- "version": "0.2.0-alpha.14",
3
+ "version": "0.2.0-alpha.15",
4
4
  "description": "FairyGUI Headless Authoring SDK — stateful backend runtime and session services.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -52,8 +52,8 @@
52
52
  "runtime"
53
53
  ],
54
54
  "dependencies": {
55
- "@openfairygui/functions": "0.2.0-alpha.14",
56
- "@openfairygui/core": "0.2.0-alpha.14"
55
+ "@openfairygui/core": "0.2.0-alpha.15",
56
+ "@openfairygui/functions": "0.2.0-alpha.15"
57
57
  },
58
58
  "devDependencies": {
59
59
  "ava": "^7.0.0",
package/src/index.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  export {
2
- BackendRuntime,
2
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
3
+ BACKEND_COMPATIBILITY_POLICY,
4
+ BACKEND_CONTRACT_VERSION,
5
+ type BackendDiagnostic,
6
+ type BackendMessage,
7
+ type BackendResponseMeta,
8
+ type BackendStage,
9
+ } from './contracts.js';
10
+ export {
3
11
  type AdvisoryLockConflictError,
4
12
  type ApplySessionTransactionInput,
5
13
  type BackendArtifactBridgeCapability,
6
14
  type BackendCacheEntry,
7
15
  type BackendCacheSnapshot,
16
+ type BackendCapabilities,
8
17
  type BackendCapabilityManifest,
9
18
  type BackendCapabilityUnavailableError,
10
- type BackendCapabilities,
11
19
  type BackendError,
12
20
  type BackendEvent,
13
21
  type BackendEventKind,
@@ -16,6 +24,7 @@ export {
16
24
  type BackendFileStat,
17
25
  type BackendFileSystem,
18
26
  type BackendHostAdapter,
27
+ type BackendJobErrors,
19
28
  type BackendJobKind,
20
29
  type BackendJobListSnapshot,
21
30
  type BackendJobListStatusFilter,
@@ -24,11 +33,12 @@ export {
24
33
  type BackendJobProgress,
25
34
  type BackendJobSnapshot,
26
35
  type BackendJobStatus,
27
- type BackendJobErrors,
36
+ type BackendProjectSessionStorage,
28
37
  type BackendResult,
38
+ BackendRuntime,
39
+ type BackendRuntimeOptions,
29
40
  type BackendSessionSnapshot,
30
41
  type BackendSuccess,
31
- type BackendRuntimeOptions,
32
42
  type CacheRefreshFailedError,
33
43
  type CancelJobInput,
34
44
  type EventCursorInvalidError,
@@ -43,25 +53,16 @@ export {
43
53
  type MaterializeValidationFailedError,
44
54
  type MaterializeWriteFailedError,
45
55
  type OpenProjectSessionInput,
46
- type BackendProjectSessionStorage,
47
56
  type RefreshCacheInput,
48
- type SaveSessionInput,
49
57
  type SavePartialFailureError,
58
+ type SaveSessionInput,
50
59
  type SessionNotFoundError,
51
60
  type SessionStaleWriteError,
61
+ type UamFidelityUnsupportedError,
52
62
  } from './runtime.js';
53
63
  export {
54
- createBackendStorageFileSystem,
55
64
  type BackendAsyncStorageAdapter,
56
65
  type BackendStorageFileSystem,
57
66
  type BackendStorageStatLike,
67
+ createBackendStorageFileSystem,
58
68
  } from './storage.js';
59
- export {
60
- type BackendDiagnostic,
61
- type BackendMessage,
62
- type BackendResponseMeta,
63
- type BackendStage,
64
- BACKEND_CAPABILITY_SCHEMA_VERSION,
65
- BACKEND_COMPATIBILITY_POLICY,
66
- BACKEND_CONTRACT_VERSION,
67
- } from './contracts.js';
package/src/runtime.ts CHANGED
@@ -181,6 +181,7 @@ export interface BackendSessionSnapshot {
181
181
  revision: number;
182
182
  lastSavedRevision: number;
183
183
  dirty: boolean;
184
+ uamFidelity: 'full' | 'unsupported';
184
185
  lockHeld: boolean;
185
186
  capabilities: BackendCapabilities;
186
187
  }
@@ -255,6 +256,13 @@ export interface SavePartialFailureError {
255
256
  diskMayBePartiallyUpdated: true;
256
257
  }
257
258
 
259
+ export interface UamFidelityUnsupportedError {
260
+ code: 'uam_fidelity_unsupported';
261
+ message: string;
262
+ sessionId: string;
263
+ canonicalPathKey: string;
264
+ }
265
+
258
266
  export interface MaterializeValidationFailedError {
259
267
  code: 'materialize_validation_failed';
260
268
  message: string;
@@ -458,6 +466,7 @@ export type BackendError =
458
466
  | InProcessLockConflictError
459
467
  | AdvisoryLockConflictError
460
468
  | SavePartialFailureError
469
+ | UamFidelityUnsupportedError
461
470
  | MaterializeValidationFailedError
462
471
  | MaterializeWriteFailedError
463
472
  | PathPolicyViolationError
@@ -704,12 +713,15 @@ export class BackendRuntime {
704
713
  return this.authoringService.applyTransaction(input);
705
714
  }
706
715
 
707
- public async saveSession(input: SaveSessionInput): Promise<
716
+ public async saveSession(
717
+ input: SaveSessionInput,
718
+ ): Promise<
708
719
  BackendResult<
709
720
  BackendSessionSnapshot | MaterializeSessionSnapshot,
710
721
  | SessionNotFoundError
711
722
  | SessionStaleWriteError
712
723
  | SavePartialFailureError
724
+ | UamFidelityUnsupportedError
713
725
  | MaterializeValidationFailedError
714
726
  | MaterializeWriteFailedError
715
727
  | PathPolicyViolationError
@@ -720,11 +732,14 @@ export class BackendRuntime {
720
732
  return this.authoringService.saveSession(input);
721
733
  }
722
734
 
723
- public async materializeSession(input: MaterializeSessionInput): Promise<
735
+ public async materializeSession(
736
+ input: MaterializeSessionInput,
737
+ ): Promise<
724
738
  BackendResult<
725
739
  MaterializeSessionSnapshot,
726
740
  | SessionNotFoundError
727
741
  | SessionStaleWriteError
742
+ | UamFidelityUnsupportedError
728
743
  | MaterializeValidationFailedError
729
744
  | MaterializeWriteFailedError
730
745
  | PathPolicyViolationError
@@ -1,9 +1,13 @@
1
- import { ProjectWriter, type FileSystem, type ProjectSourceFile } from '@openfairygui/core/project-io';
2
- import { commitUamProjectSourcePaths, materializeUamProject, validateUamProject, type UamProject } from '@openfairygui/core/uam';
3
- import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
4
- import type { CacheService } from './cache-service.js';
5
- import { failure, success, type BackendContext } from './context.js';
6
- import type { EventService } from './event-service.js';
1
+ import { type FileSystem, type ProjectSourceFile, ProjectWriter } from '@openfairygui/core/project-io';
2
+ import {
3
+ commitUamProjectSourcePaths,
4
+ materializeUamProject,
5
+ type UamProject,
6
+ validateUamProject,
7
+ } from '@openfairygui/core/uam';
8
+ import { type ApplyUamTransactionAppError, applyUamTransactionApp } from '@openfairygui/functions/uam';
9
+ import type { BackendDiagnostic } from '../contracts.js';
10
+ import { normalizeComparablePath, type PathPolicyViolationError, validateSaveTarget } from '../path-policy.js';
7
11
  import type {
8
12
  ApplySessionTransactionInput,
9
13
  BackendCapabilityUnavailableError,
@@ -15,13 +19,15 @@ import type {
15
19
  MaterializeSessionSnapshot,
16
20
  MaterializeValidationFailedError,
17
21
  MaterializeWriteFailedError,
18
- SaveSessionInput,
19
22
  SavePartialFailureError,
23
+ SaveSessionInput,
20
24
  SessionNotFoundError,
21
25
  SessionStaleWriteError,
26
+ UamFidelityUnsupportedError,
22
27
  } from '../runtime.js';
23
- import type { BackendDiagnostic } from '../contracts.js';
24
- import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
28
+ import type { CacheService } from './cache-service.js';
29
+ import { type BackendContext, failure, success } from './context.js';
30
+ import type { EventService } from './event-service.js';
25
31
  import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
26
32
 
27
33
  function createWriterFileSystem(
@@ -92,10 +98,12 @@ function projectAssetSourceFiles(project: UamProject): Map<string, ProjectSource
92
98
  if (resource.kind === 'component') continue;
93
99
  const fileName = resource.fileName ?? resource.file ?? '';
94
100
  if (!fileName) continue;
95
- result.set(
96
- `${pkg.id}/${resource.id}`,
97
- { packageName: pkg.name, branch: resource.branch, path: resource.path, fileName },
98
- );
101
+ result.set(`${pkg.id}/${resource.id}`, {
102
+ packageName: pkg.name,
103
+ branch: resource.branch,
104
+ path: resource.path,
105
+ fileName,
106
+ });
99
107
  }
100
108
  }
101
109
  return result;
@@ -126,15 +134,15 @@ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagno
126
134
  return error.diagnostics.length > 0
127
135
  ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
128
136
  : [
129
- {
130
- code: error.code,
131
- message: error.message,
132
- severity: 'error',
133
- operationKind: error.operationKind,
134
- opIndex: error.opIndex,
135
- opId: error.opId,
136
- },
137
- ];
137
+ {
138
+ code: error.code,
139
+ message: error.message,
140
+ severity: 'error',
141
+ operationKind: error.operationKind,
142
+ opIndex: error.opIndex,
143
+ opId: error.opId,
144
+ },
145
+ ];
138
146
  }
139
147
 
140
148
  function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
@@ -146,6 +154,17 @@ function createCapabilityUnavailableError(message: string): BackendCapabilityUna
146
154
  };
147
155
  }
148
156
 
157
+ function createUamFidelityUnsupportedError(
158
+ session: Parameters<typeof toSessionSnapshot>[0],
159
+ ): UamFidelityUnsupportedError {
160
+ return {
161
+ code: 'uam_fidelity_unsupported',
162
+ message: 'The source project contains formal properties that the current UAM cannot preserve.',
163
+ sessionId: session.sessionId,
164
+ canonicalPathKey: session.canonicalPathKey,
165
+ };
166
+ }
167
+
149
168
  function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
150
169
  const issues = validateUamProject(sessionProject);
151
170
  return issues.map((issue) => ({
@@ -195,12 +214,31 @@ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['stor
195
214
  }
196
215
 
197
216
  export class AuthoringService {
217
+ private readonly sessionOperations = new Map<string, Promise<void>>();
218
+
198
219
  public constructor(
199
220
  private readonly context: BackendContext,
200
221
  private readonly cacheService: CacheService,
201
222
  private readonly eventService: EventService,
202
223
  ) {}
203
224
 
225
+ private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
226
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
227
+ let release = (): void => undefined;
228
+ const current = new Promise<void>((resolve) => {
229
+ release = resolve;
230
+ });
231
+ const tail = previous.then(() => current);
232
+ this.sessionOperations.set(sessionId, tail);
233
+ await previous;
234
+ try {
235
+ return await operation();
236
+ } finally {
237
+ release();
238
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
239
+ }
240
+ }
241
+
204
242
  public async applyTransaction(
205
243
  input: ApplySessionTransactionInput,
206
244
  ): Promise<
@@ -208,6 +246,17 @@ export class AuthoringService {
208
246
  BackendSessionSnapshot,
209
247
  SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
210
248
  >
249
+ > {
250
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
251
+ }
252
+
253
+ private async applyTransactionExclusive(
254
+ input: ApplySessionTransactionInput,
255
+ ): Promise<
256
+ BackendResult<
257
+ BackendSessionSnapshot,
258
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
259
+ >
211
260
  > {
212
261
  const startedAt = Date.now();
213
262
  const session = this.context.sessions.get(input.sessionId);
@@ -284,12 +333,15 @@ export class AuthoringService {
284
333
  });
285
334
  }
286
335
 
287
- public async saveSession(input: SaveSessionInput): Promise<
336
+ public async saveSession(
337
+ input: SaveSessionInput,
338
+ ): Promise<
288
339
  BackendResult<
289
340
  BackendSessionSnapshot | MaterializeSessionSnapshot,
290
341
  | SessionNotFoundError
291
342
  | SessionStaleWriteError
292
343
  | SavePartialFailureError
344
+ | UamFidelityUnsupportedError
293
345
  | MaterializeValidationFailedError
294
346
  | MaterializeWriteFailedError
295
347
  | PathPolicyViolationError
@@ -307,6 +359,25 @@ export class AuthoringService {
307
359
  reason: 'force_save',
308
360
  });
309
361
  }
362
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
363
+ }
364
+
365
+ private async saveSessionExclusive(
366
+ input: SaveSessionInput,
367
+ ): Promise<
368
+ BackendResult<
369
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
370
+ | SessionNotFoundError
371
+ | SessionStaleWriteError
372
+ | SavePartialFailureError
373
+ | UamFidelityUnsupportedError
374
+ | MaterializeValidationFailedError
375
+ | MaterializeWriteFailedError
376
+ | PathPolicyViolationError
377
+ | InProcessLockConflictError
378
+ | BackendCapabilityUnavailableError
379
+ >
380
+ > {
310
381
  const startedAt = Date.now();
311
382
  const session = this.context.sessions.get(input.sessionId);
312
383
  if (!session || session.closed) {
@@ -317,9 +388,7 @@ export class AuthoringService {
317
388
  return failure(
318
389
  'authoring',
319
390
  startedAt,
320
- createCapabilityUnavailableError(
321
- 'saveSession requires an injected BackendFileSystem adapter.',
322
- ),
391
+ createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
323
392
  toSessionSnapshot(session, this.context.capabilities),
324
393
  {
325
394
  sessionId: session.sessionId,
@@ -358,6 +427,18 @@ export class AuthoringService {
358
427
  revision: session.revision,
359
428
  });
360
429
  }
430
+ if (session.uamFidelity === 'unsupported') {
431
+ return failure(
432
+ 'authoring',
433
+ startedAt,
434
+ createUamFidelityUnsupportedError(session),
435
+ toSessionSnapshot(session, this.context.capabilities),
436
+ {
437
+ sessionId: session.sessionId,
438
+ revision: session.revision,
439
+ },
440
+ );
441
+ }
361
442
 
362
443
  const committedPaths: string[] = [];
363
444
  const failedPaths: string[] = [];
@@ -426,11 +507,32 @@ export class AuthoringService {
426
507
  }
427
508
  }
428
509
 
429
- public async materializeSession(input: MaterializeSessionInput): Promise<
510
+ public async materializeSession(
511
+ input: MaterializeSessionInput,
512
+ ): Promise<
513
+ BackendResult<
514
+ MaterializeSessionSnapshot,
515
+ | SessionNotFoundError
516
+ | SessionStaleWriteError
517
+ | UamFidelityUnsupportedError
518
+ | MaterializeValidationFailedError
519
+ | MaterializeWriteFailedError
520
+ | PathPolicyViolationError
521
+ | InProcessLockConflictError
522
+ | BackendCapabilityUnavailableError
523
+ >
524
+ > {
525
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
526
+ }
527
+
528
+ private async materializeSessionExclusive(
529
+ input: MaterializeSessionInput,
530
+ ): Promise<
430
531
  BackendResult<
431
532
  MaterializeSessionSnapshot,
432
533
  | SessionNotFoundError
433
534
  | SessionStaleWriteError
535
+ | UamFidelityUnsupportedError
434
536
  | MaterializeValidationFailedError
435
537
  | MaterializeWriteFailedError
436
538
  | PathPolicyViolationError
@@ -457,14 +559,13 @@ export class AuthoringService {
457
559
  }
458
560
 
459
561
  const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
460
- const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
562
+ const fileSystem =
563
+ storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
461
564
  if (!fileSystem) {
462
565
  return failure(
463
566
  'authoring',
464
567
  startedAt,
465
- createCapabilityUnavailableError(
466
- 'materializeSession requires an injected BackendFileSystem adapter.',
467
- ),
568
+ createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
468
569
  toSessionSnapshot(session, this.context.capabilities),
469
570
  {
470
571
  sessionId: session.sessionId,
@@ -510,6 +611,19 @@ export class AuthoringService {
510
611
  }
511
612
  }
512
613
 
614
+ if (session.uamFidelity === 'unsupported') {
615
+ return failure(
616
+ 'authoring',
617
+ startedAt,
618
+ createUamFidelityUnsupportedError(session),
619
+ toSessionSnapshot(session, this.context.capabilities),
620
+ {
621
+ sessionId: session.sessionId,
622
+ revision: session.revision,
623
+ },
624
+ );
625
+ }
626
+
513
627
  const diagnostics = validationDiagnostics(session.project);
514
628
  if (diagnostics.length > 0) {
515
629
  const error: MaterializeValidationFailedError = {
@@ -520,17 +634,11 @@ export class AuthoringService {
520
634
  issueCount: diagnostics.length,
521
635
  diagnostics,
522
636
  };
523
- return failure(
524
- 'authoring',
525
- startedAt,
526
- error,
527
- toSessionSnapshot(session, this.context.capabilities),
528
- {
529
- sessionId: session.sessionId,
530
- revision: session.revision,
531
- diagnostics,
532
- },
533
- );
637
+ return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
638
+ sessionId: session.sessionId,
639
+ revision: session.revision,
640
+ diagnostics,
641
+ });
534
642
  }
535
643
 
536
644
  let document: ReturnType<typeof materializeUamProject>;
@@ -1,3 +1,11 @@
1
+ import {
2
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
3
+ BACKEND_CONTRACT_VERSION,
4
+ type BackendDiagnostic,
5
+ type BackendMessage,
6
+ type BackendResponseMeta,
7
+ type BackendStage,
8
+ } from '../contracts.js';
1
9
  import type {
2
10
  BackendCacheEntry,
3
11
  BackendCapabilities,
@@ -10,14 +18,6 @@ import type {
10
18
  BackendSessionSnapshot,
11
19
  BackendSuccess,
12
20
  } from '../runtime.js';
13
- import {
14
- BACKEND_CAPABILITY_SCHEMA_VERSION,
15
- BACKEND_CONTRACT_VERSION,
16
- type BackendDiagnostic,
17
- type BackendMessage,
18
- type BackendResponseMeta,
19
- type BackendStage,
20
- } from '../contracts.js';
21
21
 
22
22
  export interface BackendSessionState {
23
23
  sessionId: string;
@@ -27,6 +27,7 @@ export interface BackendSessionState {
27
27
  lockFilePath: string;
28
28
  fileSystem?: BackendFileSystem;
29
29
  project: import('@openfairygui/core/uam').UamProject;
30
+ uamFidelity: 'full' | 'unsupported';
30
31
  revision: number;
31
32
  lastSavedRevision: number;
32
33
  /** Source files deferred until a successful replacement project write. */
@@ -1,20 +1,25 @@
1
- import { ProjectReader, type FileSystem } from '@openfairygui/core/project-io';
2
- import { liftDocumentToUamProject, normalizeUamProject } from '@openfairygui/core/uam';
3
- import { failure, success, type BackendContext, type BackendSessionState } from './context.js';
4
- import type { CacheService } from './cache-service.js';
5
- import type { EventService } from './event-service.js';
6
- import type { JobService } from './job-service.js';
1
+ import { type FileSystem, ProjectReader, ProjectWriter } from '@openfairygui/core/project-io';
2
+ import {
3
+ liftDocumentToUamProject,
4
+ materializeUamProject,
5
+ normalizeUamProject,
6
+ type UamProject,
7
+ } from '@openfairygui/core/uam';
8
+ import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
7
9
  import type {
8
10
  AdvisoryLockConflictError,
11
+ BackendCapabilityUnavailableError,
9
12
  BackendFileHandle,
10
13
  BackendResult,
11
14
  BackendSessionSnapshot,
12
- BackendCapabilityUnavailableError,
13
15
  InProcessLockConflictError,
14
16
  OpenProjectSessionInput,
15
17
  SessionNotFoundError,
16
18
  } from '../runtime.js';
17
- import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
19
+ import type { CacheService } from './cache-service.js';
20
+ import { type BackendContext, type BackendSessionState, failure, success } from './context.js';
21
+ import type { EventService } from './event-service.js';
22
+ import type { JobService } from './job-service.js';
18
23
  import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
19
24
 
20
25
  function randomId(): string {
@@ -74,6 +79,82 @@ function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['f
74
79
  };
75
80
  }
76
81
 
82
+ function createCaptureFileSystem(files: Map<string, string | Uint8Array>): FileSystem {
83
+ const normalize = (filePath: string): string => filePath.replace(/\\/g, '/').replace(/\/+/g, '/');
84
+ return {
85
+ async readFile(filePath: string): Promise<string> {
86
+ const value = files.get(normalize(filePath));
87
+ if (typeof value !== 'string') throw new Error(`Captured text file was not found: ${filePath}`);
88
+ return value;
89
+ },
90
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
91
+ const value = files.get(normalize(filePath));
92
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
93
+ return value.slice();
94
+ },
95
+ async writeFile(filePath: string, content: string): Promise<void> {
96
+ files.set(normalize(filePath), content);
97
+ },
98
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
99
+ files.set(normalize(filePath), data.slice());
100
+ },
101
+ async mkdir(): Promise<void> {},
102
+ async readdir(): Promise<string[]> {
103
+ return [];
104
+ },
105
+ async exists(filePath: string): Promise<boolean> {
106
+ return files.has(normalize(filePath));
107
+ },
108
+ join(...paths: string[]): string {
109
+ return normalize(paths.filter(Boolean).join('/'));
110
+ },
111
+ dirname(filePath: string): string {
112
+ const normalized = normalize(filePath);
113
+ const separator = normalized.lastIndexOf('/');
114
+ return separator < 0 ? '' : normalized.slice(0, separator);
115
+ },
116
+ async unlink(filePath: string): Promise<void> {
117
+ files.delete(normalize(filePath));
118
+ },
119
+ };
120
+ }
121
+
122
+ function capturedFilesEqual(left: Map<string, string | Uint8Array>, right: Map<string, string | Uint8Array>): boolean {
123
+ if (left.size !== right.size) return false;
124
+ for (const [filePath, leftValue] of left) {
125
+ const rightValue = right.get(filePath);
126
+ if (typeof leftValue === 'string') {
127
+ if (leftValue !== rightValue) return false;
128
+ continue;
129
+ }
130
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
131
+ for (let index = 0; index < leftValue.length; index += 1) {
132
+ if (leftValue[index] !== rightValue[index]) return false;
133
+ }
134
+ }
135
+ return true;
136
+ }
137
+
138
+ async function hasFullUamFidelity(
139
+ document: Awaited<ReturnType<ProjectReader['read']>>,
140
+ project: UamProject,
141
+ ): Promise<boolean> {
142
+ const sourceFiles = new Map<string, string | Uint8Array>();
143
+ const materializedFiles = new Map<string, string | Uint8Array>();
144
+ try {
145
+ await Promise.all([
146
+ new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, 'Project.fairy'),
147
+ new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(
148
+ materializeUamProject(project),
149
+ 'Project.fairy',
150
+ ),
151
+ ]);
152
+ } catch {
153
+ return false;
154
+ }
155
+ return capturedFilesEqual(sourceFiles, materializedFiles);
156
+ }
157
+
77
158
  export class RuntimeService {
78
159
  public constructor(
79
160
  private readonly context: BackendContext,
@@ -129,7 +210,8 @@ export class RuntimeService {
129
210
  await advisoryLock.close();
130
211
 
131
212
  const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
132
- const project = liftDocumentToUamProject(await reader.read(fairyPath, { hydrateResourceBytes: true }));
213
+ const document = await reader.read(fairyPath, { hydrateResourceBytes: true });
214
+ const project = liftDocumentToUamProject(document);
133
215
  const sessionId = randomId();
134
216
  const session: BackendSessionState = {
135
217
  sessionId,
@@ -139,6 +221,7 @@ export class RuntimeService {
139
221
  lockFilePath,
140
222
  fileSystem,
141
223
  project,
224
+ uamFidelity: (await hasFullUamFidelity(document, project)) ? 'full' : 'unsupported',
142
225
  revision: 0,
143
226
  lastSavedRevision: 0,
144
227
  pendingStaleSourceFiles: new Map(),
@@ -178,12 +261,14 @@ export class RuntimeService {
178
261
  const sessionId = input.sessionId ?? randomId();
179
262
  const storage = input.storage;
180
263
  const memoryProjectPath = `memory://${sessionId}`;
181
- const canonicalProjectPath = storage?.canonicalProjectPath
182
- ?? input.canonicalProjectPath
183
- ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
184
- const canonicalPathKey = storage?.canonicalPathKey
185
- ?? input.canonicalPathKey
186
- ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
264
+ const canonicalProjectPath =
265
+ storage?.canonicalProjectPath ??
266
+ input.canonicalProjectPath ??
267
+ (storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
268
+ const canonicalPathKey =
269
+ storage?.canonicalPathKey ??
270
+ input.canonicalPathKey ??
271
+ (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
187
272
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
188
273
  if (existingSessionId) {
189
274
  return failure('runtime', startedAt, {
@@ -203,6 +288,7 @@ export class RuntimeService {
203
288
  lockFilePath: '',
204
289
  fileSystem: storage?.fileSystem,
205
290
  project: normalizeUamProject(input.project),
291
+ uamFidelity: 'full',
206
292
  revision: 0,
207
293
  lastSavedRevision: 0,
208
294
  pendingStaleSourceFiles: new Map(),
@@ -7,13 +7,17 @@ import type {
7
7
  import type { BackendSessionState } from './context.js';
8
8
  import { cloneCapabilitiesSnapshot } from './snapshot-utils.js';
9
9
 
10
- export function toSessionSnapshot(session: BackendSessionState, capabilities: BackendCapabilities): BackendSessionSnapshot {
10
+ export function toSessionSnapshot(
11
+ session: BackendSessionState,
12
+ capabilities: BackendCapabilities,
13
+ ): BackendSessionSnapshot {
11
14
  return {
12
15
  sessionId: session.sessionId,
13
16
  canonicalProjectPath: session.canonicalProjectPath,
14
17
  revision: session.revision,
15
18
  lastSavedRevision: session.lastSavedRevision,
16
19
  dirty: session.dirty,
20
+ uamFidelity: session.uamFidelity,
17
21
  lockHeld: session.lockHeld,
18
22
  capabilities: cloneCapabilitiesSnapshot(capabilities),
19
23
  };