@mulmoclaude/core 3.4.0 → 3.6.0

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 (63) hide show
  1. package/assets/helps/collection-skills.md +49 -1
  2. package/assets/helps/error-recovery.md +53 -0
  3. package/dist/{calendarGrid-Csy2rpjp.js → calendarGrid-CQ8MVSRb.js} +141 -2
  4. package/dist/calendarGrid-CQ8MVSRb.js.map +1 -0
  5. package/dist/{calendarGrid-p8K-7cuB.cjs → calendarGrid-DGILaVxI.cjs} +194 -1
  6. package/dist/calendarGrid-DGILaVxI.cjs.map +1 -0
  7. package/dist/collection/core/collectionKey.d.ts +67 -0
  8. package/dist/collection/core/schema.d.ts +8 -1
  9. package/dist/collection/core/schemaZ.d.ts +36 -26
  10. package/dist/collection/firestore.cjs +51 -0
  11. package/dist/collection/firestore.cjs.map +1 -0
  12. package/dist/collection/firestore.d.ts +1 -0
  13. package/dist/collection/firestore.js +50 -0
  14. package/dist/collection/firestore.js.map +1 -0
  15. package/dist/collection/index.cjs +11 -2
  16. package/dist/collection/index.d.ts +1 -0
  17. package/dist/collection/index.js +3 -3
  18. package/dist/collection/registry/server/index.cjs +2 -2
  19. package/dist/collection/registry/server/index.js +2 -2
  20. package/dist/collection/server/appManifest.d.ts +53 -0
  21. package/dist/collection/server/delete.d.ts +10 -0
  22. package/dist/collection/server/discoveredCollection.d.ts +10 -0
  23. package/dist/collection/server/discovery.d.ts +1 -0
  24. package/dist/collection/server/firestoreDocs.d.ts +39 -0
  25. package/dist/collection/server/firestoreStore.d.ts +14 -0
  26. package/dist/collection/server/host.d.ts +90 -8
  27. package/dist/collection/server/index.cjs +13 -3
  28. package/dist/collection/server/index.d.ts +4 -1
  29. package/dist/collection/server/index.js +4 -4
  30. package/dist/collection-watchers/index.cjs +161 -49
  31. package/dist/collection-watchers/index.cjs.map +1 -1
  32. package/dist/collection-watchers/index.js +161 -49
  33. package/dist/collection-watchers/index.js.map +1 -1
  34. package/dist/collection-watchers/reconciler.d.ts +36 -1
  35. package/dist/{discovery-D7LCN8gJ.js → discovery-CdkaURVY.js} +411 -57
  36. package/dist/discovery-CdkaURVY.js.map +1 -0
  37. package/dist/{discovery-CrA_EqUJ.cjs → discovery-Cw5xWzZD.cjs} +469 -55
  38. package/dist/discovery-Cw5xWzZD.cjs.map +1 -0
  39. package/dist/feeds/index.cjs +2 -2
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/server/index.cjs +4 -4
  42. package/dist/feeds/server/index.js +4 -4
  43. package/dist/google/index.cjs +2 -2
  44. package/dist/google/index.js +2 -2
  45. package/dist/{ingestTypes-F2IJhOLJ.js → ingestTypes-CEi-Ot7F.js} +2 -2
  46. package/dist/{ingestTypes-F2IJhOLJ.js.map → ingestTypes-CEi-Ot7F.js.map} +1 -1
  47. package/dist/{ingestTypes-CPR-h-OZ.cjs → ingestTypes-iaiq33Ph.cjs} +2 -2
  48. package/dist/{ingestTypes-CPR-h-OZ.cjs.map → ingestTypes-iaiq33Ph.cjs.map} +1 -1
  49. package/dist/{promptSafety-ACXw_2Yu.js → promptSafety-CpiME8pj.js} +2 -2
  50. package/dist/{promptSafety-ACXw_2Yu.js.map → promptSafety-CpiME8pj.js.map} +1 -1
  51. package/dist/{promptSafety-CNEW3_qS.cjs → promptSafety-NNGViiCr.cjs} +2 -2
  52. package/dist/{promptSafety-CNEW3_qS.cjs.map → promptSafety-NNGViiCr.cjs.map} +1 -1
  53. package/dist/{server-CorfPQVQ.cjs → server-9mFaGLdq.cjs} +25 -6
  54. package/dist/server-9mFaGLdq.cjs.map +1 -0
  55. package/dist/{server-D6QY853r.js → server-DdHaX_Uu.js} +25 -6
  56. package/dist/server-DdHaX_Uu.js.map +1 -0
  57. package/package.json +7 -1
  58. package/dist/calendarGrid-Csy2rpjp.js.map +0 -1
  59. package/dist/calendarGrid-p8K-7cuB.cjs.map +0 -1
  60. package/dist/discovery-CrA_EqUJ.cjs.map +0 -1
  61. package/dist/discovery-D7LCN8gJ.js.map +0 -1
  62. package/dist/server-CorfPQVQ.cjs.map +0 -1
  63. package/dist/server-D6QY853r.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
2
  const require_dist = require("./dist-CsgSfWwR.cjs");
3
3
  const require_root = require("./root-rPH6FGDT.cjs");
4
- const require_calendarGrid = require("./calendarGrid-p8K-7cuB.cjs");
4
+ const require_calendarGrid = require("./calendarGrid-DGILaVxI.cjs");
5
5
  const require_project = require("./project-C1ep9pvo.cjs");
6
6
  const require_templatePath = require("./templatePath-27vUZowm.cjs");
7
7
  let node_fs = require("node:fs");
@@ -51,6 +51,10 @@ function createForwardingLogger(getLogger) {
51
51
  * watcher is already running — and a host catching both in one place should
52
52
  * not have to match on message text to tell them apart. */
53
53
  var COLLECTION_ROOT_REQUIRED = "COLLECTION_ROOT_REQUIRED";
54
+ /** Build a local {@link CollectionKey}, canonicalising the root. The identity
55
+ * type itself is isomorphic and cannot canonicalise (that needs `node:path`),
56
+ * so this is the constructor server code should use. */
57
+ var localCollectionKey = (root, slug) => require_calendarGrid.localCollectionKeyOf(require_root.canonicalRoot(root), slug);
54
58
  /** Build a change payload, attaching `root` only when the engine call carried
55
59
  * an explicit one. Centralised so every publish site states the root the same
56
60
  * way, and so a single-workspace host's payload shape stays byte-identical to
@@ -61,8 +65,48 @@ function collectionChangePayload(base, root) {
61
65
  root: require_root.canonicalRoot(root)
62
66
  };
63
67
  }
68
+ /** Build a change payload for a SHARED collection. `slug` carries the `cid`,
69
+ * which is what it is called inside its app; `aid` is what makes it an
70
+ * identity. Never stamps a `root` — a shared collection does not have one. */
71
+ function sharedCollectionChangePayload(base, aid) {
72
+ require_calendarGrid.sharedCollectionKey(aid, base.slug);
73
+ return {
74
+ ...base,
75
+ aid
76
+ };
77
+ }
78
+ /** The identity a change is about, as a value — the thing to key a fan-out on.
79
+ *
80
+ * This is the one place that decides what an absent field means, so no host
81
+ * has to: `aid` present is a shared collection, otherwise it is local, and a
82
+ * local payload with no `root` means the host's configured root (which is why
83
+ * a single-workspace host's payloads still say nothing about roots).
84
+ *
85
+ * `fallbackRoot` is what a payload with no root resolves to. An explicit-root
86
+ * host has no such default and must not guess — pass the root the call was
87
+ * made for. */
88
+ /** Refuse a payload that names both an app and a root.
89
+ *
90
+ * The two arms are mutually exclusive in the TYPE, so this cannot come from
91
+ * the constructors -- it is corrupt input (a JS caller, a cast, something off
92
+ * a wire). Throwing rather than picking one: whichever way it were guessed,
93
+ * the update would be fanned out on a channel it does not belong to, and that
94
+ * is invisible where a throw is not.
95
+ *
96
+ * Takes the WIDENED shape deliberately. Written inline, TypeScript narrows the
97
+ * union to `never` inside the branch and the fields cannot be read at all --
98
+ * which is the type-level guarantee doing its job, and exactly why the runtime
99
+ * check has to be stated somewhere it can still see them. */
100
+ function requireOneScope(payload) {
101
+ if (payload.aid !== void 0 && payload.root !== void 0) throw new Error(`collectionChangeKey: payload for "${payload.slug}" carries both an app (${payload.aid}) and a root (${payload.root})`);
102
+ }
103
+ function collectionChangeKey(payload, fallbackRoot) {
104
+ requireOneScope(payload);
105
+ return payload.aid === void 0 ? localCollectionKey(payload.root ?? fallbackRoot, payload.slug) : require_calendarGrid.sharedCollectionKey(payload.aid, payload.slug);
106
+ }
64
107
  var hostSlot = createHostSlot("@mulmoclaude/core/collection/server: configureCollectionHost()");
65
108
  var changePublisher = null;
109
+ var firestoreAccessor = null;
66
110
  /** Wire the engine to a host. Call once at server startup, before any
67
111
  * collection storage operation. Re-binding to a *different* host throws —
68
112
  * silently redirecting later filesystem operations to another workspace
@@ -86,6 +130,25 @@ function setCollectionChangePublisher(publish) {
86
130
  function publishCollectionChange(payload) {
87
131
  changePublisher?.(payload);
88
132
  }
133
+ /** Wire the accessor for the host's authenticated Firestore session.
134
+ *
135
+ * Separate from `configureCollectionHost` for the same reason
136
+ * `setCollectionChangePublisher` is: the host binding is set at the top of
137
+ * server startup, but this session doesn't exist until the user connects
138
+ * remote-host (and closes again on disconnect), so it cannot be part of a
139
+ * one-shot binding. Optional — left unwired, only shared collections are
140
+ * affected, and they report "not connected". Pass `null` to detach. */
141
+ function setFirestoreAccessor(accessor) {
142
+ firestoreAccessor = accessor;
143
+ }
144
+ /** The host's live Firestore access, or null when there is no session (or the
145
+ * host never wired one — every non-shared backend leaves it unset).
146
+ * Callers MUST surface null as an actionable "connect remote-host first",
147
+ * never as an empty result: silence would be indistinguishable from a
148
+ * collection that genuinely has no records. */
149
+ function firestoreHandle() {
150
+ return firestoreAccessor?.() ?? null;
151
+ }
89
152
  function requireHost() {
90
153
  return hostSlot.get();
91
154
  }
@@ -150,6 +213,18 @@ function isPresetSlug(slug) {
150
213
  * without a workspace root. */
151
214
  var log = createForwardingLogger(() => hostSlot.peek()?.log ?? null);
152
215
  //#endregion
216
+ //#region src/collection/server/backendAvailability.ts
217
+ /** Thrown by a store when its engine or session cannot serve the request. */
218
+ var BackendUnavailableError = class extends Error {
219
+ constructor(message) {
220
+ super(message);
221
+ this.name = "BackendUnavailableError";
222
+ }
223
+ };
224
+ function isBackendUnavailable(err) {
225
+ return err instanceof BackendUnavailableError;
226
+ }
227
+ //#endregion
153
228
  //#region src/collection/server/paths.ts
154
229
  var SCHEMA_FILE = "schema.json";
155
230
  /** Sanitise a user-supplied slug into a safe directory-name leaf.
@@ -261,6 +336,280 @@ function resolveTemplatePath(skillDir, templateRelPath) {
261
336
  return resolved;
262
337
  }
263
338
  //#endregion
339
+ //#region src/collection/server/storePage.ts
340
+ /** Project `fields` (+ the primary key, always) out of each record. Thin
341
+ * server-typed alias over the shared isomorphic `projectRecordFields`
342
+ * (../core/project.ts) — kept as the store layer's exported name. */
343
+ function projectItemFields(items, fields, primaryKey) {
344
+ return require_project.projectRecordFields(items, fields, primaryKey);
345
+ }
346
+ /** Slice + project an already-ordered full read into a `ListPage` — the
347
+ * shared emulation for stores without native paging. Pure, exported for
348
+ * tests. `limit: 0` is a valid "count only" page. */
349
+ function pageFromFullRead(items, opts, primaryKey, truncated) {
350
+ const offset = Math.max(0, opts.offset ?? 0);
351
+ const end = opts.limit === void 0 ? items.length : offset + Math.max(0, opts.limit);
352
+ return {
353
+ items: projectItemFields(items.slice(offset, end), opts.fields, primaryKey),
354
+ total: items.length,
355
+ truncated
356
+ };
357
+ }
358
+ //#endregion
359
+ //#region src/collection/server/firestoreStore.ts
360
+ /** What every operation throws when there is no live session. Worded as an
361
+ * instruction because it surfaces straight to the user and the agent. */
362
+ var NOT_CONNECTED = "shared collection unavailable: connect remote-host first — these records live in the app's Firestore, not in the workspace, so nothing can be read or written while the session is closed";
363
+ /** What a schema declaring `storage.type: "firestore"` must have had resolved
364
+ * for it before it can be served. Its absence is a programming error here, not
365
+ * a user-facing state: discovery REFUSES such a schema when the repository
366
+ * declares no `aid`, so a collection that reached this store has one. */
367
+ var NO_APP = "shared collection has no app id — discovery should have refused this schema; check that the repository's app.json declares an `aid`";
368
+ /** The records subcollection of one shared collection.
369
+ *
370
+ * Takes a KEY, never loose strings, and the key is the only way to reach this
371
+ * function. `sharedCollectionKey` is where the name rule lives (the charset a
372
+ * Firestore document id, a pubsub channel segment and the completion-bell id
373
+ * must all survive), so building a path cannot be a way around it. */
374
+ function sharedItemsPath(key) {
375
+ return `apps/${key.aid}/collections/${key.cid}/items`;
376
+ }
377
+ /** The collection's identity, from what discovery resolved. Throws on a
378
+ * missing `appId` — see NO_APP. */
379
+ function keyOf(collection) {
380
+ if (collection.appId === void 0) throw new Error(NO_APP);
381
+ return require_calendarGrid.sharedCollectionKey(collection.appId, collection.slug);
382
+ }
383
+ function requireHandle() {
384
+ const handle = firestoreHandle();
385
+ if (handle === null) throw new BackendUnavailableError(NOT_CONNECTED);
386
+ return handle;
387
+ }
388
+ /** Firestore's own refusal, named.
389
+ *
390
+ * `permission-denied` is the failure a shared collection has most often and
391
+ * the one the SDK explains worst ("Missing or insufficient permissions") — it
392
+ * says nothing about WHO was refused, which is the only fact that leads to a
393
+ * fix. Authorization here is the app's member roster, keyed by email, so the
394
+ * signed-in address is what the app's owner needs in order to add it. This is
395
+ * the whole reason `FirestoreHandle` carries `email`.
396
+ *
397
+ * Reported as a `BackendUnavailableError` deliberately, even though it is a
398
+ * refusal rather than an outage: the layers above catch broadly, and without a
399
+ * type to test, `store.read(...).catch(() => null)` reports "record missing"
400
+ * and an ontology count reports 0 — a denial would read as an empty
401
+ * collection, which is the exact confusion this backend refuses to create. */
402
+ function isPermissionDenied(err) {
403
+ return require_dist.isRecord(err) && err.code === "permission-denied";
404
+ }
405
+ function deniedMessage(key, email) {
406
+ return `permission denied on shared collection '${key.cid}' of app '${key.aid}' — signed in as ${email}. A shared collection is authorized by the app's member roster (by email), so this address needs a role for '${key.cid}' (or '*'); only the app's owner can add it.`;
407
+ }
408
+ /** Run one SDK call, translating a roster denial. Every read and write goes
409
+ * through this — a denial reaching one path and not another would mean the
410
+ * message a user sees depends on which screen they were on. */
411
+ async function guarded(key, email, run) {
412
+ try {
413
+ return await run();
414
+ } catch (err) {
415
+ if (!isPermissionDenied(err)) throw err;
416
+ throw new BackendUnavailableError(deniedMessage(key, email));
417
+ }
418
+ }
419
+ /** A stored document's fields → a record. A document written by hand (or by an
420
+ * older version) can hold anything, so a non-object is dropped rather than
421
+ * surfaced as a broken record — the same fail-soft the file store applies to
422
+ * an unparseable `.json`. */
423
+ function toItem(data) {
424
+ return require_dist.isRecord(data) ? data : null;
425
+ }
426
+ /** Record ids are validated with the SAME helper every other backend uses.
427
+ * Firestore would accept ids the file store refuses, but a record should stay
428
+ * portable between backends — and an id that can't round-trip to a filename
429
+ * would break an export back to a file collection. */
430
+ function withSafeId(itemId, onInvalid, run) {
431
+ const safeId = safeRecordId(itemId);
432
+ if (safeId === null) return onInvalid();
433
+ return run(safeId, requireHandle());
434
+ }
435
+ async function firestoreList(key) {
436
+ const { docs, email } = requireHandle();
437
+ return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data)).filter((item) => item !== null);
438
+ }
439
+ /** Paging is emulated over a full ordered read rather than pushed into
440
+ * Firestore: `offset` has no server-side form there (the cursor API needs the
441
+ * preceding document, which a stateless offset/limit call doesn't have), and
442
+ * `total` needs the full count anyway. Hence `nativePaging: false` — the
443
+ * capability is honest about the cost. */
444
+ async function firestorePage(key, primaryKey, opts) {
445
+ const items = await firestoreList(key);
446
+ const offset = Math.max(0, opts.offset ?? 0);
447
+ return {
448
+ items: projectItemFields(opts.limit === void 0 ? items.slice(offset) : items.slice(offset, offset + Math.max(0, opts.limit)), opts.fields, primaryKey),
449
+ total: items.length,
450
+ truncated: false
451
+ };
452
+ }
453
+ async function firestoreRead(key, itemId) {
454
+ return withSafeId(itemId, () => Promise.resolve(null), async (safeId, { docs, email }) => toItem(await guarded(key, email, () => docs.get(sharedItemsPath(key), safeId))));
455
+ }
456
+ /** Publish the "records changed" ping for a shared collection.
457
+ *
458
+ * `sharedCollectionChangePayload` NEVER stamps a root, and that matters beyond
459
+ * tidiness: this payload is relayed to the browser and on into an
460
+ * LLM-generated custom-view iframe, so a filesystem path on it would be a
461
+ * disclosure. The type makes it unreachable rather than trusting the caller. */
462
+ function publishShared(key, ids, operation) {
463
+ publishCollectionChange(sharedCollectionChangePayload({
464
+ slug: key.cid,
465
+ ids,
466
+ op: operation
467
+ }, key.aid));
468
+ }
469
+ async function firestoreWrite(key, itemId, item, opts) {
470
+ return withSafeId(itemId, () => Promise.resolve({
471
+ kind: "invalid-id",
472
+ itemId
473
+ }), async (safeId, { docs, email }) => {
474
+ const collectionPath = sharedItemsPath(key);
475
+ if (opts.refuseOverwrite) {
476
+ if (!await guarded(key, email, () => docs.create(collectionPath, safeId, item))) return {
477
+ kind: "conflict",
478
+ itemId: safeId
479
+ };
480
+ } else await guarded(key, email, () => docs.set(collectionPath, safeId, item));
481
+ if (opts.slug) publishShared(key, [safeId], "upsert");
482
+ return {
483
+ kind: "ok",
484
+ itemId: safeId,
485
+ item
486
+ };
487
+ });
488
+ }
489
+ async function firestoreDelete(key, itemId, opts) {
490
+ return withSafeId(itemId, () => Promise.resolve({
491
+ kind: "invalid-id",
492
+ itemId
493
+ }), async (safeId, { docs, email }) => {
494
+ if (!await guarded(key, email, () => docs.delete(sharedItemsPath(key), safeId))) return {
495
+ kind: "not-found",
496
+ itemId: safeId
497
+ };
498
+ if (opts.slug) publishShared(key, [safeId], "delete");
499
+ return {
500
+ kind: "ok",
501
+ itemId: safeId
502
+ };
503
+ });
504
+ }
505
+ /** The store factory registered for `storage.type === "firestore"`.
506
+ * Synchronous and connection-agnostic by contract — see the header. */
507
+ function firestoreStoreFor(collection, opts) {
508
+ const { primaryKey } = collection.schema;
509
+ const ioOpts = {
510
+ ...opts,
511
+ slug: opts.slug ?? collection.slug
512
+ };
513
+ return {
514
+ capabilities: {
515
+ writable: true,
516
+ nativeQuery: false,
517
+ nativePaging: false
518
+ },
519
+ list: async () => firestoreList(keyOf(collection)),
520
+ page: async (pageOpts = {}) => firestorePage(keyOf(collection), primaryKey, pageOpts),
521
+ read: async (itemId) => firestoreRead(keyOf(collection), itemId),
522
+ write: async (itemId, item, writeOpts = {}) => firestoreWrite(keyOf(collection), itemId, item, {
523
+ ...ioOpts,
524
+ refuseOverwrite: writeOpts.refuseOverwrite
525
+ }),
526
+ delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts)
527
+ };
528
+ }
529
+ //#endregion
530
+ //#region src/collection/server/appManifest.ts
531
+ /** The app declaration's filename, at the repository root. */
532
+ var APP_MANIFEST_FILE = "app.json";
533
+ /** Read `<root>/app.json` and return its `aid`.
534
+ *
535
+ * SYNCHRONOUS on purpose. The caller is `acceptParsedSchema`, which is sync
536
+ * and is shared by discovery and `manageCollection`'s `putSchema` precisely so
537
+ * that a schema which would be skipped on the next discovery cannot be written
538
+ * as if it were fine. Making this async would split that gate in two, and the
539
+ * half that lost the check is the half the author sees. The file is a few
540
+ * hundred bytes and is read once per firestore collection per discovery pass.
541
+ *
542
+ * Never cached. `app.json` is edited by hand and by the agent, and a cache
543
+ * here would mean the app a collection points at is whatever it was when the
544
+ * server started. */
545
+ function loadAppManifest(root) {
546
+ let raw;
547
+ try {
548
+ raw = (0, node_fs.readFileSync)(node_path.default.join(root, APP_MANIFEST_FILE), "utf-8");
549
+ } catch (err) {
550
+ if (require_dist.isErrorWithCode(err) && err.code === "ENOENT") return {
551
+ ok: false,
552
+ kind: "missing"
553
+ };
554
+ return {
555
+ ok: false,
556
+ kind: "unreadable",
557
+ detail: String(err)
558
+ };
559
+ }
560
+ return parseAppManifest(raw);
561
+ }
562
+ /** The parse half, exported so it can be tested without a filesystem.
563
+ *
564
+ * `aid` is validated with `isValidCollectionName` — the SAME predicate the
565
+ * `CollectionKey` constructors apply — rather than a rule of its own. An `aid`
566
+ * is re-encoded downstream as a Firestore document id, a pubsub channel
567
+ * segment and a cache key, each with a different character that would break
568
+ * it; one rule, stated once, is what keeps those layers from disagreeing.
569
+ * Rejecting here rather than at `sharedCollectionKey` only changes WHERE the
570
+ * author is told: a reason on the collection they wrote, instead of a throw
571
+ * from inside a store call. */
572
+ function parseAppManifest(raw) {
573
+ let parsed;
574
+ try {
575
+ parsed = JSON.parse(raw);
576
+ } catch (err) {
577
+ return {
578
+ ok: false,
579
+ kind: "malformed",
580
+ detail: `not valid JSON (${String(err)})`
581
+ };
582
+ }
583
+ if (!require_dist.isRecord(parsed)) return {
584
+ ok: false,
585
+ kind: "malformed",
586
+ detail: "is not a JSON object"
587
+ };
588
+ const { aid } = parsed;
589
+ if (typeof aid !== "string" || aid.length === 0) return {
590
+ ok: false,
591
+ kind: "malformed",
592
+ detail: "declares no `aid` string"
593
+ };
594
+ if (!require_calendarGrid.isValidCollectionName(aid)) return {
595
+ ok: false,
596
+ kind: "malformed",
597
+ detail: `\`aid\` '${aid}' is not a valid app id`
598
+ };
599
+ return {
600
+ ok: true,
601
+ manifest: { aid }
602
+ };
603
+ }
604
+ /** The failure as the one line an author can act on. Kept next to the failure
605
+ * type so a new variant cannot be added without wording it. */
606
+ function appManifestReason(failure, root) {
607
+ const manifestPath = node_path.default.join(root, APP_MANIFEST_FILE);
608
+ if (failure.kind === "missing") return `a shared collection needs an app: create ${manifestPath} declaring an \`aid\``;
609
+ if (failure.kind === "unreadable") return `cannot read ${manifestPath}: ${failure.detail}`;
610
+ return `${manifestPath} ${failure.detail}`;
611
+ }
612
+ //#endregion
264
613
  //#region src/collection/server/io.ts
265
614
  /** True iff `filePath` exists and is a regular file (NOT a symlink).
266
615
  * Defends `listItems` / `readItem` against `*.json` symlinks placed
@@ -483,18 +832,6 @@ function resolveCreateItemId(schema, record) {
483
832
  return typeof primaryRaw === "string" && primaryRaw.length > 0 ? primaryRaw : null;
484
833
  }
485
834
  //#endregion
486
- //#region src/collection/server/backendAvailability.ts
487
- /** Thrown by a store when its engine or session cannot serve the request. */
488
- var BackendUnavailableError = class extends Error {
489
- constructor(message) {
490
- super(message);
491
- this.name = "BackendUnavailableError";
492
- }
493
- };
494
- function isBackendUnavailable(err) {
495
- return err instanceof BackendUnavailableError;
496
- }
497
- //#endregion
498
835
  //#region src/collection/core/queryZ.ts
499
836
  /** Result-column aliases double as SQL identifiers and JSON keys — keep
500
837
  * them to a conservative identifier charset so neither side needs
@@ -1088,26 +1425,6 @@ function closerFor(handle) {
1088
1425
  return handle === null ? null : () => handle.close();
1089
1426
  }
1090
1427
  //#endregion
1091
- //#region src/collection/server/storePage.ts
1092
- /** Project `fields` (+ the primary key, always) out of each record. Thin
1093
- * server-typed alias over the shared isomorphic `projectRecordFields`
1094
- * (../core/project.ts) — kept as the store layer's exported name. */
1095
- function projectItemFields(items, fields, primaryKey) {
1096
- return require_project.projectRecordFields(items, fields, primaryKey);
1097
- }
1098
- /** Slice + project an already-ordered full read into a `ListPage` — the
1099
- * shared emulation for stores without native paging. Pure, exported for
1100
- * tests. `limit: 0` is a valid "count only" page. */
1101
- function pageFromFullRead(items, opts, primaryKey, truncated) {
1102
- const offset = Math.max(0, opts.offset ?? 0);
1103
- const end = opts.limit === void 0 ? items.length : offset + Math.max(0, opts.limit);
1104
- return {
1105
- items: projectItemFields(items.slice(offset, end), opts.fields, primaryKey),
1106
- total: items.length,
1107
- truncated
1108
- };
1109
- }
1110
- //#endregion
1111
1428
  //#region src/collection/server/sqliteStore.ts
1112
1429
  /** A constructor's parameter and return types are not observable at runtime,
1113
1430
  * so the check stops at "DatabaseSync is constructible" — the only member of
@@ -1457,7 +1774,8 @@ function fileStoreFor(collection, opts) {
1457
1774
  var storeFactories = /* @__PURE__ */ new Map([
1458
1775
  ["file", fileStoreFor],
1459
1776
  ["csv", csvStoreFor],
1460
- ["sqlite", sqliteStoreFor]
1777
+ ["sqlite", sqliteStoreFor],
1778
+ ["firestore", firestoreStoreFor]
1461
1779
  ]);
1462
1780
  /** Pick the store implementation for a discovered collection via the
1463
1781
  * factory registry. An unknown kind cannot normally reach here (the
@@ -2313,15 +2631,19 @@ var DataSourceZ = zod.z.object({
2313
2631
  /** Alternative WRITABLE storage backend for a collection's records —
2314
2632
  * unlike `dataSource` (external read-only file), a `storage` collection
2315
2633
  * behaves like a normal writable collection; only where the rows live
2316
- * changes. v1: `sqlite` — records in a single SQLite database file
2317
- * (`node:sqlite`, one JSON record per row keyed by the primaryKey).
2318
- * `path` is workspace-relative and containment-checked exactly like
2319
- * `dataPath`. The store factory registry (`server/store.ts`) picks the
2320
- * implementation by `type` (plans/done/refactor-storage-virtualization.md). */
2321
- var StorageZ = zod.z.object({
2634
+ * changes. The store factory registry (`server/store.ts`) picks the
2635
+ * implementation by `type` (plans/done/refactor-storage-virtualization.md).
2636
+ *
2637
+ * A discriminated union rather than one shape with optional keys, because
2638
+ * only the sqlite variant is a workspace FILE: its `path` is
2639
+ * workspace-relative and containment-checked exactly like `dataPath`, while
2640
+ * the firestore variant has no path to check — its records are not on this
2641
+ * machine at all. Optional keys would let each arm accept the other's, and
2642
+ * the compiler would stop being the thing that tells you which. */
2643
+ var StorageZ = zod.z.discriminatedUnion("type", [zod.z.object({
2322
2644
  type: zod.z.literal("sqlite"),
2323
2645
  path: zod.z.string().min(1)
2324
- });
2646
+ }), zod.z.object({ type: zod.z.literal("firestore") }).strict()]);
2325
2647
  var BareCollectionSchemaZ = zod.z.object({
2326
2648
  title: zod.z.string().min(1),
2327
2649
  icon: zod.z.string().min(1),
@@ -2607,16 +2929,45 @@ function acceptParsedSchema(schema, opts) {
2607
2929
  dataSourceFile
2608
2930
  };
2609
2931
  }
2610
- if (schema.storage !== void 0) {
2611
- const storageFile = resolveDataDir(schema.storage.path, opts.workspaceRoot);
2932
+ if (schema.storage !== void 0) return acceptStorageSchema(schema.storage, opts);
2933
+ const dataDir = resolveDataDir(schema.dataPath ?? "", opts.workspaceRoot);
2934
+ if (dataDir === null) return {
2935
+ ok: false,
2936
+ reason: `dataPath '${schema.dataPath}' escapes the workspace`
2937
+ };
2938
+ return {
2939
+ ok: true,
2940
+ dataDir
2941
+ };
2942
+ }
2943
+ /** The `storage` arm of the acceptance gate. Every storage backend gets the
2944
+ * conventional phantom dataDir; what differs is what else has to resolve
2945
+ * before the collection can exist at all.
2946
+ *
2947
+ * A FILE-backed backend (sqlite) resolves and containment-checks a
2948
+ * `storageFile`. A SHARED one (firestore) has no path on this machine — it
2949
+ * resolves an IDENTITY instead: the `aid` from the repository's `app.json`,
2950
+ * which together with the slug as `cid` names `apps/{aid}/collections/{cid}`.
2951
+ *
2952
+ * Resolving it HERE, once, is the point. The store then receives a settled
2953
+ * `(aid, cid)` and never reads `app.json` itself — otherwise the questions of
2954
+ * caching, staleness and what to do when the file is missing would be decided
2955
+ * inside a read path, where the only cheap answer is to return nothing, and
2956
+ * "this collection is misconfigured" would reach the user as "this collection
2957
+ * is empty". A missing or malformed `app.json` is a CONFIGURATION error, so it
2958
+ * is reported the same way an escaping `storage.path` is: the schema is
2959
+ * refused, with a reason naming the file to create. */
2960
+ function acceptStorageSchema(storage, opts) {
2961
+ const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
2962
+ if (dataDir === null) return {
2963
+ ok: false,
2964
+ reason: `slug '${opts.slug}' yields no workspace-contained data dir`
2965
+ };
2966
+ if (storage.type === "sqlite") {
2967
+ const storageFile = resolveDataDir(storage.path, opts.workspaceRoot);
2612
2968
  if (storageFile === null) return {
2613
2969
  ok: false,
2614
- reason: `storage.path '${schema.storage.path}' escapes the workspace`
2615
- };
2616
- const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
2617
- if (dataDir === null) return {
2618
- ok: false,
2619
- reason: `slug '${opts.slug}' yields no workspace-contained data dir`
2970
+ reason: `storage.path '${storage.path}' escapes the workspace`
2620
2971
  };
2621
2972
  return {
2622
2973
  ok: true,
@@ -2624,14 +2975,15 @@ function acceptParsedSchema(schema, opts) {
2624
2975
  storageFile
2625
2976
  };
2626
2977
  }
2627
- const dataDir = resolveDataDir(schema.dataPath ?? "", opts.workspaceRoot);
2628
- if (dataDir === null) return {
2978
+ const manifest = loadAppManifest(opts.workspaceRoot);
2979
+ if (!manifest.ok) return {
2629
2980
  ok: false,
2630
- reason: `dataPath '${schema.dataPath}' escapes the workspace`
2981
+ reason: appManifestReason(manifest, opts.workspaceRoot)
2631
2982
  };
2632
2983
  return {
2633
2984
  ok: true,
2634
- dataDir
2985
+ dataDir,
2986
+ appId: manifest.manifest.aid
2635
2987
  };
2636
2988
  }
2637
2989
  async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
@@ -2689,6 +3041,7 @@ async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
2689
3041
  dataDir: acceptance.dataDir,
2690
3042
  ...acceptance.dataSourceFile !== void 0 ? { dataSourceFile: acceptance.dataSourceFile } : {},
2691
3043
  ...acceptance.storageFile !== void 0 ? { storageFile: acceptance.storageFile } : {},
3044
+ ...acceptance.appId !== void 0 ? { appId: acceptance.appId } : {},
2692
3045
  skillDir: node_path.default.join(skillsRoot, safeName)
2693
3046
  };
2694
3047
  }
@@ -2769,7 +3122,8 @@ function toSummary(collection) {
2769
3122
  title: collection.schema.title,
2770
3123
  icon: collection.schema.icon,
2771
3124
  source: collection.source,
2772
- ...collection.schema.dataSource !== void 0 ? { readonly: true } : {}
3125
+ ...collection.schema.dataSource !== void 0 ? { readonly: true } : {},
3126
+ ...collection.appId !== void 0 ? { appId: collection.appId } : {}
2773
3127
  };
2774
3128
  }
2775
3129
  function toDetail(collection) {
@@ -2779,6 +3133,12 @@ function toDetail(collection) {
2779
3133
  };
2780
3134
  }
2781
3135
  //#endregion
3136
+ Object.defineProperty(exports, "APP_MANIFEST_FILE", {
3137
+ enumerable: true,
3138
+ get: function() {
3139
+ return APP_MANIFEST_FILE;
3140
+ }
3141
+ });
2782
3142
  Object.defineProperty(exports, "BackendUnavailableError", {
2783
3143
  enumerable: true,
2784
3144
  get: function() {
@@ -2833,6 +3193,12 @@ Object.defineProperty(exports, "acceptParsedSchema", {
2833
3193
  return acceptParsedSchema;
2834
3194
  }
2835
3195
  });
3196
+ Object.defineProperty(exports, "appManifestReason", {
3197
+ enumerable: true,
3198
+ get: function() {
3199
+ return appManifestReason;
3200
+ }
3201
+ });
2836
3202
  Object.defineProperty(exports, "archiveDir", {
2837
3203
  enumerable: true,
2838
3204
  get: function() {
@@ -2851,6 +3217,12 @@ Object.defineProperty(exports, "checkpointSqliteDatabase", {
2851
3217
  return checkpointSqliteDatabase;
2852
3218
  }
2853
3219
  });
3220
+ Object.defineProperty(exports, "collectionChangeKey", {
3221
+ enumerable: true,
3222
+ get: function() {
3223
+ return collectionChangeKey;
3224
+ }
3225
+ });
2854
3226
  Object.defineProperty(exports, "collectionChangePayload", {
2855
3227
  enumerable: true,
2856
3228
  get: function() {
@@ -2935,6 +3307,12 @@ Object.defineProperty(exports, "encodeCsvRecordId", {
2935
3307
  return encodeCsvRecordId;
2936
3308
  }
2937
3309
  });
3310
+ Object.defineProperty(exports, "firestoreHandle", {
3311
+ enumerable: true,
3312
+ get: function() {
3313
+ return firestoreHandle;
3314
+ }
3315
+ });
2938
3316
  Object.defineProperty(exports, "generateItemId", {
2939
3317
  enumerable: true,
2940
3318
  get: function() {
@@ -2983,12 +3361,24 @@ Object.defineProperty(exports, "listItems", {
2983
3361
  return listItems;
2984
3362
  }
2985
3363
  });
3364
+ Object.defineProperty(exports, "loadAppManifest", {
3365
+ enumerable: true,
3366
+ get: function() {
3367
+ return loadAppManifest;
3368
+ }
3369
+ });
2986
3370
  Object.defineProperty(exports, "loadCollection", {
2987
3371
  enumerable: true,
2988
3372
  get: function() {
2989
3373
  return loadCollection;
2990
3374
  }
2991
3375
  });
3376
+ Object.defineProperty(exports, "localCollectionKey", {
3377
+ enumerable: true,
3378
+ get: function() {
3379
+ return localCollectionKey;
3380
+ }
3381
+ });
2992
3382
  Object.defineProperty(exports, "log", {
2993
3383
  enumerable: true,
2994
3384
  get: function() {
@@ -3007,6 +3397,12 @@ Object.defineProperty(exports, "pageFromFullRead", {
3007
3397
  return pageFromFullRead;
3008
3398
  }
3009
3399
  });
3400
+ Object.defineProperty(exports, "parseAppManifest", {
3401
+ enumerable: true,
3402
+ get: function() {
3403
+ return parseAppManifest;
3404
+ }
3405
+ });
3010
3406
  Object.defineProperty(exports, "peekWorkspaceRoot", {
3011
3407
  enumerable: true,
3012
3408
  get: function() {
@@ -3097,6 +3493,24 @@ Object.defineProperty(exports, "setCollectionChangePublisher", {
3097
3493
  return setCollectionChangePublisher;
3098
3494
  }
3099
3495
  });
3496
+ Object.defineProperty(exports, "setFirestoreAccessor", {
3497
+ enumerable: true,
3498
+ get: function() {
3499
+ return setFirestoreAccessor;
3500
+ }
3501
+ });
3502
+ Object.defineProperty(exports, "sharedCollectionChangePayload", {
3503
+ enumerable: true,
3504
+ get: function() {
3505
+ return sharedCollectionChangePayload;
3506
+ }
3507
+ });
3508
+ Object.defineProperty(exports, "sharedItemsPath", {
3509
+ enumerable: true,
3510
+ get: function() {
3511
+ return sharedItemsPath;
3512
+ }
3513
+ });
3100
3514
  Object.defineProperty(exports, "skillsStagingDir", {
3101
3515
  enumerable: true,
3102
3516
  get: function() {
@@ -3134,4 +3548,4 @@ Object.defineProperty(exports, "writeItem", {
3134
3548
  }
3135
3549
  });
3136
3550
 
3137
- //# sourceMappingURL=discovery-CrA_EqUJ.cjs.map
3551
+ //# sourceMappingURL=discovery-Cw5xWzZD.cjs.map