aurasu 0.1.0 → 0.1.2

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.
@@ -0,0 +1,1060 @@
1
+ import {
2
+ createAppState,
3
+ ensureSessionState,
4
+ ensureUiState,
5
+ normalizeAppState,
6
+ } from './app-state.js';
7
+ import {
8
+ applyUiTheme as applySharedUiTheme,
9
+ ensureUiPreferences as ensureSharedUiPreferences,
10
+ inspectUiTheme as inspectSharedUiTheme,
11
+ resetUiPreferences as resetSharedUiPreferences,
12
+ updateUiPreferences as updateSharedUiPreferences,
13
+ } from './ui-theme.js';
14
+ import projectRegistry, { resolveProjectScreen } from './project-registry.js';
15
+ import { createProjectInspector } from './project-inspector.js';
16
+ import { createSceneRegistry } from './scene-registry.js';
17
+ import { createSceneFlow } from './scene-flow.js';
18
+ import { createScreenShell } from './screen-shell.js';
19
+ import { assertRuntimeCapabilities } from './capabilities.js';
20
+
21
+ const PROJECT_STATE_SCHEMA = "aurajs.authored-project-state.v1";
22
+ const PROJECT_CONTINUITY_STATE_SCHEMA = 'aurajs.project-continuity-state.v1';
23
+ const PROJECT_CONTINUITY_STATE_VERSION = 1;
24
+
25
+ function cloneProjectValue(value) {
26
+ if (Array.isArray(value)) {
27
+ return value.map((entry) => cloneProjectValue(entry));
28
+ }
29
+ if (value && typeof value === 'object') {
30
+ const output = {};
31
+ for (const [key, entry] of Object.entries(value)) {
32
+ output[key] = cloneProjectValue(entry);
33
+ }
34
+ return output;
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function captureSceneSnapshot(scene, sceneId = null) {
40
+ if (!scene || typeof scene !== 'object') {
41
+ return {
42
+ ok: false,
43
+ sceneId: sceneId || null,
44
+ reasonCode: 'project_state_scene_missing',
45
+ detail: null,
46
+ hasSnapshot: false,
47
+ snapshot: null,
48
+ };
49
+ }
50
+ const exportFn = typeof scene.exportState === 'function'
51
+ ? scene.exportState
52
+ : typeof scene.getState === 'function'
53
+ ? scene.getState
54
+ : null;
55
+ if (!exportFn) {
56
+ return {
57
+ ok: true,
58
+ sceneId: sceneId || null,
59
+ reasonCode: 'project_state_scene_skipped',
60
+ detail: null,
61
+ hasSnapshot: false,
62
+ snapshot: null,
63
+ };
64
+ }
65
+ try {
66
+ const snapshot = exportFn.call(scene);
67
+ return {
68
+ ok: true,
69
+ sceneId: sceneId || null,
70
+ reasonCode: snapshot == null ? 'project_state_scene_empty' : 'project_state_scene_capture_ok',
71
+ detail: null,
72
+ hasSnapshot: snapshot != null,
73
+ snapshot: snapshot == null ? null : cloneProjectValue(snapshot),
74
+ };
75
+ } catch (error) {
76
+ return {
77
+ ok: false,
78
+ sceneId: sceneId || null,
79
+ reasonCode: 'project_state_scene_capture_failed',
80
+ detail: error instanceof Error ? error.message : String(error),
81
+ hasSnapshot: false,
82
+ snapshot: null,
83
+ };
84
+ }
85
+ }
86
+
87
+ function applySceneSnapshot(scene, snapshot, sceneId = null) {
88
+ if (!scene || snapshot == null) {
89
+ return {
90
+ ok: true,
91
+ sceneId: sceneId || null,
92
+ reasonCode: 'project_state_scene_skipped',
93
+ detail: null,
94
+ restored: false,
95
+ };
96
+ }
97
+ const applyFn = typeof scene.applyState === 'function'
98
+ ? scene.applyState
99
+ : typeof scene.importState === 'function'
100
+ ? scene.importState
101
+ : null;
102
+ if (!applyFn) {
103
+ return {
104
+ ok: true,
105
+ sceneId: sceneId || null,
106
+ reasonCode: 'project_state_scene_skipped',
107
+ detail: null,
108
+ restored: false,
109
+ };
110
+ }
111
+ try {
112
+ const result = applyFn.call(scene, cloneProjectValue(snapshot));
113
+ if (result === false) {
114
+ return {
115
+ ok: false,
116
+ sceneId: sceneId || null,
117
+ reasonCode: 'project_state_scene_apply_failed',
118
+ detail: null,
119
+ restored: false,
120
+ };
121
+ }
122
+ if (result && typeof result === 'object' && result.ok === false) {
123
+ return {
124
+ ok: false,
125
+ sceneId: sceneId || null,
126
+ reasonCode: typeof result.reasonCode === 'string' ? result.reasonCode : 'project_state_scene_apply_failed',
127
+ detail: result.detail || null,
128
+ restored: false,
129
+ };
130
+ }
131
+ return {
132
+ ok: true,
133
+ sceneId: sceneId || null,
134
+ reasonCode: result && typeof result.reasonCode === 'string'
135
+ ? result.reasonCode
136
+ : 'project_state_scene_apply_ok',
137
+ detail: result && typeof result === 'object' ? result.detail || null : null,
138
+ restored: true,
139
+ };
140
+ } catch (error) {
141
+ return {
142
+ ok: false,
143
+ sceneId: sceneId || null,
144
+ reasonCode: 'project_state_scene_apply_failed',
145
+ detail: error instanceof Error ? error.message : String(error),
146
+ restored: false,
147
+ };
148
+ }
149
+ }
150
+
151
+ function normalizeProjectContinuityState(snapshot, fallbackTemplate) {
152
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
153
+ return null;
154
+ }
155
+
156
+ const source = snapshot.continuity && typeof snapshot.continuity === 'object' && !Array.isArray(snapshot.continuity)
157
+ ? snapshot.continuity
158
+ : snapshot;
159
+ const sceneStateSource = source.sceneStates && typeof source.sceneStates === 'object' && !Array.isArray(source.sceneStates)
160
+ ? source.sceneStates
161
+ : source.scenes && typeof source.scenes === 'object' && !Array.isArray(source.scenes)
162
+ ? source.scenes
163
+ : {};
164
+ const sceneStates = {};
165
+ for (const sceneId of Object.keys(sceneStateSource).sort()) {
166
+ sceneStates[sceneId] = cloneProjectValue(sceneStateSource[sceneId]);
167
+ }
168
+
169
+ return {
170
+ schema: PROJECT_CONTINUITY_STATE_SCHEMA,
171
+ version: Number.isInteger(Number(source.version)) && Number(source.version) > 0
172
+ ? Number(source.version)
173
+ : PROJECT_CONTINUITY_STATE_VERSION,
174
+ template: typeof source.template === 'string' && source.template.trim()
175
+ ? source.template.trim()
176
+ : fallbackTemplate,
177
+ activeSceneId: typeof source.activeSceneId === 'string' && source.activeSceneId.trim()
178
+ ? source.activeSceneId.trim()
179
+ : null,
180
+ currentSceneData: cloneProjectValue(source.currentSceneData ?? null),
181
+ appState: source.appState && typeof source.appState === 'object' && !Array.isArray(source.appState)
182
+ ? cloneProjectValue(source.appState)
183
+ : null,
184
+ sceneFlow: source.sceneFlow && typeof source.sceneFlow === 'object' && !Array.isArray(source.sceneFlow)
185
+ ? cloneProjectValue(source.sceneFlow)
186
+ : null,
187
+ screenShell: source.screenShell && typeof source.screenShell === 'object' && !Array.isArray(source.screenShell)
188
+ ? cloneProjectValue(source.screenShell)
189
+ : null,
190
+ sceneStates,
191
+ ownership: source.ownership && typeof source.ownership === 'object' && !Array.isArray(source.ownership)
192
+ ? cloneProjectValue(source.ownership)
193
+ : null,
194
+ };
195
+ }
196
+
197
+ function normalizeContinuitySource(source) {
198
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
199
+ return null;
200
+ }
201
+ return {
202
+ kind: typeof source.kind === 'string' && source.kind.trim() ? source.kind.trim() : null,
203
+ name: typeof source.name === 'string' && source.name.trim() ? source.name.trim() : null,
204
+ path: typeof source.path === 'string' && source.path.trim() ? source.path.trim() : null,
205
+ stateSchemaVersion: typeof source.stateSchemaVersion === 'string' && source.stateSchemaVersion.trim()
206
+ ? source.stateSchemaVersion.trim()
207
+ : null,
208
+ payloadFingerprint: typeof source.payloadFingerprint === 'string' && source.payloadFingerprint.trim()
209
+ ? source.payloadFingerprint.trim()
210
+ : null,
211
+ reasonCode: typeof source.reasonCode === 'string' && source.reasonCode.trim()
212
+ ? source.reasonCode.trim()
213
+ : null,
214
+ migration: source.migration && typeof source.migration === 'object' && !Array.isArray(source.migration)
215
+ ? cloneProjectValue(source.migration)
216
+ : null,
217
+ };
218
+ }
219
+
220
+ function createContinuitySummary(
221
+ sceneResults,
222
+ {
223
+ phase = 'restore',
224
+ reasonCode = phase === 'capture' ? 'project_state_capture_ok' : 'project_state_apply_ok',
225
+ activeSceneId = null,
226
+ continuityVersion = PROJECT_CONTINUITY_STATE_VERSION,
227
+ detail = null,
228
+ source = null,
229
+ } = {},
230
+ ) {
231
+ const normalizedSceneResults = Array.isArray(sceneResults)
232
+ ? sceneResults.map((entry) => cloneProjectValue(entry))
233
+ : [];
234
+ const skippedSceneIds = normalizedSceneResults
235
+ .filter((entry) => entry?.reasonCode === 'project_state_scene_skipped')
236
+ .map((entry) => entry.sceneId)
237
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
238
+ const failedSceneIds = normalizedSceneResults
239
+ .filter((entry) => entry?.ok === false)
240
+ .map((entry) => entry.sceneId)
241
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
242
+ const capturedSceneIds = normalizedSceneResults
243
+ .filter((entry) => entry?.hasSnapshot === true)
244
+ .map((entry) => entry.sceneId)
245
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
246
+ const restoredSceneIds = normalizedSceneResults
247
+ .filter((entry) => entry?.restored === true)
248
+ .map((entry) => entry.sceneId)
249
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
250
+ return {
251
+ phase,
252
+ reasonCode,
253
+ activeSceneId: typeof activeSceneId === 'string' && activeSceneId.trim() ? activeSceneId.trim() : null,
254
+ continuityVersion: Number.isInteger(Number(continuityVersion)) && Number(continuityVersion) > 0
255
+ ? Number(continuityVersion)
256
+ : PROJECT_CONTINUITY_STATE_VERSION,
257
+ totalScenes: normalizedSceneResults.length,
258
+ capturedSceneIds,
259
+ restoredSceneIds,
260
+ skippedSceneIds,
261
+ failedSceneIds,
262
+ detail: typeof detail === 'string' && detail.trim() ? detail.trim() : null,
263
+ source: normalizeContinuitySource(source),
264
+ sceneResults: normalizedSceneResults,
265
+ };
266
+ }
267
+
268
+ export function createApp() {
269
+ const projectTitle = projectRegistry.projectTitle || 'AuraJS Game';
270
+ const appState = createAppState({
271
+ projectTitle,
272
+ template: projectRegistry.template || "blank",
273
+ });
274
+ normalizeAppState(appState, {
275
+ projectTitle,
276
+ template: projectRegistry.template || "blank",
277
+ });
278
+ ensureSharedUiPreferences(appState);
279
+ let sceneRegistry = null;
280
+ let sceneFlow = null;
281
+ let screenShell = null;
282
+ let seededSceneId = null;
283
+ let runtimeStarted = false;
284
+ let paused = false;
285
+ const recordingState = {
286
+ supported: false,
287
+ active: false,
288
+ audioSupported: false,
289
+ path: null,
290
+ reasonCode: null,
291
+ };
292
+ const currentSceneId = () => sceneFlow?.getState().currentSceneId || seededSceneId || null;
293
+ function readPendingDevRestoreMeta() {
294
+ const runtime = globalThis.aura;
295
+ return normalizeContinuitySource(
296
+ runtime?.__aurajsPendingDevRestore && typeof runtime.__aurajsPendingDevRestore === 'object'
297
+ ? runtime.__aurajsPendingDevRestore
298
+ : runtime?.__aurajsDevRestore,
299
+ );
300
+ }
301
+ function ensureRuntimeContinuityState() {
302
+ normalizeAppState(appState, {
303
+ projectTitle,
304
+ template: projectRegistry.template || "blank",
305
+ });
306
+ const runtimeState = appState.runtime && typeof appState.runtime === 'object' && !Array.isArray(appState.runtime)
307
+ ? appState.runtime
308
+ : {};
309
+ const continuityState = runtimeState.continuity && typeof runtimeState.continuity === 'object' && !Array.isArray(runtimeState.continuity)
310
+ ? runtimeState.continuity
311
+ : {
312
+ lastCapture: null,
313
+ lastRestore: null,
314
+ };
315
+ runtimeState.continuity = continuityState;
316
+ appState.runtime = runtimeState;
317
+ return continuityState;
318
+ }
319
+ function recordContinuityCapture(summary) {
320
+ const continuityState = ensureRuntimeContinuityState();
321
+ continuityState.lastCapture = summary ? cloneProjectValue(summary) : null;
322
+ return continuityState.lastCapture;
323
+ }
324
+ function recordContinuityRestore(summary) {
325
+ const continuityState = ensureRuntimeContinuityState();
326
+ continuityState.lastRestore = summary ? cloneProjectValue(summary) : null;
327
+ return continuityState.lastRestore;
328
+ }
329
+ function replaceAppState(nextValue) {
330
+ if (!nextValue || typeof nextValue !== 'object' || Array.isArray(nextValue)) {
331
+ return appState;
332
+ }
333
+ for (const key of Object.keys(appState)) {
334
+ delete appState[key];
335
+ }
336
+ for (const [key, value] of Object.entries(cloneProjectValue(nextValue))) {
337
+ appState[key] = value;
338
+ }
339
+ normalizeAppState(appState, {
340
+ projectTitle,
341
+ template: projectRegistry.template || "blank",
342
+ });
343
+ ensureSharedUiPreferences(appState);
344
+ paused = appState.runtime?.paused === true;
345
+ return appState;
346
+ }
347
+ function syncAppStateRuntime(previousSceneId = null) {
348
+ normalizeAppState(appState, {
349
+ projectTitle,
350
+ template: projectRegistry.template || "blank",
351
+ });
352
+ const recordingSnapshot = getRecordingState();
353
+ const runtimeState = appState.runtime && typeof appState.runtime === 'object' && !Array.isArray(appState.runtime)
354
+ ? appState.runtime
355
+ : {};
356
+ const continuityState = runtimeState.continuity && typeof runtimeState.continuity === 'object' && !Array.isArray(runtimeState.continuity)
357
+ ? cloneProjectValue(runtimeState.continuity)
358
+ : {
359
+ lastCapture: null,
360
+ lastRestore: null,
361
+ };
362
+ const shellState = typeof screenShell?.getState === 'function'
363
+ ? screenShell.getState()
364
+ : null;
365
+ appState.projectTitle = projectTitle;
366
+ appState.template = projectRegistry.template || "blank";
367
+ appState.runtime = {
368
+ ...runtimeState,
369
+ startSceneId: projectRegistry.startSceneId || null,
370
+ currentSceneId: currentSceneId(),
371
+ previousSceneId: previousSceneId ?? runtimeState.previousSceneId ?? null,
372
+ hudScreenId: shellState?.hud?.screenId || null,
373
+ overlayScreenId: shellState?.overlay?.screenId || null,
374
+ modalScreenIds: Array.isArray(shellState?.modals) ? shellState.modals.map((entry) => entry.screenId) : [],
375
+ paused,
376
+ recordingSupported: recordingSnapshot.supported === true,
377
+ recordingActive: recordingSnapshot.active === true,
378
+ continuity: continuityState,
379
+ };
380
+ }
381
+ const appContext = {
382
+ projectTitle,
383
+ projectRegistry,
384
+ appState,
385
+ replaceAppState,
386
+ getAppState() {
387
+ return appState;
388
+ },
389
+ ensureSessionState(featureKey, seed = {}) {
390
+ return ensureSessionState(appState, featureKey, seed);
391
+ },
392
+ ensureUiState(featureKey, seed = {}) {
393
+ return ensureUiState(appState, featureKey, seed);
394
+ },
395
+ ensureUiPreferences(seed = null) {
396
+ return ensureSharedUiPreferences(appState, seed);
397
+ },
398
+ getSessionState(featureKey, fallback = null) {
399
+ const key = typeof featureKey === 'string' ? featureKey.trim() : '';
400
+ if (!key) return fallback;
401
+ return appState.session?.[key] ?? fallback;
402
+ },
403
+ getUiState(featureKey, fallback = null) {
404
+ const key = typeof featureKey === 'string' ? featureKey.trim() : '';
405
+ if (!key) return fallback;
406
+ return appState.ui?.[key] ?? fallback;
407
+ },
408
+ getUiPreferences() {
409
+ return ensureSharedUiPreferences(appState);
410
+ },
411
+ updateUiPreferences(patch = {}) {
412
+ return updateSharedUiPreferences(appState, patch);
413
+ },
414
+ resetUiPreferences(seed = null) {
415
+ return resetSharedUiPreferences(appState, seed);
416
+ },
417
+ applyUiTheme(options = {}) {
418
+ return applySharedUiTheme(appState, options);
419
+ },
420
+ getUiThemeState() {
421
+ return inspectSharedUiTheme(appState);
422
+ },
423
+ getActiveSceneId: currentSceneId,
424
+ getCurrentScenePayload() {
425
+ return sceneFlow?.current?.()?.data ?? null;
426
+ },
427
+ getSceneFlowState() {
428
+ return sceneFlow?.getState() || null;
429
+ },
430
+ getScreenShellState() {
431
+ return screenShell?.getState() || null;
432
+ },
433
+ replaceScene(sceneId, data = null) {
434
+ const previousSceneId = currentSceneId();
435
+ const result = sceneFlow?.replace(sceneId, data);
436
+ if (!result?.ok) {
437
+ throw new Error(`Unknown scene "${sceneId}". [reason:${result?.reasonCode || 'scene_flow_unavailable'}]`);
438
+ }
439
+ syncAppStateRuntime(previousSceneId);
440
+ return result;
441
+ },
442
+ pushScene(sceneId, data = null) {
443
+ const previousSceneId = currentSceneId();
444
+ const result = sceneFlow?.push(sceneId, data);
445
+ if (!result?.ok) {
446
+ throw new Error(`Unable to push scene "${sceneId}". [reason:${result?.reasonCode || 'scene_flow_unavailable'}]`);
447
+ }
448
+ syncAppStateRuntime(previousSceneId);
449
+ return result;
450
+ },
451
+ popScene(result = null) {
452
+ const previousSceneId = currentSceneId();
453
+ const outcome = sceneFlow?.pop(result);
454
+ if (!outcome?.ok) {
455
+ throw new Error(`Unable to pop scene. [reason:${outcome?.reasonCode || 'scene_flow_unavailable'}]`);
456
+ }
457
+ syncAppStateRuntime(previousSceneId);
458
+ return outcome;
459
+ },
460
+ canPopScene() {
461
+ return sceneFlow?.canPop() === true;
462
+ },
463
+ setHudScreen(screenId, data = null) {
464
+ const result = screenShell?.setHud(screenId, data) || null;
465
+ syncAppStateRuntime(currentSceneId());
466
+ return result;
467
+ },
468
+ clearHudScreen() {
469
+ const result = screenShell?.clearHud() || null;
470
+ syncAppStateRuntime(currentSceneId());
471
+ return result;
472
+ },
473
+ showOverlayScreen(screenId, data = null) {
474
+ const result = screenShell?.showOverlay(screenId, data) || null;
475
+ syncAppStateRuntime(currentSceneId());
476
+ return result;
477
+ },
478
+ clearOverlayScreen() {
479
+ const result = screenShell?.clearOverlay() || null;
480
+ syncAppStateRuntime(currentSceneId());
481
+ return result;
482
+ },
483
+ pushModalScreen(screenId, data = null) {
484
+ const result = screenShell?.pushModal(screenId, data) || null;
485
+ syncAppStateRuntime(currentSceneId());
486
+ return result;
487
+ },
488
+ popModalScreen(result = null) {
489
+ const outcome = screenShell?.popModal(result) || null;
490
+ syncAppStateRuntime(currentSceneId());
491
+ return outcome;
492
+ },
493
+ jumpToScene(sceneId, data = null) {
494
+ return jumpToScene(sceneId, data);
495
+ },
496
+ swapScene(sceneId, data = null) {
497
+ return this.replaceScene(sceneId, data);
498
+ },
499
+ setPaused(nextValue = true) {
500
+ return setPaused(nextValue);
501
+ },
502
+ togglePaused() {
503
+ return togglePaused();
504
+ },
505
+ restartCurrentScene() {
506
+ return restartCurrentScene();
507
+ },
508
+ restartStartScene() {
509
+ return restartStartScene();
510
+ },
511
+ get sceneRegistry() {
512
+ return sceneRegistry;
513
+ },
514
+ get sceneFlow() {
515
+ return sceneFlow;
516
+ },
517
+ get screenShell() {
518
+ return screenShell;
519
+ },
520
+ };
521
+ sceneRegistry = createSceneRegistry(appContext);
522
+ seededSceneId = sceneRegistry.defaultSceneId || Object.keys(sceneRegistry.scenes || {})[0] || null;
523
+ const resolveScene = (sceneId) => sceneRegistry.scenes[sceneId] || null;
524
+ screenShell = createScreenShell({ resolveScreen: resolveProjectScreen });
525
+ sceneFlow = createSceneFlow({
526
+ initialSceneId: seededSceneId,
527
+ resolveScene,
528
+ onEnter(info) {
529
+ if (!runtimeStarted) return;
530
+ info.current?.scene?.setup?.(info.current.data);
531
+ info.current?.scene?.onEnter?.(info);
532
+ },
533
+ onExit(info) {
534
+ info.current?.scene?.onExit?.(info);
535
+ info.current?.scene?.shutdown?.(info);
536
+ },
537
+ onPause(info) {
538
+ info.current?.scene?.onPause?.(info);
539
+ },
540
+ onResume(info) {
541
+ info.current?.scene?.onResume?.(info);
542
+ },
543
+ });
544
+ syncAppStateRuntime();
545
+ const activeScene = () => {
546
+ const sceneId = currentSceneId();
547
+ return sceneId ? sceneRegistry.scenes[sceneId] || null : null;
548
+ };
549
+ function clearPlaytestScreens() {
550
+ const shellState = typeof screenShell?.getState === 'function' ? screenShell.getState() : null;
551
+ if (!shellState || typeof screenShell?.applyState !== 'function') {
552
+ return null;
553
+ }
554
+ return screenShell.applyState({
555
+ schema: shellState.schema || 'aurajs.screen-shell.v1',
556
+ hud: shellState.hud || null,
557
+ overlay: null,
558
+ modals: [],
559
+ mutationCount: Number.isInteger(Number(shellState.mutationCount))
560
+ ? Number(shellState.mutationCount) + 1
561
+ : 1,
562
+ });
563
+ }
564
+ function setPaused(nextValue = false) {
565
+ paused = nextValue === true;
566
+ syncAppStateRuntime();
567
+ return paused;
568
+ }
569
+ function togglePaused() {
570
+ return setPaused(!paused);
571
+ }
572
+ function getPaused() {
573
+ return paused === true;
574
+ }
575
+ function getRecordingState() {
576
+ const runtime = globalThis.aura;
577
+ const state = runtime?.window && typeof runtime.window.getRecordingState === 'function'
578
+ ? runtime.window.getRecordingState()
579
+ : null;
580
+ recordingState.supported = state?.supported === true;
581
+ recordingState.active = state?.active === true;
582
+ recordingState.audioSupported = state?.audioSupported === true;
583
+ recordingState.path = typeof state?.path === 'string' ? state.path : null;
584
+ recordingState.reasonCode = typeof state?.reasonCode === 'string' ? state.reasonCode : null;
585
+ return {
586
+ supported: recordingState.supported === true,
587
+ active: recordingState.active === true,
588
+ audioSupported: recordingState.audioSupported === true,
589
+ path: recordingState.path || null,
590
+ reasonCode: recordingState.reasonCode || null,
591
+ };
592
+ }
593
+ function toggleRecording() {
594
+ const runtime = globalThis.aura;
595
+ if (!runtime?.window || typeof runtime.window.toggleRecording !== 'function') {
596
+ return {
597
+ ok: false,
598
+ reasonCode: 'recording_unavailable',
599
+ };
600
+ }
601
+ runtime.window.toggleRecording();
602
+ return {
603
+ ok: true,
604
+ reasonCode: 'recording_toggle_requested',
605
+ };
606
+ }
607
+ function restartCurrentScene() {
608
+ const current = sceneFlow?.current?.();
609
+ if (!current?.sceneId) {
610
+ return {
611
+ ok: false,
612
+ reasonCode: 'scene_flow_unavailable',
613
+ };
614
+ }
615
+ clearPlaytestScreens();
616
+ paused = false;
617
+ const result = sceneFlow.replace(current.sceneId, cloneProjectValue(current.data));
618
+ if (result?.ok) {
619
+ syncAppStateRuntime(current.sceneId);
620
+ }
621
+ return result;
622
+ }
623
+ function jumpToScene(sceneId, data = null) {
624
+ const targetSceneId = typeof sceneId === 'string' ? sceneId.trim() : '';
625
+ if (!targetSceneId) {
626
+ return {
627
+ ok: false,
628
+ reasonCode: 'scene_flow_invalid_scene_id',
629
+ };
630
+ }
631
+ if (!sceneRegistry?.scenes?.[targetSceneId]) {
632
+ return {
633
+ ok: false,
634
+ reasonCode: 'scene_flow_unknown_scene',
635
+ };
636
+ }
637
+ const previousSceneId = currentSceneId();
638
+ clearPlaytestScreens();
639
+ while (sceneFlow?.canPop?.()) {
640
+ const popped = sceneFlow.pop();
641
+ if (!popped?.ok) {
642
+ syncAppStateRuntime(previousSceneId);
643
+ return popped;
644
+ }
645
+ }
646
+ paused = false;
647
+ const result = sceneFlow.replace(targetSceneId, cloneProjectValue(data));
648
+ if (result?.ok) {
649
+ syncAppStateRuntime(previousSceneId);
650
+ }
651
+ return result;
652
+ }
653
+ function restartStartScene() {
654
+ const startSceneId = projectRegistry.startSceneId || seededSceneId || currentSceneId();
655
+ if (!startSceneId) {
656
+ return {
657
+ ok: false,
658
+ reasonCode: 'scene_flow_unavailable',
659
+ };
660
+ }
661
+ return jumpToScene(startSceneId, null);
662
+ }
663
+ function exportProjectState() {
664
+ const scenes = {};
665
+ const sceneCaptureResults = [];
666
+ for (const sceneId of Object.keys(sceneRegistry.scenes || {}).sort()) {
667
+ const captureResult = captureSceneSnapshot(sceneRegistry.scenes[sceneId], sceneId);
668
+ sceneCaptureResults.push({
669
+ sceneId,
670
+ ok: captureResult.ok === true,
671
+ reasonCode: captureResult.reasonCode || null,
672
+ detail: captureResult.detail || null,
673
+ hasSnapshot: captureResult.hasSnapshot === true,
674
+ });
675
+ if (captureResult.snapshot != null) {
676
+ scenes[sceneId] = captureResult.snapshot;
677
+ }
678
+ }
679
+ const captureReasonCode = sceneCaptureResults.some((entry) => entry.ok !== true || entry.reasonCode === 'project_state_scene_skipped')
680
+ ? 'project_state_capture_partial'
681
+ : 'project_state_capture_ok';
682
+ recordContinuityCapture(createContinuitySummary(sceneCaptureResults, {
683
+ phase: 'capture',
684
+ reasonCode: captureReasonCode,
685
+ activeSceneId: currentSceneId(),
686
+ }));
687
+ syncAppStateRuntime();
688
+ const continuity = normalizeProjectContinuityState({
689
+ schema: PROJECT_CONTINUITY_STATE_SCHEMA,
690
+ version: PROJECT_CONTINUITY_STATE_VERSION,
691
+ template: projectRegistry.template || "blank",
692
+ activeSceneId: currentSceneId(),
693
+ currentSceneData: sceneFlow?.current?.()?.data ?? null,
694
+ appState,
695
+ sceneFlow: sceneFlow.getState(),
696
+ screenShell: screenShell.getState(),
697
+ sceneStates: scenes,
698
+ ownership: projectRegistry.continuity || null,
699
+ }, projectRegistry.template || "blank");
700
+ return {
701
+ schema: PROJECT_STATE_SCHEMA,
702
+ template: continuity.template,
703
+ activeSceneId: continuity.activeSceneId,
704
+ currentSceneData: continuity.currentSceneData,
705
+ appState: continuity.appState,
706
+ sceneFlow: continuity.sceneFlow,
707
+ screenShell: continuity.screenShell,
708
+ scenes: continuity.sceneStates,
709
+ continuity,
710
+ };
711
+ }
712
+ function applyProjectState(snapshot) {
713
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
714
+ return { ok: false, reasonCode: 'project_state_invalid' };
715
+ }
716
+ if (snapshot.schema && snapshot.schema !== PROJECT_STATE_SCHEMA) {
717
+ return { ok: false, reasonCode: 'project_state_invalid' };
718
+ }
719
+ const continuity = normalizeProjectContinuityState(
720
+ snapshot,
721
+ projectRegistry.template || "blank",
722
+ );
723
+ if (!continuity) {
724
+ return { ok: false, reasonCode: 'project_state_invalid' };
725
+ }
726
+
727
+ const beforeSceneId = currentSceneId();
728
+ const restoreSource = readPendingDevRestoreMeta();
729
+ if (continuity.appState) {
730
+ replaceAppState(continuity.appState);
731
+ }
732
+ const flowResult = sceneFlow.applyState(continuity.sceneFlow || null);
733
+ if (!flowResult?.ok) {
734
+ const continuitySummary = createContinuitySummary([], {
735
+ phase: 'restore',
736
+ reasonCode: flowResult?.reasonCode || 'project_state_apply_failed',
737
+ activeSceneId: beforeSceneId,
738
+ continuityVersion: continuity.version,
739
+ detail: flowResult?.detail || null,
740
+ source: restoreSource,
741
+ });
742
+ recordContinuityRestore(continuitySummary);
743
+ syncAppStateRuntime(beforeSceneId);
744
+ return {
745
+ ok: false,
746
+ reasonCode: flowResult?.reasonCode || 'project_state_apply_failed',
747
+ detail: flowResult?.detail || null,
748
+ continuity: continuitySummary,
749
+ };
750
+ }
751
+
752
+ const shellResult = screenShell.applyState(continuity.screenShell || {
753
+ schema: 'aurajs.screen-shell.v1',
754
+ hud: null,
755
+ overlay: null,
756
+ modals: [],
757
+ mutationCount: 0,
758
+ });
759
+ if (!shellResult?.ok) {
760
+ const continuitySummary = createContinuitySummary([], {
761
+ phase: 'restore',
762
+ reasonCode: shellResult?.reasonCode || 'project_state_apply_failed',
763
+ activeSceneId: currentSceneId(),
764
+ continuityVersion: continuity.version,
765
+ detail: shellResult?.detail || null,
766
+ source: restoreSource,
767
+ });
768
+ recordContinuityRestore(continuitySummary);
769
+ syncAppStateRuntime(beforeSceneId);
770
+ return {
771
+ ok: false,
772
+ reasonCode: shellResult?.reasonCode || 'project_state_apply_failed',
773
+ detail: shellResult?.detail || null,
774
+ continuity: continuitySummary,
775
+ };
776
+ }
777
+
778
+ const sceneSnapshots = continuity.sceneStates;
779
+ const sceneRestoreResults = [];
780
+ for (const [sceneId, sceneSnapshot] of Object.entries(sceneSnapshots)) {
781
+ const result = applySceneSnapshot(sceneRegistry.scenes[sceneId] || null, sceneSnapshot, sceneId);
782
+ sceneRestoreResults.push({
783
+ sceneId,
784
+ ok: result.ok === true,
785
+ reasonCode: result.reasonCode || null,
786
+ detail: result.detail || null,
787
+ restored: result.restored === true,
788
+ });
789
+ if (!result.ok) {
790
+ const continuitySummary = createContinuitySummary(sceneRestoreResults, {
791
+ phase: 'restore',
792
+ reasonCode: result.reasonCode || 'project_state_scene_apply_failed',
793
+ activeSceneId: currentSceneId(),
794
+ continuityVersion: continuity.version,
795
+ detail: result.detail || null,
796
+ source: restoreSource,
797
+ });
798
+ recordContinuityRestore(continuitySummary);
799
+ syncAppStateRuntime(beforeSceneId);
800
+ return {
801
+ ok: false,
802
+ reasonCode: result.reasonCode || 'project_state_scene_apply_failed',
803
+ failedSceneId: sceneId,
804
+ detail: result.detail || null,
805
+ continuity: continuitySummary,
806
+ };
807
+ }
808
+ }
809
+
810
+ const afterSceneId = currentSceneId();
811
+ if (runtimeStarted && afterSceneId && afterSceneId !== beforeSceneId) {
812
+ const scene = activeScene();
813
+ scene?.setup?.(sceneFlow.current()?.data);
814
+ }
815
+ const restoreReasonCode = sceneRestoreResults.some((entry) => entry.reasonCode === 'project_state_scene_skipped')
816
+ ? 'project_state_apply_partial'
817
+ : 'project_state_apply_ok';
818
+ const continuitySummary = createContinuitySummary(sceneRestoreResults, {
819
+ phase: 'restore',
820
+ reasonCode: restoreReasonCode,
821
+ activeSceneId: afterSceneId || null,
822
+ continuityVersion: continuity.version,
823
+ source: restoreSource,
824
+ });
825
+ recordContinuityRestore(continuitySummary);
826
+ syncAppStateRuntime(beforeSceneId);
827
+
828
+ return {
829
+ ok: true,
830
+ reasonCode: restoreReasonCode,
831
+ activeSceneId: afterSceneId || null,
832
+ continuityVersion: continuity.version,
833
+ continuity: continuitySummary,
834
+ };
835
+ }
836
+ function installProjectStateBridge(app) {
837
+ const auraRef = globalThis.aura;
838
+ if (!auraRef || typeof auraRef !== 'object') {
839
+ return;
840
+ }
841
+ auraRef.project = {
842
+ schemaVersion: PROJECT_STATE_SCHEMA,
843
+ continuitySchemaVersion: PROJECT_CONTINUITY_STATE_SCHEMA,
844
+ getState: () => app.getState(),
845
+ exportState: () => app.getState(),
846
+ getContinuityState: () => app.appState?.runtime?.continuity || null,
847
+ applyState: (snapshot) => app.applyState(snapshot),
848
+ apply: (snapshot) => app.applyState(snapshot),
849
+ };
850
+ }
851
+ const projectInspector = createProjectInspector({
852
+ projectTitle,
853
+ sceneRegistry,
854
+ getActiveSceneId: currentSceneId,
855
+ appState,
856
+ getUiThemeState: () => inspectSharedUiTheme(appState),
857
+ onSelectScene: jumpToScene,
858
+ onRestartScene: restartCurrentScene,
859
+ onRestartApp: restartStartScene,
860
+ getPaused,
861
+ togglePaused,
862
+ getRecordingState,
863
+ toggleRecording,
864
+ });
865
+
866
+ function renderScreenEntry(entry) {
867
+ if (!entry) return;
868
+ const screen = resolveProjectScreen(entry.screenId);
869
+ if (typeof screen === 'function') {
870
+ screen(entry.data || {}, appContext);
871
+ return;
872
+ }
873
+ const drawScreen = typeof screen?.draw === 'function'
874
+ ? screen.draw
875
+ : typeof screen?.render === 'function'
876
+ ? screen.render
877
+ : null;
878
+ if (drawScreen) {
879
+ drawScreen(entry.data || {}, appContext);
880
+ }
881
+ }
882
+
883
+ const app = {
884
+ ...appContext,
885
+ template: projectRegistry.template || "blank",
886
+ projectInspector,
887
+ get projectInspectorEnabled() {
888
+ return projectInspector.enabled;
889
+ },
890
+ get projectInspectorMinimized() {
891
+ return projectInspector.minimized;
892
+ },
893
+ get paused() {
894
+ return paused;
895
+ },
896
+ get activeSceneId() {
897
+ return currentSceneId();
898
+ },
899
+ get sceneRegistry() {
900
+ return sceneRegistry;
901
+ },
902
+ get sceneFlow() {
903
+ return sceneFlow;
904
+ },
905
+ get screenShell() {
906
+ return screenShell;
907
+ },
908
+ inspectProject() {
909
+ return projectInspector.snapshot();
910
+ },
911
+ get appState() {
912
+ return appState;
913
+ },
914
+ getProjectInspectorState() {
915
+ return projectInspector.snapshot();
916
+ },
917
+ getSceneFlowState() {
918
+ return sceneFlow.getState();
919
+ },
920
+ getScreenShellState() {
921
+ return screenShell.getState();
922
+ },
923
+ getState() {
924
+ return exportProjectState();
925
+ },
926
+ exportState() {
927
+ return exportProjectState();
928
+ },
929
+ applyState(snapshot) {
930
+ return applyProjectState(snapshot);
931
+ },
932
+ replaceScene(sceneId, data = null) {
933
+ const previousSceneId = currentSceneId();
934
+ const result = sceneFlow.replace(sceneId, data);
935
+ if (!result.ok) {
936
+ throw new Error(`Unknown scene "${sceneId}". [reason:${result.reasonCode}]`);
937
+ }
938
+ syncAppStateRuntime(previousSceneId);
939
+ return result;
940
+ },
941
+ pushScene(sceneId, data = null) {
942
+ const previousSceneId = currentSceneId();
943
+ const result = sceneFlow.push(sceneId, data);
944
+ if (!result.ok) {
945
+ throw new Error(`Unable to push scene "${sceneId}". [reason:${result.reasonCode}]`);
946
+ }
947
+ syncAppStateRuntime(previousSceneId);
948
+ return result;
949
+ },
950
+ popScene(result = null) {
951
+ const previousSceneId = currentSceneId();
952
+ const outcome = sceneFlow.pop(result);
953
+ if (!outcome.ok) {
954
+ throw new Error(`Unable to pop scene. [reason:${outcome.reasonCode}]`);
955
+ }
956
+ syncAppStateRuntime(previousSceneId);
957
+ return outcome;
958
+ },
959
+ jumpToScene(sceneId, data = null) {
960
+ return jumpToScene(sceneId, data);
961
+ },
962
+ canPopScene() {
963
+ return sceneFlow.canPop();
964
+ },
965
+ setHudScreen(screenId, data = null) {
966
+ const result = screenShell.setHud(screenId, data);
967
+ syncAppStateRuntime(currentSceneId());
968
+ return result;
969
+ },
970
+ clearHudScreen() {
971
+ const result = screenShell.clearHud();
972
+ syncAppStateRuntime(currentSceneId());
973
+ return result;
974
+ },
975
+ showOverlayScreen(screenId, data = null) {
976
+ const result = screenShell.showOverlay(screenId, data);
977
+ syncAppStateRuntime(currentSceneId());
978
+ return result;
979
+ },
980
+ clearOverlayScreen() {
981
+ const result = screenShell.clearOverlay();
982
+ syncAppStateRuntime(currentSceneId());
983
+ return result;
984
+ },
985
+ pushModalScreen(screenId, data = null) {
986
+ const result = screenShell.pushModal(screenId, data);
987
+ syncAppStateRuntime(currentSceneId());
988
+ return result;
989
+ },
990
+ popModalScreen(result = null) {
991
+ const outcome = screenShell.popModal(result);
992
+ syncAppStateRuntime(currentSceneId());
993
+ return outcome;
994
+ },
995
+ toggleProjectInspector(nextValue = null) {
996
+ if (typeof nextValue === 'boolean') {
997
+ return projectInspector.setEnabled(nextValue);
998
+ }
999
+ return projectInspector.toggle();
1000
+ },
1001
+ setProjectInspectorMinimized(nextValue = null) {
1002
+ if (typeof nextValue === 'boolean') {
1003
+ return projectInspector.setMinimized(nextValue);
1004
+ }
1005
+ return projectInspector.setMinimized(false);
1006
+ },
1007
+ toggleProjectInspectorMinimized() {
1008
+ return projectInspector.toggleMinimized();
1009
+ },
1010
+ setPaused(nextValue = true) {
1011
+ return setPaused(nextValue);
1012
+ },
1013
+ togglePaused() {
1014
+ return togglePaused();
1015
+ },
1016
+ getRecordingState() {
1017
+ return getRecordingState();
1018
+ },
1019
+ toggleRecording() {
1020
+ return toggleRecording();
1021
+ },
1022
+ restartCurrentScene() {
1023
+ return restartCurrentScene();
1024
+ },
1025
+ restartStartScene() {
1026
+ return restartStartScene();
1027
+ },
1028
+ setup() {
1029
+ assertRuntimeCapabilities();
1030
+ runtimeStarted = true;
1031
+ paused = false;
1032
+ const setupResult = activeScene()?.setup?.();
1033
+ syncAppStateRuntime();
1034
+ return setupResult;
1035
+ },
1036
+ update(dt) {
1037
+ projectInspector.update();
1038
+ if (!paused) {
1039
+ activeScene()?.update?.(dt);
1040
+ }
1041
+ syncAppStateRuntime();
1042
+ },
1043
+ draw() {
1044
+ applySharedUiTheme(appState);
1045
+ activeScene()?.draw?.();
1046
+ const shellState = screenShell.getState();
1047
+ renderScreenEntry(shellState.hud);
1048
+ renderScreenEntry(shellState.overlay);
1049
+ for (const modal of shellState.modals) {
1050
+ renderScreenEntry(modal);
1051
+ }
1052
+ projectInspector.draw();
1053
+ },
1054
+ swapScene(nextSceneId, data = null) {
1055
+ return appContext.replaceScene(nextSceneId, data);
1056
+ },
1057
+ };
1058
+ installProjectStateBridge(app);
1059
+ return app;
1060
+ }