@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
@@ -2,9 +2,9 @@ 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
4
  const require_relPath = require("./relPath-CTAjGdCL.cjs");
5
- const require_calendarGrid = require("./calendarGrid-DGILaVxI.cjs");
6
- const require_promptSafety = require("./promptSafety-NNGViiCr.cjs");
7
- const require_discovery = require("./discovery-Cw5xWzZD.cjs");
5
+ const require_itemId = require("./itemId-Dzrr3vs5.cjs");
6
+ const require_promptSafety = require("./promptSafety-CWqYq-NS.cjs");
7
+ const require_discovery = require("./discovery-BsXiDZBR.cjs");
8
8
  const require_feeds_paths = require("./feeds/paths.cjs");
9
9
  const require_skill_bridge_index = require("./skill-bridge/index.cjs");
10
10
  let node_path = require("node:path");
@@ -12,6 +12,665 @@ node_path = require_rolldown_runtime.__toESM(node_path, 1);
12
12
  let node_crypto = require("node:crypto");
13
13
  let node_fs_promises = require("node:fs/promises");
14
14
  let zod = require("zod");
15
+ //#region src/collection/server/publishManifest.ts
16
+ /** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
17
+ * that `sharedCollectionKey` applies. Stated once so a path built later
18
+ * cannot be a way around it. */
19
+ var NameZ = zod.z.string().refine(require_itemId.isValidCollectionName, { message: "is not a valid id (letters, digits, '-' and '_' only)" });
20
+ /** An address on the roster. Not validated as an email beyond "has an @":
21
+ * the rules compare it to `request.auth.token.email` verbatim, so any
22
+ * narrowing here would refuse addresses Firebase itself accepts. */
23
+ var EmailZ = zod.z.string().trim().min(3).includes("@");
24
+ /** The four roles the deployed rules understand. `participant` is the layer
25
+ * that is NAMED but reads only its own rows — see `readerOf` vs `listedIn`. */
26
+ var APP_ROLES = [
27
+ "owner",
28
+ "editor",
29
+ "viewer",
30
+ "participant"
31
+ ];
32
+ var RoleZ = zod.z.enum(APP_ROLES);
33
+ /** `{ email: { "*" | cid: role } }`. The `"*"` key is the app-wide role; a
34
+ * member may hold per-collection roles only (the stylist who is editor of
35
+ * bookings and viewer of everything else). */
36
+ var MembersZ = zod.z.record(EmailZ, zod.z.record(zod.z.union([zod.z.literal("*"), NameZ]), RoleZ));
37
+ /** The declarative mail queue, as the rules re-derive it: a transition of the
38
+ * status field, a recipient read off the RECORD, and a fixed template. */
39
+ var MailZ = zod.z.object({
40
+ toField: zod.z.string().trim().min(1),
41
+ on: zod.z.record(zod.z.string().trim().min(1), zod.z.object({
42
+ from: zod.z.array(zod.z.string().trim().min(1)).min(1),
43
+ to: zod.z.string().trim().min(1)
44
+ }).strict()),
45
+ dataFields: zod.z.array(zod.z.string().trim().min(1)).optional()
46
+ }).strict();
47
+ /** What the rules read out of `collections[cid]`. NOT the schema — the schema
48
+ * is published beside it, untouched, for clients to render from. */
49
+ var CollectionConfigZ = zod.z.object({
50
+ statusField: zod.z.string().trim().min(1).optional(),
51
+ /** `{ initial: [...], <status>: [<status>...] }`. Binds writers too, and
52
+ * binds `create` — that is the point of publishing it. */
53
+ transitions: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
54
+ immutable: zod.z.boolean().optional(),
55
+ submitOnly: zod.z.boolean().optional(),
56
+ peerVisibility: zod.z.enum(["public", "hidden"]).optional(),
57
+ revealGated: zod.z.boolean().optional(),
58
+ gatedFrom: NameZ.optional(),
59
+ revealBy: zod.z.string().trim().min(1).optional(),
60
+ mail: MailZ.optional(),
61
+ /** Which fields an aggregate groups by. Declared here rather than in the
62
+ * schema for the same reason as everything else in this file — the schema
63
+ * has no `aggregate` key yet — and it is here at all because the
64
+ * invariant that guards it ("every aggregation key is a CHECKED field")
65
+ * is about `public.submit`, which is an app-level declaration. Published
66
+ * as-is; the rules never read it. */
67
+ aggregate: zod.z.object({ by: zod.z.array(zod.z.string().trim().min(1)).min(1) }).strict().optional()
68
+ }).strict();
69
+ /** An authored submit window. ISO strings, because `app.json` is JSON and a
70
+ * Firestore `Timestamp` has no JSON form. Publish lowers it to epoch millis —
71
+ * the rules do not coerce strings, so an ISO string reaching Firestore is a
72
+ * type error that fails CLOSED (`inWindow` refuses every submission and the
73
+ * author sees "nobody can submit", not an error). */
74
+ var WindowZ = zod.z.object({
75
+ from: zod.z.iso.datetime().optional(),
76
+ until: zod.z.iso.datetime().optional()
77
+ }).strict();
78
+ var ValidateZ = zod.z.object({
79
+ required: zod.z.array(zod.z.string().trim().min(1)).optional(),
80
+ /** Capped at two by the rules themselves: rules have no iteration, so
81
+ * `keyFieldsOk` is unrolled. A third would be accepted here and silently
82
+ * unchecked there. */
83
+ keyFields: zod.z.array(zod.z.object({
84
+ field: zod.z.string().trim().min(1),
85
+ values: zod.z.array(zod.z.union([
86
+ zod.z.string(),
87
+ zod.z.number(),
88
+ zod.z.boolean()
89
+ ])).min(1)
90
+ }).strict()).optional()
91
+ }).strict();
92
+ var SubmitZ = zod.z.object({
93
+ auth: zod.z.enum([
94
+ "none",
95
+ "anonymous",
96
+ "verifiedEmail"
97
+ ]),
98
+ emailField: zod.z.string().trim().min(1).optional(),
99
+ createFields: zod.z.array(zod.z.string().trim().min(1)).min(1),
100
+ initialStatus: zod.z.string().trim().min(1).optional(),
101
+ idFrom: zod.z.enum([
102
+ "auto",
103
+ "auth.uid",
104
+ "auth.uid+field"
105
+ ]).optional(),
106
+ idField: zod.z.string().trim().min(1).optional(),
107
+ validate: ValidateZ.optional(),
108
+ window: WindowZ.optional(),
109
+ /** Per CURRENT STATUS, never a flat list: a flat list lets a customer move
110
+ * an approved booking's `startAt` without anyone re-approving it. */
111
+ selfUpdate: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
112
+ selfTransitions: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
113
+ finalize: zod.z.boolean().optional(),
114
+ audience: zod.z.literal("participant").optional(),
115
+ gateOn: zod.z.object({
116
+ phase: zod.z.string().trim().min(1),
117
+ match: zod.z.string().trim().min(1)
118
+ }).strict().optional()
119
+ }).strict();
120
+ var PublicZ = zod.z.object({
121
+ /** The master switch. Anonymous submission (`auth: "none"`) needs it as
122
+ * well as its own declaration. */
123
+ enabled: zod.z.boolean().optional(),
124
+ read: zod.z.array(NameZ).optional(),
125
+ submit: zod.z.record(NameZ, SubmitZ).optional()
126
+ }).strict();
127
+ /** The whole authored declaration.
128
+ *
129
+ * `owner` is accepted but is NOT the published value — publish stamps the
130
+ * publisher's uid (or carries the existing one forward, which is what the
131
+ * rules require on update) and refuses a declaration that disagrees. It is
132
+ * accepted rather than banned because the sample app.json in the design note
133
+ * shows it, and a hard refusal on a key the samples contain would be a worse
134
+ * first experience than a message naming the mismatch. */
135
+ var AuthoredAppZ = zod.z.object({
136
+ aid: NameZ,
137
+ name: zod.z.string().trim().min(1).optional(),
138
+ /** Per-worktree app id (design D6, implementation order 7). Accepted so a
139
+ * repository already carrying it parses; nothing reads it yet. */
140
+ aidEnv: zod.z.string().trim().min(1).optional(),
141
+ owner: zod.z.string().trim().min(1).optional(),
142
+ members: MembersZ,
143
+ collections: zod.z.record(NameZ, CollectionConfigZ).optional(),
144
+ participantRead: zod.z.array(NameZ).optional(),
145
+ public: PublicZ.optional()
146
+ }).strict();
147
+ /** Parse the authored declaration out of `app.json`'s text.
148
+ *
149
+ * Returns a LIST of problems rather than throwing, for the same reason
150
+ * `loadAppManifest` returns a failure: the caller is a gate whose entire job
151
+ * is to hand the author something to act on. Every problem is reported at
152
+ * once — publish is a manual step, and a parser that stops at the first key
153
+ * makes it N round trips. */
154
+ function parseAuthoredApp(raw) {
155
+ const manifest = require_discovery.parseAppManifest(raw);
156
+ if (!manifest.ok) return {
157
+ ok: false,
158
+ problems: [manifest.kind === "missing" ? "app.json is missing" : manifest.detail]
159
+ };
160
+ const parsed = AuthoredAppZ.safeParse(JSON.parse(raw));
161
+ if (!parsed.success) return {
162
+ ok: false,
163
+ problems: authoredProblems(parsed.error)
164
+ };
165
+ return {
166
+ ok: true,
167
+ app: parsed.data
168
+ };
169
+ }
170
+ /** zod issues as one actionable line each: `public.submit.responses.auth: …`. */
171
+ function authoredProblems(error) {
172
+ return error.issues.map((issue) => {
173
+ return `${issue.path.length > 0 ? issue.path.join(".") : "app.json"}: ${issue.message}`;
174
+ });
175
+ }
176
+ //#endregion
177
+ //#region src/collection/server/publishProject.ts
178
+ /** The document id under `apps/{aid}/config`. One document, named, rather than
179
+ * a spread of them: a second public document is a second thing to keep in
180
+ * step, and nothing yet needs one. */
181
+ var PUBLIC_CONFIG_DOC = "public";
182
+ /** Drop keys whose value is `undefined`. Firestore rejects an undefined field
183
+ * value outright, and `"k" in c` — which every optional key in the rules is
184
+ * read through — must mean "the author declared it". */
185
+ function compact(entries) {
186
+ return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== void 0));
187
+ }
188
+ /** ISO → epoch millis, the one conversion the rules cannot do for themselves.
189
+ * The caller has already refused an unparseable string (the authored parser
190
+ * requires `z.iso.datetime()`), so a NaN here would be a programming error;
191
+ * it is still checked, because a NaN written to Firestore fails closed in the
192
+ * same silent way an ISO string does. */
193
+ function windowMillis(window) {
194
+ if (!window) return void 0;
195
+ const out = {};
196
+ if (window.from !== void 0) out.fromMs = Date.parse(window.from);
197
+ if (window.until !== void 0) out.untilMs = Date.parse(window.until);
198
+ if (Object.values(out).some((value) => !Number.isFinite(value))) throw new Error(`publish: window bound is not a parseable timestamp (${JSON.stringify(window)})`);
199
+ return Object.keys(out).length > 0 ? out : void 0;
200
+ }
201
+ /** One `public.submit[cid]`, with its window lowered. Everything else passes
202
+ * through: the rules read these keys by the names the author wrote. */
203
+ function projectSubmit(submit) {
204
+ const { window, ...rest } = submit;
205
+ return compact({
206
+ ...rest,
207
+ window: windowMillis(window)
208
+ });
209
+ }
210
+ /** The roster's addresses as a set, in a stable order.
211
+ *
212
+ * Sorted so two publishes of the same declaration produce the same document
213
+ * — idempotence is a property this step is tested for, and Firestore compares
214
+ * arrays by ORDER. `membersConsistent()` compares as sets and would accept
215
+ * any order; the test that would notice is the one asserting a second publish
216
+ * changes nothing but `publishedAt`. */
217
+ function memberEmailsOf(members) {
218
+ return Object.keys(members).sort();
219
+ }
220
+ /** The previous document, kept for rollback, with its OWN `previousPublished`
221
+ * stripped.
222
+ *
223
+ * One level, deliberately. Chaining would make every publish carry the entire
224
+ * history of the app inside a single document, which grows without bound and
225
+ * meets Firestore's 1 MiB document limit as a permission-shaped failure at
226
+ * some unpredictable publish. One level answers the question rollback
227
+ * actually asks — "put back what was there before I broke it" — and the
228
+ * further history is in git, which is where the declaration came from. */
229
+ function previousOf(existing) {
230
+ if (!existing) return void 0;
231
+ const { previousPublished: __dropped, ...rest } = existing;
232
+ return rest;
233
+ }
234
+ /** Project the authored declaration into the documents publish writes.
235
+ *
236
+ * `existing` is the app document as it is in Firestore right now, or null on
237
+ * a first publish. Two things need it, both required by the rules:
238
+ * - `owner` must be UNCHANGED on update. Re-stamping the publisher's uid
239
+ * would be refused for any app whose owner ever signed in as a different
240
+ * account, and would silently transfer ownership if it were not.
241
+ * - `previousPublished` is that document, so a rollback has something to
242
+ * put back.
243
+ *
244
+ * Pure: no clock, no filesystem, no Firestore. Everything variable arrives as
245
+ * a parameter, which is what makes the conversion table testable as a table. */
246
+ function projectApp(authored, schemas, stamp, existing) {
247
+ const owner = typeof existing?.owner === "string" ? existing.owner : stamp.uid;
248
+ const submit = Object.fromEntries(Object.entries(authored.public?.submit ?? {}).map(([cid, spec]) => [cid, projectSubmit(spec)]));
249
+ const publicBlock = authored.public ? compact({
250
+ enabled: authored.public.enabled,
251
+ read: authored.public.read,
252
+ submit: Object.keys(submit).length > 0 ? submit : void 0
253
+ }) : void 0;
254
+ const app = compact({
255
+ aid: authored.aid,
256
+ name: authored.name,
257
+ owner,
258
+ members: authored.members,
259
+ memberEmails: memberEmailsOf(authored.members),
260
+ collections: authored.collections,
261
+ participantRead: authored.participantRead,
262
+ public: publicBlock,
263
+ publishedAt: stamp.publishedAt,
264
+ publishedBy: stamp.email,
265
+ publishedCommit: stamp.commit,
266
+ publishedDirty: stamp.dirty === true ? true : void 0,
267
+ previousPublished: previousOf(existing)
268
+ });
269
+ const config = {
270
+ enabled: authored.public?.enabled === true,
271
+ read: authored.public?.read ?? [],
272
+ submit,
273
+ publishedAt: stamp.publishedAt
274
+ };
275
+ if (authored.name !== void 0) config.name = authored.name;
276
+ return {
277
+ app,
278
+ schemas: schemas.map(({ cid, schema }) => ({
279
+ cid,
280
+ doc: schemaDoc(schema, stamp)
281
+ })),
282
+ config
283
+ };
284
+ }
285
+ /** Keys on the app document that publish owns: what is PUBLIC right now, and
286
+ * the rule-facing configuration anonymous access is judged against. Deploy
287
+ * carries them through from the existing document and never authors them. */
288
+ var PUBLISH_OWNED_KEYS = [
289
+ "public",
290
+ "collections",
291
+ "participantRead",
292
+ "publishedAt",
293
+ "publishedBy",
294
+ "publishedCommit",
295
+ "publishedDirty",
296
+ "previousPublished"
297
+ ];
298
+ var isPublishOwned = (key) => PUBLISH_OWNED_KEYS.includes(key);
299
+ function projectDeploy(authored, schemas, stamp, existing) {
300
+ const { app } = projectApp(authored, schemas, stamp, existing);
301
+ const deployed = Object.fromEntries(Object.entries(app).filter(([key]) => !isPublishOwned(key)));
302
+ for (const key of PUBLISH_OWNED_KEYS) {
303
+ const live = existing?.[key];
304
+ if (live !== void 0) deployed[key] = live;
305
+ }
306
+ deployed.deployedAt = stamp.publishedAt;
307
+ deployed.deployedBy = stamp.email;
308
+ if (stamp.commit !== void 0) deployed.deployedCommit = stamp.commit;
309
+ return {
310
+ app: deployed,
311
+ staging: schemas.map(({ cid, schema }) => ({
312
+ cid,
313
+ doc: stagedDoc(schema, stamp, authored, cid)
314
+ }))
315
+ };
316
+ }
317
+ /** One staged schema document, carrying this cid's rule-facing configuration
318
+ * alongside the schema so publish can promote them together. */
319
+ function stagedDoc(schema, stamp, authored, cid) {
320
+ const doc = {
321
+ publishedSchema: schema,
322
+ deployedAt: stamp.publishedAt,
323
+ deployedBy: stamp.email
324
+ };
325
+ const config = authored.collections?.[cid];
326
+ if (config !== void 0) doc.config = config;
327
+ if (authored.participantRead?.includes(cid) === true) doc.participantRead = true;
328
+ if (stamp.commit !== void 0) doc.deployedCommit = stamp.commit;
329
+ return doc;
330
+ }
331
+ /** The rule-facing configuration to promote, read from the STAGED documents
332
+ * rather than from the manifest as it reads right now.
333
+ *
334
+ * Otherwise: deploy revision A, edit `app.json` to revision B, publish — and
335
+ * the promoted schema is A's while the authorization behaviour is B's, a
336
+ * combination nobody exercised through `/staging/{aid}`.
337
+ *
338
+ * (`public` is deliberately NOT part of this: it is not staged, because it is
339
+ * the decision being made AT publish rather than something under test.) */
340
+ function stagedRuleConfig(staged) {
341
+ const entries = [];
342
+ const participantRead = [];
343
+ for (const entry of staged) {
344
+ const { config } = entry.doc;
345
+ if (config !== void 0) entries.push([entry.cid, config]);
346
+ if (entry.doc.participantRead === true) participantRead.push(entry.cid);
347
+ }
348
+ return {
349
+ collections: entries.length > 0 ? Object.fromEntries(entries) : void 0,
350
+ participantRead: participantRead.length > 0 ? participantRead : void 0
351
+ };
352
+ }
353
+ function projectPublish(authored, staged, stamp, existing) {
354
+ const { app, config } = projectApp(authored, [], stamp, existing);
355
+ const staging = stagedRuleConfig(staged);
356
+ app.collections = staging.collections;
357
+ app.participantRead = staging.participantRead;
358
+ const published = Object.fromEntries(Object.entries(existing ?? app).filter(([key]) => !isPublishOwned(key)));
359
+ for (const key of PUBLISH_OWNED_KEYS) if (key !== "public" && app[key] !== void 0) published[key] = app[key];
360
+ const publicBlock = app.public;
361
+ return {
362
+ app: published,
363
+ config,
364
+ public: require_dist.isRecord(publicBlock) ? publicBlock : void 0
365
+ };
366
+ }
367
+ /** Re-stamp a staged schema document as it is promoted to `collections/{cid}`.
368
+ *
369
+ * The stamp answers "which version is PUBLIC right now, and who made it so",
370
+ * so it is written by the operation that changes the answer — publish — not
371
+ * carried over from the deploy that staged it. */
372
+ function promoteSchema(staged, stamp) {
373
+ const doc = {
374
+ publishedSchema: staged.publishedSchema,
375
+ publishedAt: stamp.publishedAt,
376
+ publishedBy: stamp.email
377
+ };
378
+ if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
379
+ return doc;
380
+ }
381
+ /** One published schema document. Written key by key rather than through
382
+ * `compact`, so the declared type is the type — an optional commit is the
383
+ * only variable part. */
384
+ function schemaDoc(schema, stamp) {
385
+ const doc = {
386
+ publishedSchema: schema,
387
+ publishedAt: stamp.publishedAt,
388
+ publishedBy: stamp.email
389
+ };
390
+ if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
391
+ return doc;
392
+ }
393
+ /** The app documents' parent path — the `FirestoreDocs` seam takes a
394
+ * collection path plus a document id, and the app document's id is the aid. */
395
+ var APPS_COLLECTION = "apps";
396
+ /** The collection (schema) documents' parent path — what the PUBLIC page
397
+ * reads, written only by publish (promotion). */
398
+ var appSchemasPath = (aid) => `apps/${aid}/collections`;
399
+ /** The URL-slug reservations — `appSlugs/{slug}` → `{ aid, published }`.
400
+ *
401
+ * A TOP-LEVEL collection, not a field on the app: the public page resolves a
402
+ * slug to an aid BEFORE it can read anything under `apps/{aid}`, and a slug
403
+ * has to be claimable atomically (create-if-absent) so two apps cannot hold
404
+ * the same URL.
405
+ *
406
+ * `published` is what makes the reservation invisible until publish. The slug
407
+ * is human-readable, so a readable reservation would let anyone guess the URL
408
+ * and get the aid — and the aid is the `/staging/{aid}` entrance. The rule is
409
+ * `allow read: if resource.data.published == true`, which needs no `get()` and
410
+ * so costs nothing against the rules' expression budget. */
411
+ var APP_SLUGS_COLLECTION = "appSlugs";
412
+ var appSlugDoc = (aid, published) => ({
413
+ aid,
414
+ published
415
+ });
416
+ /** The staged schema documents' parent path — what `/staging/{aid}` reads,
417
+ * written by deploy. A separate DOCUMENT rather than a field beside
418
+ * `publishedSchema`, because the rules cannot hide a field: anything inside a
419
+ * document the public page may read is public. */
420
+ var appStagingPath = (aid) => `apps/${aid}/staging`;
421
+ /** The public-config documents' parent path. */
422
+ var appConfigPath = (aid) => `apps/${aid}/config`;
423
+ //#endregion
424
+ //#region src/collection/server/publishChecks.ts
425
+ /** Does this submit declaration bind a record to the submitter's identity?
426
+ *
427
+ * The condition for requiring `submitOnly`, and deliberately NOT "declares an
428
+ * `audience`": `audience` appears only in the rules' public-create branch, so
429
+ * an owner or editor never meets it and can add records freely. `immutable`
430
+ * is the wrong condition too — a survey's responses are not immutable and
431
+ * can be padded exactly the same way.
432
+ *
433
+ * What these four have in common is that each one makes the record MEAN "the
434
+ * person who submitted it said this": a per-uid id, a per-uid+field id, a
435
+ * row stamped with the submitter's verified address, or a submission
436
+ * restricted to a named participant. A record created through the writer
437
+ * branch carries the same shape and none of that meaning. */
438
+ function bindsSubmitterIdentity(submit) {
439
+ return submit.idFrom === "auth.uid" || submit.idFrom === "auth.uid+field" || submit.emailField !== void 0 || submit.audience === "participant";
440
+ }
441
+ /** The fields a rule actually CHECKS the value of, for one collection.
442
+ *
443
+ * `keyFields` pins a value against a declared set, `gateOn.match` pins it
444
+ * against the session's current question, and the status field is pinned by
445
+ * the transition machine. An aggregation grouped by anything else is grouped
446
+ * by a field any submitter may write anything into — so the published
447
+ * aggregate is whatever the noisiest respondent decided it should be. */
448
+ function checkedFields(collection, submit) {
449
+ const fields = /* @__PURE__ */ new Set();
450
+ for (const keyField of submit?.validate?.keyFields ?? []) fields.add(keyField.field);
451
+ if (submit?.gateOn) fields.add(submit.gateOn.match);
452
+ if (collection?.statusField) fields.add(collection.statusField);
453
+ return fields;
454
+ }
455
+ /** INVARIANT 1 — a submission bound to its submitter needs `submitOnly`. */
456
+ function submitOnlyProblems(app) {
457
+ const problems = [];
458
+ for (const [cid, submit] of Object.entries(app.public?.submit ?? {})) {
459
+ if (!bindsSubmitterIdentity(submit)) continue;
460
+ if (app.collections?.[cid]?.submitOnly === true) continue;
461
+ problems.push(`collections.${cid}.submitOnly must be true: public.submit.${cid} binds each record to its submitter (${identityBindings(submit).join(", ")}), so a record created any other way would carry that meaning without having earned it. Without submitOnly the rules let an owner or editor write rows directly into ${cid}.`);
462
+ }
463
+ return problems;
464
+ }
465
+ function identityBindings(submit) {
466
+ const bindings = [];
467
+ if (submit.idFrom === "auth.uid" || submit.idFrom === "auth.uid+field") bindings.push(`idFrom: "${submit.idFrom}"`);
468
+ if (submit.emailField !== void 0) bindings.push(`emailField: "${submit.emailField}"`);
469
+ if (submit.audience === "participant") bindings.push(`audience: "participant"`);
470
+ return bindings;
471
+ }
472
+ /** INVARIANT 2 — every aggregation key is a field some rule checks. */
473
+ function aggregateProblems(app) {
474
+ const problems = [];
475
+ for (const [cid, collection] of Object.entries(app.collections ?? {})) {
476
+ const keys = collection.aggregate?.by;
477
+ if (!keys) continue;
478
+ const checked = checkedFields(collection, app.public?.submit?.[cid]);
479
+ const loose = keys.filter((field) => !checked.has(field));
480
+ const spelled = loose.map((field) => `'${field}'`).join(", ");
481
+ if (loose.length > 0) problems.push(`collections.${cid}.aggregate.by names ${spelled}, which no rule checks the value of. An aggregation key must appear in public.submit.${cid}.validate.keyFields, in gateOn.match, or be the statusField — otherwise a submitter chooses their own bucket and the published aggregate is not a count of anything.`);
482
+ }
483
+ return problems;
484
+ }
485
+ /** INVARIANT 3 — `auth: "verifiedEmail"` only.
486
+ *
487
+ * A product decision, not a rules limitation: the rules keep all three stages
488
+ * and the emulator tests keep exercising them, because deleting a stage from
489
+ * the rules turns a change of mind into a cross-repo deploy. Publish is where
490
+ * the current decision is expressed, and it is one line to move. */
491
+ function authProblems(app) {
492
+ return Object.entries(app.public?.submit ?? {}).filter(([, submit]) => submit.auth !== "verifiedEmail").map(([cid, submit]) => `public.submit.${cid}.auth is "${submit.auth}": only "verifiedEmail" may be published. The rules still implement "none" and "anonymous" — this is a product decision, and lifting it is a change here, not a rules deploy.`);
493
+ }
494
+ /** INVARIANT 5 — a mail transition's origins and destination must be disjoint.
495
+ *
496
+ * Overlap means the same write can satisfy the same template twice over, and
497
+ * the deterministic mail id is the only other thing stopping a duplicate
498
+ * send. The rules also require the status to have CHANGED, so an overlapping
499
+ * declaration is not merely redundant: `from` containing `to` is a transition
500
+ * that can never fire, which is a mail nobody ever receives. */
501
+ function mailProblems(app) {
502
+ return Object.entries(app.collections ?? {}).flatMap(([cid, collection]) => collectionMailProblems(cid, collection));
503
+ }
504
+ function collectionMailProblems(cid, collection) {
505
+ const { mail } = collection;
506
+ if (!mail) return [];
507
+ const problems = [];
508
+ if (!collection.statusField) problems.push(`collections.${cid}.mail needs collections.${cid}.statusField: the rules read the status before and after the write to decide the mail is warranted.`);
509
+ for (const [template, transition] of Object.entries(mail.on)) problems.push(...templateMailProblems(cid, collection, template, transition));
510
+ return problems;
511
+ }
512
+ function templateMailProblems(cid, collection, template, transition) {
513
+ const problems = [];
514
+ if (transition.from.includes(transition.to)) problems.push(`collections.${cid}.mail.on.${template} lists "${transition.to}" in both \`from\` and \`to\`. The rules require the status to CHANGE in the same write, so this template can never send.`);
515
+ const allowed = collection.transitions;
516
+ if (allowed) {
517
+ const unreachable = transition.from.filter((from) => !(allowed[from] ?? []).includes(transition.to));
518
+ const spelled = unreachable.map((from) => `'${from}' -> '${transition.to}'`).join(", ");
519
+ if (unreachable.length > 0) problems.push(`collections.${cid}.mail.on.${template} sends on ${spelled}, which collections.${cid}.transitions does not allow. The record write is refused first, so the mail never fires.`);
520
+ }
521
+ return problems;
522
+ }
523
+ /** INVARIANTS 6 and 7 — the window is a real interval, and `keyFields` fits
524
+ * the unrolled check in the rules. */
525
+ function submitShapeProblems(app) {
526
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => [...windowProblems(cid, submit), ...keyFieldCountProblems(cid, submit)]);
527
+ }
528
+ function windowProblems(cid, submit) {
529
+ const { window } = submit;
530
+ if (window?.from === void 0 || window.until === void 0) return [];
531
+ if (Date.parse(window.until) > Date.parse(window.from)) return [];
532
+ return [`public.submit.${cid}.window closes at or before it opens (${window.from} -> ${window.until}): nothing could ever be submitted.`];
533
+ }
534
+ function keyFieldCountProblems(cid, submit) {
535
+ const keyFields = submit.validate?.keyFields ?? [];
536
+ if (keyFields.length <= 2) return [];
537
+ return [`public.submit.${cid}.validate.keyFields declares ${keyFields.length}; the rules check at most 2. Rules have no iteration, so the check is unrolled — a third would be published and never enforced.`];
538
+ }
539
+ /** The fail-closed traps: declarations the rules read together, where the
540
+ * missing half denies every write instead of loosening one. */
541
+ function coherenceProblems(app) {
542
+ const fromSubmits = Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => submitCoherenceProblems(app, cid, submit));
543
+ const fromCollections = Object.entries(app.collections ?? {}).flatMap(([cid, collection]) => gateCoherenceProblems(cid, collection));
544
+ return [...fromSubmits, ...fromCollections];
545
+ }
546
+ /** `initialStatus` is read together with the collection's `statusField` and
547
+ * with `createFields`; miss either and every submission is refused. */
548
+ function statusCoherenceProblems(cid, submit, collection) {
549
+ if (submit.initialStatus === void 0) return [];
550
+ if (!collection?.statusField) return [`public.submit.${cid}.initialStatus needs collections.${cid}.statusField: the rules look the status up by that name, and refuse every submission without it.`];
551
+ if (new Set(submit.createFields).has(collection.statusField)) return [];
552
+ return [`public.submit.${cid}.createFields must include "${collection.statusField}": a submission may carry ONLY the createFields, and the rules also require the status field to be present and equal to initialStatus. As written, every submission is refused.`];
553
+ }
554
+ /** Every field a RULE reads off a submitted record, other than the status
555
+ * field (which `statusCoherenceProblems` words for itself).
556
+ *
557
+ * `emailField` and `idField` belong here for exactly the reason `required`
558
+ * and `keyFields` do, and forgetting them was the same oversight twice: the
559
+ * rules read `request.resource.data[s.emailField]` and rebuild the document
560
+ * id from `s.idField`, while `hasOnly(createFields)` decides what a
561
+ * submission may carry at all. A field in one list and not the other is a
562
+ * contradiction the submitter cannot resolve — including it is refused,
563
+ * omitting it fails the check. */
564
+ function ruleReadFields(submit) {
565
+ const fields = [];
566
+ if (submit.emailField !== void 0) fields.push({
567
+ field: submit.emailField,
568
+ why: `public.submit.<cid>.emailField — the rules compare it to the submitter's verified address`
569
+ });
570
+ if (submit.idFrom === "auth.uid+field" && submit.idField !== void 0) fields.push({
571
+ field: submit.idField,
572
+ why: `public.submit.<cid>.idField — the rules rebuild the document id from it`
573
+ });
574
+ return fields;
575
+ }
576
+ /** A checked field a submission is not allowed to carry can never be
577
+ * satisfied: carrying it fails `hasOnly`, omitting it fails the check. */
578
+ function createFieldProblems(cid, submit) {
579
+ const createFields = new Set(submit.createFields);
580
+ const ruleRead = ruleReadFields(submit).filter((entry) => !createFields.has(entry.field)).map((entry) => `public.submit.${cid}.createFields must include "${entry.field}" (${entry.why.replace("<cid>", cid)}): a submission may carry only the createFields, so as written every submission is refused whether or not it carries the field.`);
581
+ const required = (submit.validate?.required ?? []).filter((field) => !createFields.has(field)).map((field) => `public.submit.${cid}.validate.required names "${field}", which is not in createFields: a submission may carry only the createFields, so the requirement can never be met.`);
582
+ const keyFields = (submit.validate?.keyFields ?? []).filter((keyField) => !createFields.has(keyField.field)).map((keyField) => `public.submit.${cid}.validate.keyFields checks "${keyField.field}", which is not in createFields: a submission carrying it is refused, and one omitting it fails the check.`);
583
+ return [
584
+ ...ruleRead,
585
+ ...required,
586
+ ...keyFields
587
+ ];
588
+ }
589
+ function submitCoherenceProblems(app, cid, submit) {
590
+ const collection = app.collections?.[cid];
591
+ const problems = [...statusCoherenceProblems(cid, submit, collection), ...createFieldProblems(cid, submit)];
592
+ if (submit.idFrom === "auth.uid+field" && submit.idField === void 0) problems.push(`public.submit.${cid}.idFrom is "auth.uid+field" but no idField is declared: the rules rebuild the document id from that field and refuse every create.`);
593
+ if ((submit.selfUpdate !== void 0 || submit.selfTransitions !== void 0) && !collection?.statusField) problems.push(`public.submit.${cid}.selfUpdate / selfTransitions are declared per CURRENT STATUS, but collections.${cid} declares no statusField: the rules read the current status first and refuse every self-edit without it.`);
594
+ if (submit.audience === "participant" && Object.keys(app.members).length === 0) problems.push(`public.submit.${cid}.audience is "participant" but the roster is empty: the rules resolve the submitter's role from members, so every submission is refused.`);
595
+ return problems;
596
+ }
597
+ /** The staged reveal reads its flag off the PARENT record, so the path to that
598
+ * parent is not optional decoration — without it the gate never opens. */
599
+ function gateCoherenceProblems(cid, collection) {
600
+ if (collection.revealGated !== true) return [];
601
+ if (collection.gatedFrom !== void 0 && collection.revealBy !== void 0) return [];
602
+ return [`collections.${cid}.revealGated needs both gatedFrom and revealBy: the flag is read off the PARENT record, and without the path the gate never opens.`];
603
+ }
604
+ /** The publisher must be able to write what they are about to write.
605
+ *
606
+ * On a first publish the rules require the creator to name themselves owner,
607
+ * in the roster, under `'*'`. Getting this wrong produces a bare permission
608
+ * error from Firestore with nothing in it about rosters — worth one line
609
+ * here instead. */
610
+ function publisherProblems(app, publisherEmail) {
611
+ if (app.members[publisherEmail]?.["*"] === "owner") return [];
612
+ return [`members must give you app-wide owner: add "${publisherEmail}": { "*": "owner" }. The rules require the publisher to hold that role (and to name themselves owner when the app is first created); otherwise the write is refused with no explanation.`];
613
+ }
614
+ /** Every cid the declaration mentions must be a collection that exists.
615
+ *
616
+ * A typo'd cid is not an error anywhere else: the app document simply carries
617
+ * a configuration for a collection nobody publishes, and the collection the
618
+ * author meant is published with no configuration at all — i.e. with the
619
+ * status machine and the submit path silently absent. */
620
+ function unknownCidProblems(app, collections) {
621
+ const known = new Set(collections.map((collection) => collection.cid));
622
+ return [
623
+ ["collections", Object.keys(app.collections ?? {})],
624
+ ["public.read", app.public?.read ?? []],
625
+ ["public.submit", Object.keys(app.public?.submit ?? {})],
626
+ ["participantRead", app.participantRead ?? []]
627
+ ].flatMap(([where, cids]) => cids.filter((cid) => !known.has(cid)).map((cid) => `${where} names '${cid}', which is not a shared collection in this repository. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none - a schema needs storage.type \"firestore\")"}.`));
628
+ }
629
+ /** Everything publish refuses, as lines the author can act on.
630
+ *
631
+ * All of them, every time. Publish is a manual step with a human waiting on
632
+ * it; stopping at the first problem turns one review into five. */
633
+ function publishProblems(app, collections, publisherEmail) {
634
+ return [
635
+ ...unknownCidProblems(app, collections),
636
+ ...publisherProblems(app, publisherEmail),
637
+ ...submitOnlyProblems(app),
638
+ ...aggregateProblems(app),
639
+ ...authProblems(app),
640
+ ...mailProblems(app),
641
+ ...submitShapeProblems(app),
642
+ ...coherenceProblems(app),
643
+ ...primaryKeyProblems(app, collections)
644
+ ];
645
+ }
646
+ /** A public submission must NOT be allowed to name its own primary key.
647
+ *
648
+ * The rules constrain the DOCUMENT ID (`idFrom`) and cannot constrain the
649
+ * value of a field — nothing compares `request.resource.data[primaryKey]`
650
+ * with the path being written. So a submit path that accepts the primary key
651
+ * as a `createField` lets a submitter write at their one permitted document
652
+ * id while CLAIMING another record's identity, or a duplicate.
653
+ *
654
+ * It is refused rather than tolerated because there is nothing for the field
655
+ * to do: `firestoreStore` takes a shared record's identity from the document
656
+ * id and overwrites the field on read, so a submitted value is either equal
657
+ * to the id (noise) or a lie (silently discarded). Publishing a form field
658
+ * whose value is thrown away is worse than not having it — the author will
659
+ * believe submitters choose their ids.
660
+ *
661
+ * This is the second answer to the same question. The first was the reverse —
662
+ * REQUIRE the key, because a record without one was rejected by every reader
663
+ * — and it was right about the symptom and wrong about the cure: the identity
664
+ * belongs to the id the rules can pin, not to a field they cannot. */
665
+ function primaryKeyProblems(app, collections) {
666
+ const primaryKeyOf = new Map(collections.map((collection) => [collection.cid, collection.primaryKey]));
667
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
668
+ const primaryKey = primaryKeyOf.get(cid);
669
+ if (primaryKey === void 0 || !submit.createFields.includes(primaryKey)) return [];
670
+ return [`public.submit.${cid}.createFields must NOT include "${primaryKey}", the schema's primaryKey: the rules can pin the document id but not the value of a field, so a submitter could write at their own id while claiming another record's. A shared record's identity is its document id — the store fills the field from it, and a submitted value is either the same thing or a lie that is thrown away.`];
671
+ });
672
+ }
673
+ //#endregion
15
674
  //#region src/collection/server/skillAssets.ts
16
675
  /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
17
676
  * schema-validated `views/*.html` path, resolved with realpath containment.
@@ -320,7 +979,7 @@ function toRefRecords(linked) {
320
979
  function projectBacklinks(field, schema, enriched, linked) {
321
980
  const source = linked[field.from];
322
981
  if (!source) return [];
323
- const selfId = require_calendarGrid.fieldText(enriched[schema.primaryKey]);
982
+ const selfId = require_itemId.fieldText(enriched[schema.primaryKey]);
324
983
  return require_promptSafety.backlinkRows(field, selfId, Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
325
984
  }
326
985
  /** Project the computed (never-stored) field kinds onto one derived
@@ -329,9 +988,9 @@ function projectBacklinks(field, schema, enriched, linked) {
329
988
  * `backlinks` → the matching source rows (see `projectBacklinks`). */
330
989
  function projectComputed(schema, enriched, linked) {
331
990
  for (const [key, field] of Object.entries(schema.fields)) {
332
- if (field.type === "toggle" && field.field) enriched[key] = require_calendarGrid.fieldText(enriched[field.field]) === field.onValue;
991
+ if (field.type === "toggle" && field.field) enriched[key] = require_itemId.fieldText(enriched[field.field]) === field.onValue;
333
992
  if (field.type === "embed" && field.to) {
334
- const targetId = require_calendarGrid.embedTargetId(field, enriched);
993
+ const targetId = require_itemId.embedTargetId(field, enriched);
335
994
  const target = require_promptSafety.ownProp(linked, field.to);
336
995
  enriched[key] = targetId && target && require_promptSafety.ownProp(target.byId, targetId) || null;
337
996
  }
@@ -352,7 +1011,7 @@ function projectRollups(schema, record, linked) {
352
1011
  if (field.type !== "rollup") continue;
353
1012
  if (out === record) out = { ...record };
354
1013
  const source = linked[field.from];
355
- const selfId = require_calendarGrid.fieldText(record[schema.primaryKey]);
1014
+ const selfId = require_itemId.fieldText(record[schema.primaryKey]);
356
1015
  out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
357
1016
  }
358
1017
  return out;
@@ -399,8 +1058,8 @@ function strictTypeProblem(key, spec, value) {
399
1058
  case "number":
400
1059
  case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
401
1060
  case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
402
- case "date": return require_calendarGrid.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
403
- case "datetime": return require_calendarGrid.parseIsoDateTime(value) !== null ? null : `'${key}' = '${String(value)}' is not a YYYY-MM-DDTHH:MM datetime (seconds optional, no timezone suffix — the shape the calendar parses)`;
1061
+ case "date": return require_itemId.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
1062
+ case "datetime": return require_itemId.parseIsoDateTime(value) !== null ? null : `'${key}' = '${String(value)}' is not a YYYY-MM-DDTHH:MM datetime (seconds optional, no timezone suffix — the shape the calendar parses)`;
404
1063
  default: return null;
405
1064
  }
406
1065
  }
@@ -445,7 +1104,7 @@ var compiled = /* @__PURE__ */ new WeakMap();
445
1104
  function compileRecordZ(schema, tier) {
446
1105
  const cached = compiled.get(schema)?.[tier];
447
1106
  if (cached) return cached;
448
- const stored = Object.entries(schema.fields).filter(([, spec]) => !require_calendarGrid.COMPUTED_TYPES.has(spec.type));
1107
+ const stored = Object.entries(schema.fields).filter(([, spec]) => !require_itemId.COMPUTED_TYPES.has(spec.type));
449
1108
  const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
450
1109
  for (const [key, spec] of stored) {
451
1110
  const problem = recordFieldProblem(key, spec, record[key], tier);
@@ -471,6 +1130,18 @@ function firstRecordProblem(record, schema, tier) {
471
1130
  }
472
1131
  //#endregion
473
1132
  //#region src/collection/server/validate.ts
1133
+ /** Don't flood the result; the first batch is enough to act on. Exported
1134
+ * because a caller that REPORTS a count has to know the count is a floor —
1135
+ * `publish` presents a full batch as "at least N" rather than as a total. */
1136
+ var MAX_RECORD_ISSUES = 25;
1137
+ /** The `file` of the pseudo-issue reported when the backend could not be read
1138
+ * at all.
1139
+ *
1140
+ * Exported because it is a DIFFERENT KIND of answer from "this record is
1141
+ * invalid", and a caller that treats the two alike gets it wrong in the
1142
+ * direction that matters: `publish` lets the user override invalid records,
1143
+ * and overriding this one would mean publishing without ever having looked. */
1144
+ var STORE_UNREADABLE = "(store)";
474
1145
  var MAX_ISSUES = 25;
475
1146
  /** Read every `<id>.json` under the collection's dataDir and report the
476
1147
  * ones that won't load or violate the schema. An empty list means every
@@ -516,14 +1187,14 @@ async function validateStoreRecords(collection, opts) {
516
1187
  items = await require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
517
1188
  } catch (err) {
518
1189
  return [{
519
- file: "(store)",
1190
+ file: STORE_UNREADABLE,
520
1191
  problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
521
1192
  }];
522
1193
  }
523
1194
  const issues = [];
524
1195
  for (const item of items) {
525
1196
  if (issues.length >= MAX_ISSUES) break;
526
- const itemId = require_calendarGrid.fieldText(item[collection.schema.primaryKey]);
1197
+ const itemId = require_itemId.fieldText(item[collection.schema.primaryKey]);
527
1198
  const problem = validateRecordObject(item, itemId, collection.schema, "strict");
528
1199
  if (problem) issues.push({
529
1200
  file: itemId,
@@ -638,7 +1309,7 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
638
1309
  };
639
1310
  const stored = Object.entries(existing).filter(([key]) => {
640
1311
  const spec = collection.schema.fields[key];
641
- return !spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type);
1312
+ return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
642
1313
  });
643
1314
  const merged = {
644
1315
  ...Object.fromEntries(stored),
@@ -715,7 +1386,7 @@ async function countRecordFiles(dataDir, workspaceRoot) {
715
1386
  * `countRecordFiles` (an unreadable file / missing engine must not
716
1387
  * break the whole ontology). */
717
1388
  async function countRecords(collection, workspaceRoot) {
718
- if (require_calendarGrid.storageKindFor(collection.schema) !== "file") try {
1389
+ if (require_itemId.storageKindFor(collection.schema) !== "file") try {
719
1390
  return (await require_discovery.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
720
1391
  } catch (err) {
721
1392
  if (require_discovery.isBackendUnavailable(err)) return null;
@@ -760,7 +1431,7 @@ function buildRecordsById(items, primaryKey) {
760
1431
  * ties pick a different record — and thus a different icon — between
761
1432
  * reconciles. A stable id sort pins one answer. */
762
1433
  function sortByPrimaryKey(items, primaryKey) {
763
- return [...items].sort((left, right) => require_calendarGrid.fieldText(left[primaryKey]).localeCompare(require_calendarGrid.fieldText(right[primaryKey])));
1434
+ return [...items].sort((left, right) => require_itemId.fieldText(left[primaryKey]).localeCompare(require_itemId.fieldText(right[primaryKey])));
764
1435
  }
765
1436
  /** Compute the effective launcher icon for `collection`: its static
766
1437
  * `schema.icon` when it declares no `dynamicIcon`, else the icon
@@ -897,7 +1568,7 @@ function successorId(sourceId, next) {
897
1568
  function matchesWhen(when, schema, item) {
898
1569
  if (when) {
899
1570
  const raw = item[when.field];
900
- const text = require_calendarGrid.fieldTextOrNull(raw);
1571
+ const text = require_itemId.fieldTextOrNull(raw);
901
1572
  return text !== null && when.in.includes(text);
902
1573
  }
903
1574
  return require_promptSafety.itemIsDone(schema, item);
@@ -909,10 +1580,10 @@ function matchesWhen(when, schema, item) {
909
1580
  * Discovery rejects a map that doesn't cover the enum's values, so null
910
1581
  * here means a record that predates a map/enum edit. */
911
1582
  function resolveEvery(every, sourceItem) {
912
- if (!require_calendarGrid.isFieldDrivenEvery(every)) return every;
1583
+ if (!require_itemId.isFieldDrivenEvery(every)) return every;
913
1584
  const raw = sourceItem[every.fromField];
914
1585
  if (raw === void 0 || raw === null || raw === "") return null;
915
- const key = require_calendarGrid.fieldTextOrNull(raw);
1586
+ const key = require_itemId.fieldTextOrNull(raw);
916
1587
  if (key === null || !Object.hasOwn(every.map, key)) return null;
917
1588
  return every.map[key] ?? null;
918
1589
  }
@@ -951,7 +1622,7 @@ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
951
1622
  });
952
1623
  return;
953
1624
  }
954
- const fromField = require_calendarGrid.isFieldDrivenEvery(every) ? every.fromField : void 0;
1625
+ const fromField = require_itemId.isFieldDrivenEvery(every) ? every.fromField : void 0;
955
1626
  require_discovery.log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
956
1627
  slug,
957
1628
  sourceId,
@@ -1653,7 +2324,7 @@ async function handleGetItems(collection, args, deps) {
1653
2324
  function computedKeyProblem(record, schema) {
1654
2325
  for (const key of Object.keys(record)) {
1655
2326
  const spec = schema.fields[key];
1656
- if (!spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type)) continue;
2327
+ if (!spec || !require_itemId.COMPUTED_TYPES.has(spec.type)) continue;
1657
2328
  if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
1658
2329
  return `'${key}' is ${{
1659
2330
  derived: "derived",
@@ -1693,7 +2364,7 @@ async function mergeWithExisting(collection, store, record, itemId) {
1693
2364
  if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
1694
2365
  const stored = Object.entries(existing).filter(([key]) => {
1695
2366
  const spec = collection.schema.fields[key];
1696
- return !spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type);
2367
+ return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
1697
2368
  });
1698
2369
  return {
1699
2370
  ...Object.fromEntries(stored),
@@ -2069,6 +2740,36 @@ function makeManageCollectionTool(deps = {}) {
2069
2740
  };
2070
2741
  }
2071
2742
  //#endregion
2743
+ Object.defineProperty(exports, "APPS_COLLECTION", {
2744
+ enumerable: true,
2745
+ get: function() {
2746
+ return APPS_COLLECTION;
2747
+ }
2748
+ });
2749
+ Object.defineProperty(exports, "APP_ROLES", {
2750
+ enumerable: true,
2751
+ get: function() {
2752
+ return APP_ROLES;
2753
+ }
2754
+ });
2755
+ Object.defineProperty(exports, "APP_SLUGS_COLLECTION", {
2756
+ enumerable: true,
2757
+ get: function() {
2758
+ return APP_SLUGS_COLLECTION;
2759
+ }
2760
+ });
2761
+ Object.defineProperty(exports, "AuthoredAppZ", {
2762
+ enumerable: true,
2763
+ get: function() {
2764
+ return AuthoredAppZ;
2765
+ }
2766
+ });
2767
+ Object.defineProperty(exports, "MAX_RECORD_ISSUES", {
2768
+ enumerable: true,
2769
+ get: function() {
2770
+ return MAX_RECORD_ISSUES;
2771
+ }
2772
+ });
2072
2773
  Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
2073
2774
  enumerable: true,
2074
2775
  get: function() {
@@ -2087,18 +2788,60 @@ Object.defineProperty(exports, "ONE_SECOND_MS", {
2087
2788
  return ONE_SECOND_MS;
2088
2789
  }
2089
2790
  });
2791
+ Object.defineProperty(exports, "PUBLIC_CONFIG_DOC", {
2792
+ enumerable: true,
2793
+ get: function() {
2794
+ return PUBLIC_CONFIG_DOC;
2795
+ }
2796
+ });
2797
+ Object.defineProperty(exports, "STORE_UNREADABLE", {
2798
+ enumerable: true,
2799
+ get: function() {
2800
+ return STORE_UNREADABLE;
2801
+ }
2802
+ });
2090
2803
  Object.defineProperty(exports, "advanceTriggerDate", {
2091
2804
  enumerable: true,
2092
2805
  get: function() {
2093
2806
  return advanceTriggerDate;
2094
2807
  }
2095
2808
  });
2809
+ Object.defineProperty(exports, "appConfigPath", {
2810
+ enumerable: true,
2811
+ get: function() {
2812
+ return appConfigPath;
2813
+ }
2814
+ });
2815
+ Object.defineProperty(exports, "appSchemasPath", {
2816
+ enumerable: true,
2817
+ get: function() {
2818
+ return appSchemasPath;
2819
+ }
2820
+ });
2821
+ Object.defineProperty(exports, "appSlugDoc", {
2822
+ enumerable: true,
2823
+ get: function() {
2824
+ return appSlugDoc;
2825
+ }
2826
+ });
2827
+ Object.defineProperty(exports, "appStagingPath", {
2828
+ enumerable: true,
2829
+ get: function() {
2830
+ return appStagingPath;
2831
+ }
2832
+ });
2096
2833
  Object.defineProperty(exports, "applyMutateAction", {
2097
2834
  enumerable: true,
2098
2835
  get: function() {
2099
2836
  return applyMutateAction;
2100
2837
  }
2101
2838
  });
2839
+ Object.defineProperty(exports, "bindsSubmitterIdentity", {
2840
+ enumerable: true,
2841
+ get: function() {
2842
+ return bindsSubmitterIdentity;
2843
+ }
2844
+ });
2102
2845
  Object.defineProperty(exports, "buildActionSeedPrompt", {
2103
2846
  enumerable: true,
2104
2847
  get: function() {
@@ -2195,18 +2938,54 @@ Object.defineProperty(exports, "maybeSpawnSuccessor", {
2195
2938
  return maybeSpawnSuccessor;
2196
2939
  }
2197
2940
  });
2941
+ Object.defineProperty(exports, "parseAuthoredApp", {
2942
+ enumerable: true,
2943
+ get: function() {
2944
+ return parseAuthoredApp;
2945
+ }
2946
+ });
2198
2947
  Object.defineProperty(exports, "parseCivil", {
2199
2948
  enumerable: true,
2200
2949
  get: function() {
2201
2950
  return parseCivil;
2202
2951
  }
2203
2952
  });
2953
+ Object.defineProperty(exports, "projectApp", {
2954
+ enumerable: true,
2955
+ get: function() {
2956
+ return projectApp;
2957
+ }
2958
+ });
2959
+ Object.defineProperty(exports, "projectDeploy", {
2960
+ enumerable: true,
2961
+ get: function() {
2962
+ return projectDeploy;
2963
+ }
2964
+ });
2965
+ Object.defineProperty(exports, "projectPublish", {
2966
+ enumerable: true,
2967
+ get: function() {
2968
+ return projectPublish;
2969
+ }
2970
+ });
2971
+ Object.defineProperty(exports, "promoteSchema", {
2972
+ enumerable: true,
2973
+ get: function() {
2974
+ return promoteSchema;
2975
+ }
2976
+ });
2204
2977
  Object.defineProperty(exports, "promptPathsFor", {
2205
2978
  enumerable: true,
2206
2979
  get: function() {
2207
2980
  return promptPathsFor;
2208
2981
  }
2209
2982
  });
2983
+ Object.defineProperty(exports, "publishProblems", {
2984
+ enumerable: true,
2985
+ get: function() {
2986
+ return publishProblems;
2987
+ }
2988
+ });
2210
2989
  Object.defineProperty(exports, "readCustomViewHtml", {
2211
2990
  enumerable: true,
2212
2991
  get: function() {
@@ -2255,6 +3034,12 @@ Object.defineProperty(exports, "schemaRelations", {
2255
3034
  return schemaRelations;
2256
3035
  }
2257
3036
  });
3037
+ Object.defineProperty(exports, "stagedRuleConfig", {
3038
+ enumerable: true,
3039
+ get: function() {
3040
+ return stagedRuleConfig;
3041
+ }
3042
+ });
2258
3043
  Object.defineProperty(exports, "successorId", {
2259
3044
  enumerable: true,
2260
3045
  get: function() {
@@ -2274,4 +3059,4 @@ Object.defineProperty(exports, "validateRecordObject", {
2274
3059
  }
2275
3060
  });
2276
3061
 
2277
- //# sourceMappingURL=server-9mFaGLdq.cjs.map
3062
+ //# sourceMappingURL=server-CgkkWWBd.cjs.map