@openfairygui/backend 0.2.0-alpha.1 → 0.2.0-alpha.13

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,5 +1,6 @@
1
- import { materializeUamProject, ProjectWriter } from '@openfairygui/core';
2
- import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions';
1
+ import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
2
+ import { materializeUamProject, validateUamProject } from '@openfairygui/core/uam';
3
+ import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
3
4
  import type { CacheService } from './cache-service.js';
4
5
  import { failure, success, type BackendContext } from './context.js';
5
6
  import type { EventService } from './event-service.js';
@@ -9,18 +10,25 @@ import type {
9
10
  BackendFileSystem,
10
11
  BackendResult,
11
12
  BackendSessionSnapshot,
13
+ InProcessLockConflictError,
14
+ MaterializeSessionInput,
15
+ MaterializeSessionSnapshot,
16
+ MaterializeValidationFailedError,
17
+ MaterializeWriteFailedError,
18
+ SaveSessionInput,
12
19
  SavePartialFailureError,
13
20
  SessionNotFoundError,
14
21
  SessionStaleWriteError,
15
22
  } from '../runtime.js';
16
- import { validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
23
+ import type { BackendDiagnostic } from '../contracts.js';
24
+ import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
17
25
  import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
18
26
 
19
27
  function createWriterFileSystem(
20
28
  fileSystem: BackendFileSystem,
21
29
  committedPaths: string[],
22
30
  failedPaths: string[],
23
- ): import('@openfairygui/core').FileSystem {
31
+ ): FileSystem {
24
32
  async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
25
33
  try {
26
34
  const result = await fn();
@@ -74,6 +82,78 @@ function createWriterFileSystem(
74
82
  };
75
83
  }
76
84
 
85
+ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
86
+ return error.diagnostics.length > 0
87
+ ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
88
+ : [
89
+ {
90
+ code: error.code,
91
+ message: error.message,
92
+ severity: 'error',
93
+ operationKind: error.operationKind,
94
+ opIndex: error.opIndex,
95
+ opId: error.opId,
96
+ },
97
+ ];
98
+ }
99
+
100
+ function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
101
+ return {
102
+ code: 'capability_unavailable',
103
+ message,
104
+ capability: 'fileSystem',
105
+ requiredAdapter: 'BackendFileSystem',
106
+ };
107
+ }
108
+
109
+ function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
110
+ const issues = validateUamProject(sessionProject);
111
+ return issues.map((issue) => ({
112
+ code: 'materialize_validation_failed',
113
+ message: issue.message,
114
+ severity: 'error',
115
+ path: issue.path,
116
+ operationKind: 'materializeSession',
117
+ }));
118
+ }
119
+
120
+ function toMaterializeSnapshot(
121
+ session: Parameters<typeof toSessionSnapshot>[0],
122
+ capabilities: Parameters<typeof toSessionSnapshot>[1],
123
+ input: {
124
+ reason?: string;
125
+ writtenPaths: string[];
126
+ skippedPaths: string[];
127
+ diagnostics: BackendDiagnostic[];
128
+ },
129
+ ): MaterializeSessionSnapshot {
130
+ return {
131
+ ...toSessionSnapshot(session, capabilities),
132
+ mode: 'fullProject',
133
+ reason: input.reason,
134
+ materializeRevision: session.revision,
135
+ saveRevision: session.lastSavedRevision,
136
+ writtenPaths: [...input.writtenPaths],
137
+ skippedPaths: [...input.skippedPaths],
138
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic })),
139
+ };
140
+ }
141
+
142
+ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['storage']>): {
143
+ fileSystem: BackendFileSystem;
144
+ fairyPath: string;
145
+ canonicalProjectPath: string;
146
+ canonicalPathKey: string;
147
+ } {
148
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || '.');
149
+ return {
150
+ fileSystem: input.fileSystem,
151
+ fairyPath: input.fairyPath,
152
+ canonicalProjectPath,
153
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath),
154
+ };
155
+ }
156
+
77
157
  export class AuthoringService {
78
158
  public constructor(
79
159
  private readonly context: BackendContext,
@@ -83,18 +163,34 @@ export class AuthoringService {
83
163
 
84
164
  public async applyTransaction(
85
165
  input: ApplySessionTransactionInput,
86
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>> {
166
+ ): Promise<
167
+ BackendResult<
168
+ BackendSessionSnapshot,
169
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
170
+ >
171
+ > {
87
172
  const startedAt = Date.now();
88
173
  const session = this.context.sessions.get(input.sessionId);
89
174
  if (!session || session.closed) {
90
175
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
91
176
  }
92
177
  if (input.expectedRevision !== session.revision) {
93
- this.eventService.emit({ kind: 'transaction.rejected', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
94
- return failure('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
178
+ this.eventService.emit({
179
+ kind: 'transaction.rejected',
95
180
  sessionId: session.sessionId,
181
+ canonicalPathKey: session.canonicalPathKey,
96
182
  revision: session.revision,
97
183
  });
184
+ return failure(
185
+ 'authoring',
186
+ startedAt,
187
+ createStaleWriteError(session, input.expectedRevision),
188
+ toSessionSnapshot(session, this.context.capabilities),
189
+ {
190
+ sessionId: session.sessionId,
191
+ revision: session.revision,
192
+ },
193
+ );
98
194
  }
99
195
 
100
196
  const result = applyUamTransactionApp({
@@ -102,19 +198,44 @@ export class AuthoringService {
102
198
  operations: input.operations,
103
199
  });
104
200
  if (result.ok === false) {
105
- this.eventService.emit({ kind: 'transaction.rejected', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, diagnostics: result.error.issues?.map((issue) => ({ code: result.error.code, message: issue.message, severity: 'error' as const })) ?? [] });
106
- return failure('authoring', startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
201
+ const diagnostics = toBackendDiagnostics(result.error);
202
+ this.eventService.emit({
203
+ kind: 'transaction.rejected',
107
204
  sessionId: session.sessionId,
205
+ canonicalPathKey: session.canonicalPathKey,
108
206
  revision: session.revision,
207
+ diagnostics,
109
208
  });
209
+ return failure(
210
+ 'authoring',
211
+ startedAt,
212
+ result.error,
213
+ toSessionSnapshot(session, this.context.capabilities),
214
+ {
215
+ sessionId: session.sessionId,
216
+ revision: session.revision,
217
+ diagnostics,
218
+ },
219
+ );
110
220
  }
111
221
 
112
222
  session.project = result.project;
113
223
  session.revision += 1;
114
224
  session.dirty = true;
115
225
  const cacheEntry = this.cacheService.invalidateSession(session);
116
- this.eventService.emit({ kind: 'transaction.applied', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
117
- this.eventService.emit({ kind: 'cache.invalidated', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, cacheRevision: cacheEntry.revision });
226
+ this.eventService.emit({
227
+ kind: 'transaction.applied',
228
+ sessionId: session.sessionId,
229
+ canonicalPathKey: session.canonicalPathKey,
230
+ revision: session.revision,
231
+ });
232
+ this.eventService.emit({
233
+ kind: 'cache.invalidated',
234
+ sessionId: session.sessionId,
235
+ canonicalPathKey: session.canonicalPathKey,
236
+ revision: session.revision,
237
+ cacheRevision: cacheEntry.revision,
238
+ });
118
239
 
119
240
  return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
120
241
  sessionId: session.sessionId,
@@ -122,38 +243,73 @@ export class AuthoringService {
122
243
  });
123
244
  }
124
245
 
125
- public async saveSession(
126
- input: { sessionId: string; expectedRevision?: number; targetPath?: string },
127
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>> {
246
+ public async saveSession(input: SaveSessionInput): Promise<
247
+ BackendResult<
248
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
249
+ | SessionNotFoundError
250
+ | SessionStaleWriteError
251
+ | SavePartialFailureError
252
+ | MaterializeValidationFailedError
253
+ | MaterializeWriteFailedError
254
+ | PathPolicyViolationError
255
+ | InProcessLockConflictError
256
+ | BackendCapabilityUnavailableError
257
+ >
258
+ > {
259
+ if (input.force === true || input.mode === 'materializeCleanSession') {
260
+ return this.materializeSession({
261
+ sessionId: input.sessionId,
262
+ expectedRevision: input.expectedRevision,
263
+ targetPath: input.targetPath,
264
+ fileSystem: input.fileSystem,
265
+ mode: 'fullProject',
266
+ reason: 'force_save',
267
+ });
268
+ }
128
269
  const startedAt = Date.now();
129
270
  const session = this.context.sessions.get(input.sessionId);
130
271
  if (!session || session.closed) {
131
272
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
132
273
  }
133
- if (!this.context.fileSystem) {
134
- return failure('authoring', startedAt, {
135
- code: 'capability_unavailable',
136
- message: 'saveSession requires an injected BackendFileSystem adapter.',
137
- capability: 'fileSystem',
138
- requiredAdapter: 'BackendFileSystem',
139
- }, toSessionSnapshot(session, this.context.capabilities), {
140
- sessionId: session.sessionId,
141
- revision: session.revision,
142
- });
274
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
275
+ if (!fileSystem) {
276
+ return failure(
277
+ 'authoring',
278
+ startedAt,
279
+ createCapabilityUnavailableError(
280
+ 'saveSession requires an injected BackendFileSystem adapter.',
281
+ ),
282
+ toSessionSnapshot(session, this.context.capabilities),
283
+ {
284
+ sessionId: session.sessionId,
285
+ revision: session.revision,
286
+ },
287
+ );
143
288
  }
144
289
  if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
145
- return failure('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
146
- sessionId: session.sessionId,
147
- revision: session.revision,
148
- });
290
+ return failure(
291
+ 'authoring',
292
+ startedAt,
293
+ createStaleWriteError(session, input.expectedRevision),
294
+ toSessionSnapshot(session, this.context.capabilities),
295
+ {
296
+ sessionId: session.sessionId,
297
+ revision: session.revision,
298
+ },
299
+ );
149
300
  }
150
- const fileSystem = this.context.fileSystem;
151
301
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
152
302
  if (targetViolation) {
153
- return failure('authoring', startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
154
- sessionId: session.sessionId,
155
- revision: session.revision,
156
- });
303
+ return failure(
304
+ 'authoring',
305
+ startedAt,
306
+ targetViolation,
307
+ toSessionSnapshot(session, this.context.capabilities),
308
+ {
309
+ sessionId: session.sessionId,
310
+ revision: session.revision,
311
+ },
312
+ );
157
313
  }
158
314
  if (!session.dirty) {
159
315
  return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
@@ -164,36 +320,296 @@ export class AuthoringService {
164
320
 
165
321
  const committedPaths: string[] = [];
166
322
  const failedPaths: string[] = [];
167
- this.eventService.emit({ kind: 'save.started', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
323
+ this.eventService.emit({
324
+ kind: 'save.started',
325
+ sessionId: session.sessionId,
326
+ canonicalPathKey: session.canonicalPathKey,
327
+ revision: session.revision,
328
+ });
168
329
  try {
169
330
  const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
170
331
  await writer.write(materializeUamProject(session.project), session.fairyPath);
171
332
  session.lastSavedRevision = session.revision;
172
333
  session.dirty = false;
173
334
  const cacheEntry = this.cacheService.refreshSession(session);
174
- this.eventService.emit({ kind: 'save.completed', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
175
- this.eventService.emit({ kind: 'cache.updated', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, cacheRevision: cacheEntry.revision });
335
+ this.eventService.emit({
336
+ kind: 'save.completed',
337
+ sessionId: session.sessionId,
338
+ canonicalPathKey: session.canonicalPathKey,
339
+ revision: session.revision,
340
+ });
341
+ this.eventService.emit({
342
+ kind: 'cache.updated',
343
+ sessionId: session.sessionId,
344
+ canonicalPathKey: session.canonicalPathKey,
345
+ revision: session.revision,
346
+ cacheRevision: cacheEntry.revision,
347
+ });
176
348
  return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
177
349
  sessionId: session.sessionId,
178
350
  revision: session.revision,
179
351
  });
180
352
  } catch (error) {
181
353
  this.cacheService.invalidateSession(session);
182
- this.eventService.emit({ kind: 'save.failed', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
183
- return failure('authoring', startedAt, {
184
- code: 'save_partial_failure',
185
- message: error instanceof Error ? error.message : String(error),
354
+ this.eventService.emit({
355
+ kind: 'save.failed',
186
356
  sessionId: session.sessionId,
187
357
  canonicalPathKey: session.canonicalPathKey,
188
- attemptedRevision: session.revision,
189
- lastSavedRevision: session.lastSavedRevision,
190
- committedPaths,
191
- failedPaths,
192
- diskMayBePartiallyUpdated: true,
193
- }, toSessionSnapshot(session, this.context.capabilities), {
358
+ revision: session.revision,
359
+ });
360
+ return failure(
361
+ 'authoring',
362
+ startedAt,
363
+ {
364
+ code: 'save_partial_failure',
365
+ message: error instanceof Error ? error.message : String(error),
366
+ sessionId: session.sessionId,
367
+ canonicalPathKey: session.canonicalPathKey,
368
+ attemptedRevision: session.revision,
369
+ lastSavedRevision: session.lastSavedRevision,
370
+ committedPaths,
371
+ failedPaths,
372
+ diskMayBePartiallyUpdated: true,
373
+ },
374
+ toSessionSnapshot(session, this.context.capabilities),
375
+ {
376
+ sessionId: session.sessionId,
377
+ revision: session.revision,
378
+ },
379
+ );
380
+ }
381
+ }
382
+
383
+ public async materializeSession(input: MaterializeSessionInput): Promise<
384
+ BackendResult<
385
+ MaterializeSessionSnapshot,
386
+ | SessionNotFoundError
387
+ | SessionStaleWriteError
388
+ | MaterializeValidationFailedError
389
+ | MaterializeWriteFailedError
390
+ | PathPolicyViolationError
391
+ | InProcessLockConflictError
392
+ | BackendCapabilityUnavailableError
393
+ >
394
+ > {
395
+ const startedAt = Date.now();
396
+ const session = this.context.sessions.get(input.sessionId);
397
+ if (!session || session.closed) {
398
+ return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
399
+ }
400
+ if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
401
+ return failure(
402
+ 'authoring',
403
+ startedAt,
404
+ createStaleWriteError(session, input.expectedRevision),
405
+ toSessionSnapshot(session, this.context.capabilities),
406
+ {
407
+ sessionId: session.sessionId,
408
+ revision: session.revision,
409
+ },
410
+ );
411
+ }
412
+
413
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
414
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
415
+ if (!fileSystem) {
416
+ return failure(
417
+ 'authoring',
418
+ startedAt,
419
+ createCapabilityUnavailableError(
420
+ 'materializeSession requires an injected BackendFileSystem adapter.',
421
+ ),
422
+ toSessionSnapshot(session, this.context.capabilities),
423
+ {
424
+ sessionId: session.sessionId,
425
+ revision: session.revision,
426
+ },
427
+ );
428
+ }
429
+
430
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
431
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
432
+ if (targetViolation) {
433
+ return failure(
434
+ 'authoring',
435
+ startedAt,
436
+ targetViolation,
437
+ toSessionSnapshot(session, this.context.capabilities),
438
+ {
439
+ sessionId: session.sessionId,
440
+ revision: session.revision,
441
+ },
442
+ );
443
+ }
444
+
445
+ if (storageTarget) {
446
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
447
+ if (holderSessionId && holderSessionId !== session.sessionId) {
448
+ return failure(
449
+ 'authoring',
450
+ startedAt,
451
+ {
452
+ code: 'lock_conflict',
453
+ kind: 'in_process_session_exists',
454
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
455
+ canonicalPathKey: storageTarget.canonicalPathKey,
456
+ holderSessionId,
457
+ },
458
+ toSessionSnapshot(session, this.context.capabilities),
459
+ {
460
+ sessionId: session.sessionId,
461
+ revision: session.revision,
462
+ },
463
+ );
464
+ }
465
+ }
466
+
467
+ const diagnostics = validationDiagnostics(session.project);
468
+ if (diagnostics.length > 0) {
469
+ const error: MaterializeValidationFailedError = {
470
+ code: 'materialize_validation_failed',
471
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
194
472
  sessionId: session.sessionId,
473
+ canonicalPathKey: session.canonicalPathKey,
474
+ issueCount: diagnostics.length,
475
+ diagnostics,
476
+ };
477
+ return failure(
478
+ 'authoring',
479
+ startedAt,
480
+ error,
481
+ toSessionSnapshot(session, this.context.capabilities),
482
+ {
483
+ sessionId: session.sessionId,
484
+ revision: session.revision,
485
+ diagnostics,
486
+ },
487
+ );
488
+ }
489
+
490
+ let document: ReturnType<typeof materializeUamProject>;
491
+ try {
492
+ document = materializeUamProject(session.project);
493
+ } catch (error) {
494
+ const diagnosticsFromError: BackendDiagnostic[] = [
495
+ {
496
+ code: 'materialize_validation_failed',
497
+ message: error instanceof Error ? error.message : String(error),
498
+ severity: 'error',
499
+ operationKind: 'materializeSession',
500
+ },
501
+ ];
502
+ return failure(
503
+ 'authoring',
504
+ startedAt,
505
+ {
506
+ code: 'materialize_validation_failed',
507
+ message: error instanceof Error ? error.message : String(error),
508
+ sessionId: session.sessionId,
509
+ canonicalPathKey: session.canonicalPathKey,
510
+ issueCount: diagnosticsFromError.length,
511
+ diagnostics: diagnosticsFromError,
512
+ },
513
+ toSessionSnapshot(session, this.context.capabilities),
514
+ {
515
+ sessionId: session.sessionId,
516
+ revision: session.revision,
517
+ diagnostics: diagnosticsFromError,
518
+ },
519
+ );
520
+ }
521
+
522
+ const writtenPaths: string[] = [];
523
+ const failedPaths: string[] = [];
524
+ const skippedPaths: string[] = [];
525
+ this.eventService.emit({
526
+ kind: 'save.started',
527
+ sessionId: session.sessionId,
528
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
529
+ revision: session.revision,
530
+ });
531
+ try {
532
+ const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
533
+ await writer.write(document, fairyPath);
534
+ if (storageTarget) {
535
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
536
+ session.fileSystem = storageTarget.fileSystem;
537
+ session.fairyPath = storageTarget.fairyPath;
538
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
539
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
540
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
541
+ }
542
+ session.lastSavedRevision = session.revision;
543
+ session.dirty = false;
544
+ const cacheEntry = this.cacheService.refreshSession(session);
545
+ this.eventService.emit({
546
+ kind: 'save.completed',
547
+ sessionId: session.sessionId,
548
+ canonicalPathKey: session.canonicalPathKey,
549
+ revision: session.revision,
550
+ });
551
+ this.eventService.emit({
552
+ kind: 'cache.updated',
553
+ sessionId: session.sessionId,
554
+ canonicalPathKey: session.canonicalPathKey,
555
+ revision: session.revision,
556
+ cacheRevision: cacheEntry.revision,
557
+ });
558
+ return success(
559
+ 'authoring',
560
+ startedAt,
561
+ toMaterializeSnapshot(session, this.context.capabilities, {
562
+ reason: input.reason,
563
+ writtenPaths,
564
+ skippedPaths,
565
+ diagnostics: [],
566
+ }),
567
+ {
568
+ sessionId: session.sessionId,
569
+ revision: session.revision,
570
+ },
571
+ );
572
+ } catch (error) {
573
+ const diagnosticsFromError: BackendDiagnostic[] = [
574
+ {
575
+ code: 'write_failed',
576
+ message: error instanceof Error ? error.message : String(error),
577
+ severity: 'error',
578
+ path: failedPaths[0],
579
+ operationKind: 'materializeSession',
580
+ },
581
+ ];
582
+ this.cacheService.invalidateSession(session);
583
+ this.eventService.emit({
584
+ kind: 'save.failed',
585
+ sessionId: session.sessionId,
586
+ canonicalPathKey: session.canonicalPathKey,
195
587
  revision: session.revision,
588
+ diagnostics: diagnosticsFromError,
196
589
  });
590
+ return failure(
591
+ 'authoring',
592
+ startedAt,
593
+ {
594
+ code: 'write_failed',
595
+ message: error instanceof Error ? error.message : String(error),
596
+ sessionId: session.sessionId,
597
+ canonicalPathKey: session.canonicalPathKey,
598
+ attemptedRevision: session.revision,
599
+ lastSavedRevision: session.lastSavedRevision,
600
+ writtenPaths,
601
+ failedPaths,
602
+ skippedPaths,
603
+ diagnostics: diagnosticsFromError,
604
+ diskMayBePartiallyUpdated: true,
605
+ },
606
+ toSessionSnapshot(session, this.context.capabilities),
607
+ {
608
+ sessionId: session.sessionId,
609
+ revision: session.revision,
610
+ diagnostics: diagnosticsFromError,
611
+ },
612
+ );
197
613
  }
198
614
  }
199
615
  }
@@ -25,7 +25,8 @@ export interface BackendSessionState {
25
25
  canonicalProjectPath: string;
26
26
  canonicalPathKey: string;
27
27
  lockFilePath: string;
28
- project: import('@openfairygui/core').UamProject;
28
+ fileSystem?: BackendFileSystem;
29
+ project: import('@openfairygui/core/uam').UamProject;
29
30
  revision: number;
30
31
  lastSavedRevision: number;
31
32
  dirty: boolean;