@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.1

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.
Files changed (78) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +8 -4
  3. package/dist/dist-manifest.json +70 -62
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
  6. package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
  7. package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
  8. package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
  9. package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
  10. package/dist/frqtl/objects/FrqtlApp.js +136 -30
  11. package/dist/frqtl/objects/ObjectFactory.js +4 -5
  12. package/dist/frqtl/objects/RecipeContext.js +617 -9
  13. package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
  14. package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
  15. package/dist/frqtl/objects/record/Record.js +326 -0
  16. package/dist/frqtl/objects/record/open-record.js +22 -0
  17. package/dist/frqtl/objects/schema/index.js +72 -0
  18. package/dist/frqtl/parts/DetectorWall.js +1 -1
  19. package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
  20. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  21. package/dist/frqtl/parts/Wall.js +2 -0
  22. package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
  23. package/dist/frqtl/scenes/absorbing-wall.js +18 -14
  24. package/dist/frqtl/scenes/atomic-model.js +3 -0
  25. package/dist/frqtl/scenes/big-bang.js +3 -0
  26. package/dist/frqtl/scenes/blackbody.js +14 -11
  27. package/dist/frqtl/scenes/circuit-sat.js +7 -4
  28. package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
  29. package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
  30. package/dist/frqtl/scenes/counting-wall.js +19 -15
  31. package/dist/frqtl/scenes/double-slit.js +3 -0
  32. package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
  33. package/dist/frqtl/scenes/dse-one-slit.js +59 -55
  34. package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
  35. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
  36. package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
  37. package/dist/frqtl/scenes/dse-two-slits.js +69 -65
  38. package/dist/frqtl/scenes/empty.js +3 -0
  39. package/dist/frqtl/scenes/laser-buildup.js +23 -16
  40. package/dist/frqtl/scenes/mirror-3d.js +31 -27
  41. package/dist/frqtl/scenes/mirror-route.js +4 -1
  42. package/dist/frqtl/scenes/mirror.js +24 -20
  43. package/dist/frqtl/scenes/omni-3d.js +7 -4
  44. package/dist/frqtl/scenes/omni-source.js +7 -4
  45. package/dist/frqtl/scenes/self-interference.js +13 -10
  46. package/dist/frqtl/scenes/smoking-gun.js +15 -11
  47. package/dist/frqtl/scenes/straight-walk.js +7 -4
  48. package/dist/frqtl/scenes/sun.js +11 -8
  49. package/dist/frqtl/scenes/two-source.js +22 -17
  50. package/dist/frqtl/scenes/wave.js +10 -7
  51. package/dist/shim/bundle/index.js +112 -96
  52. package/dist/shim/bundle/indexHtml.js +66 -39
  53. package/dist/shim/engine/router.js +2 -2
  54. package/dist/shim/index.js +1 -1
  55. package/dist/shim/notebook/format.js +1 -1
  56. package/dist/shim/notebook/store.js +2 -3
  57. package/dist/shim/page-params/index.js +2 -2
  58. package/dist/shim/project/index.js +20 -5
  59. package/dist/shim/project/link.js +182 -37
  60. package/dist/shim/project/manifest.js +151 -0
  61. package/dist/shim/project/parts-module.js +67 -0
  62. package/dist/shim/project/parts-writer.js +457 -0
  63. package/dist/shim/project/record-format.js +345 -0
  64. package/dist/shim/project/specifiers.js +41 -13
  65. package/dist/shim/registry/index.js +1 -1
  66. package/dist/shim/registry/scene-source.js +44 -30
  67. package/dist/shim/registry/walkthrough.js +13 -12
  68. package/dist/shim/render-state/apply.js +11 -14
  69. package/dist/shim/storage/codec-chains.js +88 -0
  70. package/dist/shim/storage/folders.js +205 -0
  71. package/dist/shim/storage/index.js +64 -8
  72. package/dist/shim/storage/make-namespace-store.js +5 -5
  73. package/llms.txt +2 -2
  74. package/package.json +1 -1
  75. package/dist/shim/bundle/frqnb.js +0 -112
  76. package/dist/shim/bundle/manifest.js +0 -95
  77. package/dist/shim/storage/modules.js +0 -249
  78. package/dist/shim/storage/projects.js +0 -150
@@ -1,95 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * lib/shim/bundle/manifest.js — the bundle manifest as an ENVELOPE FERRY
5
- * (E1).
6
- *
7
- * ── The rule (E1): manifest = the Module envelope minus `source` ─────────────
8
- *
9
- * The bundle is a projection of the `lib/shim/storage/modules.js` Module
10
- * envelope. `experiment.js` = `module.source`. The manifest carries EVERYTHING
11
- * ELSE from the envelope, ferried VERBATIM (`{...envelope}` minus `source`) plus
12
- * a small set of bundle-only fields. Critically this module NEVER re-enumerates
13
- * the envelope's field list (`{name: mod.name, renderState: mod.renderState, …}`)
14
- * — that would be a schema mirror of modules.js. It spreads the whole envelope
15
- * and deletes the ONE key (`source`) the sibling `experiment.js` file owns. Any
16
- * field modules.js adds in future rides through automatically; `renderState`
17
- * rides through as an ordinary envelope field, opaque (never parsed here).
18
- *
19
- * ── Bundle-only fields ───────────────────────────────────────────────────────
20
- *
21
- * sdkVersion : string — the `@descix/frqtl-web@<pin>` the index.html imports.
22
- * A VALUE passed IN by the caller (the feature side
23
- * DERIVES it from the built SDK's package.json at export
24
- * time — truth-in-labeling; NEVER a literal in bundle
25
- * code). REQUIRED — FAIL-LOUD when absent.
26
- * preview : string|null — the preview image filename ref (e.g. 'preview.png'),
27
- * or null when no preview was captured.
28
- * descix : {} — reserved inert publish block (DeSciX now-vs-later).
29
- *
30
- * Layer 2 (physics): none — pure metadata projection.
31
- */
32
-
33
- 'use strict';
34
-
35
- /** The envelope key the sibling `experiment.js` file owns (never in the manifest). */
36
- export var SOURCE_KEY = 'source';
37
-
38
- /** Bundle-only manifest keys (NOT envelope fields — stripped on import). */
39
- export var BUNDLE_ONLY_KEYS = ['sdkVersion', 'preview', 'descix'];
40
-
41
- /**
42
- * Build the manifest object from a Module envelope + bundle-only inputs.
43
- *
44
- * @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
45
- * (MUST carry `source`; every other field is ferried verbatim).
46
- * @param {object} opts
47
- * @param {string} opts.sdkVersion — REQUIRED; the CDN pin (e.g. '0.6.0-p0.2').
48
- * @param {string|null} [opts.preview] — preview filename ref, or null/omitted.
49
- * @returns {object} the manifest (JSON-serializable).
50
- */
51
- export function buildManifest(envelope, opts) {
52
- if (!envelope || typeof envelope !== 'object') {
53
- throw new Error('[bundle/manifest] buildManifest(envelope): envelope must be an object.');
54
- }
55
- opts = opts || {};
56
- if (typeof opts.sdkVersion !== 'string' || opts.sdkVersion.length === 0) {
57
- throw new Error('[bundle/manifest] buildManifest: opts.sdkVersion is REQUIRED (a derived value, ' +
58
- 'never a literal) — the index.html CDN pin depends on it.');
59
- }
60
-
61
- // Ferry the WHOLE envelope minus `source` (no per-field re-enumeration).
62
- var manifest = Object.assign({}, envelope);
63
- delete manifest[SOURCE_KEY];
64
-
65
- // Bundle-only fields.
66
- manifest.sdkVersion = opts.sdkVersion;
67
- manifest.preview = (opts.preview != null) ? opts.preview : null;
68
- manifest.descix = {}; // reserved inert publish block
69
-
70
- return manifest;
71
- }
72
-
73
- /**
74
- * Recover the Module envelope from a manifest + the `experiment.js` source.
75
- * Inverse of `buildManifest`: strips the bundle-only fields and re-attaches
76
- * `source`. The result is a full Module envelope (validatable by modules.js).
77
- *
78
- * @param {object} manifest — the parsed manifest.json object.
79
- * @param {string} source — the `experiment.js` recipe source.
80
- * @returns {object} the reconstructed Module envelope.
81
- */
82
- export function manifestToEnvelope(manifest, source) {
83
- if (!manifest || typeof manifest !== 'object') {
84
- throw new Error('[bundle/manifest] manifestToEnvelope(manifest): manifest must be an object.');
85
- }
86
- if (typeof source !== 'string') {
87
- throw new Error('[bundle/manifest] manifestToEnvelope: source (experiment.js) must be a string.');
88
- }
89
- var envelope = Object.assign({}, manifest);
90
- for (var i = 0; i < BUNDLE_ONLY_KEYS.length; i++) {
91
- delete envelope[BUNDLE_ONLY_KEYS[i]];
92
- }
93
- envelope[SOURCE_KEY] = source;
94
- return envelope;
95
- }
@@ -1,249 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * lib/shim/storage/modules.js — Experiment module (.js text) storage helpers.
5
- *
6
- * Defines the Module envelope shape and CRUD helpers that operate on top of
7
- * any StorageProvider (lib/shim/storage/index.js) using the "files" namespace.
8
- *
9
- * ── Module envelope ────────────────────────────────────────────────────────
10
- *
11
- * {
12
- * id: string — unique identifier (newModuleId())
13
- * name: string — human-readable module name
14
- * language: 'javascript'
15
- * source: string — the .js module text
16
- * templateId: string|null — origin experiment id or null if blank
17
- * kind: 'frqtl' | 'math' — which SDK the recipe targets (v2+)
18
- * createdAt: string — ISO-8601 creation timestamp
19
- * updatedAt: string — ISO-8601 last-updated timestamp
20
- * schemaVersion: number — integer version (see MODULE_SCHEMA_VERSION,
21
- * the authority; currently 4)
22
- * scripts: string[] — v4+. OPAQUE frqtl-relative setup-factory
23
- * delta the load path injects (engineRouter.resolve) BEFORE
24
- * eval, so a ferried recipe whose source calls a setup factory
25
- * (a walk-through rung) resolves that window global. `[]`/absent
26
- * for an ordinary saved project (DX-3, the rung-source ferry).
27
- * renderState: object|null — v3+. Studio's Save realignment
28
- * (Studio Controls Realignment §2.6) capture bag —
29
- * everything the .js recipe itself does NOT round-trip:
30
- * renderer/seed (load-time), render capability
31
- * values + camera + backdrop visibility + bind-backed
32
- * page-level control values (runtime replay). Shape owned
33
- * by `lib/features/ide/save-state.js`'s
34
- * `normalizeSavedRenderState` — this module does NOT
35
- * re-validate its internal shape (an optional, best-effort
36
- * replay bag, never load-bearing for the recipe itself);
37
- * `null`/absent is the pre-S6 legacy state and stays
38
- * perfectly valid (R6 discipline extended to v3).
39
- * }
40
- *
41
- * ── Schema migration ────────────────────────────────────────────────────────
42
- *
43
- * v1 → v2: `kind` field added. Legacy v1 envelopes (no `kind` field) are
44
- * read without error and default to `kind:'frqtl'` (R6 binding: must never
45
- * throw on legacy envelopes). The `kind` field is the ONLY new field (AM-2).
46
- *
47
- * v2 → v3: `renderState` field added (Studio Controls Realignment §2.6,
48
- * S6). Legacy v1/v2 envelopes (no `renderState` field) are read without
49
- * error and default to `renderState: null` (R6 extended) — a saved project
50
- * from before S6 simply replays with NO render/camera/page-control
51
- * state (byte-identical to its own pre-S6 behavior), never a throw.
52
- *
53
- * ── Namespace ──────────────────────────────────────────────────────────────
54
- *
55
- * All helpers use the "files" namespace. Keys are module ids.
56
- *
57
- * ── Usage ──────────────────────────────────────────────────────────────────
58
- *
59
- * import { newModuleId, saveModule, loadModule,
60
- * listModules, deleteModule } from 'lib/shim/storage/modules.js';
61
- * import { getProvider } from 'lib/shim/storage';
62
- *
63
- * const provider = getProvider();
64
- *
65
- * const id = newModuleId();
66
- * const mod = {
67
- * id,
68
- * name: 'My wave experiment',
69
- * language: 'javascript',
70
- * source: 'function buildExperiment(exp) { ... }',
71
- * templateId: 'double-slit',
72
- * kind: 'frqtl', // or 'math' for EGPTMath recipes
73
- * createdAt: new Date().toISOString(),
74
- * updatedAt: new Date().toISOString(),
75
- * schemaVersion: 2,
76
- * };
77
- * saveModule(provider, mod);
78
- *
79
- * const loaded = loadModule(provider, id);
80
- * const all = listModules(provider);
81
- * deleteModule(provider, id);
82
- */
83
-
84
- 'use strict';
85
-
86
- import { makeNamespaceStore } from './make-namespace-store.js';
87
-
88
- /** @type {string} The storage namespace for all module records. */
89
- export var MODULES_NAMESPACE = 'files';
90
-
91
- /** @type {number} Current schema version. Increment on breaking envelope changes. */
92
- export var MODULE_SCHEMA_VERSION = 4;
93
-
94
- // ── Envelope validation ────────────────────────────────────────────────────
95
-
96
- function _validateModule(mod) {
97
- if (!mod || typeof mod !== 'object') {
98
- throw new Error('[modules] module must be a non-null object');
99
- }
100
- if (typeof mod.id !== 'string' || mod.id.length === 0) {
101
- throw new Error('[modules] module.id must be a non-empty string');
102
- }
103
- if (typeof mod.name !== 'string' || mod.name.length === 0) {
104
- throw new Error('[modules] module.name must be a non-empty string');
105
- }
106
- if (typeof mod.source !== 'string') {
107
- throw new Error('[modules] module.source must be a string');
108
- }
109
- if (mod.source.length === 0) {
110
- throw new Error('[modules] module.source must not be empty');
111
- }
112
- if (mod.templateId !== null && typeof mod.templateId !== 'string') {
113
- throw new Error('[modules] module.templateId must be a string or null');
114
- }
115
- if (mod.kind !== 'frqtl' && mod.kind !== 'math') {
116
- throw new Error('[modules] module.kind must be "frqtl" or "math"');
117
- }
118
- if (typeof mod.schemaVersion !== 'number') {
119
- throw new Error('[modules] module.schemaVersion must be a number');
120
- }
121
- // renderState (v3+) is OPTIONAL and best-effort (see envelope doc above) —
122
- // the only shape check here is "object or null/undefined", never a deep
123
- // validation of its internal fields (that tolerance lives in save-state.js's
124
- // own normalizeSavedRenderState, applied at REPLAY time, not at save time).
125
- if (mod.renderState !== undefined && mod.renderState !== null &&
126
- (typeof mod.renderState !== 'object' || Array.isArray(mod.renderState))) {
127
- throw new Error('[modules] module.renderState must be an object, null, or omitted');
128
- }
129
- // scripts (v4+) is OPTIONAL — an OPAQUE frqtl-relative setup-factory delta the
130
- // load path injects (engineRouter.resolve) BEFORE eval, so a recipe whose source
131
- // calls a setup factory (e.g. a ferried walk-through rung) resolves that global.
132
- // Shape check only: an array of strings, or omitted. The loaders own the
133
- // resolution + FAIL-LOUD injection, not this schema.
134
- if (mod.scripts !== undefined && mod.scripts !== null) {
135
- if (!Array.isArray(mod.scripts) || !mod.scripts.every(function (s) { return typeof s === 'string'; })) {
136
- throw new Error('[modules] module.scripts must be an array of strings, null, or omitted');
137
- }
138
- }
139
- }
140
-
141
- // ── v1 → v2 migration ─────────────────────────────────────────────────────
142
-
143
- /**
144
- * Migrate a raw stored envelope to the current schema version.
145
- * Safe to call on already-current envelopes (idempotent).
146
- *
147
- * v1 → v2: default `kind` to `'frqtl'` when the field is absent.
148
- * R6 (binding): this must NEVER throw on legacy v1 envelopes.
149
- *
150
- * v2 → v3: default `renderState` to `null` when the field is absent
151
- * (Studio Controls Realignment §2.6, S6). R6 extended: never throw on
152
- * a legacy v1/v2 envelope with no `renderState` field.
153
- *
154
- * v3 → v4: default `scripts` to `[]` when the field is absent (DX-3 —
155
- * the rung-source ferry). R6 extended again: never throw on
156
- * a legacy envelope with no `scripts` field (an ordinary saved project has none).
157
- *
158
- * @param {object} mod Raw object from storage.
159
- * @returns {object} Envelope at the current schema version.
160
- */
161
- function _migrate(mod) {
162
- if (!mod || typeof mod !== 'object') { return mod; }
163
- var patch = {};
164
- if (mod.kind === undefined || mod.kind === null) { patch.kind = 'frqtl'; }
165
- if (mod.renderState === undefined) { patch.renderState = null; }
166
- if (mod.scripts === undefined) { patch.scripts = []; }
167
- if (Object.keys(patch).length === 0) { return mod; }
168
- return Object.assign({}, mod, patch);
169
- }
170
-
171
- // ── kind filter ────────────────────────────────────────────────────────────
172
-
173
- function _kindFilter(mod, opts) {
174
- var kindFilter = (opts && opts.kind) ? opts.kind : null;
175
- if (kindFilter !== null && mod.kind !== kindFilter) { return false; }
176
- return true;
177
- }
178
-
179
- // ── Summary projection ─────────────────────────────────────────────────────
180
-
181
- function _summaryFields(mod) {
182
- // Skip malformed records that are missing required summary fields.
183
- if (typeof mod.name !== 'string') { return null; }
184
- return {
185
- id: mod.id,
186
- name: mod.name,
187
- kind: mod.kind,
188
- templateId: mod.templateId || null,
189
- updatedAt: mod.updatedAt || '',
190
- };
191
- }
192
-
193
- // ── Store instance ─────────────────────────────────────────────────────────
194
-
195
- var _store = makeNamespaceStore({
196
- namespace: MODULES_NAMESPACE,
197
- schemaVersion: MODULE_SCHEMA_VERSION,
198
- idPrefix: 'mod_',
199
- validate: _validateModule,
200
- summaryFields: _summaryFields,
201
- migrate: _migrate,
202
- listFilter: _kindFilter,
203
- });
204
-
205
- // ── Public API (names and signatures unchanged) ────────────────────────────
206
-
207
- /**
208
- * Generate a new unique module id.
209
- * @returns {string} e.g. "mod_lxyz0abc1234"
210
- */
211
- export var newModuleId = _store.newId;
212
-
213
- /**
214
- * Save (create or update) a module.
215
- * @param {import('./index.js').StorageProvider} provider
216
- * @param {object} mod
217
- * @throws {Error} on validation failure or provider write failure.
218
- */
219
- export var saveModule = _store.save;
220
-
221
- /**
222
- * Load a module by id.
223
- * Legacy v1 envelopes (no `kind` field) are migrated transparently (R6).
224
- * @param {import('./index.js').StorageProvider} provider
225
- * @param {string} id
226
- * @returns {object|null}
227
- */
228
- export var loadModule = _store.load;
229
-
230
- /**
231
- * List modules, returning summary objects suitable for a module picker.
232
- *
233
- * Returns an array of { id, name, kind, templateId, updatedAt } objects.
234
- * Sorted by updatedAt descending.
235
- * AM-2 (binding): filtering via opts.kind — pass to restrict, omit for all.
236
- *
237
- * @param {import('./index.js').StorageProvider} provider
238
- * @param {object} [opts]
239
- * @param {'frqtl'|'math'|undefined} [opts.kind]
240
- * @returns {Array<{id: string, name: string, kind: string, templateId: string|null, updatedAt: string}>}
241
- */
242
- export var listModules = _store.list;
243
-
244
- /**
245
- * Delete a module by id. No-op if absent.
246
- * @param {import('./index.js').StorageProvider} provider
247
- * @param {string} id
248
- */
249
- export var deleteModule = _store['delete'];
@@ -1,150 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * lib/shim/storage/projects.js — Project-envelope helpers.
5
- *
6
- * Defines the Project envelope shape and CRUD helpers that operate on top of
7
- * any StorageProvider (lib/shim/storage/index.js) using the "projects"
8
- * namespace.
9
- *
10
- * ── Project envelope ───────────────────────────────────────────────────────
11
- *
12
- * {
13
- * id: string — UUID-style unique identifier (newProjectId())
14
- * name: string — human-readable project name
15
- * templateId: string|null — origin experiment id ("double-slit",
16
- * "blackbody", etc.) or null if blank
17
- * createdAt: string — ISO-8601 creation timestamp
18
- * updatedAt: string — ISO-8601 last-updated timestamp
19
- * schemaVersion: number — integer version; currently 1
20
- * design: object — opaque JSON authored by AuthorView
21
- * (do NOT interpret here — that is the
22
- * AuthorView workstream's responsibility)
23
- * }
24
- *
25
- * ── Namespace ──────────────────────────────────────────────────────────────
26
- *
27
- * All helpers use the "projects" namespace. Keys are project ids.
28
- *
29
- * ── Usage ──────────────────────────────────────────────────────────────────
30
- *
31
- * import { newProjectId, saveProject, loadProject,
32
- * listProjects, deleteProject } from 'lib/shim/storage/projects.js';
33
- * import { getProvider } from 'lib/shim/storage';
34
- *
35
- * const provider = getProvider();
36
- *
37
- * const id = newProjectId();
38
- * const project = {
39
- * id,
40
- * name: 'My first project',
41
- * templateId: 'double-slit',
42
- * createdAt: new Date().toISOString(),
43
- * updatedAt: new Date().toISOString(),
44
- * schemaVersion: 1,
45
- * design: {},
46
- * };
47
- * saveProject(provider, project);
48
- *
49
- * const loaded = loadProject(provider, id);
50
- * const all = listProjects(provider);
51
- * deleteProject(provider, id);
52
- */
53
-
54
- 'use strict';
55
-
56
- import { makeNamespaceStore } from './make-namespace-store.js';
57
-
58
- /** @type {string} The storage namespace for all project records. */
59
- export var PROJECTS_NAMESPACE = 'projects';
60
-
61
- /** @type {number} Current schema version. Increment on breaking envelope changes. */
62
- export var PROJECT_SCHEMA_VERSION = 1;
63
-
64
- // ── Envelope validation ────────────────────────────────────────────────────
65
-
66
- function _validateProject(project) {
67
- if (!project || typeof project !== 'object') {
68
- throw new Error('[projects] project must be a non-null object');
69
- }
70
- if (typeof project.id !== 'string' || project.id.length === 0) {
71
- throw new Error('[projects] project.id must be a non-empty string');
72
- }
73
- if (typeof project.name !== 'string' || project.name.length === 0) {
74
- throw new Error('[projects] project.name must be a non-empty string');
75
- }
76
- if (project.templateId !== null && typeof project.templateId !== 'string') {
77
- throw new Error('[projects] project.templateId must be a string or null');
78
- }
79
- if (typeof project.schemaVersion !== 'number') {
80
- throw new Error('[projects] project.schemaVersion must be a number');
81
- }
82
- if (typeof project.design !== 'object' || project.design === null) {
83
- throw new Error('[projects] project.design must be a non-null object');
84
- }
85
- }
86
-
87
- // ── Summary projection ─────────────────────────────────────────────────────
88
-
89
- function _summaryFields(project) {
90
- // Skip malformed records that are missing required summary fields.
91
- if (typeof project.name !== 'string') { return null; }
92
- return {
93
- id: project.id,
94
- name: project.name,
95
- templateId: project.templateId || null,
96
- updatedAt: project.updatedAt || '',
97
- };
98
- }
99
-
100
- // ── Store instance ─────────────────────────────────────────────────────────
101
-
102
- var _store = makeNamespaceStore({
103
- namespace: PROJECTS_NAMESPACE,
104
- schemaVersion: PROJECT_SCHEMA_VERSION,
105
- idPrefix: 'proj_',
106
- validate: _validateProject,
107
- summaryFields: _summaryFields,
108
- });
109
-
110
- // ── Public API (names and signatures unchanged) ────────────────────────────
111
-
112
- /**
113
- * Generate a new unique project id.
114
- * @returns {string} e.g. "proj_lxyz0abc1234"
115
- */
116
- export var newProjectId = _store.newId;
117
-
118
- /**
119
- * Save (create or update) a project.
120
- * @param {import('./index.js').StorageProvider} provider
121
- * @param {object} project
122
- * @throws {Error} on validation failure or provider write failure.
123
- */
124
- export var saveProject = _store.save;
125
-
126
- /**
127
- * Load a project by id.
128
- * @param {import('./index.js').StorageProvider} provider
129
- * @param {string} id
130
- * @returns {object|null}
131
- */
132
- export var loadProject = _store.load;
133
-
134
- /**
135
- * List all projects, returning summary objects suitable for a project picker.
136
- *
137
- * Returns an array of { id, name, templateId, updatedAt } objects.
138
- * Sorted by updatedAt descending.
139
- *
140
- * @param {import('./index.js').StorageProvider} provider
141
- * @returns {Array<{id: string, name: string, templateId: string|null, updatedAt: string}>}
142
- */
143
- export var listProjects = _store.list;
144
-
145
- /**
146
- * Delete a project by id. No-op if absent.
147
- * @param {import('./index.js').StorageProvider} provider
148
- * @param {string} id
149
- */
150
- export var deleteProject = _store['delete'];