@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
@@ -59,9 +59,9 @@ export declare const SubFieldSpecZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
59
59
  string: "string";
60
60
  number: "number";
61
61
  boolean: "boolean";
62
+ date: "date";
62
63
  text: "text";
63
64
  email: "email";
64
- date: "date";
65
65
  datetime: "datetime";
66
66
  markdown: "markdown";
67
67
  }>;
@@ -94,13 +94,13 @@ export declare const FieldSpecZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
94
94
  string: "string";
95
95
  number: "number";
96
96
  boolean: "boolean";
97
+ date: "date";
98
+ file: "file";
97
99
  text: "text";
98
100
  email: "email";
99
- date: "date";
100
101
  datetime: "datetime";
101
102
  markdown: "markdown";
102
103
  image: "image";
103
- file: "file";
104
104
  }>;
105
105
  }, z.core.$strip>, z.ZodObject<{
106
106
  to: z.ZodString;
@@ -141,9 +141,9 @@ export declare const FieldSpecZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
141
141
  string: "string";
142
142
  number: "number";
143
143
  boolean: "boolean";
144
+ date: "date";
144
145
  text: "text";
145
146
  email: "email";
146
- date: "date";
147
147
  datetime: "datetime";
148
148
  markdown: "markdown";
149
149
  }>;
@@ -308,9 +308,9 @@ export declare const ActionSpecZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
308
308
  string: "string";
309
309
  number: "number";
310
310
  boolean: "boolean";
311
+ date: "date";
311
312
  text: "text";
312
313
  email: "email";
313
- date: "date";
314
314
  datetime: "datetime";
315
315
  markdown: "markdown";
316
316
  }>;
@@ -672,15 +672,21 @@ export declare const DataSourceZ: z.ZodObject<{
672
672
  /** Alternative WRITABLE storage backend for a collection's records —
673
673
  * unlike `dataSource` (external read-only file), a `storage` collection
674
674
  * behaves like a normal writable collection; only where the rows live
675
- * changes. v1: `sqlite` — records in a single SQLite database file
676
- * (`node:sqlite`, one JSON record per row keyed by the primaryKey).
677
- * `path` is workspace-relative and containment-checked exactly like
678
- * `dataPath`. The store factory registry (`server/store.ts`) picks the
679
- * implementation by `type` (plans/done/refactor-storage-virtualization.md). */
680
- export declare const StorageZ: z.ZodObject<{
675
+ * changes. The store factory registry (`server/store.ts`) picks the
676
+ * implementation by `type` (plans/done/refactor-storage-virtualization.md).
677
+ *
678
+ * A discriminated union rather than one shape with optional keys, because
679
+ * only the sqlite variant is a workspace FILE: its `path` is
680
+ * workspace-relative and containment-checked exactly like `dataPath`, while
681
+ * the firestore variant has no path to check — its records are not on this
682
+ * machine at all. Optional keys would let each arm accept the other's, and
683
+ * the compiler would stop being the thing that tells you which. */
684
+ export declare const StorageZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
681
685
  type: z.ZodLiteral<"sqlite">;
682
686
  path: z.ZodString;
683
- }, z.core.$strip>;
687
+ }, z.core.$strip>, z.ZodObject<{
688
+ type: z.ZodLiteral<"firestore">;
689
+ }, z.core.$strict>], "type">;
684
690
  /** The schema's SHAPE, before any cross-field rule runs. Named so the rules in
685
691
  * `./schemaRules` can type their argument against it — they receive whatever
686
692
  * this parses to, and nothing narrower. */
@@ -692,10 +698,12 @@ declare const CollectionObjectZ: z.ZodObject<{
692
698
  type: z.ZodLiteral<"csv">;
693
699
  path: z.ZodString;
694
700
  }, z.core.$strip>>;
695
- storage: z.ZodOptional<z.ZodObject<{
701
+ storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
696
702
  type: z.ZodLiteral<"sqlite">;
697
703
  path: z.ZodString;
698
- }, z.core.$strip>>;
704
+ }, z.core.$strip>, z.ZodObject<{
705
+ type: z.ZodLiteral<"firestore">;
706
+ }, z.core.$strict>], "type">>;
699
707
  primaryKey: z.ZodString;
700
708
  singleton: z.ZodOptional<z.ZodString>;
701
709
  fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -710,13 +718,13 @@ declare const CollectionObjectZ: z.ZodObject<{
710
718
  string: "string";
711
719
  number: "number";
712
720
  boolean: "boolean";
721
+ date: "date";
722
+ file: "file";
713
723
  text: "text";
714
724
  email: "email";
715
- date: "date";
716
725
  datetime: "datetime";
717
726
  markdown: "markdown";
718
727
  image: "image";
719
- file: "file";
720
728
  }>;
721
729
  }, z.core.$strip>, z.ZodObject<{
722
730
  to: z.ZodString;
@@ -757,9 +765,9 @@ declare const CollectionObjectZ: z.ZodObject<{
757
765
  string: "string";
758
766
  number: "number";
759
767
  boolean: "boolean";
768
+ date: "date";
760
769
  text: "text";
761
770
  email: "email";
762
- date: "date";
763
771
  datetime: "datetime";
764
772
  markdown: "markdown";
765
773
  }>;
@@ -920,9 +928,9 @@ declare const CollectionObjectZ: z.ZodObject<{
920
928
  string: "string";
921
929
  number: "number";
922
930
  boolean: "boolean";
931
+ date: "date";
923
932
  text: "text";
924
933
  email: "email";
925
- date: "date";
926
934
  datetime: "datetime";
927
935
  markdown: "markdown";
928
936
  }>;
@@ -975,9 +983,9 @@ declare const CollectionObjectZ: z.ZodObject<{
975
983
  string: "string";
976
984
  number: "number";
977
985
  boolean: "boolean";
986
+ date: "date";
978
987
  text: "text";
979
988
  email: "email";
980
- date: "date";
981
989
  datetime: "datetime";
982
990
  markdown: "markdown";
983
991
  }>;
@@ -1171,10 +1179,12 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1171
1179
  type: z.ZodLiteral<"csv">;
1172
1180
  path: z.ZodString;
1173
1181
  }, z.core.$strip>>;
1174
- storage: z.ZodOptional<z.ZodObject<{
1182
+ storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
1175
1183
  type: z.ZodLiteral<"sqlite">;
1176
1184
  path: z.ZodString;
1177
- }, z.core.$strip>>;
1185
+ }, z.core.$strip>, z.ZodObject<{
1186
+ type: z.ZodLiteral<"firestore">;
1187
+ }, z.core.$strict>], "type">>;
1178
1188
  primaryKey: z.ZodString;
1179
1189
  singleton: z.ZodOptional<z.ZodString>;
1180
1190
  fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -1189,13 +1199,13 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1189
1199
  string: "string";
1190
1200
  number: "number";
1191
1201
  boolean: "boolean";
1202
+ date: "date";
1203
+ file: "file";
1192
1204
  text: "text";
1193
1205
  email: "email";
1194
- date: "date";
1195
1206
  datetime: "datetime";
1196
1207
  markdown: "markdown";
1197
1208
  image: "image";
1198
- file: "file";
1199
1209
  }>;
1200
1210
  }, z.core.$strip>, z.ZodObject<{
1201
1211
  to: z.ZodString;
@@ -1236,9 +1246,9 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1236
1246
  string: "string";
1237
1247
  number: "number";
1238
1248
  boolean: "boolean";
1249
+ date: "date";
1239
1250
  text: "text";
1240
1251
  email: "email";
1241
- date: "date";
1242
1252
  datetime: "datetime";
1243
1253
  markdown: "markdown";
1244
1254
  }>;
@@ -1399,9 +1409,9 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1399
1409
  string: "string";
1400
1410
  number: "number";
1401
1411
  boolean: "boolean";
1412
+ date: "date";
1402
1413
  text: "text";
1403
1414
  email: "email";
1404
- date: "date";
1405
1415
  datetime: "datetime";
1406
1416
  markdown: "markdown";
1407
1417
  }>;
@@ -1454,9 +1464,9 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1454
1464
  string: "string";
1455
1465
  number: "number";
1456
1466
  boolean: "boolean";
1467
+ date: "date";
1457
1468
  text: "text";
1458
1469
  email: "email";
1459
- date: "date";
1460
1470
  datetime: "datetime";
1461
1471
  markdown: "markdown";
1462
1472
  }>;
@@ -0,0 +1,51 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let firebase_firestore = require("firebase/firestore");
3
+ //#region src/collection/server/firestoreDocs.ts
4
+ /** The real implementation over the modular SDK.
5
+ *
6
+ * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not
7
+ * nested under a `data` key. This is not a matter of taste: the deployed
8
+ * security rules read `request.resource.data.<field>` and
9
+ * `resource.data[submit.emailField]` — a wrapper would put every field one
10
+ * level down, so the required-field checks, the status state machine and the
11
+ * "your own row" predicate would all read absent values and fail closed. A
12
+ * shared record's shape is part of the authorization contract.
13
+ *
14
+ * `orderBy("__name__")` orders by DOCUMENT ID. Ordering by a record field
15
+ * would silently EXCLUDE documents missing that field (the trap
16
+ * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a
17
+ * partial one; the document id is always present and gives the stable order
18
+ * the store contract requires. */
19
+ function createFirestoreDocs(database) {
20
+ return {
21
+ list: async (collectionPath) => {
22
+ return (await (0, firebase_firestore.getDocs)((0, firebase_firestore.query)((0, firebase_firestore.collection)(database, collectionPath), (0, firebase_firestore.orderBy)("__name__")))).docs.map((entry) => ({
23
+ id: entry.id,
24
+ data: entry.data()
25
+ }));
26
+ },
27
+ get: async (collectionPath, docId) => {
28
+ const snapshot = await (0, firebase_firestore.getDoc)((0, firebase_firestore.doc)(database, collectionPath, docId));
29
+ return snapshot.exists() ? snapshot.data() : null;
30
+ },
31
+ set: async (collectionPath, docId, data) => {
32
+ await (0, firebase_firestore.setDoc)((0, firebase_firestore.doc)(database, collectionPath, docId), data);
33
+ },
34
+ create: (collectionPath, docId, data) => (0, firebase_firestore.runTransaction)(database, async (transaction) => {
35
+ const ref = (0, firebase_firestore.doc)(database, collectionPath, docId);
36
+ if ((await transaction.get(ref)).exists()) return false;
37
+ transaction.set(ref, data);
38
+ return true;
39
+ }),
40
+ delete: (collectionPath, docId) => (0, firebase_firestore.runTransaction)(database, async (transaction) => {
41
+ const ref = (0, firebase_firestore.doc)(database, collectionPath, docId);
42
+ if (!(await transaction.get(ref)).exists()) return false;
43
+ transaction.delete(ref);
44
+ return true;
45
+ })
46
+ };
47
+ }
48
+ //#endregion
49
+ exports.createFirestoreDocs = createFirestoreDocs;
50
+
51
+ //# sourceMappingURL=firestore.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.cjs","names":[],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n runTransaction,\n setDoc,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqEA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,OAAA,GADgB,mBAAA,QAAA,EAAA,GAAQ,mBAAA,MAAA,EAAA,GAAe,mBAAA,WAAA,CAAoB,UAAU,cAAc,IAAA,GAAG,mBAAA,QAAA,CAAQ,UAAU,CAAC,CAAC,EAAA,CACjG,KAAK,KAAK,WAAW;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM,KAAK;GAAE,EAAE;EAC5E;EACA,KAAK,OAAO,gBAAgB,UAAU;GACpC,MAAM,WAAW,OAAA,GAAM,mBAAA,OAAA,EAAA,GAAO,mBAAA,IAAA,CAAI,UAAU,gBAAgB,KAAK,CAAC;GAClE,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,IAAI;EAC/C;EACA,KAAK,OAAO,gBAAgB,OAAO,SAAS;GAC1C,OAAA,GAAM,mBAAA,OAAA,EAAA,GAAO,mBAAA,IAAA,CAAI,UAAU,gBAAgB,KAAK,GAAG,IAAI;EACzD;EACA,SAAS,gBAAgB,OAAO,UAAA,GAC9B,mBAAA,eAAA,CAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,OAAA,GAAM,mBAAA,IAAA,CAAI,UAAU,gBAAgB,KAAK;GAE/C,KAAI,MADmB,YAAY,IAAI,GAAG,EAAA,CAC7B,OAAO,GAAG,OAAO;GAC9B,YAAY,IAAI,KAAK,IAAI;GACzB,OAAO;EACT,CAAC;EAMH,SAAS,gBAAgB,WAAA,GACvB,mBAAA,eAAA,CAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,OAAA,GAAM,mBAAA,IAAA,CAAI,UAAU,gBAAgB,KAAK;GAE/C,IAAI,EAAC,MADkB,YAAY,IAAI,GAAG,EAAA,CAC5B,OAAO,GAAG,OAAO;GAC/B,YAAY,OAAO,GAAG;GACtB,OAAO;EACT,CAAC;CACL;AACF"}
@@ -0,0 +1 @@
1
+ export { createFirestoreDocs, type FirestoreDoc, type FirestoreDocs } from './server/firestoreDocs';
@@ -0,0 +1,50 @@
1
+ import { collection, doc, getDoc, getDocs, orderBy, query, runTransaction, setDoc } from "firebase/firestore";
2
+ //#region src/collection/server/firestoreDocs.ts
3
+ /** The real implementation over the modular SDK.
4
+ *
5
+ * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not
6
+ * nested under a `data` key. This is not a matter of taste: the deployed
7
+ * security rules read `request.resource.data.<field>` and
8
+ * `resource.data[submit.emailField]` — a wrapper would put every field one
9
+ * level down, so the required-field checks, the status state machine and the
10
+ * "your own row" predicate would all read absent values and fail closed. A
11
+ * shared record's shape is part of the authorization contract.
12
+ *
13
+ * `orderBy("__name__")` orders by DOCUMENT ID. Ordering by a record field
14
+ * would silently EXCLUDE documents missing that field (the trap
15
+ * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a
16
+ * partial one; the document id is always present and gives the stable order
17
+ * the store contract requires. */
18
+ function createFirestoreDocs(database) {
19
+ return {
20
+ list: async (collectionPath) => {
21
+ return (await getDocs(query(collection(database, collectionPath), orderBy("__name__")))).docs.map((entry) => ({
22
+ id: entry.id,
23
+ data: entry.data()
24
+ }));
25
+ },
26
+ get: async (collectionPath, docId) => {
27
+ const snapshot = await getDoc(doc(database, collectionPath, docId));
28
+ return snapshot.exists() ? snapshot.data() : null;
29
+ },
30
+ set: async (collectionPath, docId, data) => {
31
+ await setDoc(doc(database, collectionPath, docId), data);
32
+ },
33
+ create: (collectionPath, docId, data) => runTransaction(database, async (transaction) => {
34
+ const ref = doc(database, collectionPath, docId);
35
+ if ((await transaction.get(ref)).exists()) return false;
36
+ transaction.set(ref, data);
37
+ return true;
38
+ }),
39
+ delete: (collectionPath, docId) => runTransaction(database, async (transaction) => {
40
+ const ref = doc(database, collectionPath, docId);
41
+ if (!(await transaction.get(ref)).exists()) return false;
42
+ transaction.delete(ref);
43
+ return true;
44
+ })
45
+ };
46
+ }
47
+ //#endregion
48
+ export { createFirestoreDocs };
49
+
50
+ //# sourceMappingURL=firestore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.js","names":[],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n runTransaction,\n setDoc,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqEA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,MADgB,QAAQ,MAAe,WAAoB,UAAU,cAAc,GAAG,QAAQ,UAAU,CAAC,CAAC,EAAA,CACjG,KAAK,KAAK,WAAW;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM,KAAK;GAAE,EAAE;EAC5E;EACA,KAAK,OAAO,gBAAgB,UAAU;GACpC,MAAM,WAAW,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,CAAC;GAClE,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,IAAI;EAC/C;EACA,KAAK,OAAO,gBAAgB,OAAO,SAAS;GAC1C,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,GAAG,IAAI;EACzD;EACA,SAAS,gBAAgB,OAAO,SAC9B,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,KAAI,MADmB,YAAY,IAAI,GAAG,EAAA,CAC7B,OAAO,GAAG,OAAO;GAC9B,YAAY,IAAI,KAAK,IAAI;GACzB,OAAO;EACT,CAAC;EAMH,SAAS,gBAAgB,UACvB,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,IAAI,EAAC,MADkB,YAAY,IAAI,GAAG,EAAA,CAC5B,OAAO,GAAG,OAAO;GAC/B,YAAY,OAAO,GAAG;GACtB,OAAO;EACT,CAAC;CACL;AACF"}
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_dist = require("../dist-CsgSfWwR.cjs");
3
- const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
3
+ const require_calendarGrid = require("../calendarGrid-DGILaVxI.cjs");
4
4
  const require_project = require("../project-C1ep9pvo.cjs");
5
- const require_promptSafety = require("../promptSafety-CNEW3_qS.cjs");
5
+ const require_promptSafety = require("../promptSafety-NNGViiCr.cjs");
6
6
  //#region src/collection/core/presentCollection.ts
7
7
  var TOOL_NAME = "presentCollection";
8
8
  /** Stamp a host's opaque project scope onto a card payload. The ONLY way a
@@ -665,6 +665,8 @@ exports.chipMatches = require_promptSafety.chipMatches;
665
665
  exports.coerceInlineValue = coerceInlineValue;
666
666
  exports.coerceNumeric = require_promptSafety.coerceNumeric;
667
667
  exports.collectionCardKey = collectionCardKey;
668
+ exports.collectionKeyId = require_calendarGrid.collectionKeyId;
669
+ exports.collectionKeyName = require_calendarGrid.collectionKeyName;
668
670
  exports.compareSortValues = compareSortValues;
669
671
  exports.compareYmd = require_calendarGrid.compareYmd;
670
672
  exports.completionCoveredByFieldChip = require_promptSafety.completionCoveredByFieldChip;
@@ -691,21 +693,26 @@ exports.firstMissingRequiredField = firstMissingRequiredField;
691
693
  exports.flagFieldValue = require_promptSafety.flagFieldValue;
692
694
  exports.generateUniqueId = require_calendarGrid.generateUniqueId;
693
695
  exports.isFieldDrivenEvery = require_calendarGrid.isFieldDrivenEvery;
696
+ exports.isLocalCollectionKey = require_calendarGrid.isLocalCollectionKey;
694
697
  exports.isReadOnlySchema = require_calendarGrid.isReadOnlySchema;
695
698
  exports.isSafeRecordId = require_calendarGrid.isSafeRecordId;
696
699
  exports.isSafeSlug = require_calendarGrid.isSafeSlug;
700
+ exports.isSharedCollectionKey = require_calendarGrid.isSharedCollectionKey;
697
701
  exports.isSortableField = isSortableField;
702
+ exports.isValidCollectionName = require_calendarGrid.isValidCollectionName;
698
703
  exports.itemIdOf = itemIdOf;
699
704
  exports.itemIsDone = require_promptSafety.itemIsDone;
700
705
  exports.itemLabelOf = itemLabelOf;
701
706
  exports.itemMatchesQuery = itemMatchesQuery;
702
707
  exports.labelFieldFor = labelFieldFor;
708
+ exports.localCollectionKeyOf = require_calendarGrid.localCollectionKeyOf;
703
709
  exports.matchesWhere = require_promptSafety.matchesWhere;
704
710
  exports.mergeIntoExisting = require_project.mergeIntoExisting;
705
711
  exports.monthAnchorDate = require_calendarGrid.monthAnchorDate;
706
712
  exports.nextSortDirection = nextSortDirection;
707
713
  exports.nextUniqueItemId = require_calendarGrid.nextUniqueItemId;
708
714
  exports.numericSortValue = numericSortValue;
715
+ exports.parseCollectionKeyId = require_calendarGrid.parseCollectionKeyId;
709
716
  exports.parseIsoDate = require_calendarGrid.parseIsoDate;
710
717
  exports.parseIsoDateTime = require_calendarGrid.parseIsoDateTime;
711
718
  exports.parseTimeRange = require_calendarGrid.parseTimeRange;
@@ -719,8 +726,10 @@ exports.rollupValue = require_promptSafety.rollupValue;
719
726
  exports.rowFromItem = rowFromItem;
720
727
  exports.rowIdOf = rowIdOf;
721
728
  exports.sameCollectionCard = sameCollectionCard;
729
+ exports.sameCollectionKey = require_calendarGrid.sameCollectionKey;
722
730
  exports.scalarSortValue = scalarSortValue;
723
731
  exports.selectDynamicRecord = require_promptSafety.selectDynamicRecord;
732
+ exports.sharedCollectionKey = require_calendarGrid.sharedCollectionKey;
724
733
  exports.shortHexId = shortHexId;
725
734
  exports.skillCommandSeed = skillCommandSeed;
726
735
  exports.snapshotEmptyEnums = snapshotEmptyEnums;
@@ -1,6 +1,7 @@
1
1
  export * from './core/schema';
2
2
  export type { CollectionQuery, CollectionQueryAggregate, CollectionQueryOrder, CollectionQueryWhere } from './core/queryZ';
3
3
  export * from './core/ids';
4
+ export * from './core/collectionKey';
4
5
  export * from './core/fieldText';
5
6
  export * from './core/project';
6
7
  export * from './core/uiTypes';
@@ -1,7 +1,7 @@
1
1
  import { t as errorMessage } from "../dist-D8zokgGo.js";
2
- import { A as fieldTextOrNull, C as FEED_SCHEDULES, D as isReadOnlySchema, E as isFieldDrivenEvery, O as storageKindFor, S as COMPUTED_TYPES, T as embedTargetId, _ as generateUniqueId, a as compareYmd, b as nextUniqueItemId, c as monthAnchorDate, d as parseTimeRange, f as recordSpan, g as SAFE_SLUG_PATTERN, h as SAFE_RECORD_ID_PATTERN, i as buildMonthGrid, k as fieldText, l as parseIsoDate, m as ymdKey, n as assignLanes, o as dateOf, p as spanCoversDay, r as bucketRecords, s as daySlice, t as MINUTES_PER_DAY, u as parseIsoDateTime, v as isSafeRecordId, w as INGEST_KINDS, x as AGENT_INGEST_KIND, y as isSafeSlug } from "../calendarGrid-Csy2rpjp.js";
2
+ import { A as AGENT_INGEST_KIND, C as sharedCollectionKey, D as isSafeRecordId, E as generateUniqueId, F as isFieldDrivenEvery, I as isReadOnlySchema, L as storageKindFor, M as FEED_SCHEDULES, N as INGEST_KINDS, O as isSafeSlug, P as embedTargetId, R as fieldText, S as sameCollectionKey, T as SAFE_SLUG_PATTERN, _ as isLocalCollectionKey, a as compareYmd, b as localCollectionKeyOf, c as monthAnchorDate, d as parseTimeRange, f as recordSpan, g as collectionKeyName, h as collectionKeyId, i as buildMonthGrid, j as COMPUTED_TYPES, k as nextUniqueItemId, l as parseIsoDate, m as ymdKey, n as assignLanes, o as dateOf, p as spanCoversDay, r as bucketRecords, s as daySlice, t as MINUTES_PER_DAY, u as parseIsoDateTime, v as isSharedCollectionKey, w as SAFE_RECORD_ID_PATTERN, x as parseCollectionKeyId, y as isValidCollectionName, z as fieldTextOrNull } from "../calendarGrid-CQ8MVSRb.js";
3
3
  import { n as projectRecordFields, t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
4
- import { C as actionVisible, E as whenMatches, S as viaMatches, T as fieldVisible, _ as uniqueRefTargets, a as evaluateDerived, b as projectBacklinkRow, c as selectDynamicRecord, d as flagFieldValue, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, l as chipMatches, m as matchesWhere, n as deriveAll, o as firstDateField, p as toggleChecked, r as resolveRowRefs, s as resolveIcon, t as defangForPrompt, u as completionCoveredByFieldChip, v as backlinkRows, w as agentActionRunKey, x as rollupValue, y as coerceNumeric } from "../promptSafety-ACXw_2Yu.js";
4
+ import { C as actionVisible, E as whenMatches, S as viaMatches, T as fieldVisible, _ as uniqueRefTargets, a as evaluateDerived, b as projectBacklinkRow, c as selectDynamicRecord, d as flagFieldValue, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, l as chipMatches, m as matchesWhere, n as deriveAll, o as firstDateField, p as toggleChecked, r as resolveRowRefs, s as resolveIcon, t as defangForPrompt, u as completionCoveredByFieldChip, v as backlinkRows, w as agentActionRunKey, x as rollupValue, y as coerceNumeric } from "../promptSafety-CpiME8pj.js";
5
5
  //#region src/collection/core/presentCollection.ts
6
6
  var TOOL_NAME = "presentCollection";
7
7
  /** Stamp a host's opaque project scope onto a card payload. The ONLY way a
@@ -638,6 +638,6 @@ var buildOntologyGraph = (entries) => {
638
638
  };
639
639
  };
640
640
  //#endregion
641
- export { AGENT_INGEST_KIND, COMPUTED_TYPES, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, SAFE_RECORD_ID_PATTERN, SAFE_SLUG_PATTERN, TOOL_DEFINITION, TOOL_NAME, actionVisible, agentActionRunKey, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildOntologyGraph, buildUpdatedRecord, cellKey, chipMatches, coerceInlineValue, coerceNumeric, collectionCardKey, compareSortValues, compareYmd, completionCoveredByFieldChip, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, derivedSortValue, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldText, fieldTextOrNull, fieldVisible, firstDateField, firstMissingRequiredField, flagFieldValue, generateUniqueId, isFieldDrivenEvery, isReadOnlySchema, isSafeRecordId, isSafeSlug, isSortableField, itemIdOf, itemIsDone, itemLabelOf, itemMatchesQuery, labelFieldFor, matchesWhere, mergeIntoExisting, monthAnchorDate, nextSortDirection, nextUniqueItemId, numericSortValue, parseIsoDate, parseIsoDateTime, parseTimeRange, projectBacklinkRow, projectRecordFields, recordSpan, resolveEnumColor, resolveIcon, resolveRowRefs, rollupValue, rowFromItem, rowIdOf, sameCollectionCard, scalarSortValue, selectDynamicRecord, shortHexId, skillCommandSeed, snapshotEmptyEnums, sortItems, sortValueOf, spanCoversDay, storageKindFor, stringSortValue, toPresentCollectionData, toggleChecked, uniqueBacklinkSources, uniqueEmbedTargets, uniqueRefTargets, viaMatches, whenMatches, withCardScope, ymdKey };
641
+ export { AGENT_INGEST_KIND, COMPUTED_TYPES, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, SAFE_RECORD_ID_PATTERN, SAFE_SLUG_PATTERN, TOOL_DEFINITION, TOOL_NAME, actionVisible, agentActionRunKey, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildOntologyGraph, buildUpdatedRecord, cellKey, chipMatches, coerceInlineValue, coerceNumeric, collectionCardKey, collectionKeyId, collectionKeyName, compareSortValues, compareYmd, completionCoveredByFieldChip, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, derivedSortValue, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldText, fieldTextOrNull, fieldVisible, firstDateField, firstMissingRequiredField, flagFieldValue, generateUniqueId, isFieldDrivenEvery, isLocalCollectionKey, isReadOnlySchema, isSafeRecordId, isSafeSlug, isSharedCollectionKey, isSortableField, isValidCollectionName, itemIdOf, itemIsDone, itemLabelOf, itemMatchesQuery, labelFieldFor, localCollectionKeyOf, matchesWhere, mergeIntoExisting, monthAnchorDate, nextSortDirection, nextUniqueItemId, numericSortValue, parseCollectionKeyId, parseIsoDate, parseIsoDateTime, parseTimeRange, projectBacklinkRow, projectRecordFields, recordSpan, resolveEnumColor, resolveIcon, resolveRowRefs, rollupValue, rowFromItem, rowIdOf, sameCollectionCard, sameCollectionKey, scalarSortValue, selectDynamicRecord, sharedCollectionKey, shortHexId, skillCommandSeed, snapshotEmptyEnums, sortItems, sortValueOf, spanCoversDay, storageKindFor, stringSortValue, toPresentCollectionData, toggleChecked, uniqueBacklinkSources, uniqueEmbedTargets, uniqueRefTargets, viaMatches, whenMatches, withCardScope, ymdKey };
642
642
 
643
643
  //# sourceMappingURL=index.js.map
@@ -4,9 +4,9 @@ const require_dist = require("../../../dist-CsgSfWwR.cjs");
4
4
  const require_root = require("../../../root-rPH6FGDT.cjs");
5
5
  const require_relPath = require("../../../relPath-CTAjGdCL.cjs");
6
6
  const require_utils_fetch = require("../../../utils/fetch.cjs");
7
- const require_discovery = require("../../../discovery-CrA_EqUJ.cjs");
7
+ const require_discovery = require("../../../discovery-Cw5xWzZD.cjs");
8
8
  const require_templatePath = require("../../../templatePath-27vUZowm.cjs");
9
- const require_server = require("../../../server-CorfPQVQ.cjs");
9
+ const require_server = require("../../../server-9mFaGLdq.cjs");
10
10
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
11
11
  const require_types = require("../../../types-CNqkLT4M.cjs");
12
12
  let node_fs = require("node:fs");
@@ -2,9 +2,9 @@ import { t as errorMessage } from "../../../dist-D8zokgGo.js";
2
2
  import { n as writeFileAtomic } from "../../../root-BMroU_mB.js";
3
3
  import { n as toPosixRelPath } from "../../../relPath-DW8MC8VO.js";
4
4
  import { fetchWithTimeout } from "../../../utils/fetch.js";
5
- import { $ as projectSkillsDir, H as safeRecordId, Y as getWorkspaceRoot, Z as log, nt as skillsStagingDir, q as collectionsRegistriesConfigPath, r as loadCollection, s as CollectionSchemaZ, t as acceptParsedSchema } from "../../../discovery-D7LCN8gJ.js";
5
+ import { $ as collectionsRegistriesConfigPath, G as safeRecordId, at as log, ft as skillsStagingDir, nt as getWorkspaceRoot, r as loadCollection, s as CollectionSchemaZ, st as projectSkillsDir, t as acceptParsedSchema } from "../../../discovery-CdkaURVY.js";
6
6
  import { t as isSafeActionTemplatePath } from "../../../templatePath-k_WNbL_Q.js";
7
- import { g as ONE_SECOND_MS } from "../../../server-D6QY853r.js";
7
+ import { g as ONE_SECOND_MS } from "../../../server-DdHaX_Uu.js";
8
8
  import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
9
9
  import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
10
10
  import { readFileSync } from "node:fs";
@@ -0,0 +1,53 @@
1
+ /** The app declaration's filename, at the repository root. */
2
+ export declare const APP_MANIFEST_FILE = "app.json";
3
+ /** What this step reads out of `app.json`. Deliberately one field: every key
4
+ * added here is a key `publish` and this loader could disagree about. */
5
+ export interface AppManifest {
6
+ /** The app id — the `{aid}` in `apps/{aid}/collections/{cid}/items`. */
7
+ aid: string;
8
+ }
9
+ /** Why a root has no usable `aid`. Returned rather than thrown because the
10
+ * caller is an acceptance gate whose whole job is to turn this into a
11
+ * one-line reason a collection was skipped. */
12
+ export type AppManifestFailure = {
13
+ kind: "missing";
14
+ } | {
15
+ kind: "unreadable";
16
+ detail: string;
17
+ } | {
18
+ kind: "malformed";
19
+ detail: string;
20
+ };
21
+ export type AppManifestResult = {
22
+ ok: true;
23
+ manifest: AppManifest;
24
+ } | ({
25
+ ok: false;
26
+ } & AppManifestFailure);
27
+ /** Read `<root>/app.json` and return its `aid`.
28
+ *
29
+ * SYNCHRONOUS on purpose. The caller is `acceptParsedSchema`, which is sync
30
+ * and is shared by discovery and `manageCollection`'s `putSchema` precisely so
31
+ * that a schema which would be skipped on the next discovery cannot be written
32
+ * as if it were fine. Making this async would split that gate in two, and the
33
+ * half that lost the check is the half the author sees. The file is a few
34
+ * hundred bytes and is read once per firestore collection per discovery pass.
35
+ *
36
+ * Never cached. `app.json` is edited by hand and by the agent, and a cache
37
+ * here would mean the app a collection points at is whatever it was when the
38
+ * server started. */
39
+ export declare function loadAppManifest(root: string): AppManifestResult;
40
+ /** The parse half, exported so it can be tested without a filesystem.
41
+ *
42
+ * `aid` is validated with `isValidCollectionName` — the SAME predicate the
43
+ * `CollectionKey` constructors apply — rather than a rule of its own. An `aid`
44
+ * is re-encoded downstream as a Firestore document id, a pubsub channel
45
+ * segment and a cache key, each with a different character that would break
46
+ * it; one rule, stated once, is what keeps those layers from disagreeing.
47
+ * Rejecting here rather than at `sharedCollectionKey` only changes WHERE the
48
+ * author is told: a reason on the collection they wrote, instead of a throw
49
+ * from inside a store call. */
50
+ export declare function parseAppManifest(raw: string): AppManifestResult;
51
+ /** The failure as the one line an author can act on. Kept next to the failure
52
+ * type so a new variant cannot be added without wording it. */
53
+ export declare function appManifestReason(failure: AppManifestFailure, root: string): string;
@@ -15,6 +15,16 @@ export type DeleteCollectionResult = {
15
15
  } | {
16
16
  kind: "path-escape";
17
17
  slug: string;
18
+ }
19
+ /** A `storage: firestore` collection: its records are documents of a SHARED
20
+ * app, which this delete can neither archive nor remove. Deleting the skill
21
+ * would leave them in `apps/{aid}/collections/{cid}/items` with no
22
+ * collection left to reach them through — and they are not only this
23
+ * machine's: other members' live views are served from the same documents.
24
+ * Refused outright rather than done partially. */
25
+ | {
26
+ kind: "unsupported-backend";
27
+ slug: string;
18
28
  };
19
29
  type DeleteRefusal = Exclude<DeleteCollectionResult, {
20
30
  kind: "ok";
@@ -24,6 +24,16 @@ export interface LoadedCollection {
24
24
  * `storage.path`, e.g. a SQLite database — same containment as dataDir).
25
25
  * Present iff the schema declares `storage`. */
26
26
  storageFile?: string;
27
+ /** The app id this collection's records belong to, from the repository's
28
+ * `app.json`. Present iff the schema declares `storage.type: "firestore"` —
29
+ * a shared collection's identity is `(appId, slug)`, and the store builds
30
+ * `apps/{appId}/collections/{slug}/items` from it.
31
+ *
32
+ * Resolved by discovery, exactly like `storageFile`, so the identity is
33
+ * settled ONCE and the store never reads `app.json` on a read path. Absent
34
+ * is not a fallback: a firestore schema whose root declares no `aid` is
35
+ * REFUSED at discovery, so this is present whenever the backend needs it. */
36
+ appId?: string;
27
37
  /** Absolute path to the skill directory this collection was loaded from
28
38
  * (`<skillsRoot>/<slug>/`). Action templates are read from here, path-safely. */
29
39
  skillDir: string;
@@ -10,6 +10,7 @@ export type SchemaAcceptance = {
10
10
  dataDir: string;
11
11
  dataSourceFile?: string;
12
12
  storageFile?: string;
13
+ appId?: string;
13
14
  } | {
14
15
  ok: false;
15
16
  reason: string;
@@ -0,0 +1,39 @@
1
+ import { Firestore } from 'firebase/firestore';
2
+ /** One stored record document. `data` is the document's own fields — the
3
+ * record itself, not a wrapper around it; see `set` below. The store
4
+ * validates its shape, because a document written by hand could be
5
+ * anything. */
6
+ export interface FirestoreDoc {
7
+ id: string;
8
+ data: unknown;
9
+ }
10
+ export interface FirestoreDocs {
11
+ /** Every document under `collectionPath`, ordered by document id. */
12
+ list: (collectionPath: string) => Promise<FirestoreDoc[]>;
13
+ /** One document's fields, or null when it doesn't exist. */
14
+ get: (collectionPath: string, docId: string) => Promise<unknown | null>;
15
+ /** Create or replace. */
16
+ set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;
17
+ /** Create only. Returns false when the id already exists — atomic, so two
18
+ * concurrent creates can't both observe "missing". */
19
+ create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;
20
+ /** Delete. Returns false when the id didn't exist, so a caller can tell a
21
+ * real delete from a typo'd id. */
22
+ delete: (collectionPath: string, docId: string) => Promise<boolean>;
23
+ }
24
+ /** The real implementation over the modular SDK.
25
+ *
26
+ * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not
27
+ * nested under a `data` key. This is not a matter of taste: the deployed
28
+ * security rules read `request.resource.data.<field>` and
29
+ * `resource.data[submit.emailField]` — a wrapper would put every field one
30
+ * level down, so the required-field checks, the status state machine and the
31
+ * "your own row" predicate would all read absent values and fail closed. A
32
+ * shared record's shape is part of the authorization contract.
33
+ *
34
+ * `orderBy("__name__")` orders by DOCUMENT ID. Ordering by a record field
35
+ * would silently EXCLUDE documents missing that field (the trap
36
+ * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a
37
+ * partial one; the document id is always present and gives the stable order
38
+ * the store contract requires. */
39
+ export declare function createFirestoreDocs(database: Firestore): FirestoreDocs;
@@ -0,0 +1,14 @@
1
+ import { SharedCollectionKey } from '../core/collectionKey';
2
+ import { LoadedCollection } from './discoveredCollection';
3
+ import { IoOptions } from './io';
4
+ import { CollectionStore } from './store';
5
+ /** The records subcollection of one shared collection.
6
+ *
7
+ * Takes a KEY, never loose strings, and the key is the only way to reach this
8
+ * function. `sharedCollectionKey` is where the name rule lives (the charset a
9
+ * Firestore document id, a pubsub channel segment and the completion-bell id
10
+ * must all survive), so building a path cannot be a way around it. */
11
+ export declare function sharedItemsPath(key: SharedCollectionKey): string;
12
+ /** The store factory registered for `storage.type === "firestore"`.
13
+ * Synchronous and connection-agnostic by contract — see the header. */
14
+ export declare function firestoreStoreFor(collection: LoadedCollection, opts: IoOptions): CollectionStore;