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

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.
@@ -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(
@@ -85,17 +91,36 @@ function createWriterFileSystem(
85
91
  };
86
92
  }
87
93
 
88
- function projectAssetSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
94
+ function projectSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
89
95
  const result = new Map<string, ProjectSourceFile>();
90
96
  for (const pkg of project.packages) {
97
+ result.set(`${pkg.id}/package.xml`, {
98
+ packageName: pkg.name,
99
+ branch: '',
100
+ path: '',
101
+ fileName: 'package.xml',
102
+ });
103
+ const branches = new Set<string>();
91
104
  for (const resource of pkg.resources) {
92
- if (resource.kind === 'component') continue;
93
- const fileName = resource.fileName ?? resource.file ?? '';
105
+ if (resource.branch) branches.add(resource.branch);
106
+ const fileName = resource.kind === 'component'
107
+ ? `${resource.name}.xml`
108
+ : resource.fileName ?? resource.file ?? '';
94
109
  if (!fileName) continue;
95
- result.set(
96
- `${pkg.id}/${resource.id}`,
97
- { packageName: pkg.name, branch: resource.branch, path: resource.path, fileName },
98
- );
110
+ result.set(`${pkg.id}/${resource.id}`, {
111
+ packageName: pkg.name,
112
+ branch: resource.branch,
113
+ path: resource.path,
114
+ fileName,
115
+ });
116
+ }
117
+ for (const branch of branches) {
118
+ result.set(`${pkg.id}/branch/${branch}`, {
119
+ packageName: pkg.name,
120
+ branch,
121
+ path: '',
122
+ fileName: 'package_branch.xml',
123
+ });
99
124
  }
100
125
  }
101
126
  return result;
@@ -105,14 +130,14 @@ function sourceFileKey(source: ProjectSourceFile): string {
105
130
  return [source.branch, source.packageName, source.path, source.fileName].join('\0');
106
131
  }
107
132
 
108
- function recordStaleResourceSources(
133
+ function recordStaleProjectFiles(
109
134
  session: Parameters<typeof toSessionSnapshot>[0],
110
135
  previousProject: UamProject,
111
136
  nextProject: UamProject,
112
137
  ): void {
113
138
  if (!session.fileSystem) return;
114
- const previousSources = projectAssetSourceFiles(previousProject);
115
- const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
139
+ const previousSources = projectSourceFiles(previousProject);
140
+ const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
116
141
  for (const source of previousSources.values()) {
117
142
  const key = sourceFileKey(source);
118
143
  if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
@@ -126,15 +151,15 @@ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagno
126
151
  return error.diagnostics.length > 0
127
152
  ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
128
153
  : [
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
- ];
154
+ {
155
+ code: error.code,
156
+ message: error.message,
157
+ severity: 'error',
158
+ operationKind: error.operationKind,
159
+ opIndex: error.opIndex,
160
+ opId: error.opId,
161
+ },
162
+ ];
138
163
  }
139
164
 
140
165
  function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
@@ -146,6 +171,17 @@ function createCapabilityUnavailableError(message: string): BackendCapabilityUna
146
171
  };
147
172
  }
148
173
 
174
+ function createUamFidelityUnsupportedError(
175
+ session: Parameters<typeof toSessionSnapshot>[0],
176
+ ): UamFidelityUnsupportedError {
177
+ return {
178
+ code: 'uam_fidelity_unsupported',
179
+ message: 'The source project contains formal properties that the current UAM cannot preserve.',
180
+ sessionId: session.sessionId,
181
+ canonicalPathKey: session.canonicalPathKey,
182
+ };
183
+ }
184
+
149
185
  function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
150
186
  const issues = validateUamProject(sessionProject);
151
187
  return issues.map((issue) => ({
@@ -195,12 +231,31 @@ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['stor
195
231
  }
196
232
 
197
233
  export class AuthoringService {
234
+ private readonly sessionOperations = new Map<string, Promise<void>>();
235
+
198
236
  public constructor(
199
237
  private readonly context: BackendContext,
200
238
  private readonly cacheService: CacheService,
201
239
  private readonly eventService: EventService,
202
240
  ) {}
203
241
 
242
+ private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
243
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
244
+ let release = (): void => undefined;
245
+ const current = new Promise<void>((resolve) => {
246
+ release = resolve;
247
+ });
248
+ const tail = previous.then(() => current);
249
+ this.sessionOperations.set(sessionId, tail);
250
+ await previous;
251
+ try {
252
+ return await operation();
253
+ } finally {
254
+ release();
255
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
256
+ }
257
+ }
258
+
204
259
  public async applyTransaction(
205
260
  input: ApplySessionTransactionInput,
206
261
  ): Promise<
@@ -208,6 +263,17 @@ export class AuthoringService {
208
263
  BackendSessionSnapshot,
209
264
  SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
210
265
  >
266
+ > {
267
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
268
+ }
269
+
270
+ private async applyTransactionExclusive(
271
+ input: ApplySessionTransactionInput,
272
+ ): Promise<
273
+ BackendResult<
274
+ BackendSessionSnapshot,
275
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
276
+ >
211
277
  > {
212
278
  const startedAt = Date.now();
213
279
  const session = this.context.sessions.get(input.sessionId);
@@ -259,7 +325,7 @@ export class AuthoringService {
259
325
  );
260
326
  }
261
327
 
262
- recordStaleResourceSources(session, session.project, result.project);
328
+ recordStaleProjectFiles(session, session.project, result.project);
263
329
  session.project = result.project;
264
330
  session.revision += 1;
265
331
  session.dirty = true;
@@ -284,12 +350,15 @@ export class AuthoringService {
284
350
  });
285
351
  }
286
352
 
287
- public async saveSession(input: SaveSessionInput): Promise<
353
+ public async saveSession(
354
+ input: SaveSessionInput,
355
+ ): Promise<
288
356
  BackendResult<
289
357
  BackendSessionSnapshot | MaterializeSessionSnapshot,
290
358
  | SessionNotFoundError
291
359
  | SessionStaleWriteError
292
360
  | SavePartialFailureError
361
+ | UamFidelityUnsupportedError
293
362
  | MaterializeValidationFailedError
294
363
  | MaterializeWriteFailedError
295
364
  | PathPolicyViolationError
@@ -307,6 +376,25 @@ export class AuthoringService {
307
376
  reason: 'force_save',
308
377
  });
309
378
  }
379
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
380
+ }
381
+
382
+ private async saveSessionExclusive(
383
+ input: SaveSessionInput,
384
+ ): Promise<
385
+ BackendResult<
386
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
387
+ | SessionNotFoundError
388
+ | SessionStaleWriteError
389
+ | SavePartialFailureError
390
+ | UamFidelityUnsupportedError
391
+ | MaterializeValidationFailedError
392
+ | MaterializeWriteFailedError
393
+ | PathPolicyViolationError
394
+ | InProcessLockConflictError
395
+ | BackendCapabilityUnavailableError
396
+ >
397
+ > {
310
398
  const startedAt = Date.now();
311
399
  const session = this.context.sessions.get(input.sessionId);
312
400
  if (!session || session.closed) {
@@ -317,9 +405,7 @@ export class AuthoringService {
317
405
  return failure(
318
406
  'authoring',
319
407
  startedAt,
320
- createCapabilityUnavailableError(
321
- 'saveSession requires an injected BackendFileSystem adapter.',
322
- ),
408
+ createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
323
409
  toSessionSnapshot(session, this.context.capabilities),
324
410
  {
325
411
  sessionId: session.sessionId,
@@ -358,6 +444,18 @@ export class AuthoringService {
358
444
  revision: session.revision,
359
445
  });
360
446
  }
447
+ if (session.uamFidelity === 'unsupported') {
448
+ return failure(
449
+ 'authoring',
450
+ startedAt,
451
+ createUamFidelityUnsupportedError(session),
452
+ toSessionSnapshot(session, this.context.capabilities),
453
+ {
454
+ sessionId: session.sessionId,
455
+ revision: session.revision,
456
+ },
457
+ );
458
+ }
361
459
 
362
460
  const committedPaths: string[] = [];
363
461
  const failedPaths: string[] = [];
@@ -426,11 +524,32 @@ export class AuthoringService {
426
524
  }
427
525
  }
428
526
 
429
- public async materializeSession(input: MaterializeSessionInput): Promise<
527
+ public async materializeSession(
528
+ input: MaterializeSessionInput,
529
+ ): Promise<
530
+ BackendResult<
531
+ MaterializeSessionSnapshot,
532
+ | SessionNotFoundError
533
+ | SessionStaleWriteError
534
+ | UamFidelityUnsupportedError
535
+ | MaterializeValidationFailedError
536
+ | MaterializeWriteFailedError
537
+ | PathPolicyViolationError
538
+ | InProcessLockConflictError
539
+ | BackendCapabilityUnavailableError
540
+ >
541
+ > {
542
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
543
+ }
544
+
545
+ private async materializeSessionExclusive(
546
+ input: MaterializeSessionInput,
547
+ ): Promise<
430
548
  BackendResult<
431
549
  MaterializeSessionSnapshot,
432
550
  | SessionNotFoundError
433
551
  | SessionStaleWriteError
552
+ | UamFidelityUnsupportedError
434
553
  | MaterializeValidationFailedError
435
554
  | MaterializeWriteFailedError
436
555
  | PathPolicyViolationError
@@ -457,14 +576,13 @@ export class AuthoringService {
457
576
  }
458
577
 
459
578
  const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
460
- const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
579
+ const fileSystem =
580
+ storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
461
581
  if (!fileSystem) {
462
582
  return failure(
463
583
  'authoring',
464
584
  startedAt,
465
- createCapabilityUnavailableError(
466
- 'materializeSession requires an injected BackendFileSystem adapter.',
467
- ),
585
+ createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
468
586
  toSessionSnapshot(session, this.context.capabilities),
469
587
  {
470
588
  sessionId: session.sessionId,
@@ -510,6 +628,19 @@ export class AuthoringService {
510
628
  }
511
629
  }
512
630
 
631
+ if (session.uamFidelity === 'unsupported') {
632
+ return failure(
633
+ 'authoring',
634
+ startedAt,
635
+ createUamFidelityUnsupportedError(session),
636
+ toSessionSnapshot(session, this.context.capabilities),
637
+ {
638
+ sessionId: session.sessionId,
639
+ revision: session.revision,
640
+ },
641
+ );
642
+ }
643
+
513
644
  const diagnostics = validationDiagnostics(session.project);
514
645
  if (diagnostics.length > 0) {
515
646
  const error: MaterializeValidationFailedError = {
@@ -520,17 +651,11 @@ export class AuthoringService {
520
651
  issueCount: diagnostics.length,
521
652
  diagnostics,
522
653
  };
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
- );
654
+ return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
655
+ sessionId: session.sessionId,
656
+ revision: session.revision,
657
+ diagnostics,
658
+ });
534
659
  }
535
660
 
536
661
  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,9 +27,10 @@ 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
- /** Source files deferred until a successful replacement project write. */
33
+ /** Package-controlled files deferred until a successful replacement project write. */
33
34
  pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
34
35
  dirty: boolean;
35
36
  lockHeld: boolean;
@@ -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
  };