@mulmoclaude/core 3.7.0 → 3.11.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 (75) 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 +19 -19
  12. package/dist/collection/registry/server/index.cjs.map +1 -1
  13. package/dist/collection/registry/server/index.js +2 -2
  14. package/dist/collection/server/firestoreDocs.d.ts +18 -0
  15. package/dist/collection/server/host.d.ts +29 -0
  16. package/dist/collection/server/index.cjs +70 -62
  17. package/dist/collection/server/index.d.ts +2 -3
  18. package/dist/collection/server/index.js +4 -4
  19. package/dist/collection/server/io.d.ts +5 -3
  20. package/dist/collection/server/manageTool.d.ts +0 -4
  21. package/dist/collection/server/publishManifest.d.ts +1 -0
  22. package/dist/collection/server/publishProject.d.ts +171 -2
  23. package/dist/collection-watchers/index.cjs +82 -38
  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/{store-_61sO8K8.js → discovery-B9zdNkgW.js} +2525 -2354
  28. package/dist/discovery-B9zdNkgW.js.map +1 -0
  29. package/dist/{store-5_P_NsGa.cjs → discovery-BsXiDZBR.cjs} +2545 -2362
  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 +14 -14
  34. package/dist/feeds/server/index.cjs.map +1 -1
  35. package/dist/feeds/server/index.js +4 -4
  36. package/dist/firestore/listen.d.ts +4 -0
  37. package/dist/google/index.cjs +16 -16
  38. package/dist/google/index.cjs.map +1 -1
  39. package/dist/google/index.js +2 -2
  40. package/dist/{ingestTypes-CEi-Ot7F.js → ingestTypes-B312BwXg.js} +2 -2
  41. package/dist/{ingestTypes-CEi-Ot7F.js.map → ingestTypes-B312BwXg.js.map} +1 -1
  42. package/dist/{ingestTypes-iaiq33Ph.cjs → ingestTypes-B4XfrUoq.cjs} +3 -3
  43. package/dist/{ingestTypes-iaiq33Ph.cjs.map → ingestTypes-B4XfrUoq.cjs.map} +1 -1
  44. package/dist/{calendarGrid-CQ8MVSRb.js → itemId-DfTm08jm.js} +34 -2
  45. package/dist/itemId-DfTm08jm.js.map +1 -0
  46. package/dist/{calendarGrid-DGILaVxI.cjs → itemId-Dzrr3vs5.cjs} +45 -1
  47. package/dist/itemId-Dzrr3vs5.cjs.map +1 -0
  48. package/dist/listen-D5av7GzB.js +22 -0
  49. package/dist/listen-D5av7GzB.js.map +1 -0
  50. package/dist/listen-UZA68Upt.cjs +45 -0
  51. package/dist/listen-UZA68Upt.cjs.map +1 -0
  52. package/dist/{promptSafety-CpiME8pj.js → promptSafety-CSdyp7RF.js} +2 -2
  53. package/dist/{promptSafety-CpiME8pj.js.map → promptSafety-CSdyp7RF.js.map} +1 -1
  54. package/dist/{promptSafety-NNGViiCr.cjs → promptSafety-CWqYq-NS.cjs} +8 -8
  55. package/dist/{promptSafety-NNGViiCr.cjs.map → promptSafety-CWqYq-NS.cjs.map} +1 -1
  56. package/dist/remote-host/server/hostRunner.d.ts +2 -4
  57. package/dist/remote-host/server/index.cjs +4 -19
  58. package/dist/remote-host/server/index.cjs.map +1 -1
  59. package/dist/remote-host/server/index.js +1 -16
  60. package/dist/remote-host/server/index.js.map +1 -1
  61. package/dist/{server-CWZyg8fn.cjs → server-X_magqUa.cjs} +620 -734
  62. package/dist/server-X_magqUa.cjs.map +1 -0
  63. package/dist/{server-B48Jyxcj.js → server-zcM1ViCE.js} +418 -568
  64. package/dist/server-zcM1ViCE.js.map +1 -0
  65. package/dist/whisper/index.cjs +1 -1
  66. package/dist/whisper/index.js +1 -1
  67. package/package.json +1 -1
  68. package/dist/calendarGrid-CQ8MVSRb.js.map +0 -1
  69. package/dist/calendarGrid-DGILaVxI.cjs.map +0 -1
  70. package/dist/collection/core/shortHexId.d.ts +0 -8
  71. package/dist/collection/server/publish.d.ts +0 -56
  72. package/dist/server-B48Jyxcj.js.map +0 -1
  73. package/dist/server-CWZyg8fn.cjs.map +0 -1
  74. package/dist/store-5_P_NsGa.cjs.map +0 -1
  75. package/dist/store-_61sO8K8.js.map +0 -1
@@ -1,17 +1,15 @@
1
1
  import { a as isErrorWithCode, c as isStringArray, l as isUnknownArray, s as isRecord, 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
- import { F as isFieldDrivenEvery, L as storageKindFor, P as embedTargetId, R as fieldText, j as COMPUTED_TYPES, l as parseIsoDate, u as parseIsoDateTime, y as isValidCollectionName, z as fieldTextOrNull } from "./calendarGrid-CQ8MVSRb.js";
5
- import { C as actionVisible, _ as uniqueRefTargets, b as projectBacklinkRow, c as selectDynamicRecord, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, i as ownProp, n as deriveAll, o as firstDateField, s as resolveIcon, t as defangForPrompt, v as backlinkRows, x as rollupValue, y as coerceNumeric } from "./promptSafety-CpiME8pj.js";
6
- import { B as SCHEMA_FILE$1, C as resolveCreateItemId, D as loadCollection, E as discoverCollections, I as parseAppManifest, J as isBackendUnavailable, K as safeSlugName, M as resolveMutateSet, N as APP_MANIFEST_FILE, O as resolvePrimaryField, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, X as archiveDir, at as log, b as isRegularFile, d as normalizeCsvValue, f as queryCsv, h as CollectionQueryZ, i as checkpointSqliteDatabase, j as CollectionSchemaZ, m as compileJsonlQuery, n as readOnlyRefusal, nt as getWorkspaceRoot, o as cacheDir, pt as stagingSkillDir, r as storeFor, rt as isPresetSlug$1, tt as firestoreHandle } from "./store-_61sO8K8.js";
4
+ import { B as fieldText, I as embedTargetId, L as isFieldDrivenEvery, N as COMPUTED_TYPES, V as fieldTextOrNull, d as parseIsoDate, f as parseIsoDateTime, x as isValidCollectionName, z as storageKindFor } from "./itemId-DfTm08jm.js";
5
+ import { C as actionVisible, _ as uniqueRefTargets, b as projectBacklinkRow, c as selectDynamicRecord, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, i as ownProp, n as deriveAll, o as firstDateField, s as resolveIcon, t as defangForPrompt, v as backlinkRows, x as rollupValue, y as coerceNumeric } from "./promptSafety-CSdyp7RF.js";
6
+ import { B as SCHEMA_FILE$1, C as CollectionQueryZ, I as parseAppManifest, J as isBackendUnavailable, K as safeSlugName, O as isRegularFile, S as compileJsonlQuery, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, X as archiveDir, b as queryCsv, c as resolveMutateSet, d as storeFor, f as checkpointSqliteDatabase, ht as stagingSkillDir, i as resolvePrimaryField, it as isPresetSlug$1, j as resolveCreateItemId, m as cacheDir, n as discoverCollections, nt as getWorkspaceRoot, ot as log, r as loadCollection, s as CollectionSchemaZ, u as readOnlyRefusal, y as normalizeCsvValue } from "./discovery-B9zdNkgW.js";
7
7
  import { ingestStatePath } from "./feeds/paths.js";
8
8
  import { mirrorSkillWrite } from "./skill-bridge/index.js";
9
9
  import path from "node:path";
10
10
  import { randomBytes, randomUUID } from "node:crypto";
11
11
  import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
12
12
  import { z } from "zod";
13
- import { execFile } from "node:child_process";
14
- import { promisify } from "node:util";
15
13
  //#region src/collection/server/publishManifest.ts
16
14
  /** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
17
15
  * that `sharedCollectionKey` applies. Stated once so a path built later
@@ -124,6 +122,34 @@ var PublicZ = z.object({
124
122
  read: z.array(NameZ).optional(),
125
123
  submit: z.record(NameZ, SubmitZ).optional()
126
124
  }).strict();
125
+ /** The URL name an app is handed out under: `https://<host>/{slug}`.
126
+ *
127
+ * A SEPARATE name from the `aid`, and that separation is the point (design
128
+ * D2b). `apps/{aid}` is a shelf every user of the deployment shares and the
129
+ * rules' `allow create` asks only that you name yourself owner — so a
130
+ * memorable aid is first-come-first-served, cannot be checked for
131
+ * availability, and frees up again when an app is deleted. The aid is
132
+ * therefore a UUID, and the thing people can fight over is moved to the name
133
+ * that costs nothing to change.
134
+ *
135
+ * Declared here rather than kept beside `app.json` because a RESERVATION has
136
+ * to travel with the repository: `appSlugs/{slug}` is unreadable until the app
137
+ * is published (`allow read: if resource.data.published == true`), so nothing
138
+ * can recover which slug an app holds by asking Firestore. A second file to
139
+ * keep in step with the declaration is the alternative, and it is the kind of
140
+ * pair that goes out of step silently.
141
+ *
142
+ * The shape is stricter than `NameZ` on purpose: it is BOTH a URL path
143
+ * segment people read aloud and a Firestore document id. Lowercase
144
+ * alphanumerics separated by single hyphens covers both without a case rule
145
+ * that would make two slugs collide in one place and not the other.
146
+ *
147
+ * Which slug an app ended up with is the HOST's business — a wanted slug can
148
+ * be taken, and the host writes the one it reserved back here. Nothing in this
149
+ * package reads the key; it is declared so that writing it back does not make
150
+ * the file unparseable. */
151
+ var SLUG_SHAPE = "must be lowercase letters, digits and single hyphens, and must not start or end with one (e.g. sakura-hair)";
152
+ var SlugZ = z.string().trim().max(64).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, SLUG_SHAPE).refine((slug) => !slug.includes("--"), SLUG_SHAPE);
127
153
  /** The whole authored declaration.
128
154
  *
129
155
  * `owner` is accepted but is NOT the published value — publish stamps the
@@ -135,6 +161,8 @@ var PublicZ = z.object({
135
161
  var AuthoredAppZ = z.object({
136
162
  aid: NameZ,
137
163
  name: z.string().trim().min(1).optional(),
164
+ /** The wanted (or reserved) URL name — see {@link SlugZ}. */
165
+ slug: SlugZ.optional(),
138
166
  /** Per-worktree app id (design D6, implementation order 7). Accepted so a
139
167
  * repository already carrying it parses; nothing reads it yet. */
140
168
  aidEnv: z.string().trim().min(1).optional(),
@@ -263,6 +291,7 @@ function projectApp(authored, schemas, stamp, existing) {
263
291
  publishedAt: stamp.publishedAt,
264
292
  publishedBy: stamp.email,
265
293
  publishedCommit: stamp.commit,
294
+ publishedDirty: stamp.dirty === true ? true : void 0,
266
295
  previousPublished: previousOf(existing)
267
296
  });
268
297
  const config = {
@@ -281,6 +310,102 @@ function projectApp(authored, schemas, stamp, existing) {
281
310
  config
282
311
  };
283
312
  }
313
+ /** Keys on the app document that publish owns: what is PUBLIC right now, and
314
+ * the rule-facing configuration anonymous access is judged against. Deploy
315
+ * carries them through from the existing document and never authors them. */
316
+ var PUBLISH_OWNED_KEYS = [
317
+ "public",
318
+ "collections",
319
+ "participantRead",
320
+ "publishedAt",
321
+ "publishedBy",
322
+ "publishedCommit",
323
+ "publishedDirty",
324
+ "previousPublished"
325
+ ];
326
+ var isPublishOwned = (key) => PUBLISH_OWNED_KEYS.includes(key);
327
+ function projectDeploy(authored, schemas, stamp, existing) {
328
+ const { app } = projectApp(authored, schemas, stamp, existing);
329
+ const deployed = Object.fromEntries(Object.entries(app).filter(([key]) => !isPublishOwned(key)));
330
+ for (const key of PUBLISH_OWNED_KEYS) {
331
+ const live = existing?.[key];
332
+ if (live !== void 0) deployed[key] = live;
333
+ }
334
+ deployed.deployedAt = stamp.publishedAt;
335
+ deployed.deployedBy = stamp.email;
336
+ if (stamp.commit !== void 0) deployed.deployedCommit = stamp.commit;
337
+ return {
338
+ app: deployed,
339
+ staging: schemas.map(({ cid, schema }) => ({
340
+ cid,
341
+ doc: stagedDoc(schema, stamp, authored, cid)
342
+ }))
343
+ };
344
+ }
345
+ /** One staged schema document, carrying this cid's rule-facing configuration
346
+ * alongside the schema so publish can promote them together. */
347
+ function stagedDoc(schema, stamp, authored, cid) {
348
+ const doc = {
349
+ publishedSchema: schema,
350
+ deployedAt: stamp.publishedAt,
351
+ deployedBy: stamp.email
352
+ };
353
+ const config = authored.collections?.[cid];
354
+ if (config !== void 0) doc.config = config;
355
+ if (authored.participantRead?.includes(cid) === true) doc.participantRead = true;
356
+ if (stamp.commit !== void 0) doc.deployedCommit = stamp.commit;
357
+ return doc;
358
+ }
359
+ /** The rule-facing configuration to promote, read from the STAGED documents
360
+ * rather than from the manifest as it reads right now.
361
+ *
362
+ * Otherwise: deploy revision A, edit `app.json` to revision B, publish — and
363
+ * the promoted schema is A's while the authorization behaviour is B's, a
364
+ * combination nobody exercised through `/staging/{aid}`.
365
+ *
366
+ * (`public` is deliberately NOT part of this: it is not staged, because it is
367
+ * the decision being made AT publish rather than something under test.) */
368
+ function stagedRuleConfig(staged) {
369
+ const entries = [];
370
+ const participantRead = [];
371
+ for (const entry of staged) {
372
+ const { config } = entry.doc;
373
+ if (config !== void 0) entries.push([entry.cid, config]);
374
+ if (entry.doc.participantRead === true) participantRead.push(entry.cid);
375
+ }
376
+ return {
377
+ collections: entries.length > 0 ? Object.fromEntries(entries) : void 0,
378
+ participantRead: participantRead.length > 0 ? participantRead : void 0
379
+ };
380
+ }
381
+ function projectPublish(authored, staged, stamp, existing) {
382
+ const { app, config } = projectApp(authored, [], stamp, existing);
383
+ const staging = stagedRuleConfig(staged);
384
+ app.collections = staging.collections;
385
+ app.participantRead = staging.participantRead;
386
+ const published = Object.fromEntries(Object.entries(existing ?? app).filter(([key]) => !isPublishOwned(key)));
387
+ for (const key of PUBLISH_OWNED_KEYS) if (key !== "public" && app[key] !== void 0) published[key] = app[key];
388
+ const publicBlock = app.public;
389
+ return {
390
+ app: published,
391
+ config,
392
+ public: isRecord(publicBlock) ? publicBlock : void 0
393
+ };
394
+ }
395
+ /** Re-stamp a staged schema document as it is promoted to `collections/{cid}`.
396
+ *
397
+ * The stamp answers "which version is PUBLIC right now, and who made it so",
398
+ * so it is written by the operation that changes the answer — publish — not
399
+ * carried over from the deploy that staged it. */
400
+ function promoteSchema(staged, stamp) {
401
+ const doc = {
402
+ publishedSchema: staged.publishedSchema,
403
+ publishedAt: stamp.publishedAt,
404
+ publishedBy: stamp.email
405
+ };
406
+ if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
407
+ return doc;
408
+ }
284
409
  /** One published schema document. Written key by key rather than through
285
410
  * `compact`, so the declared type is the type — an optional commit is the
286
411
  * only variable part. */
@@ -296,8 +421,31 @@ function schemaDoc(schema, stamp) {
296
421
  /** The app documents' parent path — the `FirestoreDocs` seam takes a
297
422
  * collection path plus a document id, and the app document's id is the aid. */
298
423
  var APPS_COLLECTION = "apps";
299
- /** The collection (schema) documents' parent path. */
424
+ /** The collection (schema) documents' parent path — what the PUBLIC page
425
+ * reads, written only by publish (promotion). */
300
426
  var appSchemasPath = (aid) => `apps/${aid}/collections`;
427
+ /** The URL-slug reservations — `appSlugs/{slug}` → `{ aid, published }`.
428
+ *
429
+ * A TOP-LEVEL collection, not a field on the app: the public page resolves a
430
+ * slug to an aid BEFORE it can read anything under `apps/{aid}`, and a slug
431
+ * has to be claimable atomically (create-if-absent) so two apps cannot hold
432
+ * the same URL.
433
+ *
434
+ * `published` is what makes the reservation invisible until publish. The slug
435
+ * is human-readable, so a readable reservation would let anyone guess the URL
436
+ * and get the aid — and the aid is the `/staging/{aid}` entrance. The rule is
437
+ * `allow read: if resource.data.published == true`, which needs no `get()` and
438
+ * so costs nothing against the rules' expression budget. */
439
+ var APP_SLUGS_COLLECTION = "appSlugs";
440
+ var appSlugDoc = (aid, published) => ({
441
+ aid,
442
+ published
443
+ });
444
+ /** The staged schema documents' parent path — what `/staging/{aid}` reads,
445
+ * written by deploy. A separate DOCUMENT rather than a field beside
446
+ * `publishedSchema`, because the rules cannot hide a field: anything inside a
447
+ * document the public page may read is public. */
448
+ var appStagingPath = (aid) => `apps/${aid}/staging`;
301
449
  /** The public-config documents' parent path. */
302
450
  var appConfigPath = (aid) => `apps/${aid}/config`;
303
451
  //#endregion
@@ -523,530 +671,32 @@ function publishProblems(app, collections, publisherEmail) {
523
671
  ...primaryKeyProblems(app, collections)
524
672
  ];
525
673
  }
526
- /** A public submission must be able to produce a record the HOST can read.
527
- *
528
- * The rules and the engine disagree about what identifies a record, and the
529
- * gap is invisible from either side alone. The rules bind the DOCUMENT ID
530
- * (`idFrom`) and let a submission carry only `createFields`; the engine
531
- * identifies a record by its schema's `primaryKey` FIELD, and the firestore
532
- * store hands back the document's fields verbatim — `toItem` does not
533
- * synthesize the key from the document id. So a submit path whose
534
- * `createFields` omits the primary key writes rows that Firestore accepts and
535
- * every reader rejects: `validateRecordObject` fails them, the collection
536
- * renders empty-ish, and the next publish's own pre-check reports them as
537
- * broken records the publisher never wrote.
538
- *
539
- * Not checkable by the rules (they have never heard of a schema) and not
540
- * catchable at write time (nothing is wrong with the write). Publish is the
541
- * only place that holds both halves. */
542
- function primaryKeyProblems(app, collections) {
543
- const primaryKeyOf = new Map(collections.map((collection) => [collection.cid, collection.primaryKey]));
544
- return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
545
- const primaryKey = primaryKeyOf.get(cid);
546
- if (primaryKey === void 0 || submit.createFields.includes(primaryKey)) return [];
547
- return [`public.submit.${cid}.createFields must include "${primaryKey}", the schema's primaryKey: a submission may carry only the createFields, and a shared record is stored as exactly the fields it was written with — the document id is not copied into the record. Without it every submission is accepted by the rules and then rejected by every reader.`];
548
- });
549
- }
550
- //#endregion
551
- //#region src/collection/core/recordZ.ts
552
- /** The emptiness rule shared by `required` and the "only check present
553
- * values" gate. NOT a truthiness check — `0` and `false` are filled. */
554
- var isEmptyValue = (value) => value === void 0 || value === null || value === "";
555
- /** The historical write-gate checks, verbatim: required non-empty, enum
556
- * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
557
- function enforcedProblem(key, spec, value) {
558
- const empty = isEmptyValue(value);
559
- if (spec.required && empty) return `missing required field '${key}'`;
560
- if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
561
- return null;
562
- }
563
- /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
564
- * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
565
- * the lint flags exactly the values the calendar / trigger / spawn code
566
- * would silently drop — impossible days like `2026-02-30`, and datetimes
567
- * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
568
- * which the day view can't place). `string`-backed types accept anything
569
- * stringifiable; `ref` existence is out of scope. */
570
- function strictTypeProblem(key, spec, value) {
571
- switch (spec.type) {
572
- case "number":
573
- case "money": return Number.isFinite(coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
574
- case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
575
- case "date": return parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
576
- case "datetime": return 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)`;
577
- default: return null;
578
- }
579
- }
580
- /** Strict check for a PRESENT `table` value: an array of row objects, each
581
- * row conforming to the sub-schema (required / enum / typed sub-values).
582
- * First row problem wins, prefixed with the row number so the fix is
583
- * locatable. */
584
- function strictTableProblem(key, spec, value) {
585
- if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
586
- for (let index = 0; index < value.length; index++) {
587
- const row = value[index];
588
- if (!isRecord(row)) return `'${key}' row ${index + 1} is not an object`;
589
- for (const [subKey, subSpec] of Object.entries(spec.of)) {
590
- const subValue = row[subKey];
591
- const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
592
- if (problem) return `'${key}' row ${index + 1}: ${problem}`;
593
- }
594
- }
595
- return null;
596
- }
597
- /** First problem for one field's stored value under `tier`, or null.
598
- * Enforced checks always run (and their messages never vary by tier — the
599
- * scan and the write gate must agree on them); strict adds the per-type
600
- * layer on present values only. */
601
- function recordFieldProblem(key, spec, value, tier) {
602
- const enforced = enforcedProblem(key, spec, value);
603
- if (enforced || tier === "enforced") return enforced;
604
- if (isEmptyValue(value)) return null;
605
- if (spec.type === "table") return strictTableProblem(key, spec, value);
606
- return strictTypeProblem(key, spec, value);
607
- }
608
- var compiled = /* @__PURE__ */ new WeakMap();
609
- /** Compile `schema.fields` into a zod validator for a stored record.
610
- * Loose object: unknown keys are allowed and any declared key may be
611
- * absent (records are user files, not parse-and-rewrite targets —
612
- * callers validate, they never persist the parse output). The checks run
613
- * as ONE object-level refine iterating fields in declaration order —
614
- * per-key shape schemas can't express "key may be absent BUT its absence
615
- * must still reach the required check", and the single loop keeps the
616
- * first reported issue identical to the historical first-problem-wins
617
- * contract. */
618
- function compileRecordZ(schema, tier) {
619
- const cached = compiled.get(schema)?.[tier];
620
- if (cached) return cached;
621
- const stored = Object.entries(schema.fields).filter(([, spec]) => !COMPUTED_TYPES.has(spec.type));
622
- const validator = z.looseObject({}).superRefine((record, ctx) => {
623
- for (const [key, spec] of stored) {
624
- const problem = recordFieldProblem(key, spec, record[key], tier);
625
- if (problem) ctx.addIssue({
626
- code: "custom",
627
- message: problem,
628
- path: [key]
629
- });
630
- }
631
- });
632
- const entry = compiled.get(schema) ?? {};
633
- entry[tier] = validator;
634
- compiled.set(schema, entry);
635
- return validator;
636
- }
637
- /** First schema problem on an in-memory record under `tier`, or null. One
638
- * issue per record keeps the report short and the fix obvious (the
639
- * historical contract of `validateRecordObject`). */
640
- function firstRecordProblem(record, schema, tier) {
641
- const result = compileRecordZ(schema, tier).safeParse(record);
642
- if (result.success) return null;
643
- return result.error.issues[0]?.message ?? "record failed schema validation";
644
- }
645
- //#endregion
646
- //#region src/collection/server/validate.ts
647
- /** Don't flood the result; the first batch is enough to act on. Exported
648
- * because a caller that REPORTS a count has to know the count is a floor —
649
- * `publish` presents a full batch as "at least N" rather than as a total. */
650
- var MAX_RECORD_ISSUES = 25;
651
- /** The `file` of the pseudo-issue reported when the backend could not be read
652
- * at all.
653
- *
654
- * Exported because it is a DIFFERENT KIND of answer from "this record is
655
- * invalid", and a caller that treats the two alike gets it wrong in the
656
- * direction that matters: `publish` lets the user override invalid records,
657
- * and overriding this one would mean publishing without ever having looked. */
658
- var STORE_UNREADABLE = "(store)";
659
- var MAX_ISSUES = 25;
660
- /** Read every `<id>.json` under the collection's dataDir and report the
661
- * ones that won't load or violate the schema. An empty list means every
662
- * record is fine. */
663
- /** List entries under the data dir, guarding realpath containment (against a
664
- * symlinked dir swapped in after discovery, like `listItems`) and treating a
665
- * missing dir as empty while surfacing real I/O faults. */
666
- async function listRecordFilenames(dataDir, workspaceRoot) {
667
- if (!isContainedInRoot(dataDir, workspaceRoot)) {
668
- log.warn("collections", "validate refused: dataDir escapes workspace via symlink", { dataDir });
669
- return [];
670
- }
671
- try {
672
- return await readdir(dataDir);
673
- } catch (err) {
674
- if (isErrorWithCode(err) && err.code === "ENOENT") return [];
675
- throw err;
676
- }
677
- }
678
- async function validateCollectionRecords(collection, opts = {}) {
679
- if (collection.schema.dataSource !== void 0) return [];
680
- if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
681
- const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
682
- const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
683
- const issues = [];
684
- for (const name of entries.sort()) {
685
- if (!name.endsWith(".json") || name.startsWith(".")) continue;
686
- if (issues.length >= MAX_ISSUES) break;
687
- const issue = await inspectRecord(path.join(collection.dataDir, name), name, collection.schema);
688
- if (issue) issues.push(issue);
689
- }
690
- return issues;
691
- }
692
- /** Store-backed twin of the file scan: list every record through the
693
- * collection's store and lint it with the same "strict" report-only tier.
694
- * A row the store can't even parse is invisible here (the store skips
695
- * it), so the read/parse classifications of the file scan don't apply —
696
- * schema violations are what this catches. `file` carries the record id
697
- * (there is no per-record filename). */
698
- async function validateStoreRecords(collection, opts) {
699
- let items;
700
- try {
701
- items = await storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
702
- } catch (err) {
703
- return [{
704
- file: STORE_UNREADABLE,
705
- problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
706
- }];
707
- }
708
- const issues = [];
709
- for (const item of items) {
710
- if (issues.length >= MAX_ISSUES) break;
711
- const itemId = fieldText(item[collection.schema.primaryKey]);
712
- const problem = validateRecordObject(item, itemId, collection.schema, "strict");
713
- if (problem) issues.push({
714
- file: itemId,
715
- problem
716
- });
717
- }
718
- return issues;
719
- }
720
- async function readRecordText(fullPath, name) {
721
- try {
722
- if (!(await lstat(fullPath)).isFile()) return {
723
- file: name,
724
- problem: "not a regular file (symlink?) — skipped, won't appear"
725
- };
726
- return { raw: await readFile(fullPath, "utf-8") };
727
- } catch {
728
- return {
729
- file: name,
730
- problem: "could not be read — skipped, won't appear"
731
- };
732
- }
733
- }
734
- /** Classify a single record file: unreadable / unparseable / non-object /
735
- * schema violation, or null when it's fine. */
736
- async function inspectRecord(fullPath, name, schema) {
737
- const read = await readRecordText(fullPath, name);
738
- if ("problem" in read) return read;
739
- let parsed;
740
- try {
741
- parsed = JSON.parse(read.raw);
742
- } catch (err) {
743
- return {
744
- file: name,
745
- problem: `invalid JSON (${err instanceof Error ? err.message : String(err)}) — SKIPPED, won't appear. Usual cause: an unescaped " inside a string value; use 「」/『』 or write \\" instead.`
746
- };
747
- }
748
- if (!isRecord(parsed)) return {
749
- file: name,
750
- problem: "not a JSON object — skipped, won't appear"
751
- };
752
- const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
753
- return problem ? {
754
- file: name,
755
- problem
756
- } : null;
757
- }
758
- /** What a non-string primary key actually is, for the error message. Names the
759
- * shape rather than stringifying the value — "[object Object]" tells the reader
760
- * nothing about what is wrong. */
761
- function describeIdType(value) {
762
- if (value === null) return "null";
763
- if (value === void 0) return "missing";
764
- if (Array.isArray(value)) return "an array";
765
- return `a ${typeof value}`;
766
- }
767
- /** First schema problem on an in-memory record (primaryKey↔id mismatch,
768
- * then the compiled per-field checks — see `../core/recordZ` for the two
769
- * tiers), or null when it's fine. One issue per record keeps the report
770
- * short and the fix obvious. Pure + exported so write paths
771
- * (manageCollection putItems) can gate on the SAME enforced rules the
772
- * post-hoc file scan reports — `itemId` is the id the record is (or
773
- * would be) stored under. The default `"enforced"` tier keeps every
774
- * write gate on the historical three checks; only pass `"strict"` from
775
- * report-only surfaces. */
776
- function validateRecordObject(record, itemId, schema, tier = "enforced") {
777
- const idValue = record[schema.primaryKey];
778
- if (typeof idValue !== "string") return `'${schema.primaryKey}' must be a string, but is ${describeIdType(idValue)} — must equal the filename ('${itemId}'), or the record can't be opened`;
779
- if (idValue !== itemId) return `'${schema.primaryKey}' is '${idValue}' but must equal the filename ('${itemId}'), or the record can't be opened`;
780
- return firstRecordProblem(record, schema, tier);
781
- }
782
- //#endregion
783
- //#region src/collection/server/publish.ts
784
- var execFileAsync = promisify(execFile);
785
- /** How many broken records to name before summarising. A publish that would
786
- * break a thousand rows is answered by the count and a sample; dumping all of
787
- * them buries the number, which is the part the decision turns on. */
788
- var MAX_LISTED_ISSUES = 10;
789
- /** `git rev-parse HEAD` plus a dirty check, or nothing.
790
- *
791
- * A missing git, a repository with no commits and a non-repository are all
792
- * the same answer here — no commit — because the stamp is attribution, not a
793
- * requirement. What is NOT acceptable is a stamp that lies, which is why the
794
- * dirty flag exists: publishing from a modified tree records a commit that
795
- * does not describe what was published, and the flag is the only thing that
796
- * would ever tell a reader so. */
797
- async function gitStamp(root) {
798
- try {
799
- const { stdout } = await execFileAsync("git", [
800
- "-C",
801
- root,
802
- "rev-parse",
803
- "HEAD"
804
- ]);
805
- const commit = stdout.trim();
806
- const { stdout: status } = await execFileAsync("git", [
807
- "-C",
808
- root,
809
- "status",
810
- "--porcelain"
811
- ]);
812
- return {
813
- commit: commit.length > 0 ? commit : void 0,
814
- dirty: status.trim().length > 0
815
- };
816
- } catch {
817
- return {};
818
- }
819
- }
820
- /** The shared collections of THIS REPOSITORY, by cid.
821
- *
822
- * `userSkillsDir: null` — not a test convenience, a boundary. Discovery
823
- * resolves every schema it finds against the WORKSPACE root, user-scope
824
- * included, so a globally installed skill under `~/.claude/skills` carrying
825
- * `storage.type: "firestore"` picks up whichever repository's `aid` it
826
- * happens to be discovered from. Left in, publish would write that schema
827
- * into this app — and into every other app the same user publishes, since the
828
- * skill is installed once per machine and the repositories are not.
829
- *
830
- * An app is a REPOSITORY (design D1): its collections are the ones committed
831
- * beside its `app.json`, which is what makes a clone resolve the same
832
- * collections and an invitation a matter of authorization rather than
833
- * discovery. A schema that is not in the repository has no claim on a cid
834
- * there. And because a view is HTML, publishing one is not a tidiness
835
- * question: it is the machine's own skills reaching every member's browser.
836
- *
837
- * The consequence is deliberate: a cid named in `app.json` that exists only
838
- * in user scope is now an unknown cid, and publish says so by name instead of
839
- * quietly publishing a schema from outside the repository. */
840
- async function sharedCollections(opts, root) {
841
- return (await discoverCollections({
842
- ...opts,
843
- workspaceRoot: root,
844
- userSkillsDir: null
845
- })).filter((collection) => collection.appId !== void 0);
846
- }
847
- /** Existing records that would not satisfy the schema about to be published.
848
- *
849
- * Read from FIRESTORE, not from disk: a shared collection's records live in
850
- * the app, and the question this answers is "what does the live data look
851
- * like under the new schema" — which is the migration question. Reported as
852
- * a refusal the publisher can override, because a breaking change is
853
- * sometimes exactly what is intended and the point is that it is a decision
854
- * rather than a discovery. */
855
- async function recordProblems(collections, opts) {
856
- const lines = [];
857
- const unreadable = [];
858
- let records = 0;
859
- let cappedAnywhere = false;
860
- for (const collection of collections) {
861
- const issues = await validateCollectionRecords(collection, opts);
862
- if (issues.length === 0) continue;
863
- const unread = issues.filter((issue) => issue.file === STORE_UNREADABLE);
864
- if (unread.length > 0) {
865
- unreadable.push(`${collection.slug}: ${unread.map((issue) => issue.problem).join("; ")}`);
866
- continue;
867
- }
868
- records += issues.length;
869
- const capped = issues.length >= 25;
870
- cappedAnywhere = cappedAnywhere || capped;
871
- const count = capped ? `at least ${issues.length}` : String(issues.length);
872
- const plural = issues.length === 1 ? "" : "s";
873
- const note = capped ? " (the scan stops there)" : "";
874
- lines.push(`${collection.slug}: ${count} existing record${plural} would not satisfy the schema about to be published${note}`);
875
- for (const issue of issues.slice(0, MAX_LISTED_ISSUES)) lines.push(` - ${issue.file}: ${issue.problem}`);
876
- if (issues.length > MAX_LISTED_ISSUES) lines.push(` - … and ${issues.length - MAX_LISTED_ISSUES} more`);
877
- }
878
- return {
879
- lines,
880
- records,
881
- capped: cappedAnywhere,
882
- unreadable
883
- };
884
- }
885
- function schemasOf(collections) {
886
- return collections.map((collection) => ({
887
- cid: collection.slug,
888
- schema: collection.schema
889
- })).sort((left, right) => left.cid < right.cid ? -1 : left.cid > right.cid ? 1 : 0);
890
- }
891
- /** Read and parse `<root>/app.json`'s full declaration. */
892
- async function readAuthored(root) {
893
- let raw;
894
- try {
895
- raw = await readFile(path.join(root, APP_MANIFEST_FILE), "utf-8");
896
- } catch (err) {
897
- return {
898
- ok: false,
899
- problems: [`cannot read ${path.join(root, APP_MANIFEST_FILE)}: ${String(err)}`]
900
- };
901
- }
902
- return parseAuthoredApp(raw);
903
- }
904
- /** Everything wrong with the declaration itself, publisher included. */
905
- function declarationProblems(app, collections, handle) {
906
- const problems = publishProblems(app, collections.map((collection) => ({
907
- cid: collection.slug,
908
- primaryKey: collection.schema.primaryKey
909
- })), handle.email);
910
- if (app.owner !== void 0 && app.owner !== handle.uid) problems.push(`app.json declares owner "${app.owner}", which is not your uid (${handle.uid}). \`owner\` is stamped by publish and carried forward unchanged afterwards — remove it from app.json rather than maintaining it by hand.`);
911
- return problems;
912
- }
913
- /** Put the three kinds of document, in the order the rules require, and turn a
914
- * rejected write into the result type instead of letting it escape.
915
- *
916
- * Returns null when everything was written; a failure result otherwise.
917
- *
918
- * A raw rejection here would reach the agent as a tool crash rather than the
919
- * actionable text this tool promises. But "actionable" is a strong claim for
920
- * a half-finished publish, so the message ENUMERATES what landed rather than
921
- * summarising it: the order is app → every schema → config, and a summary
922
- * written for one failure point is wrong at the others. Saying "the roster
923
- * and configuration are live" after a SCHEMA write failed names a config
924
- * document this publish never wrote — which still holds whatever the last
925
- * publish left, and is exactly the state the caller is trying to repair. */
926
- async function writeDocuments(handle, aid, published) {
927
- const steps = [
928
- {
929
- what: `the app document (apps/${aid})`,
930
- run: () => handle.docs.set(APPS_COLLECTION, aid, published.app)
931
- },
932
- ...published.schemas.map(({ cid, doc }) => ({
933
- what: `the published schema for '${cid}'`,
934
- run: () => handle.docs.set(appSchemasPath(aid), cid, doc)
935
- })),
936
- {
937
- what: `the public config document (apps/${aid}/config/${PUBLIC_CONFIG_DOC})`,
938
- run: () => handle.docs.set(appConfigPath(aid), PUBLIC_CONFIG_DOC, published.config)
939
- }
940
- ];
941
- const landed = [];
942
- for (const [index, step] of steps.entries()) try {
943
- await step.run();
944
- landed.push(step.what);
945
- } catch (err) {
946
- const reason = err instanceof Error ? err.message : String(err);
947
- return {
948
- ok: false,
949
- partial: index > 0,
950
- problems: [`publish failed while writing ${step.what}: ${reason}`, ...partialState(landed, [step.what, ...steps.slice(index + 1).map((rest) => rest.what)])]
951
- };
952
- }
953
- return null;
954
- }
955
- /** What is live and what is not, listed rather than summarised.
956
- *
957
- * Two facts, and both matter for the repair: a document this publish wrote is
958
- * live NOW, and a document it did not write still holds what the LAST publish
959
- * left — which is not the same as being absent, and not the same as matching
960
- * the declaration that was just half-applied. */
961
- function partialState(landed, notWritten) {
962
- const repair = "Publishing again is the repair: the write is idempotent, and it re-does every step, including the ones that did land.";
963
- if (landed.length === 0) return [`Nothing was written. ${repair}`];
964
- return [
965
- `Written by this publish, and live now: ${landed.join("; ")}.`,
966
- `NOT written: ${notWritten.join("; ")} — ${notWritten.length === 1 ? "it still holds" : "they still hold"} whatever the previous publish left.`,
967
- repair
968
- ];
969
- }
970
- /** Publish this repository's declaration to its app.
971
- *
972
- * Everything variable is a parameter or comes from the host binding, so the
973
- * whole path is exercisable against an in-memory `FirestoreDocs` with no
974
- * network and no API key — which is the only way the conversion table gets
975
- * tested as a table. */
976
- async function publishApp(opts = {}) {
977
- const root = opts.workspaceRoot ?? getWorkspaceRoot();
978
- const handle = firestoreHandle();
979
- if (!handle) return {
980
- ok: false,
981
- partial: false,
982
- problems: ["publish needs a signed-in Firestore session: connect remote-host first. Publishing writes the app's roster and configuration as the app's owner, which is an authenticated write."]
983
- };
984
- const authored = await readAuthored(root);
985
- if (!authored.ok) return {
986
- ...authored,
987
- partial: false
988
- };
989
- const collections = await sharedCollections(opts, root);
990
- const problems = declarationProblems(authored.app, collections, handle);
991
- if (problems.length > 0) return {
992
- ok: false,
993
- partial: false,
994
- problems
995
- };
996
- const issues = await recordProblems(collections, {
997
- ...opts,
998
- workspaceRoot: root
999
- });
1000
- if (issues.unreadable.length > 0) return {
1001
- ok: false,
1002
- partial: false,
1003
- problems: [...issues.unreadable, "publish stopped: the live records could not be read, so nothing checked whether the schemas about to be published still fit them. This is not something `confirm` overrides — confirming means accepting a known breakage, and here there is no reading at all. Fix the access (or the connection) and publish again."]
1004
- };
1005
- if (issues.records > 0 && opts.confirm !== true) return {
1006
- ok: false,
1007
- partial: false,
1008
- problems: [...issues.lines, "publish stopped: these records are live and members are reading them. Migrate them first, or re-run with confirm to publish the schema anyway and repair the records afterwards."]
1009
- };
1010
- return writePublished(authored.app, collections, handle, opts, root, issues);
1011
- }
1012
- /** The write half: stamp, project, and put the documents in the order the
1013
- * rules require. Split from the gate above so neither half hides the other —
1014
- * everything up to here can refuse, and nothing from here on does. */
1015
- async function writePublished(authored, collections, handle, opts, root, issues) {
1016
- const { aid } = authored;
1017
- let existing;
1018
- try {
1019
- existing = await handle.docs.get(APPS_COLLECTION, aid);
1020
- } catch (err) {
1021
- return {
1022
- ok: false,
1023
- partial: false,
1024
- problems: [`publish failed while reading the current app document (apps/${aid}): ${err instanceof Error ? err.message : String(err)}`, "Nothing was written. Publishing again is safe — this read only decides whether the app is created or updated."]
1025
- };
1026
- }
1027
- const stampSource = await (opts.resolveCommit ?? gitStamp)(root);
1028
- const stamp = {
1029
- uid: handle.uid,
1030
- email: handle.email,
1031
- publishedAt: (opts.now ?? Date.now)(),
1032
- commit: stampSource.commit
1033
- };
1034
- const existingApp = isRecord(existing) ? existing : null;
1035
- const published = projectApp(authored, schemasOf(collections), stamp, existingApp);
1036
- if (stampSource.dirty === true) published.app.publishedDirty = true;
1037
- const written = await writeDocuments(handle, aid, published);
1038
- if (written !== null) return written;
1039
- return {
1040
- ok: true,
1041
- aid,
1042
- cids: published.schemas.map((entry) => entry.cid),
1043
- created: existingApp === null,
1044
- commit: stamp.commit,
1045
- dirty: stampSource.dirty === true,
1046
- recordIssues: issues.records,
1047
- recordIssuesCapped: issues.capped,
1048
- published
1049
- };
674
+ /** A public submission must NOT be allowed to name its own primary key.
675
+ *
676
+ * The rules constrain the DOCUMENT ID (`idFrom`) and cannot constrain the
677
+ * value of a field — nothing compares `request.resource.data[primaryKey]`
678
+ * with the path being written. So a submit path that accepts the primary key
679
+ * as a `createField` lets a submitter write at their one permitted document
680
+ * id while CLAIMING another record's identity, or a duplicate.
681
+ *
682
+ * It is refused rather than tolerated because there is nothing for the field
683
+ * to do: `firestoreStore` takes a shared record's identity from the document
684
+ * id and overwrites the field on read, so a submitted value is either equal
685
+ * to the id (noise) or a lie (silently discarded). Publishing a form field
686
+ * whose value is thrown away is worse than not having it — the author will
687
+ * believe submitters choose their ids.
688
+ *
689
+ * This is the second answer to the same question. The first was the reverse —
690
+ * REQUIRE the key, because a record without one was rejected by every reader
691
+ * — and it was right about the symptom and wrong about the cure: the identity
692
+ * belongs to the id the rules can pin, not to a field they cannot. */
693
+ function primaryKeyProblems(app, collections) {
694
+ const primaryKeyOf = new Map(collections.map((collection) => [collection.cid, collection.primaryKey]));
695
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
696
+ const primaryKey = primaryKeyOf.get(cid);
697
+ if (primaryKey === void 0 || !submit.createFields.includes(primaryKey)) return [];
698
+ 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.`];
699
+ });
1050
700
  }
1051
701
  //#endregion
1052
702
  //#region src/collection/server/skillAssets.ts
@@ -1412,6 +1062,238 @@ async function runCollectionQuery(collection, query, opts = {}) {
1412
1062
  return runQueryOverRows(await enrichItems(collection, await store.list(), opts), query);
1413
1063
  }
1414
1064
  //#endregion
1065
+ //#region src/collection/core/recordZ.ts
1066
+ /** The emptiness rule shared by `required` and the "only check present
1067
+ * values" gate. NOT a truthiness check — `0` and `false` are filled. */
1068
+ var isEmptyValue = (value) => value === void 0 || value === null || value === "";
1069
+ /** The historical write-gate checks, verbatim: required non-empty, enum
1070
+ * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
1071
+ function enforcedProblem(key, spec, value) {
1072
+ const empty = isEmptyValue(value);
1073
+ if (spec.required && empty) return `missing required field '${key}'`;
1074
+ if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
1075
+ return null;
1076
+ }
1077
+ /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
1078
+ * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
1079
+ * the lint flags exactly the values the calendar / trigger / spawn code
1080
+ * would silently drop — impossible days like `2026-02-30`, and datetimes
1081
+ * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
1082
+ * which the day view can't place). `string`-backed types accept anything
1083
+ * stringifiable; `ref` existence is out of scope. */
1084
+ function strictTypeProblem(key, spec, value) {
1085
+ switch (spec.type) {
1086
+ case "number":
1087
+ case "money": return Number.isFinite(coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
1088
+ case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
1089
+ case "date": return parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
1090
+ case "datetime": return 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)`;
1091
+ default: return null;
1092
+ }
1093
+ }
1094
+ /** Strict check for a PRESENT `table` value: an array of row objects, each
1095
+ * row conforming to the sub-schema (required / enum / typed sub-values).
1096
+ * First row problem wins, prefixed with the row number so the fix is
1097
+ * locatable. */
1098
+ function strictTableProblem(key, spec, value) {
1099
+ if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
1100
+ for (let index = 0; index < value.length; index++) {
1101
+ const row = value[index];
1102
+ if (!isRecord(row)) return `'${key}' row ${index + 1} is not an object`;
1103
+ for (const [subKey, subSpec] of Object.entries(spec.of)) {
1104
+ const subValue = row[subKey];
1105
+ const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
1106
+ if (problem) return `'${key}' row ${index + 1}: ${problem}`;
1107
+ }
1108
+ }
1109
+ return null;
1110
+ }
1111
+ /** First problem for one field's stored value under `tier`, or null.
1112
+ * Enforced checks always run (and their messages never vary by tier — the
1113
+ * scan and the write gate must agree on them); strict adds the per-type
1114
+ * layer on present values only. */
1115
+ function recordFieldProblem(key, spec, value, tier) {
1116
+ const enforced = enforcedProblem(key, spec, value);
1117
+ if (enforced || tier === "enforced") return enforced;
1118
+ if (isEmptyValue(value)) return null;
1119
+ if (spec.type === "table") return strictTableProblem(key, spec, value);
1120
+ return strictTypeProblem(key, spec, value);
1121
+ }
1122
+ var compiled = /* @__PURE__ */ new WeakMap();
1123
+ /** Compile `schema.fields` into a zod validator for a stored record.
1124
+ * Loose object: unknown keys are allowed and any declared key may be
1125
+ * absent (records are user files, not parse-and-rewrite targets —
1126
+ * callers validate, they never persist the parse output). The checks run
1127
+ * as ONE object-level refine iterating fields in declaration order —
1128
+ * per-key shape schemas can't express "key may be absent BUT its absence
1129
+ * must still reach the required check", and the single loop keeps the
1130
+ * first reported issue identical to the historical first-problem-wins
1131
+ * contract. */
1132
+ function compileRecordZ(schema, tier) {
1133
+ const cached = compiled.get(schema)?.[tier];
1134
+ if (cached) return cached;
1135
+ const stored = Object.entries(schema.fields).filter(([, spec]) => !COMPUTED_TYPES.has(spec.type));
1136
+ const validator = z.looseObject({}).superRefine((record, ctx) => {
1137
+ for (const [key, spec] of stored) {
1138
+ const problem = recordFieldProblem(key, spec, record[key], tier);
1139
+ if (problem) ctx.addIssue({
1140
+ code: "custom",
1141
+ message: problem,
1142
+ path: [key]
1143
+ });
1144
+ }
1145
+ });
1146
+ const entry = compiled.get(schema) ?? {};
1147
+ entry[tier] = validator;
1148
+ compiled.set(schema, entry);
1149
+ return validator;
1150
+ }
1151
+ /** First schema problem on an in-memory record under `tier`, or null. One
1152
+ * issue per record keeps the report short and the fix obvious (the
1153
+ * historical contract of `validateRecordObject`). */
1154
+ function firstRecordProblem(record, schema, tier) {
1155
+ const result = compileRecordZ(schema, tier).safeParse(record);
1156
+ if (result.success) return null;
1157
+ return result.error.issues[0]?.message ?? "record failed schema validation";
1158
+ }
1159
+ //#endregion
1160
+ //#region src/collection/server/validate.ts
1161
+ /** Don't flood the result; the first batch is enough to act on. Exported
1162
+ * because a caller that REPORTS a count has to know the count is a floor —
1163
+ * `publish` presents a full batch as "at least N" rather than as a total. */
1164
+ var MAX_RECORD_ISSUES = 25;
1165
+ /** The `file` of the pseudo-issue reported when the backend could not be read
1166
+ * at all.
1167
+ *
1168
+ * Exported because it is a DIFFERENT KIND of answer from "this record is
1169
+ * invalid", and a caller that treats the two alike gets it wrong in the
1170
+ * direction that matters: `publish` lets the user override invalid records,
1171
+ * and overriding this one would mean publishing without ever having looked. */
1172
+ var STORE_UNREADABLE = "(store)";
1173
+ var MAX_ISSUES = 25;
1174
+ /** Read every `<id>.json` under the collection's dataDir and report the
1175
+ * ones that won't load or violate the schema. An empty list means every
1176
+ * record is fine. */
1177
+ /** List entries under the data dir, guarding realpath containment (against a
1178
+ * symlinked dir swapped in after discovery, like `listItems`) and treating a
1179
+ * missing dir as empty while surfacing real I/O faults. */
1180
+ async function listRecordFilenames(dataDir, workspaceRoot) {
1181
+ if (!isContainedInRoot(dataDir, workspaceRoot)) {
1182
+ log.warn("collections", "validate refused: dataDir escapes workspace via symlink", { dataDir });
1183
+ return [];
1184
+ }
1185
+ try {
1186
+ return await readdir(dataDir);
1187
+ } catch (err) {
1188
+ if (isErrorWithCode(err) && err.code === "ENOENT") return [];
1189
+ throw err;
1190
+ }
1191
+ }
1192
+ async function validateCollectionRecords(collection, opts = {}) {
1193
+ if (collection.schema.dataSource !== void 0) return [];
1194
+ if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
1195
+ const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
1196
+ const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
1197
+ const issues = [];
1198
+ for (const name of entries.sort()) {
1199
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
1200
+ if (issues.length >= MAX_ISSUES) break;
1201
+ const issue = await inspectRecord(path.join(collection.dataDir, name), name, collection.schema);
1202
+ if (issue) issues.push(issue);
1203
+ }
1204
+ return issues;
1205
+ }
1206
+ /** Store-backed twin of the file scan: list every record through the
1207
+ * collection's store and lint it with the same "strict" report-only tier.
1208
+ * A row the store can't even parse is invisible here (the store skips
1209
+ * it), so the read/parse classifications of the file scan don't apply —
1210
+ * schema violations are what this catches. `file` carries the record id
1211
+ * (there is no per-record filename). */
1212
+ async function validateStoreRecords(collection, opts) {
1213
+ let items;
1214
+ try {
1215
+ items = await storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
1216
+ } catch (err) {
1217
+ return [{
1218
+ file: STORE_UNREADABLE,
1219
+ problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
1220
+ }];
1221
+ }
1222
+ const issues = [];
1223
+ for (const item of items) {
1224
+ if (issues.length >= MAX_ISSUES) break;
1225
+ const itemId = fieldText(item[collection.schema.primaryKey]);
1226
+ const problem = validateRecordObject(item, itemId, collection.schema, "strict");
1227
+ if (problem) issues.push({
1228
+ file: itemId,
1229
+ problem
1230
+ });
1231
+ }
1232
+ return issues;
1233
+ }
1234
+ async function readRecordText(fullPath, name) {
1235
+ try {
1236
+ if (!(await lstat(fullPath)).isFile()) return {
1237
+ file: name,
1238
+ problem: "not a regular file (symlink?) — skipped, won't appear"
1239
+ };
1240
+ return { raw: await readFile(fullPath, "utf-8") };
1241
+ } catch {
1242
+ return {
1243
+ file: name,
1244
+ problem: "could not be read — skipped, won't appear"
1245
+ };
1246
+ }
1247
+ }
1248
+ /** Classify a single record file: unreadable / unparseable / non-object /
1249
+ * schema violation, or null when it's fine. */
1250
+ async function inspectRecord(fullPath, name, schema) {
1251
+ const read = await readRecordText(fullPath, name);
1252
+ if ("problem" in read) return read;
1253
+ let parsed;
1254
+ try {
1255
+ parsed = JSON.parse(read.raw);
1256
+ } catch (err) {
1257
+ return {
1258
+ file: name,
1259
+ problem: `invalid JSON (${err instanceof Error ? err.message : String(err)}) — SKIPPED, won't appear. Usual cause: an unescaped " inside a string value; use 「」/『』 or write \\" instead.`
1260
+ };
1261
+ }
1262
+ if (!isRecord(parsed)) return {
1263
+ file: name,
1264
+ problem: "not a JSON object — skipped, won't appear"
1265
+ };
1266
+ const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
1267
+ return problem ? {
1268
+ file: name,
1269
+ problem
1270
+ } : null;
1271
+ }
1272
+ /** What a non-string primary key actually is, for the error message. Names the
1273
+ * shape rather than stringifying the value — "[object Object]" tells the reader
1274
+ * nothing about what is wrong. */
1275
+ function describeIdType(value) {
1276
+ if (value === null) return "null";
1277
+ if (value === void 0) return "missing";
1278
+ if (Array.isArray(value)) return "an array";
1279
+ return `a ${typeof value}`;
1280
+ }
1281
+ /** First schema problem on an in-memory record (primaryKey↔id mismatch,
1282
+ * then the compiled per-field checks — see `../core/recordZ` for the two
1283
+ * tiers), or null when it's fine. One issue per record keeps the report
1284
+ * short and the fix obvious. Pure + exported so write paths
1285
+ * (manageCollection putItems) can gate on the SAME enforced rules the
1286
+ * post-hoc file scan reports — `itemId` is the id the record is (or
1287
+ * would be) stored under. The default `"enforced"` tier keeps every
1288
+ * write gate on the historical three checks; only pass `"strict"` from
1289
+ * report-only surfaces. */
1290
+ function validateRecordObject(record, itemId, schema, tier = "enforced") {
1291
+ const idValue = record[schema.primaryKey];
1292
+ if (typeof idValue !== "string") return `'${schema.primaryKey}' must be a string, but is ${describeIdType(idValue)} — must equal the filename ('${itemId}'), or the record can't be opened`;
1293
+ if (idValue !== itemId) return `'${schema.primaryKey}' is '${idValue}' but must equal the filename ('${itemId}'), or the record can't be opened`;
1294
+ return firstRecordProblem(record, schema, tier);
1295
+ }
1296
+ //#endregion
1415
1297
  //#region src/collection/server/mutate.ts
1416
1298
  /** First problem with the submitted params, or null. Every declared param
1417
1299
  * is checked by the shared record-field validator; keys the action never
@@ -2651,32 +2533,6 @@ async function handleGetOntology(deps) {
2651
2533
  collections
2652
2534
  });
2653
2535
  }
2654
- /** Publish this repository's `app.json` + shared schemas to its Firestore app.
2655
- *
2656
- * Named as a whole-app action because it IS one: publish takes the repository
2657
- * as its unit (one roster, one public config, every shared collection), so it
2658
- * carries no `slug` and refusing one is not a limitation to work around.
2659
- *
2660
- * The reply is prose rather than a status code on purpose. This is the one
2661
- * operation in the collection surface that changes what every member sees the
2662
- * moment it lands, and the two things the caller has to relay — what will
2663
- * break, and that `confirm` is how it proceeds anyway — are sentences, not
2664
- * fields. */
2665
- async function handlePublishApp(deps, confirm) {
2666
- const result = await publishApp({
2667
- ...deps,
2668
- confirm
2669
- });
2670
- if (!result.ok) {
2671
- const bullets = result.problems.map((problem) => `- ${problem}`).join("\n");
2672
- return `${result.partial ? "publish FAILED PART-WAY — some documents are already live:" : "publish refused — nothing was written:"}\n${bullets}`;
2673
- }
2674
- const dirtyNote = result.dirty ? " (WORKING TREE DIRTY — the commit does not describe what was published)" : "";
2675
- const stamp = result.commit ? `commit ${result.commit.slice(0, 12)}${dirtyNote}` : "no commit (not a git repository, or no HEAD)";
2676
- const brokenCount = result.recordIssuesCapped ? `at least ${result.recordIssues}` : `${result.recordIssues}`;
2677
- const forced = result.recordIssues > 0 ? ` Published over ${brokenCount} record(s) that do not satisfy the new schema — repair them now; members are reading them.` : "";
2678
- return `${result.created ? "Created" : "Updated"} app '${result.aid}' and published ${result.cids.length} collection(s): ${result.cids.join(", ")}. Signed ${stamp}. The previous app document is kept in \`previousPublished\` for rollback.${forced}`;
2679
- }
2680
2536
  /** Return the collection-authoring reference (`collection-skills.md`),
2681
2537
  * rendered by `renderSchemaDocs` — the full doc overflows the agent's
2682
2538
  * per-result limit, so the default reply is the core guide + a table of
@@ -2785,7 +2641,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
2785
2641
  written: true
2786
2642
  });
2787
2643
  }
2788
- var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. `publishApp` publishes the whole repository — its `app.json` (member roster, public read/submit configuration) and every shared collection's schema — to the app's Firestore. It is the ONE operation here that changes what every member sees the moment it runs, and it cannot be undone by reverting a commit: publishing again is the only undo (the previous app document is kept as `previousPublished`). Call it when the user asks to publish, invite, or open an app; never as a follow-up to an edit the user did not ask you to ship. Its refusals are the product, not an obstacle — relay them verbatim, and do not work around one by rewriting `app.json` until the user has said what they actually want. If it reports live records the new schemas would break, show the user that list and ask before re-running with `confirm`. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
2644
+ var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
2789
2645
  /** Validate getItems' optional `ids`/`fields` args, then delegate. */
2790
2646
  async function dispatchGetItems(collection, args, deps) {
2791
2647
  const ids = optionalStringArray(args.ids, "ids");
@@ -2830,19 +2686,18 @@ async function dispatchManageCollection(deps, args) {
2830
2686
  const action = typeof args.action === "string" ? args.action : "";
2831
2687
  if (action === "schemaDocs") return handleSchemaDocs(deps, typeof args.topic === "string" ? args.topic : void 0);
2832
2688
  if (action === "getOntology") return handleGetOntology(deps);
2833
- if (action === "publishApp") return handlePublishApp(deps, args.confirm === true);
2834
2689
  const slug = typeof args.slug === "string" ? args.slug.trim() : "";
2835
2690
  if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
2836
2691
  if (action === "getSchema") return handleGetSchema(slug, deps);
2837
2692
  if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
2838
- if (!RECORD_ACTIONS.has(action)) return "manageCollection: `action` must be \"getItems\", \"putItems\", \"deleteItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", \"putSchema\", or \"publishApp\".";
2693
+ if (!RECORD_ACTIONS.has(action)) return "manageCollection: `action` must be \"getItems\", \"putItems\", \"deleteItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
2839
2694
  const collection = await loadCollection(slug, deps);
2840
2695
  if (!collection) return unknownCollection(slug);
2841
2696
  return dispatchRecordAction(action, collection, args, deps);
2842
2697
  }
2843
2698
  var MANAGE_COLLECTION_DEFINITION = {
2844
2699
  name: "manageCollection",
2845
- description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing; deleteItems removes records by id. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference — the core guide plus a table of contents by default; pass `topic` for a specific section. getSchema/putSchema read and validate-then-write the collection's schema.json. publishApp publishes the repository's app.json + shared schemas to Firestore, where every member sees them immediately -- it refuses declarations that would be silently permissive or silently deny everyone, and refuses to publish over records the new schemas would break unless `confirm` is set. Prefer it over raw file I/O on collections.",
2700
+ description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing; deleteItems removes records by id. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference — the core guide plus a table of contents by default; pass `topic` for a specific section. getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
2846
2701
  inputSchema: {
2847
2702
  type: "object",
2848
2703
  properties: {
@@ -2856,8 +2711,7 @@ var MANAGE_COLLECTION_DEFINITION = {
2856
2711
  "getOntology",
2857
2712
  "schemaDocs",
2858
2713
  "getSchema",
2859
- "putSchema",
2860
- "publishApp"
2714
+ "putSchema"
2861
2715
  ],
2862
2716
  description: "What to do."
2863
2717
  },
@@ -2893,10 +2747,6 @@ var MANAGE_COLLECTION_DEFINITION = {
2893
2747
  type: "object",
2894
2748
  description: "putSchema: the full collection schema object (same shape as schema.json — title, icon, dataPath, primaryKey, fields, …). Call getSchema first for the current one, and schemaDocs for the field DSL."
2895
2749
  },
2896
- confirm: {
2897
- type: "boolean",
2898
- description: "publishApp: publish even though existing records fail the schemas being published. Omit it first — the refusal lists what would break, and that list is the thing to show the user before asking."
2899
- },
2900
2750
  topic: {
2901
2751
  type: "string",
2902
2752
  description: "schemaDocs: fetch one section of the reference by heading (case-insensitive substring — e.g. \"field types\", \"kanban\", \"calendar\", \"dataSource\"). Omit for the core authoring guide plus a table of contents of every section; \"all\" returns the full document (large — it can exceed your tool-result limit)."
@@ -2918,6 +2768,6 @@ function makeManageCollectionTool(deps = {}) {
2918
2768
  };
2919
2769
  }
2920
2770
  //#endregion
2921
- export { readSkillTemplate as A, APPS_COLLECTION as B, enrichItems as C, promptPathsFor as D, buildCollectionActionSeedPrompt as E, validateRecordObject as F, APP_ROLES as G, appConfigPath as H, compileRecordZ as I, AuthoredAppZ as K, recordFieldProblem as L, MAX_RECORD_ISSUES as M, STORE_UNREADABLE as N, readCustomViewHtml as O, validateCollectionRecords as P, bindsSubmitterIdentity as R, runCollectionQuery as S, buildActionSeedPrompt as T, appSchemasPath as U, PUBLIC_CONFIG_DOC as V, projectApp as W, computeCollectionIcon as _, deleteCollection as a, applyMutateAction as b, computeSuccessor as c, isTriggerDue as d, maybeSpawnSuccessor as f, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, publishApp as j, readCustomViewI18n as k, daysInMonth as l, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, deleteCollectionRefusalMessage as o, parseCivil as p, parseAuthoredApp as q, makeManageCollectionTool as r, advanceTriggerDate as s, MAX_SCHEMA_ISSUES as t, formatCivil as u, buildWorkspaceOntology as v, runQueryOverRows as w, firstMutateParamProblem as x, schemaRelations as y, publishProblems as z };
2771
+ export { parseAuthoredApp as $, runQueryOverRows as A, APP_SLUGS_COLLECTION as B, STORE_UNREADABLE as C, recordFieldProblem as D, compileRecordZ as E, readCustomViewI18n as F, appStagingPath as G, appConfigPath as H, readSkillTemplate as I, projectPublish as J, projectApp as K, bindsSubmitterIdentity as L, buildCollectionActionSeedPrompt as M, promptPathsFor as N, runCollectionQuery as O, readCustomViewHtml as P, AuthoredAppZ as Q, publishProblems as R, MAX_RECORD_ISSUES as S, validateRecordObject as T, appSchemasPath as U, PUBLIC_CONFIG_DOC as V, appSlugDoc as W, stagedRuleConfig as X, promoteSchema as Y, APP_ROLES as Z, computeCollectionIcon as _, deleteCollection as a, applyMutateAction as b, computeSuccessor as c, isTriggerDue as d, maybeSpawnSuccessor as f, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, buildActionSeedPrompt as j, enrichItems as k, daysInMonth as l, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, deleteCollectionRefusalMessage as o, parseCivil as p, projectDeploy as q, makeManageCollectionTool as r, advanceTriggerDate as s, MAX_SCHEMA_ISSUES as t, formatCivil as u, buildWorkspaceOntology as v, validateCollectionRecords as w, firstMutateParamProblem as x, schemaRelations as y, APPS_COLLECTION as z };
2922
2772
 
2923
- //# sourceMappingURL=server-B48Jyxcj.js.map
2773
+ //# sourceMappingURL=server-zcM1ViCE.js.map