@mulmoclaude/core 3.6.0 → 3.10.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 (71) hide show
  1. package/dist/collection/core/itemId.d.ts +28 -0
  2. package/dist/collection/firestore.cjs +9 -1
  3. package/dist/collection/firestore.cjs.map +1 -1
  4. package/dist/collection/firestore.js +10 -2
  5. package/dist/collection/firestore.js.map +1 -1
  6. package/dist/collection/index.cjs +55 -66
  7. package/dist/collection/index.cjs.map +1 -1
  8. package/dist/collection/index.d.ts +1 -1
  9. package/dist/collection/index.js +3 -15
  10. package/dist/collection/index.js.map +1 -1
  11. package/dist/collection/registry/server/index.cjs +2 -2
  12. package/dist/collection/registry/server/index.js +2 -2
  13. package/dist/collection/server/firestoreDocs.d.ts +18 -0
  14. package/dist/collection/server/host.d.ts +40 -0
  15. package/dist/collection/server/index.cjs +25 -4
  16. package/dist/collection/server/index.d.ts +4 -1
  17. package/dist/collection/server/index.js +4 -4
  18. package/dist/collection/server/io.d.ts +5 -3
  19. package/dist/collection/server/publishChecks.d.ts +29 -0
  20. package/dist/collection/server/publishManifest.d.ts +181 -0
  21. package/dist/collection/server/publishProject.d.ts +255 -0
  22. package/dist/collection/server/validate.d.ts +12 -0
  23. package/dist/collection-watchers/index.cjs +70 -26
  24. package/dist/collection-watchers/index.cjs.map +1 -1
  25. package/dist/collection-watchers/index.js +67 -23
  26. package/dist/collection-watchers/index.js.map +1 -1
  27. package/dist/{discovery-CdkaURVY.js → discovery-B9zdNkgW.js} +189 -18
  28. package/dist/discovery-B9zdNkgW.js.map +1 -0
  29. package/dist/{discovery-Cw5xWzZD.cjs → discovery-BsXiDZBR.cjs} +226 -43
  30. package/dist/discovery-BsXiDZBR.cjs.map +1 -0
  31. package/dist/feeds/index.cjs +4 -4
  32. package/dist/feeds/index.js +2 -2
  33. package/dist/feeds/server/index.cjs +6 -6
  34. package/dist/feeds/server/index.js +4 -4
  35. package/dist/firestore/listen.d.ts +4 -0
  36. package/dist/google/index.cjs +5 -5
  37. package/dist/google/index.cjs.map +1 -1
  38. package/dist/google/index.js +2 -2
  39. package/dist/{ingestTypes-CEi-Ot7F.js → ingestTypes-B312BwXg.js} +2 -2
  40. package/dist/{ingestTypes-CEi-Ot7F.js.map → ingestTypes-B312BwXg.js.map} +1 -1
  41. package/dist/{ingestTypes-iaiq33Ph.cjs → ingestTypes-B4XfrUoq.cjs} +3 -3
  42. package/dist/{ingestTypes-iaiq33Ph.cjs.map → ingestTypes-B4XfrUoq.cjs.map} +1 -1
  43. package/dist/{calendarGrid-CQ8MVSRb.js → itemId-DfTm08jm.js} +34 -2
  44. package/dist/itemId-DfTm08jm.js.map +1 -0
  45. package/dist/{calendarGrid-DGILaVxI.cjs → itemId-Dzrr3vs5.cjs} +45 -1
  46. package/dist/itemId-Dzrr3vs5.cjs.map +1 -0
  47. package/dist/listen-D5av7GzB.js +22 -0
  48. package/dist/listen-D5av7GzB.js.map +1 -0
  49. package/dist/listen-UZA68Upt.cjs +45 -0
  50. package/dist/listen-UZA68Upt.cjs.map +1 -0
  51. package/dist/{promptSafety-CpiME8pj.js → promptSafety-CSdyp7RF.js} +2 -2
  52. package/dist/{promptSafety-CpiME8pj.js.map → promptSafety-CSdyp7RF.js.map} +1 -1
  53. package/dist/{promptSafety-NNGViiCr.cjs → promptSafety-CWqYq-NS.cjs} +8 -8
  54. package/dist/{promptSafety-NNGViiCr.cjs.map → promptSafety-CWqYq-NS.cjs.map} +1 -1
  55. package/dist/remote-host/server/hostRunner.d.ts +2 -4
  56. package/dist/remote-host/server/index.cjs +4 -19
  57. package/dist/remote-host/server/index.cjs.map +1 -1
  58. package/dist/remote-host/server/index.js +1 -16
  59. package/dist/remote-host/server/index.js.map +1 -1
  60. package/dist/{server-DdHaX_Uu.js → server-BNe8XMtu.js} +677 -6
  61. package/dist/server-BNe8XMtu.js.map +1 -0
  62. package/dist/{server-9mFaGLdq.cjs → server-CgkkWWBd.cjs} +807 -22
  63. package/dist/server-CgkkWWBd.cjs.map +1 -0
  64. package/package.json +1 -1
  65. package/dist/calendarGrid-CQ8MVSRb.js.map +0 -1
  66. package/dist/calendarGrid-DGILaVxI.cjs.map +0 -1
  67. package/dist/collection/core/shortHexId.d.ts +0 -8
  68. package/dist/discovery-CdkaURVY.js.map +0 -1
  69. package/dist/discovery-Cw5xWzZD.cjs.map +0 -1
  70. package/dist/server-9mFaGLdq.cjs.map +0 -1
  71. package/dist/server-DdHaX_Uu.js.map +0 -1
@@ -1,18 +1,19 @@
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-DGILaVxI.cjs");
4
+ const require_itemId = require("./itemId-Dzrr3vs5.cjs");
5
5
  const require_project = require("./project-C1ep9pvo.cjs");
6
+ const require_listen = require("./listen-UZA68Upt.cjs");
6
7
  const require_templatePath = require("./templatePath-27vUZowm.cjs");
7
8
  let node_fs = require("node:fs");
8
9
  let node_path = require("node:path");
9
10
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
10
11
  let node_crypto = require("node:crypto");
11
12
  let node_fs_promises = require("node:fs/promises");
13
+ let zod = require("zod");
12
14
  let node_os = require("node:os");
13
15
  let iconv_lite = require("iconv-lite");
14
16
  iconv_lite = require_rolldown_runtime.__toESM(iconv_lite, 1);
15
- let zod = require("zod");
16
17
  //#region src/host/hostSlot.ts
17
18
  function createHostSlot(name) {
18
19
  let current = null;
@@ -54,7 +55,7 @@ var COLLECTION_ROOT_REQUIRED = "COLLECTION_ROOT_REQUIRED";
54
55
  /** Build a local {@link CollectionKey}, canonicalising the root. The identity
55
56
  * type itself is isomorphic and cannot canonicalise (that needs `node:path`),
56
57
  * so this is the constructor server code should use. */
57
- var localCollectionKey = (root, slug) => require_calendarGrid.localCollectionKeyOf(require_root.canonicalRoot(root), slug);
58
+ var localCollectionKey = (root, slug) => require_itemId.localCollectionKeyOf(require_root.canonicalRoot(root), slug);
58
59
  /** Build a change payload, attaching `root` only when the engine call carried
59
60
  * an explicit one. Centralised so every publish site states the root the same
60
61
  * way, and so a single-workspace host's payload shape stays byte-identical to
@@ -69,7 +70,7 @@ function collectionChangePayload(base, root) {
69
70
  * which is what it is called inside its app; `aid` is what makes it an
70
71
  * identity. Never stamps a `root` — a shared collection does not have one. */
71
72
  function sharedCollectionChangePayload(base, aid) {
72
- require_calendarGrid.sharedCollectionKey(aid, base.slug);
73
+ require_itemId.sharedCollectionKey(aid, base.slug);
73
74
  return {
74
75
  ...base,
75
76
  aid
@@ -102,11 +103,12 @@ function requireOneScope(payload) {
102
103
  }
103
104
  function collectionChangeKey(payload, fallbackRoot) {
104
105
  requireOneScope(payload);
105
- return payload.aid === void 0 ? localCollectionKey(payload.root ?? fallbackRoot, payload.slug) : require_calendarGrid.sharedCollectionKey(payload.aid, payload.slug);
106
+ return payload.aid === void 0 ? localCollectionKey(payload.root ?? fallbackRoot, payload.slug) : require_itemId.sharedCollectionKey(payload.aid, payload.slug);
106
107
  }
107
108
  var hostSlot = createHostSlot("@mulmoclaude/core/collection/server: configureCollectionHost()");
108
109
  var changePublisher = null;
109
110
  var firestoreAccessor = null;
111
+ var sharedCollectionsSupported = false;
110
112
  /** Wire the engine to a host. Call once at server startup, before any
111
113
  * collection storage operation. Re-binding to a *different* host throws —
112
114
  * silently redirecting later filesystem operations to another workspace
@@ -130,6 +132,39 @@ function setCollectionChangePublisher(publish) {
130
132
  function publishCollectionChange(payload) {
131
133
  changePublisher?.(payload);
132
134
  }
135
+ /** Does this host serve SHARED (firestore-backed) collections at all?
136
+ *
137
+ * Opt-in, and `false` until a host says otherwise, because the default has to
138
+ * be the safe one: a host that cannot reach Firestore must not accept a
139
+ * shared schema and then report the collection as empty.
140
+ *
141
+ * WHY A CAPABILITY AND NOT A HOST CHECK. The engine must not know which host
142
+ * it is running in. The rule being expressed — shared collections live in a
143
+ * PROJECT REPOSITORY, not in a single managed workspace where one roster
144
+ * would govern every unrelated collection beside it — is a property the host
145
+ * knows about ITSELF. Asking the engine to test for a particular host's
146
+ * workspace would put a host's name in shared code and make every change to
147
+ * a one-host feature a change to this package.
148
+ *
149
+ * WHY NOT A FIELD ON `CollectionHost`. Same reason the accessor below is not
150
+ * one: `configureCollectionHost` is a ONE-SHOT binding a host sets at startup
151
+ * and cannot re-bind, so a suite exercising this engine could not turn the
152
+ * capability on for itself without owning the whole host. This is the same
153
+ * concern as the accessor and belongs beside it.
154
+ *
155
+ * NOT DERIVED FROM THE ACCESSOR. "Has a session right now" and "serves shared
156
+ * collections at all" are different questions: the accessor answers null
157
+ * between connections, and a collection must not stop being ACCEPTABLE
158
+ * because nobody is signed in — that would turn "connect first" into "this
159
+ * schema is invalid". */
160
+ function setSharedCollectionsSupport(supported) {
161
+ sharedCollectionsSupported = supported;
162
+ }
163
+ /** Whether the host declared support. Consulted from the schema ACCEPTANCE
164
+ * gate, whose contract is to return a reason rather than raise. */
165
+ function hostSupportsSharedCollections() {
166
+ return sharedCollectionsSupported;
167
+ }
133
168
  /** Wire the accessor for the host's authenticated Firestore session.
134
169
  *
135
170
  * Separate from `configureCollectionHost` for the same reason
@@ -234,7 +269,7 @@ var SCHEMA_FILE = "schema.json";
234
269
  * `js/path-injection` sanitiser. */
235
270
  function safeSlugName(slug) {
236
271
  if (typeof slug !== "string") return null;
237
- if (!require_calendarGrid.SAFE_SLUG_PATTERN.test(slug)) return null;
272
+ if (!require_itemId.SAFE_SLUG_PATTERN.test(slug)) return null;
238
273
  const basename = node_path.default.basename(slug);
239
274
  if (basename !== slug) return null;
240
275
  return basename;
@@ -246,7 +281,7 @@ function safeSlugName(slug) {
246
281
  * `js/path-injection` sanitiser CodeQL recognises on `safeSlugName`. */
247
282
  function safeRecordId(recordId) {
248
283
  if (typeof recordId !== "string") return null;
249
- if (!require_calendarGrid.SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
284
+ if (!require_itemId.SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
250
285
  if (recordId.includes("..")) return null;
251
286
  const basename = node_path.default.basename(recordId);
252
287
  if (basename !== recordId) return null;
@@ -378,7 +413,7 @@ function sharedItemsPath(key) {
378
413
  * missing `appId` — see NO_APP. */
379
414
  function keyOf(collection) {
380
415
  if (collection.appId === void 0) throw new Error(NO_APP);
381
- return require_calendarGrid.sharedCollectionKey(collection.appId, collection.slug);
416
+ return require_itemId.sharedCollectionKey(collection.appId, collection.slug);
382
417
  }
383
418
  function requireHandle() {
384
419
  const handle = firestoreHandle();
@@ -420,8 +455,32 @@ async function guarded(key, email, run) {
420
455
  * older version) can hold anything, so a non-object is dropped rather than
421
456
  * surfaced as a broken record — the same fail-soft the file store applies to
422
457
  * an unparseable `.json`. */
423
- function toItem(data) {
424
- return require_dist.isRecord(data) ? data : null;
458
+ /** A stored document as a record — with its identity taken from the DOCUMENT
459
+ * ID, never from the document's own fields.
460
+ *
461
+ * This is the one place a shared record's identity is decided, and it is
462
+ * decided against the field the writer supplied on purpose.
463
+ *
464
+ * The rules can constrain the document id (`idFrom` pins it to the
465
+ * submitter's uid, or to uid+field) and they CANNOT constrain the value of a
466
+ * field: `validateOk` checks which keys are present, `keyFieldsOk` checks a
467
+ * declared enum, and nothing compares `request.resource.data[primaryKey]`
468
+ * with the path being written. So a public submitter writing at their one
469
+ * permitted document id could put ANY value in the primary-key field —
470
+ * another member's record id, or a duplicate — and every reader would take it
471
+ * as the record's identity. Overwriting it here makes that unreachable rather
472
+ * than merely discouraged, and it costs nothing: for a record written through
473
+ * this store the two already agree, because `firestoreWrite` writes at the id
474
+ * it was given.
475
+ *
476
+ * It also removes the reason a submit path would have had to accept the
477
+ * primary key as a `createField` at all — a submission that cannot name its
478
+ * own id is exactly right when the id is the thing being assigned. */
479
+ function toItem(data, docId, primaryKey) {
480
+ return require_dist.isRecord(data) ? {
481
+ ...data,
482
+ [primaryKey]: docId
483
+ } : null;
425
484
  }
426
485
  /** Record ids are validated with the SAME helper every other backend uses.
427
486
  * Firestore would accept ids the file store refuses, but a record should stay
@@ -432,9 +491,9 @@ function withSafeId(itemId, onInvalid, run) {
432
491
  if (safeId === null) return onInvalid();
433
492
  return run(safeId, requireHandle());
434
493
  }
435
- async function firestoreList(key) {
494
+ async function firestoreList(key, primaryKey) {
436
495
  const { docs, email } = requireHandle();
437
- return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data)).filter((item) => item !== null);
496
+ return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data, entry.id, primaryKey)).filter((item) => item !== null);
438
497
  }
439
498
  /** Paging is emulated over a full ordered read rather than pushed into
440
499
  * Firestore: `offset` has no server-side form there (the cursor API needs the
@@ -442,7 +501,7 @@ async function firestoreList(key) {
442
501
  * `total` needs the full count anyway. Hence `nativePaging: false` — the
443
502
  * capability is honest about the cost. */
444
503
  async function firestorePage(key, primaryKey, opts) {
445
- const items = await firestoreList(key);
504
+ const items = await firestoreList(key, primaryKey);
446
505
  const offset = Math.max(0, opts.offset ?? 0);
447
506
  return {
448
507
  items: projectItemFields(opts.limit === void 0 ? items.slice(offset) : items.slice(offset, offset + Math.max(0, opts.limit)), opts.fields, primaryKey),
@@ -450,8 +509,8 @@ async function firestorePage(key, primaryKey, opts) {
450
509
  truncated: false
451
510
  };
452
511
  }
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))));
512
+ async function firestoreRead(key, itemId, primaryKey) {
513
+ return withSafeId(itemId, () => Promise.resolve(null), async (safeId, { docs, email }) => toItem(await guarded(key, email, () => docs.get(sharedItemsPath(key), safeId)), safeId, primaryKey));
455
514
  }
456
515
  /** Publish the "records changed" ping for a shared collection.
457
516
  *
@@ -502,6 +561,111 @@ async function firestoreDelete(key, itemId, opts) {
502
561
  };
503
562
  });
504
563
  }
564
+ /** Deliver one snapshot's changes.
565
+ *
566
+ * THE FIRST SNAPSHOT IS ONE COLLECTION-LEVEL REPORT, not N per-record ones.
567
+ * `onSnapshot` hands over the collection's current contents immediately, as a
568
+ * snapshot in which every existing document reads as `added`.
569
+ *
570
+ * Dropping it is wrong. There is a GAP on either side of a subscription — the
571
+ * boot reconcile finishes before the listener arms, and a listener that died
572
+ * is re-armed after a backoff — and a record that moved inside one of those
573
+ * gaps appears only in that first snapshot. Since this backend now HAS a
574
+ * `watch`, the watcher's periodic re-derivation no longer covers it, so a
575
+ * dropped first snapshot means stale bells and stale views until that record
576
+ * happens to change again. That is exactly the failure this step was supposed
577
+ * to remove.
578
+ *
579
+ * Announcing every record individually is also wrong: it is a refresh storm
580
+ * to every open view, on every mount and every reconnect. `{ kind: "collection" }`
581
+ * says the same thing in one event — the watcher answers it with a full
582
+ * re-derivation plus a sweep, which is precisely "work out what changed while
583
+ * I was not listening".
584
+ *
585
+ * A change this process made itself also arrives here — the write path has
586
+ * already published for it, so the record is reconciled twice. Harmless
587
+ * (reconciling is idempotent) and left alone: suppressing it would mean
588
+ * tracking our own in-flight writes, and getting that wrong loses a real
589
+ * change rather than a duplicate one. */
590
+ function deliverSnapshot(run, ids, initial) {
591
+ if (run.stopped) return;
592
+ if (initial) {
593
+ run.onChange({ kind: "collection" });
594
+ return;
595
+ }
596
+ for (const itemId of ids) run.onChange({
597
+ kind: "item",
598
+ itemId
599
+ });
600
+ }
601
+ /** A listener died. Firestore never revives one on its own, so the choice is
602
+ * re-subscribe or go dark.
603
+ *
604
+ * NO OVERALL RETRY WINDOW, unlike `hostRunner`'s listener. There, giving up
605
+ * escalates to a lifecycle owner that can re-authenticate; here there is
606
+ * nobody above to escalate to — the watcher registers a mounted collection
607
+ * once and never re-arms it, so "give up" means this collection serves stale
608
+ * data until the server restarts. A capped 30s re-subscribe is cheap and
609
+ * makes recovery automatic when the session comes back.
610
+ *
611
+ * A FATAL error still stops: `permission-denied` is what a revoked membership
612
+ * looks like, and re-listening cannot restore a grant. It is logged as such
613
+ * rather than retried in silence. */
614
+ function handleWatchError(run, error) {
615
+ if (run.stopped) return;
616
+ if (require_listen.classifyListenerError(error) === "fatal") {
617
+ log.warn("collections", "shared collection listener stopped", {
618
+ aid: run.key.aid,
619
+ cid: run.key.cid,
620
+ error: require_dist.isRecord(error) && typeof error.message === "string" ? error.message : String(error),
621
+ detail: "live updates for this collection are off until the server re-syncs; a revoked membership looks exactly like this"
622
+ });
623
+ return;
624
+ }
625
+ run.retryTimer = setTimeout(() => subscribeShared(run), require_listen.backoffDelayMs(run.attempt));
626
+ run.attempt += 1;
627
+ }
628
+ /** (Re-)arm the listener. The handle is fetched HERE rather than captured: a
629
+ * retry may land after the session was replaced, and listening through a
630
+ * closed session's handle would fail forever. */
631
+ function subscribeShared(run) {
632
+ run.retryTimer = null;
633
+ if (run.stopped) return;
634
+ const handle = firestoreHandle();
635
+ if (handle === null) {
636
+ run.retryTimer = setTimeout(() => subscribeShared(run), require_listen.backoffDelayMs(run.attempt));
637
+ run.attempt += 1;
638
+ return;
639
+ }
640
+ run.detach = handle.docs.watch(sharedItemsPath(run.key), (ids, meta) => {
641
+ run.attempt = 0;
642
+ deliverSnapshot(run, ids, meta.initial);
643
+ }, (error) => handleWatchError(run, error));
644
+ }
645
+ /** Subscribe to a shared collection's records.
646
+ *
647
+ * Returns `null` when there is no session: the watcher reads that as "has a
648
+ * watch but could not arm it", leaves the collection unmounted and retries on
649
+ * the next sync tick — which is the right channel for a connection that has
650
+ * not happened yet. Arming a dead listener instead would mark the slug mounted
651
+ * forever. */
652
+ function armSharedWatch(key, onChange) {
653
+ if (firestoreHandle() === null) return null;
654
+ const run = {
655
+ key,
656
+ onChange,
657
+ stopped: false,
658
+ detach: () => {},
659
+ retryTimer: null,
660
+ attempt: 0
661
+ };
662
+ subscribeShared(run);
663
+ return () => {
664
+ run.stopped = true;
665
+ if (run.retryTimer !== null) clearTimeout(run.retryTimer);
666
+ run.detach();
667
+ };
668
+ }
505
669
  /** The store factory registered for `storage.type === "firestore"`.
506
670
  * Synchronous and connection-agnostic by contract — see the header. */
507
671
  function firestoreStoreFor(collection, opts) {
@@ -516,14 +680,15 @@ function firestoreStoreFor(collection, opts) {
516
680
  nativeQuery: false,
517
681
  nativePaging: false
518
682
  },
519
- list: async () => firestoreList(keyOf(collection)),
683
+ list: async () => firestoreList(keyOf(collection), primaryKey),
520
684
  page: async (pageOpts = {}) => firestorePage(keyOf(collection), primaryKey, pageOpts),
521
- read: async (itemId) => firestoreRead(keyOf(collection), itemId),
685
+ read: async (itemId) => firestoreRead(keyOf(collection), itemId, primaryKey),
522
686
  write: async (itemId, item, writeOpts = {}) => firestoreWrite(keyOf(collection), itemId, item, {
523
687
  ...ioOpts,
524
688
  refuseOverwrite: writeOpts.refuseOverwrite
525
689
  }),
526
- delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts)
690
+ delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts),
691
+ watch: async (onChange) => armSharedWatch(keyOf(collection), onChange)
527
692
  };
528
693
  }
529
694
  //#endregion
@@ -591,7 +756,7 @@ function parseAppManifest(raw) {
591
756
  kind: "malformed",
592
757
  detail: "declares no `aid` string"
593
758
  };
594
- if (!require_calendarGrid.isValidCollectionName(aid)) return {
759
+ if (!require_itemId.isValidCollectionName(aid)) return {
595
760
  ok: false,
596
761
  kind: "malformed",
597
762
  detail: `\`aid\` '${aid}' is not a valid app id`
@@ -813,11 +978,13 @@ async function deleteItem(dataDir, itemId, opts = {}) {
813
978
  throw err;
814
979
  }
815
980
  }
816
- /** Generate a short random hex id. Used by POST when the form doesn't
817
- * carry a primary-key value (UI shortcut — Claude normally derives a
818
- * semantic id from the record's name). */
981
+ /** Generate a record id. Used by POST when the form doesn't carry a
982
+ * primary-key value (UI shortcut — Claude normally derives a semantic
983
+ * id from the record's name). Delegates to the isomorphic generator, so
984
+ * the id the UI pre-fills and the id a blank-id POST mints stay the
985
+ * same thing rather than two implementations that have to agree. */
819
986
  function generateItemId() {
820
- return (0, node_crypto.randomBytes)(4).toString("hex");
987
+ return require_itemId.newItemId();
821
988
  }
822
989
  /** The item id a CREATE should use for `schema`, or null when the
823
990
  * caller should generate one. A singleton collection pins every
@@ -1095,7 +1262,7 @@ function normalizeCsvValue(value) {
1095
1262
  function csvRowToItem(row, primaryKey) {
1096
1263
  const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
1097
1264
  const rawKey = normalized[primaryKey];
1098
- const keyText = require_calendarGrid.fieldTextOrNull(rawKey);
1265
+ const keyText = require_itemId.fieldTextOrNull(rawKey);
1099
1266
  if (keyText === null || keyText === "") return null;
1100
1267
  return {
1101
1268
  ...normalized,
@@ -1703,8 +1870,8 @@ function sqliteStoreFor(collection, opts) {
1703
1870
  * is filesystem-dependent; paging needs determinism. */
1704
1871
  function sortByRecordId(items, primaryKey) {
1705
1872
  return [...items].sort((left, right) => {
1706
- const leftId = require_calendarGrid.fieldText(left[primaryKey]);
1707
- const rightId = require_calendarGrid.fieldText(right[primaryKey]);
1873
+ const leftId = require_itemId.fieldText(left[primaryKey]);
1874
+ const rightId = require_itemId.fieldText(right[primaryKey]);
1708
1875
  if (leftId < rightId) return -1;
1709
1876
  return leftId > rightId ? 1 : 0;
1710
1877
  });
@@ -1714,7 +1881,7 @@ function sortByRecordId(items, primaryKey) {
1714
1881
  * data file itself. Every write entry point checks this BEFORE calling
1715
1882
  * `writeItem`/`deleteItem` — server-enforced, not just UI-hidden. */
1716
1883
  function collectionWritable(collection) {
1717
- return !require_calendarGrid.isReadOnlySchema(collection.schema);
1884
+ return !require_itemId.isReadOnlySchema(collection.schema);
1718
1885
  }
1719
1886
  /** The one-line refusal write paths surface (HTTP 405 / MCP error text). */
1720
1887
  function readOnlyRefusal(slug) {
@@ -1782,7 +1949,7 @@ var storeFactories = /* @__PURE__ */ new Map([
1782
1949
  * schema's `StorageZ` union gates it), so the throw is a loud invariant
1783
1950
  * breach, not a user-facing path. */
1784
1951
  function storeFor(collection, opts = {}) {
1785
- const kind = require_calendarGrid.storageKindFor(collection.schema);
1952
+ const kind = require_itemId.storageKindFor(collection.schema);
1786
1953
  const factory = storeFactories.get(kind);
1787
1954
  if (!factory) throw new Error(`no store factory registered for storage kind '${kind}'`);
1788
1955
  return factory(collection, opts);
@@ -1825,7 +1992,7 @@ var CODE_FIELD_TYPES = /* @__PURE__ */ new Set([
1825
1992
  ]);
1826
1993
  var namesStoredField = (fields, name, primaryKey) => {
1827
1994
  const target = declaredField(fields, name);
1828
- return target !== void 0 && !require_calendarGrid.COMPUTED_TYPES.has(target.type) && name !== primaryKey;
1995
+ return target !== void 0 && !require_itemId.COMPUTED_TYPES.has(target.type) && name !== primaryKey;
1829
1996
  };
1830
1997
  var hasUniqueIds = (entries) => entries === void 0 || new Set(entries.map((entry) => entry.id)).size === entries.length;
1831
1998
  /** Exactly one storage declaration: native records need `dataPath`, an external
@@ -1884,7 +2051,7 @@ function collectionActionsAreNotMutate(schema) {
1884
2051
  * otherwise the create form would lock the primary key to a value the POST
1885
2052
  * route then rejects, making the collection impossible to initialize. */
1886
2053
  function singletonIsAValidRecordId(schema) {
1887
- return schema.singleton === void 0 || require_calendarGrid.isSafeRecordId(schema.singleton);
2054
+ return schema.singleton === void 0 || require_itemId.isSafeRecordId(schema.singleton);
1888
2055
  }
1889
2056
  function collectCurrencyFieldRefs(fields) {
1890
2057
  const refs = [];
@@ -1929,7 +2096,7 @@ function completionFlagReadsOnlyStoredFields(schema) {
1929
2096
  if (spec?.type !== "flag") return true;
1930
2097
  return spec.where.every((cond) => [cond.field, ...cond.valueFrom ? [cond.valueFrom.field] : []].every((name) => {
1931
2098
  const target = declaredField(schema.fields, name);
1932
- return target !== void 0 && !require_calendarGrid.COMPUTED_TYPES.has(target.type);
2099
+ return target !== void 0 && !require_itemId.COMPUTED_TYPES.has(target.type);
1933
2100
  }));
1934
2101
  }
1935
2102
  /** `displayField`, like `completionField`, must name a real top-level field —
@@ -2077,7 +2244,7 @@ function fieldDrivenFromFieldCarried(schema) {
2077
2244
  if (set && Object.prototype.hasOwnProperty.call(set, driven.fromField)) {
2078
2245
  const raw = set[driven.fromField];
2079
2246
  if (raw === void 0 || raw === null || raw === "") return false;
2080
- const key = require_calendarGrid.fieldTextOrNull(raw);
2247
+ const key = require_itemId.fieldTextOrNull(raw);
2081
2248
  return key !== null && Object.prototype.hasOwnProperty.call(driven.map, key);
2082
2249
  }
2083
2250
  return (carry ?? []).includes(driven.fromField);
@@ -2131,7 +2298,7 @@ function notifyWhenFieldIsDeclared(schema) {
2131
2298
  * selector key (`custom:<id>`) and the capability-token clamp key, both of
2132
2299
  * which expect a path-safe token. */
2133
2300
  function viewIdsAreSlugs(schema) {
2134
- return schema.views === void 0 || schema.views.every((view) => require_calendarGrid.isSafeSlug(view.id));
2301
+ return schema.views === void 0 || schema.views.every((view) => require_itemId.isSafeSlug(view.id));
2135
2302
  }
2136
2303
  /** Custom view ids must be unique so the selector + token clamp resolve
2137
2304
  * unambiguously. */
@@ -2231,7 +2398,7 @@ var RefFieldZ = zod.z.object({
2231
2398
  type: zod.z.literal("ref"),
2232
2399
  ...fieldBase,
2233
2400
  to: zod.z.string().min(1)
2234
- }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to), slugMessage("to"));
2401
+ }).refine((spec) => require_itemId.isSafeSlug(spec.to), slugMessage("to"));
2235
2402
  /** A money amount. See `currencyKeys` for the currency-source contract. */
2236
2403
  var MoneyFieldZ = zod.z.object({
2237
2404
  type: zod.z.literal("money"),
@@ -2266,7 +2433,7 @@ var SubRefFieldZ = zod.z.object({
2266
2433
  type: zod.z.literal("ref"),
2267
2434
  ...subFieldBase,
2268
2435
  to: zod.z.string().min(1)
2269
- }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to), slugMessage("to"));
2436
+ }).refine((spec) => require_itemId.isSafeSlug(spec.to), slugMessage("to"));
2270
2437
  var SubMoneyFieldZ = zod.z.object({
2271
2438
  type: zod.z.literal("money"),
2272
2439
  ...subFieldBase,
@@ -2329,7 +2496,7 @@ var EmbedFieldZ = zod.z.object({
2329
2496
  to: zod.z.string().min(1),
2330
2497
  id: zod.z.string().trim().min(1).optional(),
2331
2498
  idField: zod.z.string().trim().min(1).optional()
2332
- }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to) && spec.id !== void 0 !== (spec.idField !== void 0), {
2499
+ }).refine((spec) => require_itemId.isSafeSlug(spec.to) && spec.id !== void 0 !== (spec.idField !== void 0), {
2333
2500
  message: "fields with type 'embed' must declare a `to` (valid collection slug) and exactly one of `id` (a fixed record's primary key) or `idField` (a sibling field naming the per-record target)",
2334
2501
  path: ["id"]
2335
2502
  });
@@ -2350,7 +2517,7 @@ var BacklinksFieldZ = zod.z.object({
2350
2517
  via: zod.z.string().trim().min(1),
2351
2518
  display: zod.z.array(zod.z.string().trim().min(1)).min(1),
2352
2519
  filter: WhenZ.optional()
2353
- }).refine((spec) => require_calendarGrid.isSafeSlug(spec.from), slugMessage("from"));
2520
+ }).refine((spec) => require_itemId.isSafeSlug(spec.from), slugMessage("from"));
2354
2521
  /** A cross-collection AGGREGATE over a backlink relation (plan step ⑤ of
2355
2522
  * plans/done/collection-ontology.md): a computed number — never stored — that
2356
2523
  * sums a source column (or counts rows) over the records in `from` whose
@@ -2371,7 +2538,7 @@ var RollupFieldZ = zod.z.object({
2371
2538
  op: zod.z.enum(["sum", "count"]),
2372
2539
  column: zod.z.string().trim().min(1).optional(),
2373
2540
  filter: WhenZ.optional()
2374
- }).refine((spec) => require_calendarGrid.isSafeSlug(spec.from), slugMessage("from")).refine((spec) => spec.op === "sum" === (spec.column !== void 0), {
2541
+ }).refine((spec) => require_itemId.isSafeSlug(spec.from), slugMessage("from")).refine((spec) => spec.op === "sum" === (spec.column !== void 0), {
2375
2542
  message: "a rollup's `column` names the source column to aggregate: required for op \"sum\", meaningless for op \"count\"",
2376
2543
  path: ["column"]
2377
2544
  });
@@ -2545,9 +2712,9 @@ var SpawnZ = zod.z.object({
2545
2712
  * array) only when the response body isn't itself the array; rss/atom yield
2546
2713
  * items natively and ignore it — so no kind-specific requirement here. */
2547
2714
  var DeclarativeIngestZ = zod.z.object({
2548
- kind: zod.z.enum(require_calendarGrid.INGEST_KINDS),
2715
+ kind: zod.z.enum(require_itemId.INGEST_KINDS),
2549
2716
  url: zod.z.url(),
2550
- schedule: zod.z.enum(require_calendarGrid.FEED_SCHEDULES),
2717
+ schedule: zod.z.enum(require_itemId.FEED_SCHEDULES),
2551
2718
  atHour: zod.z.number().int().min(0).max(23).optional(),
2552
2719
  itemsAt: zod.z.string().trim().min(1).optional(),
2553
2720
  map: zod.z.record(zod.z.string().trim().min(1), zod.z.string().trim().min(1)),
@@ -2561,8 +2728,8 @@ var DeclarativeIngestZ = zod.z.object({
2561
2728
  * validated the SAME way an action's template is (safe path under
2562
2729
  * `templates/`), so the skill-bridge mirrors it identically. */
2563
2730
  var AgentIngestZ = zod.z.object({
2564
- kind: zod.z.literal(require_calendarGrid.AGENT_INGEST_KIND),
2565
- schedule: zod.z.enum(require_calendarGrid.FEED_SCHEDULES),
2731
+ kind: zod.z.literal(require_itemId.AGENT_INGEST_KIND),
2732
+ schedule: zod.z.enum(require_itemId.FEED_SCHEDULES),
2566
2733
  atHour: zod.z.number().int().min(0).max(23).optional(),
2567
2734
  role: zod.z.string().trim().min(1),
2568
2735
  template: zod.z.string().trim().min(1).refine(require_templatePath.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/refresh.md`; no `..`, no leading `/`, no backslash)")
@@ -2975,6 +3142,10 @@ function acceptStorageSchema(storage, opts) {
2975
3142
  storageFile
2976
3143
  };
2977
3144
  }
3145
+ if (!hostSupportsSharedCollections()) return {
3146
+ ok: false,
3147
+ reason: "this host does not support shared collections — they live in a project repository, not a managed workspace"
3148
+ };
2978
3149
  const manifest = loadAppManifest(opts.workspaceRoot);
2979
3150
  if (!manifest.ok) return {
2980
3151
  ok: false,
@@ -3325,6 +3496,12 @@ Object.defineProperty(exports, "getWorkspaceRoot", {
3325
3496
  return getWorkspaceRoot;
3326
3497
  }
3327
3498
  });
3499
+ Object.defineProperty(exports, "hostSupportsSharedCollections", {
3500
+ enumerable: true,
3501
+ get: function() {
3502
+ return hostSupportsSharedCollections;
3503
+ }
3504
+ });
3328
3505
  Object.defineProperty(exports, "isBackendUnavailable", {
3329
3506
  enumerable: true,
3330
3507
  get: function() {
@@ -3499,6 +3676,12 @@ Object.defineProperty(exports, "setFirestoreAccessor", {
3499
3676
  return setFirestoreAccessor;
3500
3677
  }
3501
3678
  });
3679
+ Object.defineProperty(exports, "setSharedCollectionsSupport", {
3680
+ enumerable: true,
3681
+ get: function() {
3682
+ return setSharedCollectionsSupport;
3683
+ }
3684
+ });
3502
3685
  Object.defineProperty(exports, "sharedCollectionChangePayload", {
3503
3686
  enumerable: true,
3504
3687
  get: function() {
@@ -3548,4 +3731,4 @@ Object.defineProperty(exports, "writeItem", {
3548
3731
  }
3549
3732
  });
3550
3733
 
3551
- //# sourceMappingURL=discovery-Cw5xWzZD.cjs.map
3734
+ //# sourceMappingURL=discovery-BsXiDZBR.cjs.map