@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
@@ -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_store = require("./store-5_P_NsGa.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,13 +12,11 @@ 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
- let node_child_process = require("node:child_process");
16
- let node_util = require("node:util");
17
15
  //#region src/collection/server/publishManifest.ts
18
16
  /** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
19
17
  * that `sharedCollectionKey` applies. Stated once so a path built later
20
18
  * cannot be a way around it. */
21
- var NameZ = zod.z.string().refine(require_calendarGrid.isValidCollectionName, { message: "is not a valid id (letters, digits, '-' and '_' only)" });
19
+ var NameZ = zod.z.string().refine(require_itemId.isValidCollectionName, { message: "is not a valid id (letters, digits, '-' and '_' only)" });
22
20
  /** An address on the roster. Not validated as an email beyond "has an @":
23
21
  * the rules compare it to `request.auth.token.email` verbatim, so any
24
22
  * narrowing here would refuse addresses Firebase itself accepts. */
@@ -126,6 +124,34 @@ var PublicZ = zod.z.object({
126
124
  read: zod.z.array(NameZ).optional(),
127
125
  submit: zod.z.record(NameZ, SubmitZ).optional()
128
126
  }).strict();
127
+ /** The URL name an app is handed out under: `https://<host>/{slug}`.
128
+ *
129
+ * A SEPARATE name from the `aid`, and that separation is the point (design
130
+ * D2b). `apps/{aid}` is a shelf every user of the deployment shares and the
131
+ * rules' `allow create` asks only that you name yourself owner — so a
132
+ * memorable aid is first-come-first-served, cannot be checked for
133
+ * availability, and frees up again when an app is deleted. The aid is
134
+ * therefore a UUID, and the thing people can fight over is moved to the name
135
+ * that costs nothing to change.
136
+ *
137
+ * Declared here rather than kept beside `app.json` because a RESERVATION has
138
+ * to travel with the repository: `appSlugs/{slug}` is unreadable until the app
139
+ * is published (`allow read: if resource.data.published == true`), so nothing
140
+ * can recover which slug an app holds by asking Firestore. A second file to
141
+ * keep in step with the declaration is the alternative, and it is the kind of
142
+ * pair that goes out of step silently.
143
+ *
144
+ * The shape is stricter than `NameZ` on purpose: it is BOTH a URL path
145
+ * segment people read aloud and a Firestore document id. Lowercase
146
+ * alphanumerics separated by single hyphens covers both without a case rule
147
+ * that would make two slugs collide in one place and not the other.
148
+ *
149
+ * Which slug an app ended up with is the HOST's business — a wanted slug can
150
+ * be taken, and the host writes the one it reserved back here. Nothing in this
151
+ * package reads the key; it is declared so that writing it back does not make
152
+ * the file unparseable. */
153
+ var SLUG_SHAPE = "must be lowercase letters, digits and single hyphens, and must not start or end with one (e.g. sakura-hair)";
154
+ var SlugZ = zod.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);
129
155
  /** The whole authored declaration.
130
156
  *
131
157
  * `owner` is accepted but is NOT the published value — publish stamps the
@@ -137,6 +163,8 @@ var PublicZ = zod.z.object({
137
163
  var AuthoredAppZ = zod.z.object({
138
164
  aid: NameZ,
139
165
  name: zod.z.string().trim().min(1).optional(),
166
+ /** The wanted (or reserved) URL name — see {@link SlugZ}. */
167
+ slug: SlugZ.optional(),
140
168
  /** Per-worktree app id (design D6, implementation order 7). Accepted so a
141
169
  * repository already carrying it parses; nothing reads it yet. */
142
170
  aidEnv: zod.z.string().trim().min(1).optional(),
@@ -154,7 +182,7 @@ var AuthoredAppZ = zod.z.object({
154
182
  * once — publish is a manual step, and a parser that stops at the first key
155
183
  * makes it N round trips. */
156
184
  function parseAuthoredApp(raw) {
157
- const manifest = require_store.parseAppManifest(raw);
185
+ const manifest = require_discovery.parseAppManifest(raw);
158
186
  if (!manifest.ok) return {
159
187
  ok: false,
160
188
  problems: [manifest.kind === "missing" ? "app.json is missing" : manifest.detail]
@@ -265,6 +293,7 @@ function projectApp(authored, schemas, stamp, existing) {
265
293
  publishedAt: stamp.publishedAt,
266
294
  publishedBy: stamp.email,
267
295
  publishedCommit: stamp.commit,
296
+ publishedDirty: stamp.dirty === true ? true : void 0,
268
297
  previousPublished: previousOf(existing)
269
298
  });
270
299
  const config = {
@@ -283,6 +312,102 @@ function projectApp(authored, schemas, stamp, existing) {
283
312
  config
284
313
  };
285
314
  }
315
+ /** Keys on the app document that publish owns: what is PUBLIC right now, and
316
+ * the rule-facing configuration anonymous access is judged against. Deploy
317
+ * carries them through from the existing document and never authors them. */
318
+ var PUBLISH_OWNED_KEYS = [
319
+ "public",
320
+ "collections",
321
+ "participantRead",
322
+ "publishedAt",
323
+ "publishedBy",
324
+ "publishedCommit",
325
+ "publishedDirty",
326
+ "previousPublished"
327
+ ];
328
+ var isPublishOwned = (key) => PUBLISH_OWNED_KEYS.includes(key);
329
+ function projectDeploy(authored, schemas, stamp, existing) {
330
+ const { app } = projectApp(authored, schemas, stamp, existing);
331
+ const deployed = Object.fromEntries(Object.entries(app).filter(([key]) => !isPublishOwned(key)));
332
+ for (const key of PUBLISH_OWNED_KEYS) {
333
+ const live = existing?.[key];
334
+ if (live !== void 0) deployed[key] = live;
335
+ }
336
+ deployed.deployedAt = stamp.publishedAt;
337
+ deployed.deployedBy = stamp.email;
338
+ if (stamp.commit !== void 0) deployed.deployedCommit = stamp.commit;
339
+ return {
340
+ app: deployed,
341
+ staging: schemas.map(({ cid, schema }) => ({
342
+ cid,
343
+ doc: stagedDoc(schema, stamp, authored, cid)
344
+ }))
345
+ };
346
+ }
347
+ /** One staged schema document, carrying this cid's rule-facing configuration
348
+ * alongside the schema so publish can promote them together. */
349
+ function stagedDoc(schema, stamp, authored, cid) {
350
+ const doc = {
351
+ publishedSchema: schema,
352
+ deployedAt: stamp.publishedAt,
353
+ deployedBy: stamp.email
354
+ };
355
+ const config = authored.collections?.[cid];
356
+ if (config !== void 0) doc.config = config;
357
+ if (authored.participantRead?.includes(cid) === true) doc.participantRead = true;
358
+ if (stamp.commit !== void 0) doc.deployedCommit = stamp.commit;
359
+ return doc;
360
+ }
361
+ /** The rule-facing configuration to promote, read from the STAGED documents
362
+ * rather than from the manifest as it reads right now.
363
+ *
364
+ * Otherwise: deploy revision A, edit `app.json` to revision B, publish — and
365
+ * the promoted schema is A's while the authorization behaviour is B's, a
366
+ * combination nobody exercised through `/staging/{aid}`.
367
+ *
368
+ * (`public` is deliberately NOT part of this: it is not staged, because it is
369
+ * the decision being made AT publish rather than something under test.) */
370
+ function stagedRuleConfig(staged) {
371
+ const entries = [];
372
+ const participantRead = [];
373
+ for (const entry of staged) {
374
+ const { config } = entry.doc;
375
+ if (config !== void 0) entries.push([entry.cid, config]);
376
+ if (entry.doc.participantRead === true) participantRead.push(entry.cid);
377
+ }
378
+ return {
379
+ collections: entries.length > 0 ? Object.fromEntries(entries) : void 0,
380
+ participantRead: participantRead.length > 0 ? participantRead : void 0
381
+ };
382
+ }
383
+ function projectPublish(authored, staged, stamp, existing) {
384
+ const { app, config } = projectApp(authored, [], stamp, existing);
385
+ const staging = stagedRuleConfig(staged);
386
+ app.collections = staging.collections;
387
+ app.participantRead = staging.participantRead;
388
+ const published = Object.fromEntries(Object.entries(existing ?? app).filter(([key]) => !isPublishOwned(key)));
389
+ for (const key of PUBLISH_OWNED_KEYS) if (key !== "public" && app[key] !== void 0) published[key] = app[key];
390
+ const publicBlock = app.public;
391
+ return {
392
+ app: published,
393
+ config,
394
+ public: require_dist.isRecord(publicBlock) ? publicBlock : void 0
395
+ };
396
+ }
397
+ /** Re-stamp a staged schema document as it is promoted to `collections/{cid}`.
398
+ *
399
+ * The stamp answers "which version is PUBLIC right now, and who made it so",
400
+ * so it is written by the operation that changes the answer — publish — not
401
+ * carried over from the deploy that staged it. */
402
+ function promoteSchema(staged, stamp) {
403
+ const doc = {
404
+ publishedSchema: staged.publishedSchema,
405
+ publishedAt: stamp.publishedAt,
406
+ publishedBy: stamp.email
407
+ };
408
+ if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
409
+ return doc;
410
+ }
286
411
  /** One published schema document. Written key by key rather than through
287
412
  * `compact`, so the declared type is the type — an optional commit is the
288
413
  * only variable part. */
@@ -298,8 +423,31 @@ function schemaDoc(schema, stamp) {
298
423
  /** The app documents' parent path — the `FirestoreDocs` seam takes a
299
424
  * collection path plus a document id, and the app document's id is the aid. */
300
425
  var APPS_COLLECTION = "apps";
301
- /** The collection (schema) documents' parent path. */
426
+ /** The collection (schema) documents' parent path — what the PUBLIC page
427
+ * reads, written only by publish (promotion). */
302
428
  var appSchemasPath = (aid) => `apps/${aid}/collections`;
429
+ /** The URL-slug reservations — `appSlugs/{slug}` → `{ aid, published }`.
430
+ *
431
+ * A TOP-LEVEL collection, not a field on the app: the public page resolves a
432
+ * slug to an aid BEFORE it can read anything under `apps/{aid}`, and a slug
433
+ * has to be claimable atomically (create-if-absent) so two apps cannot hold
434
+ * the same URL.
435
+ *
436
+ * `published` is what makes the reservation invisible until publish. The slug
437
+ * is human-readable, so a readable reservation would let anyone guess the URL
438
+ * and get the aid — and the aid is the `/staging/{aid}` entrance. The rule is
439
+ * `allow read: if resource.data.published == true`, which needs no `get()` and
440
+ * so costs nothing against the rules' expression budget. */
441
+ var APP_SLUGS_COLLECTION = "appSlugs";
442
+ var appSlugDoc = (aid, published) => ({
443
+ aid,
444
+ published
445
+ });
446
+ /** The staged schema documents' parent path — what `/staging/{aid}` reads,
447
+ * written by deploy. A separate DOCUMENT rather than a field beside
448
+ * `publishedSchema`, because the rules cannot hide a field: anything inside a
449
+ * document the public page may read is public. */
450
+ var appStagingPath = (aid) => `apps/${aid}/staging`;
303
451
  /** The public-config documents' parent path. */
304
452
  var appConfigPath = (aid) => `apps/${aid}/config`;
305
453
  //#endregion
@@ -525,532 +673,34 @@ function publishProblems(app, collections, publisherEmail) {
525
673
  ...primaryKeyProblems(app, collections)
526
674
  ];
527
675
  }
528
- /** A public submission must be able to produce a record the HOST can read.
676
+ /** A public submission must NOT be allowed to name its own primary key.
677
+ *
678
+ * The rules constrain the DOCUMENT ID (`idFrom`) and cannot constrain the
679
+ * value of a field — nothing compares `request.resource.data[primaryKey]`
680
+ * with the path being written. So a submit path that accepts the primary key
681
+ * as a `createField` lets a submitter write at their one permitted document
682
+ * id while CLAIMING another record's identity, or a duplicate.
529
683
  *
530
- * The rules and the engine disagree about what identifies a record, and the
531
- * gap is invisible from either side alone. The rules bind the DOCUMENT ID
532
- * (`idFrom`) and let a submission carry only `createFields`; the engine
533
- * identifies a record by its schema's `primaryKey` FIELD, and the firestore
534
- * store hands back the document's fields verbatim — `toItem` does not
535
- * synthesize the key from the document id. So a submit path whose
536
- * `createFields` omits the primary key writes rows that Firestore accepts and
537
- * every reader rejects: `validateRecordObject` fails them, the collection
538
- * renders empty-ish, and the next publish's own pre-check reports them as
539
- * broken records the publisher never wrote.
684
+ * It is refused rather than tolerated because there is nothing for the field
685
+ * to do: `firestoreStore` takes a shared record's identity from the document
686
+ * id and overwrites the field on read, so a submitted value is either equal
687
+ * to the id (noise) or a lie (silently discarded). Publishing a form field
688
+ * whose value is thrown away is worse than not having it — the author will
689
+ * believe submitters choose their ids.
540
690
  *
541
- * Not checkable by the rules (they have never heard of a schema) and not
542
- * catchable at write time (nothing is wrong with the write). Publish is the
543
- * only place that holds both halves. */
691
+ * This is the second answer to the same question. The first was the reverse —
692
+ * REQUIRE the key, because a record without one was rejected by every reader
693
+ * — and it was right about the symptom and wrong about the cure: the identity
694
+ * belongs to the id the rules can pin, not to a field they cannot. */
544
695
  function primaryKeyProblems(app, collections) {
545
696
  const primaryKeyOf = new Map(collections.map((collection) => [collection.cid, collection.primaryKey]));
546
697
  return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
547
698
  const primaryKey = primaryKeyOf.get(cid);
548
- if (primaryKey === void 0 || submit.createFields.includes(primaryKey)) return [];
549
- 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.`];
699
+ if (primaryKey === void 0 || !submit.createFields.includes(primaryKey)) return [];
700
+ 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.`];
550
701
  });
551
702
  }
552
703
  //#endregion
553
- //#region src/collection/core/recordZ.ts
554
- /** The emptiness rule shared by `required` and the "only check present
555
- * values" gate. NOT a truthiness check — `0` and `false` are filled. */
556
- var isEmptyValue = (value) => value === void 0 || value === null || value === "";
557
- /** The historical write-gate checks, verbatim: required non-empty, enum
558
- * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
559
- function enforcedProblem(key, spec, value) {
560
- const empty = isEmptyValue(value);
561
- if (spec.required && empty) return `missing required field '${key}'`;
562
- if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
563
- return null;
564
- }
565
- /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
566
- * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
567
- * the lint flags exactly the values the calendar / trigger / spawn code
568
- * would silently drop — impossible days like `2026-02-30`, and datetimes
569
- * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
570
- * which the day view can't place). `string`-backed types accept anything
571
- * stringifiable; `ref` existence is out of scope. */
572
- function strictTypeProblem(key, spec, value) {
573
- switch (spec.type) {
574
- case "number":
575
- case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
576
- case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
577
- case "date": return require_calendarGrid.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
578
- 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)`;
579
- default: return null;
580
- }
581
- }
582
- /** Strict check for a PRESENT `table` value: an array of row objects, each
583
- * row conforming to the sub-schema (required / enum / typed sub-values).
584
- * First row problem wins, prefixed with the row number so the fix is
585
- * locatable. */
586
- function strictTableProblem(key, spec, value) {
587
- if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
588
- for (let index = 0; index < value.length; index++) {
589
- const row = value[index];
590
- if (!require_dist.isRecord(row)) return `'${key}' row ${index + 1} is not an object`;
591
- for (const [subKey, subSpec] of Object.entries(spec.of)) {
592
- const subValue = row[subKey];
593
- const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
594
- if (problem) return `'${key}' row ${index + 1}: ${problem}`;
595
- }
596
- }
597
- return null;
598
- }
599
- /** First problem for one field's stored value under `tier`, or null.
600
- * Enforced checks always run (and their messages never vary by tier — the
601
- * scan and the write gate must agree on them); strict adds the per-type
602
- * layer on present values only. */
603
- function recordFieldProblem(key, spec, value, tier) {
604
- const enforced = enforcedProblem(key, spec, value);
605
- if (enforced || tier === "enforced") return enforced;
606
- if (isEmptyValue(value)) return null;
607
- if (spec.type === "table") return strictTableProblem(key, spec, value);
608
- return strictTypeProblem(key, spec, value);
609
- }
610
- var compiled = /* @__PURE__ */ new WeakMap();
611
- /** Compile `schema.fields` into a zod validator for a stored record.
612
- * Loose object: unknown keys are allowed and any declared key may be
613
- * absent (records are user files, not parse-and-rewrite targets —
614
- * callers validate, they never persist the parse output). The checks run
615
- * as ONE object-level refine iterating fields in declaration order —
616
- * per-key shape schemas can't express "key may be absent BUT its absence
617
- * must still reach the required check", and the single loop keeps the
618
- * first reported issue identical to the historical first-problem-wins
619
- * contract. */
620
- function compileRecordZ(schema, tier) {
621
- const cached = compiled.get(schema)?.[tier];
622
- if (cached) return cached;
623
- const stored = Object.entries(schema.fields).filter(([, spec]) => !require_calendarGrid.COMPUTED_TYPES.has(spec.type));
624
- const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
625
- for (const [key, spec] of stored) {
626
- const problem = recordFieldProblem(key, spec, record[key], tier);
627
- if (problem) ctx.addIssue({
628
- code: "custom",
629
- message: problem,
630
- path: [key]
631
- });
632
- }
633
- });
634
- const entry = compiled.get(schema) ?? {};
635
- entry[tier] = validator;
636
- compiled.set(schema, entry);
637
- return validator;
638
- }
639
- /** First schema problem on an in-memory record under `tier`, or null. One
640
- * issue per record keeps the report short and the fix obvious (the
641
- * historical contract of `validateRecordObject`). */
642
- function firstRecordProblem(record, schema, tier) {
643
- const result = compileRecordZ(schema, tier).safeParse(record);
644
- if (result.success) return null;
645
- return result.error.issues[0]?.message ?? "record failed schema validation";
646
- }
647
- //#endregion
648
- //#region src/collection/server/validate.ts
649
- /** Don't flood the result; the first batch is enough to act on. Exported
650
- * because a caller that REPORTS a count has to know the count is a floor —
651
- * `publish` presents a full batch as "at least N" rather than as a total. */
652
- var MAX_RECORD_ISSUES = 25;
653
- /** The `file` of the pseudo-issue reported when the backend could not be read
654
- * at all.
655
- *
656
- * Exported because it is a DIFFERENT KIND of answer from "this record is
657
- * invalid", and a caller that treats the two alike gets it wrong in the
658
- * direction that matters: `publish` lets the user override invalid records,
659
- * and overriding this one would mean publishing without ever having looked. */
660
- var STORE_UNREADABLE = "(store)";
661
- var MAX_ISSUES = 25;
662
- /** Read every `<id>.json` under the collection's dataDir and report the
663
- * ones that won't load or violate the schema. An empty list means every
664
- * record is fine. */
665
- /** List entries under the data dir, guarding realpath containment (against a
666
- * symlinked dir swapped in after discovery, like `listItems`) and treating a
667
- * missing dir as empty while surfacing real I/O faults. */
668
- async function listRecordFilenames(dataDir, workspaceRoot) {
669
- if (!require_store.isContainedInRoot(dataDir, workspaceRoot)) {
670
- require_store.log.warn("collections", "validate refused: dataDir escapes workspace via symlink", { dataDir });
671
- return [];
672
- }
673
- try {
674
- return await (0, node_fs_promises.readdir)(dataDir);
675
- } catch (err) {
676
- if (require_dist.isErrorWithCode(err) && err.code === "ENOENT") return [];
677
- throw err;
678
- }
679
- }
680
- async function validateCollectionRecords(collection, opts = {}) {
681
- if (collection.schema.dataSource !== void 0) return [];
682
- if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
683
- const workspaceRoot = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
684
- const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
685
- const issues = [];
686
- for (const name of entries.sort()) {
687
- if (!name.endsWith(".json") || name.startsWith(".")) continue;
688
- if (issues.length >= MAX_ISSUES) break;
689
- const issue = await inspectRecord(node_path.default.join(collection.dataDir, name), name, collection.schema);
690
- if (issue) issues.push(issue);
691
- }
692
- return issues;
693
- }
694
- /** Store-backed twin of the file scan: list every record through the
695
- * collection's store and lint it with the same "strict" report-only tier.
696
- * A row the store can't even parse is invisible here (the store skips
697
- * it), so the read/parse classifications of the file scan don't apply —
698
- * schema violations are what this catches. `file` carries the record id
699
- * (there is no per-record filename). */
700
- async function validateStoreRecords(collection, opts) {
701
- let items;
702
- try {
703
- items = await require_store.storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
704
- } catch (err) {
705
- return [{
706
- file: STORE_UNREADABLE,
707
- problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
708
- }];
709
- }
710
- const issues = [];
711
- for (const item of items) {
712
- if (issues.length >= MAX_ISSUES) break;
713
- const itemId = require_calendarGrid.fieldText(item[collection.schema.primaryKey]);
714
- const problem = validateRecordObject(item, itemId, collection.schema, "strict");
715
- if (problem) issues.push({
716
- file: itemId,
717
- problem
718
- });
719
- }
720
- return issues;
721
- }
722
- async function readRecordText(fullPath, name) {
723
- try {
724
- if (!(await (0, node_fs_promises.lstat)(fullPath)).isFile()) return {
725
- file: name,
726
- problem: "not a regular file (symlink?) — skipped, won't appear"
727
- };
728
- return { raw: await (0, node_fs_promises.readFile)(fullPath, "utf-8") };
729
- } catch {
730
- return {
731
- file: name,
732
- problem: "could not be read — skipped, won't appear"
733
- };
734
- }
735
- }
736
- /** Classify a single record file: unreadable / unparseable / non-object /
737
- * schema violation, or null when it's fine. */
738
- async function inspectRecord(fullPath, name, schema) {
739
- const read = await readRecordText(fullPath, name);
740
- if ("problem" in read) return read;
741
- let parsed;
742
- try {
743
- parsed = JSON.parse(read.raw);
744
- } catch (err) {
745
- return {
746
- file: name,
747
- 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.`
748
- };
749
- }
750
- if (!require_dist.isRecord(parsed)) return {
751
- file: name,
752
- problem: "not a JSON object — skipped, won't appear"
753
- };
754
- const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
755
- return problem ? {
756
- file: name,
757
- problem
758
- } : null;
759
- }
760
- /** What a non-string primary key actually is, for the error message. Names the
761
- * shape rather than stringifying the value — "[object Object]" tells the reader
762
- * nothing about what is wrong. */
763
- function describeIdType(value) {
764
- if (value === null) return "null";
765
- if (value === void 0) return "missing";
766
- if (Array.isArray(value)) return "an array";
767
- return `a ${typeof value}`;
768
- }
769
- /** First schema problem on an in-memory record (primaryKey↔id mismatch,
770
- * then the compiled per-field checks — see `../core/recordZ` for the two
771
- * tiers), or null when it's fine. One issue per record keeps the report
772
- * short and the fix obvious. Pure + exported so write paths
773
- * (manageCollection putItems) can gate on the SAME enforced rules the
774
- * post-hoc file scan reports — `itemId` is the id the record is (or
775
- * would be) stored under. The default `"enforced"` tier keeps every
776
- * write gate on the historical three checks; only pass `"strict"` from
777
- * report-only surfaces. */
778
- function validateRecordObject(record, itemId, schema, tier = "enforced") {
779
- const idValue = record[schema.primaryKey];
780
- 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`;
781
- if (idValue !== itemId) return `'${schema.primaryKey}' is '${idValue}' but must equal the filename ('${itemId}'), or the record can't be opened`;
782
- return firstRecordProblem(record, schema, tier);
783
- }
784
- //#endregion
785
- //#region src/collection/server/publish.ts
786
- var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
787
- /** How many broken records to name before summarising. A publish that would
788
- * break a thousand rows is answered by the count and a sample; dumping all of
789
- * them buries the number, which is the part the decision turns on. */
790
- var MAX_LISTED_ISSUES = 10;
791
- /** `git rev-parse HEAD` plus a dirty check, or nothing.
792
- *
793
- * A missing git, a repository with no commits and a non-repository are all
794
- * the same answer here — no commit — because the stamp is attribution, not a
795
- * requirement. What is NOT acceptable is a stamp that lies, which is why the
796
- * dirty flag exists: publishing from a modified tree records a commit that
797
- * does not describe what was published, and the flag is the only thing that
798
- * would ever tell a reader so. */
799
- async function gitStamp(root) {
800
- try {
801
- const { stdout } = await execFileAsync("git", [
802
- "-C",
803
- root,
804
- "rev-parse",
805
- "HEAD"
806
- ]);
807
- const commit = stdout.trim();
808
- const { stdout: status } = await execFileAsync("git", [
809
- "-C",
810
- root,
811
- "status",
812
- "--porcelain"
813
- ]);
814
- return {
815
- commit: commit.length > 0 ? commit : void 0,
816
- dirty: status.trim().length > 0
817
- };
818
- } catch {
819
- return {};
820
- }
821
- }
822
- /** The shared collections of THIS REPOSITORY, by cid.
823
- *
824
- * `userSkillsDir: null` — not a test convenience, a boundary. Discovery
825
- * resolves every schema it finds against the WORKSPACE root, user-scope
826
- * included, so a globally installed skill under `~/.claude/skills` carrying
827
- * `storage.type: "firestore"` picks up whichever repository's `aid` it
828
- * happens to be discovered from. Left in, publish would write that schema
829
- * into this app — and into every other app the same user publishes, since the
830
- * skill is installed once per machine and the repositories are not.
831
- *
832
- * An app is a REPOSITORY (design D1): its collections are the ones committed
833
- * beside its `app.json`, which is what makes a clone resolve the same
834
- * collections and an invitation a matter of authorization rather than
835
- * discovery. A schema that is not in the repository has no claim on a cid
836
- * there. And because a view is HTML, publishing one is not a tidiness
837
- * question: it is the machine's own skills reaching every member's browser.
838
- *
839
- * The consequence is deliberate: a cid named in `app.json` that exists only
840
- * in user scope is now an unknown cid, and publish says so by name instead of
841
- * quietly publishing a schema from outside the repository. */
842
- async function sharedCollections(opts, root) {
843
- return (await require_store.discoverCollections({
844
- ...opts,
845
- workspaceRoot: root,
846
- userSkillsDir: null
847
- })).filter((collection) => collection.appId !== void 0);
848
- }
849
- /** Existing records that would not satisfy the schema about to be published.
850
- *
851
- * Read from FIRESTORE, not from disk: a shared collection's records live in
852
- * the app, and the question this answers is "what does the live data look
853
- * like under the new schema" — which is the migration question. Reported as
854
- * a refusal the publisher can override, because a breaking change is
855
- * sometimes exactly what is intended and the point is that it is a decision
856
- * rather than a discovery. */
857
- async function recordProblems(collections, opts) {
858
- const lines = [];
859
- const unreadable = [];
860
- let records = 0;
861
- let cappedAnywhere = false;
862
- for (const collection of collections) {
863
- const issues = await validateCollectionRecords(collection, opts);
864
- if (issues.length === 0) continue;
865
- const unread = issues.filter((issue) => issue.file === STORE_UNREADABLE);
866
- if (unread.length > 0) {
867
- unreadable.push(`${collection.slug}: ${unread.map((issue) => issue.problem).join("; ")}`);
868
- continue;
869
- }
870
- records += issues.length;
871
- const capped = issues.length >= 25;
872
- cappedAnywhere = cappedAnywhere || capped;
873
- const count = capped ? `at least ${issues.length}` : String(issues.length);
874
- const plural = issues.length === 1 ? "" : "s";
875
- const note = capped ? " (the scan stops there)" : "";
876
- lines.push(`${collection.slug}: ${count} existing record${plural} would not satisfy the schema about to be published${note}`);
877
- for (const issue of issues.slice(0, MAX_LISTED_ISSUES)) lines.push(` - ${issue.file}: ${issue.problem}`);
878
- if (issues.length > MAX_LISTED_ISSUES) lines.push(` - … and ${issues.length - MAX_LISTED_ISSUES} more`);
879
- }
880
- return {
881
- lines,
882
- records,
883
- capped: cappedAnywhere,
884
- unreadable
885
- };
886
- }
887
- function schemasOf(collections) {
888
- return collections.map((collection) => ({
889
- cid: collection.slug,
890
- schema: collection.schema
891
- })).sort((left, right) => left.cid < right.cid ? -1 : left.cid > right.cid ? 1 : 0);
892
- }
893
- /** Read and parse `<root>/app.json`'s full declaration. */
894
- async function readAuthored(root) {
895
- let raw;
896
- try {
897
- raw = await (0, node_fs_promises.readFile)(node_path.default.join(root, require_store.APP_MANIFEST_FILE), "utf-8");
898
- } catch (err) {
899
- return {
900
- ok: false,
901
- problems: [`cannot read ${node_path.default.join(root, require_store.APP_MANIFEST_FILE)}: ${String(err)}`]
902
- };
903
- }
904
- return parseAuthoredApp(raw);
905
- }
906
- /** Everything wrong with the declaration itself, publisher included. */
907
- function declarationProblems(app, collections, handle) {
908
- const problems = publishProblems(app, collections.map((collection) => ({
909
- cid: collection.slug,
910
- primaryKey: collection.schema.primaryKey
911
- })), handle.email);
912
- 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.`);
913
- return problems;
914
- }
915
- /** Put the three kinds of document, in the order the rules require, and turn a
916
- * rejected write into the result type instead of letting it escape.
917
- *
918
- * Returns null when everything was written; a failure result otherwise.
919
- *
920
- * A raw rejection here would reach the agent as a tool crash rather than the
921
- * actionable text this tool promises. But "actionable" is a strong claim for
922
- * a half-finished publish, so the message ENUMERATES what landed rather than
923
- * summarising it: the order is app → every schema → config, and a summary
924
- * written for one failure point is wrong at the others. Saying "the roster
925
- * and configuration are live" after a SCHEMA write failed names a config
926
- * document this publish never wrote — which still holds whatever the last
927
- * publish left, and is exactly the state the caller is trying to repair. */
928
- async function writeDocuments(handle, aid, published) {
929
- const steps = [
930
- {
931
- what: `the app document (apps/${aid})`,
932
- run: () => handle.docs.set(APPS_COLLECTION, aid, published.app)
933
- },
934
- ...published.schemas.map(({ cid, doc }) => ({
935
- what: `the published schema for '${cid}'`,
936
- run: () => handle.docs.set(appSchemasPath(aid), cid, doc)
937
- })),
938
- {
939
- what: `the public config document (apps/${aid}/config/${PUBLIC_CONFIG_DOC})`,
940
- run: () => handle.docs.set(appConfigPath(aid), PUBLIC_CONFIG_DOC, published.config)
941
- }
942
- ];
943
- const landed = [];
944
- for (const [index, step] of steps.entries()) try {
945
- await step.run();
946
- landed.push(step.what);
947
- } catch (err) {
948
- const reason = err instanceof Error ? err.message : String(err);
949
- return {
950
- ok: false,
951
- partial: index > 0,
952
- problems: [`publish failed while writing ${step.what}: ${reason}`, ...partialState(landed, [step.what, ...steps.slice(index + 1).map((rest) => rest.what)])]
953
- };
954
- }
955
- return null;
956
- }
957
- /** What is live and what is not, listed rather than summarised.
958
- *
959
- * Two facts, and both matter for the repair: a document this publish wrote is
960
- * live NOW, and a document it did not write still holds what the LAST publish
961
- * left — which is not the same as being absent, and not the same as matching
962
- * the declaration that was just half-applied. */
963
- function partialState(landed, notWritten) {
964
- const repair = "Publishing again is the repair: the write is idempotent, and it re-does every step, including the ones that did land.";
965
- if (landed.length === 0) return [`Nothing was written. ${repair}`];
966
- return [
967
- `Written by this publish, and live now: ${landed.join("; ")}.`,
968
- `NOT written: ${notWritten.join("; ")} — ${notWritten.length === 1 ? "it still holds" : "they still hold"} whatever the previous publish left.`,
969
- repair
970
- ];
971
- }
972
- /** Publish this repository's declaration to its app.
973
- *
974
- * Everything variable is a parameter or comes from the host binding, so the
975
- * whole path is exercisable against an in-memory `FirestoreDocs` with no
976
- * network and no API key — which is the only way the conversion table gets
977
- * tested as a table. */
978
- async function publishApp(opts = {}) {
979
- const root = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
980
- const handle = require_store.firestoreHandle();
981
- if (!handle) return {
982
- ok: false,
983
- partial: false,
984
- 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."]
985
- };
986
- const authored = await readAuthored(root);
987
- if (!authored.ok) return {
988
- ...authored,
989
- partial: false
990
- };
991
- const collections = await sharedCollections(opts, root);
992
- const problems = declarationProblems(authored.app, collections, handle);
993
- if (problems.length > 0) return {
994
- ok: false,
995
- partial: false,
996
- problems
997
- };
998
- const issues = await recordProblems(collections, {
999
- ...opts,
1000
- workspaceRoot: root
1001
- });
1002
- if (issues.unreadable.length > 0) return {
1003
- ok: false,
1004
- partial: false,
1005
- 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."]
1006
- };
1007
- if (issues.records > 0 && opts.confirm !== true) return {
1008
- ok: false,
1009
- partial: false,
1010
- 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."]
1011
- };
1012
- return writePublished(authored.app, collections, handle, opts, root, issues);
1013
- }
1014
- /** The write half: stamp, project, and put the documents in the order the
1015
- * rules require. Split from the gate above so neither half hides the other —
1016
- * everything up to here can refuse, and nothing from here on does. */
1017
- async function writePublished(authored, collections, handle, opts, root, issues) {
1018
- const { aid } = authored;
1019
- let existing;
1020
- try {
1021
- existing = await handle.docs.get(APPS_COLLECTION, aid);
1022
- } catch (err) {
1023
- return {
1024
- ok: false,
1025
- partial: false,
1026
- 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."]
1027
- };
1028
- }
1029
- const stampSource = await (opts.resolveCommit ?? gitStamp)(root);
1030
- const stamp = {
1031
- uid: handle.uid,
1032
- email: handle.email,
1033
- publishedAt: (opts.now ?? Date.now)(),
1034
- commit: stampSource.commit
1035
- };
1036
- const existingApp = require_dist.isRecord(existing) ? existing : null;
1037
- const published = projectApp(authored, schemasOf(collections), stamp, existingApp);
1038
- if (stampSource.dirty === true) published.app.publishedDirty = true;
1039
- const written = await writeDocuments(handle, aid, published);
1040
- if (written !== null) return written;
1041
- return {
1042
- ok: true,
1043
- aid,
1044
- cids: published.schemas.map((entry) => entry.cid),
1045
- created: existingApp === null,
1046
- commit: stamp.commit,
1047
- dirty: stampSource.dirty === true,
1048
- recordIssues: issues.records,
1049
- recordIssuesCapped: issues.capped,
1050
- published
1051
- };
1052
- }
1053
- //#endregion
1054
704
  //#region src/collection/server/skillAssets.ts
1055
705
  /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
1056
706
  * schema-validated `views/*.html` path, resolved with realpath containment.
@@ -1075,13 +725,13 @@ async function readCustomViewHtml(collection, viewFile, opts = {}) {
1075
725
  * `readCustomViewHtml`. Used by both `readCustomViewHtml` and
1076
726
  * `readCustomViewI18n` so the two stay in lockstep. */
1077
727
  async function readSourceAwareFile(collection, relPath, opts) {
1078
- const safeSlug = require_store.safeSlugName(collection.slug);
728
+ const safeSlug = require_discovery.safeSlugName(collection.slug);
1079
729
  if (safeSlug === null) return null;
1080
- const workspaceRoot = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
1081
- const staging = collection.source === "project" ? require_store.stagingSkillDir(workspaceRoot, safeSlug) : null;
730
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
731
+ const staging = collection.source === "project" ? require_discovery.stagingSkillDir(workspaceRoot, safeSlug) : null;
1082
732
  const bases = staging === null ? [collection.skillDir] : [staging, collection.skillDir];
1083
733
  for (const base of bases) {
1084
- const resolved = require_store.resolveTemplatePath(base, relPath);
734
+ const resolved = require_discovery.resolveTemplatePath(base, relPath);
1085
735
  if (resolved === null) continue;
1086
736
  try {
1087
737
  return await (0, node_fs_promises.readFile)(resolved, "utf-8");
@@ -1139,9 +789,9 @@ async function readCustomViewI18n(collection, i18nFile, locale, opts = {}) {
1139
789
  * the file contents, or null when the path escapes the skill dir, the
1140
790
  * resolved target isn't a regular file, or the read fails. */
1141
791
  async function readSkillTemplate(skillDir, templateRelPath) {
1142
- const resolved = require_store.resolveTemplatePath(skillDir, templateRelPath);
792
+ const resolved = require_discovery.resolveTemplatePath(skillDir, templateRelPath);
1143
793
  if (resolved === null) return null;
1144
- if (!await require_store.isRegularFile(resolved)) return null;
794
+ if (!await require_discovery.isRegularFile(resolved)) return null;
1145
795
  try {
1146
796
  return await (0, node_fs_promises.readFile)(resolved, "utf-8");
1147
797
  } catch {
@@ -1283,11 +933,11 @@ function absentReferencedColumns(rows, query) {
1283
933
  /** Run a validated query over enriched collection rows. */
1284
934
  async function runQueryOverRows(rows, query) {
1285
935
  if (rows.length === 0) return emptyCollectionResult(query);
1286
- await (0, node_fs_promises.mkdir)(require_store.cacheDir(), {
936
+ await (0, node_fs_promises.mkdir)(require_discovery.cacheDir(), {
1287
937
  recursive: true,
1288
938
  mode: 448
1289
939
  });
1290
- const jsonlPath = node_path.default.join(require_store.cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
940
+ const jsonlPath = node_path.default.join(require_discovery.cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
1291
941
  try {
1292
942
  const nullPads = Object.fromEntries(absentReferencedColumns(rows, query).map((column) => [column, null]));
1293
943
  const handle = await (0, node_fs_promises.open)(jsonlPath, "wx", 384);
@@ -1303,115 +953,347 @@ async function runQueryOverRows(rows, query) {
1303
953
  } finally {
1304
954
  await handle.close();
1305
955
  }
1306
- const { sql, params } = require_store.compileJsonlQuery(query);
1307
- return (await require_store.queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, require_store.normalizeCsvValue(value)])));
956
+ const { sql, params } = require_discovery.compileJsonlQuery(query);
957
+ return (await require_discovery.queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, require_discovery.normalizeCsvValue(value)])));
1308
958
  } finally {
1309
959
  await (0, node_fs_promises.unlink)(jsonlPath).catch((err) => {
1310
- if (!require_dist.isErrorWithCode(err) || err.code !== "ENOENT") require_store.log.warn("collections", "temp JSONL cleanup failed", {
960
+ if (!require_dist.isErrorWithCode(err) || err.code !== "ENOENT") require_discovery.log.warn("collections", "temp JSONL cleanup failed", {
1311
961
  path: jsonlPath,
1312
962
  error: String(err)
1313
963
  });
1314
- });
1315
- }
964
+ });
965
+ }
966
+ }
967
+ //#endregion
968
+ //#region src/collection/server/derive.ts
969
+ async function loadTarget(slug, opts) {
970
+ const target = await require_discovery.loadCollection(slug, opts);
971
+ if (!target) return null;
972
+ const items = await require_discovery.storeFor(target, { workspaceRoot: opts.workspaceRoot }).list();
973
+ const byId = {};
974
+ for (const item of items) {
975
+ const itemId = item[target.schema.primaryKey];
976
+ if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_promptSafety.deriveAll(target.schema, item, {});
977
+ }
978
+ return {
979
+ schema: target.schema,
980
+ byId
981
+ };
982
+ }
983
+ /** Load every ref/embed target and backlink source collection once.
984
+ * Unknown / unloadable targets are simply absent — downstream derefs
985
+ * resolve to null (em-dash) and backlinks to an empty row set, the
986
+ * same fail-soft the UI renders. */
987
+ async function loadLinkedTargets(schema, opts) {
988
+ const slugs = [.../* @__PURE__ */ new Set([
989
+ ...require_promptSafety.uniqueRefTargets(schema),
990
+ ...require_promptSafety.uniqueEmbedTargets(schema),
991
+ ...require_promptSafety.uniqueBacklinkSources(schema)
992
+ ])];
993
+ const loaded = {};
994
+ for (const slug of slugs) {
995
+ const target = await loadTarget(slug, opts);
996
+ if (target) loaded[slug] = target;
997
+ }
998
+ return loaded;
999
+ }
1000
+ function toRefRecords(linked) {
1001
+ return Object.fromEntries(Object.entries(linked).map(([slug, target]) => [slug, target.byId]));
1002
+ }
1003
+ /** The matching source rows for one `backlinks` field, projected to the
1004
+ * source primaryKey + `display` columns — so getItems on a
1005
+ * heavily-referenced record stays a summary, not a dump of the source
1006
+ * collection. Missing source ⇒ [] (fail-soft). The rows come from the
1007
+ * DERIVED source records (`byId`), so `display`/`filter` on a derived
1008
+ * source column (an invoice `total`) works. */
1009
+ function projectBacklinks(field, schema, enriched, linked) {
1010
+ const source = linked[field.from];
1011
+ if (!source) return [];
1012
+ const selfId = require_itemId.fieldText(enriched[schema.primaryKey]);
1013
+ return require_promptSafety.backlinkRows(field, selfId, Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
1014
+ }
1015
+ /** Project the computed (never-stored) field kinds onto one derived
1016
+ * record: `toggle` → boolean off its enum, `embed` → the target record
1017
+ * (fixed `id` or per-record `idField`), or null when missing,
1018
+ * `backlinks` → the matching source rows (see `projectBacklinks`). */
1019
+ function projectComputed(schema, enriched, linked) {
1020
+ for (const [key, field] of Object.entries(schema.fields)) {
1021
+ if (field.type === "toggle" && field.field) enriched[key] = require_itemId.fieldText(enriched[field.field]) === field.onValue;
1022
+ if (field.type === "embed" && field.to) {
1023
+ const targetId = require_itemId.embedTargetId(field, enriched);
1024
+ const target = require_promptSafety.ownProp(linked, field.to);
1025
+ enriched[key] = targetId && target && require_promptSafety.ownProp(target.byId, targetId) || null;
1026
+ }
1027
+ if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
1028
+ }
1029
+ return enriched;
1030
+ }
1031
+ /** Resolve every rollup field onto a COPY of the record, BEFORE the
1032
+ * formula pass — a `derived` formula may reference rollup values as
1033
+ * plain identifiers (`played = homePlayed + awayPlayed`). Same reverse
1034
+ * machinery as backlinks, collapsed to a number: an unresolvable source
1035
+ * is null (a formula reading it fails soft to em-dash); an empty match
1036
+ * set is a real 0. Returns `record` unchanged when the schema declares
1037
+ * no rollups. */
1038
+ function projectRollups(schema, record, linked) {
1039
+ let out = record;
1040
+ for (const [key, field] of Object.entries(schema.fields)) {
1041
+ if (field.type !== "rollup") continue;
1042
+ if (out === record) out = { ...record };
1043
+ const source = linked[field.from];
1044
+ const selfId = require_itemId.fieldText(record[schema.primaryKey]);
1045
+ out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
1046
+ }
1047
+ return out;
1048
+ }
1049
+ /** Enrich records with every host-computed field: derived formulas
1050
+ * evaluated (cross-collection derefs included), toggles projected,
1051
+ * embeds resolved. Loads each linked collection ONCE per call. Input
1052
+ * records are not mutated. */
1053
+ async function enrichItems(collection, items, opts = {}) {
1054
+ const { schema } = collection;
1055
+ const linked = await loadLinkedTargets(schema, opts);
1056
+ const refRecords = toRefRecords(linked);
1057
+ return items.map((item) => projectComputed(schema, require_promptSafety.deriveAll(schema, projectRollups(schema, item, linked), refRecords), linked));
1058
+ }
1059
+ //#endregion
1060
+ //#region src/collection/server/queryRunner.ts
1061
+ async function runCollectionQuery(collection, query, opts = {}) {
1062
+ const store = require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot });
1063
+ if (store.query) return store.query(query);
1064
+ return runQueryOverRows(await enrichItems(collection, await store.list(), opts), query);
1065
+ }
1066
+ //#endregion
1067
+ //#region src/collection/core/recordZ.ts
1068
+ /** The emptiness rule shared by `required` and the "only check present
1069
+ * values" gate. NOT a truthiness check — `0` and `false` are filled. */
1070
+ var isEmptyValue = (value) => value === void 0 || value === null || value === "";
1071
+ /** The historical write-gate checks, verbatim: required non-empty, enum
1072
+ * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
1073
+ function enforcedProblem(key, spec, value) {
1074
+ const empty = isEmptyValue(value);
1075
+ if (spec.required && empty) return `missing required field '${key}'`;
1076
+ if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
1077
+ return null;
1078
+ }
1079
+ /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
1080
+ * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
1081
+ * the lint flags exactly the values the calendar / trigger / spawn code
1082
+ * would silently drop — impossible days like `2026-02-30`, and datetimes
1083
+ * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
1084
+ * which the day view can't place). `string`-backed types accept anything
1085
+ * stringifiable; `ref` existence is out of scope. */
1086
+ function strictTypeProblem(key, spec, value) {
1087
+ switch (spec.type) {
1088
+ case "number":
1089
+ case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
1090
+ case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
1091
+ case "date": return require_itemId.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
1092
+ 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)`;
1093
+ default: return null;
1094
+ }
1095
+ }
1096
+ /** Strict check for a PRESENT `table` value: an array of row objects, each
1097
+ * row conforming to the sub-schema (required / enum / typed sub-values).
1098
+ * First row problem wins, prefixed with the row number so the fix is
1099
+ * locatable. */
1100
+ function strictTableProblem(key, spec, value) {
1101
+ if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
1102
+ for (let index = 0; index < value.length; index++) {
1103
+ const row = value[index];
1104
+ if (!require_dist.isRecord(row)) return `'${key}' row ${index + 1} is not an object`;
1105
+ for (const [subKey, subSpec] of Object.entries(spec.of)) {
1106
+ const subValue = row[subKey];
1107
+ const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
1108
+ if (problem) return `'${key}' row ${index + 1}: ${problem}`;
1109
+ }
1110
+ }
1111
+ return null;
1112
+ }
1113
+ /** First problem for one field's stored value under `tier`, or null.
1114
+ * Enforced checks always run (and their messages never vary by tier — the
1115
+ * scan and the write gate must agree on them); strict adds the per-type
1116
+ * layer on present values only. */
1117
+ function recordFieldProblem(key, spec, value, tier) {
1118
+ const enforced = enforcedProblem(key, spec, value);
1119
+ if (enforced || tier === "enforced") return enforced;
1120
+ if (isEmptyValue(value)) return null;
1121
+ if (spec.type === "table") return strictTableProblem(key, spec, value);
1122
+ return strictTypeProblem(key, spec, value);
1123
+ }
1124
+ var compiled = /* @__PURE__ */ new WeakMap();
1125
+ /** Compile `schema.fields` into a zod validator for a stored record.
1126
+ * Loose object: unknown keys are allowed and any declared key may be
1127
+ * absent (records are user files, not parse-and-rewrite targets —
1128
+ * callers validate, they never persist the parse output). The checks run
1129
+ * as ONE object-level refine iterating fields in declaration order —
1130
+ * per-key shape schemas can't express "key may be absent BUT its absence
1131
+ * must still reach the required check", and the single loop keeps the
1132
+ * first reported issue identical to the historical first-problem-wins
1133
+ * contract. */
1134
+ function compileRecordZ(schema, tier) {
1135
+ const cached = compiled.get(schema)?.[tier];
1136
+ if (cached) return cached;
1137
+ const stored = Object.entries(schema.fields).filter(([, spec]) => !require_itemId.COMPUTED_TYPES.has(spec.type));
1138
+ const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
1139
+ for (const [key, spec] of stored) {
1140
+ const problem = recordFieldProblem(key, spec, record[key], tier);
1141
+ if (problem) ctx.addIssue({
1142
+ code: "custom",
1143
+ message: problem,
1144
+ path: [key]
1145
+ });
1146
+ }
1147
+ });
1148
+ const entry = compiled.get(schema) ?? {};
1149
+ entry[tier] = validator;
1150
+ compiled.set(schema, entry);
1151
+ return validator;
1152
+ }
1153
+ /** First schema problem on an in-memory record under `tier`, or null. One
1154
+ * issue per record keeps the report short and the fix obvious (the
1155
+ * historical contract of `validateRecordObject`). */
1156
+ function firstRecordProblem(record, schema, tier) {
1157
+ const result = compileRecordZ(schema, tier).safeParse(record);
1158
+ if (result.success) return null;
1159
+ return result.error.issues[0]?.message ?? "record failed schema validation";
1316
1160
  }
1317
1161
  //#endregion
1318
- //#region src/collection/server/derive.ts
1319
- async function loadTarget(slug, opts) {
1320
- const target = await require_store.loadCollection(slug, opts);
1321
- if (!target) return null;
1322
- const items = await require_store.storeFor(target, { workspaceRoot: opts.workspaceRoot }).list();
1323
- const byId = {};
1324
- for (const item of items) {
1325
- const itemId = item[target.schema.primaryKey];
1326
- if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_promptSafety.deriveAll(target.schema, item, {});
1162
+ //#region src/collection/server/validate.ts
1163
+ /** Don't flood the result; the first batch is enough to act on. Exported
1164
+ * because a caller that REPORTS a count has to know the count is a floor —
1165
+ * `publish` presents a full batch as "at least N" rather than as a total. */
1166
+ var MAX_RECORD_ISSUES = 25;
1167
+ /** The `file` of the pseudo-issue reported when the backend could not be read
1168
+ * at all.
1169
+ *
1170
+ * Exported because it is a DIFFERENT KIND of answer from "this record is
1171
+ * invalid", and a caller that treats the two alike gets it wrong in the
1172
+ * direction that matters: `publish` lets the user override invalid records,
1173
+ * and overriding this one would mean publishing without ever having looked. */
1174
+ var STORE_UNREADABLE = "(store)";
1175
+ var MAX_ISSUES = 25;
1176
+ /** Read every `<id>.json` under the collection's dataDir and report the
1177
+ * ones that won't load or violate the schema. An empty list means every
1178
+ * record is fine. */
1179
+ /** List entries under the data dir, guarding realpath containment (against a
1180
+ * symlinked dir swapped in after discovery, like `listItems`) and treating a
1181
+ * missing dir as empty while surfacing real I/O faults. */
1182
+ async function listRecordFilenames(dataDir, workspaceRoot) {
1183
+ if (!require_discovery.isContainedInRoot(dataDir, workspaceRoot)) {
1184
+ require_discovery.log.warn("collections", "validate refused: dataDir escapes workspace via symlink", { dataDir });
1185
+ return [];
1327
1186
  }
1328
- return {
1329
- schema: target.schema,
1330
- byId
1331
- };
1332
- }
1333
- /** Load every ref/embed target and backlink source collection once.
1334
- * Unknown / unloadable targets are simply absent — downstream derefs
1335
- * resolve to null (em-dash) and backlinks to an empty row set, the
1336
- * same fail-soft the UI renders. */
1337
- async function loadLinkedTargets(schema, opts) {
1338
- const slugs = [.../* @__PURE__ */ new Set([
1339
- ...require_promptSafety.uniqueRefTargets(schema),
1340
- ...require_promptSafety.uniqueEmbedTargets(schema),
1341
- ...require_promptSafety.uniqueBacklinkSources(schema)
1342
- ])];
1343
- const loaded = {};
1344
- for (const slug of slugs) {
1345
- const target = await loadTarget(slug, opts);
1346
- if (target) loaded[slug] = target;
1187
+ try {
1188
+ return await (0, node_fs_promises.readdir)(dataDir);
1189
+ } catch (err) {
1190
+ if (require_dist.isErrorWithCode(err) && err.code === "ENOENT") return [];
1191
+ throw err;
1347
1192
  }
1348
- return loaded;
1349
1193
  }
1350
- function toRefRecords(linked) {
1351
- return Object.fromEntries(Object.entries(linked).map(([slug, target]) => [slug, target.byId]));
1194
+ async function validateCollectionRecords(collection, opts = {}) {
1195
+ if (collection.schema.dataSource !== void 0) return [];
1196
+ if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
1197
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
1198
+ const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
1199
+ const issues = [];
1200
+ for (const name of entries.sort()) {
1201
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
1202
+ if (issues.length >= MAX_ISSUES) break;
1203
+ const issue = await inspectRecord(node_path.default.join(collection.dataDir, name), name, collection.schema);
1204
+ if (issue) issues.push(issue);
1205
+ }
1206
+ return issues;
1352
1207
  }
1353
- /** The matching source rows for one `backlinks` field, projected to the
1354
- * source primaryKey + `display` columns — so getItems on a
1355
- * heavily-referenced record stays a summary, not a dump of the source
1356
- * collection. Missing source ⇒ [] (fail-soft). The rows come from the
1357
- * DERIVED source records (`byId`), so `display`/`filter` on a derived
1358
- * source column (an invoice `total`) works. */
1359
- function projectBacklinks(field, schema, enriched, linked) {
1360
- const source = linked[field.from];
1361
- if (!source) return [];
1362
- const selfId = require_calendarGrid.fieldText(enriched[schema.primaryKey]);
1363
- return require_promptSafety.backlinkRows(field, selfId, Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
1208
+ /** Store-backed twin of the file scan: list every record through the
1209
+ * collection's store and lint it with the same "strict" report-only tier.
1210
+ * A row the store can't even parse is invisible here (the store skips
1211
+ * it), so the read/parse classifications of the file scan don't apply —
1212
+ * schema violations are what this catches. `file` carries the record id
1213
+ * (there is no per-record filename). */
1214
+ async function validateStoreRecords(collection, opts) {
1215
+ let items;
1216
+ try {
1217
+ items = await require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
1218
+ } catch (err) {
1219
+ return [{
1220
+ file: STORE_UNREADABLE,
1221
+ problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
1222
+ }];
1223
+ }
1224
+ const issues = [];
1225
+ for (const item of items) {
1226
+ if (issues.length >= MAX_ISSUES) break;
1227
+ const itemId = require_itemId.fieldText(item[collection.schema.primaryKey]);
1228
+ const problem = validateRecordObject(item, itemId, collection.schema, "strict");
1229
+ if (problem) issues.push({
1230
+ file: itemId,
1231
+ problem
1232
+ });
1233
+ }
1234
+ return issues;
1364
1235
  }
1365
- /** Project the computed (never-stored) field kinds onto one derived
1366
- * record: `toggle` → boolean off its enum, `embed` → the target record
1367
- * (fixed `id` or per-record `idField`), or null when missing,
1368
- * `backlinks` → the matching source rows (see `projectBacklinks`). */
1369
- function projectComputed(schema, enriched, linked) {
1370
- for (const [key, field] of Object.entries(schema.fields)) {
1371
- if (field.type === "toggle" && field.field) enriched[key] = require_calendarGrid.fieldText(enriched[field.field]) === field.onValue;
1372
- if (field.type === "embed" && field.to) {
1373
- const targetId = require_calendarGrid.embedTargetId(field, enriched);
1374
- const target = require_promptSafety.ownProp(linked, field.to);
1375
- enriched[key] = targetId && target && require_promptSafety.ownProp(target.byId, targetId) || null;
1376
- }
1377
- if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
1236
+ async function readRecordText(fullPath, name) {
1237
+ try {
1238
+ if (!(await (0, node_fs_promises.lstat)(fullPath)).isFile()) return {
1239
+ file: name,
1240
+ problem: "not a regular file (symlink?) — skipped, won't appear"
1241
+ };
1242
+ return { raw: await (0, node_fs_promises.readFile)(fullPath, "utf-8") };
1243
+ } catch {
1244
+ return {
1245
+ file: name,
1246
+ problem: "could not be read — skipped, won't appear"
1247
+ };
1378
1248
  }
1379
- return enriched;
1380
1249
  }
1381
- /** Resolve every rollup field onto a COPY of the record, BEFORE the
1382
- * formula pass — a `derived` formula may reference rollup values as
1383
- * plain identifiers (`played = homePlayed + awayPlayed`). Same reverse
1384
- * machinery as backlinks, collapsed to a number: an unresolvable source
1385
- * is null (a formula reading it fails soft to em-dash); an empty match
1386
- * set is a real 0. Returns `record` unchanged when the schema declares
1387
- * no rollups. */
1388
- function projectRollups(schema, record, linked) {
1389
- let out = record;
1390
- for (const [key, field] of Object.entries(schema.fields)) {
1391
- if (field.type !== "rollup") continue;
1392
- if (out === record) out = { ...record };
1393
- const source = linked[field.from];
1394
- const selfId = require_calendarGrid.fieldText(record[schema.primaryKey]);
1395
- out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
1250
+ /** Classify a single record file: unreadable / unparseable / non-object /
1251
+ * schema violation, or null when it's fine. */
1252
+ async function inspectRecord(fullPath, name, schema) {
1253
+ const read = await readRecordText(fullPath, name);
1254
+ if ("problem" in read) return read;
1255
+ let parsed;
1256
+ try {
1257
+ parsed = JSON.parse(read.raw);
1258
+ } catch (err) {
1259
+ return {
1260
+ file: name,
1261
+ 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.`
1262
+ };
1396
1263
  }
1397
- return out;
1264
+ if (!require_dist.isRecord(parsed)) return {
1265
+ file: name,
1266
+ problem: "not a JSON object — skipped, won't appear"
1267
+ };
1268
+ const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
1269
+ return problem ? {
1270
+ file: name,
1271
+ problem
1272
+ } : null;
1398
1273
  }
1399
- /** Enrich records with every host-computed field: derived formulas
1400
- * evaluated (cross-collection derefs included), toggles projected,
1401
- * embeds resolved. Loads each linked collection ONCE per call. Input
1402
- * records are not mutated. */
1403
- async function enrichItems(collection, items, opts = {}) {
1404
- const { schema } = collection;
1405
- const linked = await loadLinkedTargets(schema, opts);
1406
- const refRecords = toRefRecords(linked);
1407
- return items.map((item) => projectComputed(schema, require_promptSafety.deriveAll(schema, projectRollups(schema, item, linked), refRecords), linked));
1274
+ /** What a non-string primary key actually is, for the error message. Names the
1275
+ * shape rather than stringifying the value — "[object Object]" tells the reader
1276
+ * nothing about what is wrong. */
1277
+ function describeIdType(value) {
1278
+ if (value === null) return "null";
1279
+ if (value === void 0) return "missing";
1280
+ if (Array.isArray(value)) return "an array";
1281
+ return `a ${typeof value}`;
1408
1282
  }
1409
- //#endregion
1410
- //#region src/collection/server/queryRunner.ts
1411
- async function runCollectionQuery(collection, query, opts = {}) {
1412
- const store = require_store.storeFor(collection, { workspaceRoot: opts.workspaceRoot });
1413
- if (store.query) return store.query(query);
1414
- return runQueryOverRows(await enrichItems(collection, await store.list(), opts), query);
1283
+ /** First schema problem on an in-memory record (primaryKey↔id mismatch,
1284
+ * then the compiled per-field checks — see `../core/recordZ` for the two
1285
+ * tiers), or null when it's fine. One issue per record keeps the report
1286
+ * short and the fix obvious. Pure + exported so write paths
1287
+ * (manageCollection putItems) can gate on the SAME enforced rules the
1288
+ * post-hoc file scan reports — `itemId` is the id the record is (or
1289
+ * would be) stored under. The default `"enforced"` tier keeps every
1290
+ * write gate on the historical three checks; only pass `"strict"` from
1291
+ * report-only surfaces. */
1292
+ function validateRecordObject(record, itemId, schema, tier = "enforced") {
1293
+ const idValue = record[schema.primaryKey];
1294
+ 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`;
1295
+ if (idValue !== itemId) return `'${schema.primaryKey}' is '${idValue}' but must equal the filename ('${itemId}'), or the record can't be opened`;
1296
+ return firstRecordProblem(record, schema, tier);
1415
1297
  }
1416
1298
  //#endregion
1417
1299
  //#region src/collection/server/mutate.ts
@@ -1438,7 +1320,7 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
1438
1320
  status: "invalid-params",
1439
1321
  problem: paramProblem
1440
1322
  };
1441
- const store = require_store.storeFor(collection, opts);
1323
+ const store = require_discovery.storeFor(collection, opts);
1442
1324
  if (!store.write) return {
1443
1325
  ok: false,
1444
1326
  status: "write-refused",
@@ -1457,11 +1339,11 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
1457
1339
  };
1458
1340
  const stored = Object.entries(existing).filter(([key]) => {
1459
1341
  const spec = collection.schema.fields[key];
1460
- return !spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type);
1342
+ return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
1461
1343
  });
1462
1344
  const merged = {
1463
1345
  ...Object.fromEntries(stored),
1464
- ...require_store.resolveMutateSet(action.set, params)
1346
+ ...require_discovery.resolveMutateSet(action.set, params)
1465
1347
  };
1466
1348
  const invalid = validateRecordObject(merged, itemId, collection.schema);
1467
1349
  if (invalid) return {
@@ -1519,10 +1401,10 @@ function schemaRelations(schema) {
1519
1401
  * Fail-soft: a missing dir or a dataDir escaping the workspace via
1520
1402
  * symlink counts 0. */
1521
1403
  async function countRecordFiles(dataDir, workspaceRoot) {
1522
- if (!require_store.isContainedInRoot(dataDir, workspaceRoot)) return 0;
1404
+ if (!require_discovery.isContainedInRoot(dataDir, workspaceRoot)) return 0;
1523
1405
  try {
1524
1406
  const names = (await (0, node_fs_promises.readdir)(dataDir)).filter((name) => name.endsWith(".json") && !name.startsWith("."));
1525
- return (await Promise.all(names.map((name) => require_store.isRegularFile(node_path.default.join(dataDir, name))))).filter(Boolean).length;
1407
+ return (await Promise.all(names.map((name) => require_discovery.isRegularFile(node_path.default.join(dataDir, name))))).filter(Boolean).length;
1526
1408
  } catch {
1527
1409
  return 0;
1528
1410
  }
@@ -1534,10 +1416,10 @@ async function countRecordFiles(dataDir, workspaceRoot) {
1534
1416
  * `countRecordFiles` (an unreadable file / missing engine must not
1535
1417
  * break the whole ontology). */
1536
1418
  async function countRecords(collection, workspaceRoot) {
1537
- if (require_calendarGrid.storageKindFor(collection.schema) !== "file") try {
1538
- return (await require_store.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
1419
+ if (require_itemId.storageKindFor(collection.schema) !== "file") try {
1420
+ return (await require_discovery.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
1539
1421
  } catch (err) {
1540
- if (require_store.isBackendUnavailable(err)) return null;
1422
+ if (require_discovery.isBackendUnavailable(err)) return null;
1541
1423
  return 0;
1542
1424
  }
1543
1425
  return countRecordFiles(collection.dataDir, workspaceRoot);
@@ -1558,8 +1440,8 @@ async function toOntologyEntry(collection, workspaceRoot) {
1558
1440
  * discovery's order), each with its outbound relations and a
1559
1441
  * readdir-cheap record count — fine to call on demand. */
1560
1442
  async function buildWorkspaceOntology(opts = {}) {
1561
- const workspaceRoot = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
1562
- const collections = await require_store.discoverCollections(opts);
1443
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
1444
+ const collections = await require_discovery.discoverCollections(opts);
1563
1445
  return Promise.all(collections.map((collection) => toOntologyEntry(collection, workspaceRoot)));
1564
1446
  }
1565
1447
  //#endregion
@@ -1579,7 +1461,7 @@ function buildRecordsById(items, primaryKey) {
1579
1461
  * ties pick a different record — and thus a different icon — between
1580
1462
  * reconciles. A stable id sort pins one answer. */
1581
1463
  function sortByPrimaryKey(items, primaryKey) {
1582
- return [...items].sort((left, right) => require_calendarGrid.fieldText(left[primaryKey]).localeCompare(require_calendarGrid.fieldText(right[primaryKey])));
1464
+ return [...items].sort((left, right) => require_itemId.fieldText(left[primaryKey]).localeCompare(require_itemId.fieldText(right[primaryKey])));
1583
1465
  }
1584
1466
  /** Compute the effective launcher icon for `collection`: its static
1585
1467
  * `schema.icon` when it declares no `dynamicIcon`, else the icon
@@ -1593,15 +1475,15 @@ async function computeCollectionIcon(collection, opts = {}) {
1593
1475
  const spec = schema.dynamicIcon;
1594
1476
  if (!spec) return schema.icon;
1595
1477
  try {
1596
- const source = await require_store.loadCollection(spec.source.collection, opts);
1478
+ const source = await require_discovery.loadCollection(spec.source.collection, opts);
1597
1479
  if (!source) return spec.fallback ?? schema.icon;
1598
- const ordered = sortByPrimaryKey(await require_store.storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
1480
+ const ordered = sortByPrimaryKey(await require_discovery.storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
1599
1481
  const orderBy = spec.source.orderBy ?? require_promptSafety.firstDateField(source.schema);
1600
1482
  const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
1601
1483
  const record = require_promptSafety.selectDynamicRecord(ordered, spec.source, orderBy, recordsById);
1602
1484
  return require_promptSafety.resolveIcon(record, spec, schema.icon, recordsById);
1603
1485
  } catch (err) {
1604
- require_store.log.warn("collections", "dynamic icon compute failed, falling back", {
1486
+ require_discovery.log.warn("collections", "dynamic icon compute failed, falling back", {
1605
1487
  slug: collection.slug,
1606
1488
  source: spec.source.collection,
1607
1489
  error: String(err)
@@ -1716,7 +1598,7 @@ function successorId(sourceId, next) {
1716
1598
  function matchesWhen(when, schema, item) {
1717
1599
  if (when) {
1718
1600
  const raw = item[when.field];
1719
- const text = require_calendarGrid.fieldTextOrNull(raw);
1601
+ const text = require_itemId.fieldTextOrNull(raw);
1720
1602
  return text !== null && when.in.includes(text);
1721
1603
  }
1722
1604
  return require_promptSafety.itemIsDone(schema, item);
@@ -1728,10 +1610,10 @@ function matchesWhen(when, schema, item) {
1728
1610
  * Discovery rejects a map that doesn't cover the enum's values, so null
1729
1611
  * here means a record that predates a map/enum edit. */
1730
1612
  function resolveEvery(every, sourceItem) {
1731
- if (!require_calendarGrid.isFieldDrivenEvery(every)) return every;
1613
+ if (!require_itemId.isFieldDrivenEvery(every)) return every;
1732
1614
  const raw = sourceItem[every.fromField];
1733
1615
  if (raw === void 0 || raw === null || raw === "") return null;
1734
- const key = require_calendarGrid.fieldTextOrNull(raw);
1616
+ const key = require_itemId.fieldTextOrNull(raw);
1735
1617
  if (key === null || !Object.hasOwn(every.map, key)) return null;
1736
1618
  return every.map[key] ?? null;
1737
1619
  }
@@ -1763,15 +1645,15 @@ function computeSuccessor(schema, sourceItem, sourceId) {
1763
1645
  * predates a map/enum edit — discovery rejects this statically otherwise). */
1764
1646
  function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
1765
1647
  if (parseCivil(sourceItem[triggerField]) === null) {
1766
- require_store.log.warn("collections", "spawn skipped: source trigger date unparseable", {
1648
+ require_discovery.log.warn("collections", "spawn skipped: source trigger date unparseable", {
1767
1649
  slug,
1768
1650
  sourceId,
1769
1651
  triggerField
1770
1652
  });
1771
1653
  return;
1772
1654
  }
1773
- const fromField = require_calendarGrid.isFieldDrivenEvery(every) ? every.fromField : void 0;
1774
- require_store.log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
1655
+ const fromField = require_itemId.isFieldDrivenEvery(every) ? every.fromField : void 0;
1656
+ require_discovery.log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
1775
1657
  slug,
1776
1658
  sourceId,
1777
1659
  fromField,
@@ -1794,7 +1676,7 @@ async function maybeSpawnSuccessor(collection, sourceItem, sourceId, ioOpts = {}
1794
1676
  return;
1795
1677
  }
1796
1678
  if (matchesWhen(spawn.when, schema, computed.record)) {
1797
- require_store.log.warn("collections", "spawn skipped: successor would be born matching its own predicate (unbounded respawn)", {
1679
+ require_discovery.log.warn("collections", "spawn skipped: successor would be born matching its own predicate (unbounded respawn)", {
1798
1680
  slug,
1799
1681
  sourceId,
1800
1682
  successorId: computed.id
@@ -1802,28 +1684,28 @@ async function maybeSpawnSuccessor(collection, sourceItem, sourceId, ioOpts = {}
1802
1684
  return;
1803
1685
  }
1804
1686
  try {
1805
- const { write } = require_store.storeFor(collection, ioOpts);
1687
+ const { write } = require_discovery.storeFor(collection, ioOpts);
1806
1688
  if (!write) {
1807
- require_store.log.warn("collections", "spawn skipped: collection store is read-only", {
1689
+ require_discovery.log.warn("collections", "spawn skipped: collection store is read-only", {
1808
1690
  slug,
1809
1691
  sourceId
1810
1692
  });
1811
1693
  return;
1812
1694
  }
1813
1695
  const result = await write(computed.id, computed.record, { refuseOverwrite: true });
1814
- if (result.kind === "ok") require_store.log.info("collections", "spawned successor", {
1696
+ if (result.kind === "ok") require_discovery.log.info("collections", "spawned successor", {
1815
1697
  slug,
1816
1698
  sourceId,
1817
1699
  successorId: computed.id
1818
1700
  });
1819
- else if (result.kind !== "conflict") require_store.log.warn("collections", "spawn write failed", {
1701
+ else if (result.kind !== "conflict") require_discovery.log.warn("collections", "spawn write failed", {
1820
1702
  slug,
1821
1703
  sourceId,
1822
1704
  successorId: computed.id,
1823
1705
  kind: result.kind
1824
1706
  });
1825
1707
  } catch (err) {
1826
- require_store.log.warn("collections", "spawn write threw", {
1708
+ require_discovery.log.warn("collections", "spawn write threw", {
1827
1709
  slug,
1828
1710
  sourceId,
1829
1711
  successorId: computed.id,
@@ -1862,7 +1744,7 @@ function todayStamp() {
1862
1744
  /** Every directory the delete will touch must resolve under the
1863
1745
  * workspace root — guards against a symlinked ancestor escaping it. */
1864
1746
  function deleteTargets(collection, workspaceRoot) {
1865
- const staging = require_store.stagingSkillDir(workspaceRoot, collection.slug);
1747
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
1866
1748
  return [
1867
1749
  ...staging === null ? [] : [staging],
1868
1750
  collection.skillDir,
@@ -1974,12 +1856,12 @@ function recordLocationLabel(schema) {
1974
1856
  }
1975
1857
  /** Copy one skill copy + the records + RESTORE.md into `archiveDir`. */
1976
1858
  async function writeArchive(collection, archiveDir, workspaceRoot) {
1977
- const staging = require_store.stagingSkillDir(workspaceRoot, collection.slug);
1859
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
1978
1860
  const skillSrc = staging !== null && await pathExists(staging) ? staging : collection.skillDir;
1979
1861
  await (0, node_fs_promises.cp)(skillSrc, node_path.default.join(archiveDir, "skill"), { recursive: true });
1980
1862
  if (await pathExists(collection.dataDir)) await (0, node_fs_promises.cp)(collection.dataDir, node_path.default.join(archiveDir, "records"), { recursive: true });
1981
1863
  if (collection.storageFile !== void 0 && await pathExists(collection.storageFile)) {
1982
- const checkpointed = await require_store.checkpointSqliteDatabase(collection.storageFile);
1864
+ const checkpointed = await require_discovery.checkpointSqliteDatabase(collection.storageFile);
1983
1865
  await (0, node_fs_promises.cp)(collection.storageFile, node_path.default.join(archiveDir, node_path.default.basename(collection.storageFile)));
1984
1866
  if (!checkpointed) for (const suffix of [
1985
1867
  "-wal",
@@ -1996,7 +1878,7 @@ async function writeArchive(collection, archiveDir, workspaceRoot) {
1996
1878
  * is a no-op; the now-empty data parent (`data/<slug>/` after its
1997
1879
  * `items/` is gone) is swept too, but only when empty. */
1998
1880
  async function removeLocations(collection, workspaceRoot) {
1999
- const staging = require_store.stagingSkillDir(workspaceRoot, collection.slug);
1881
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, collection.slug);
2000
1882
  if (staging !== null) await (0, node_fs_promises.rm)(staging, {
2001
1883
  recursive: true,
2002
1884
  force: true
@@ -2020,24 +1902,24 @@ async function removeLocations(collection, workspaceRoot) {
2020
1902
  }
2021
1903
  async function deleteCollection(collection, opts = {}) {
2022
1904
  const { slug } = collection;
2023
- const workspaceRoot = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
1905
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
2024
1906
  if (collection.source === "user") return {
2025
1907
  kind: "user-scope",
2026
1908
  slug
2027
1909
  };
2028
- if (require_store.isPresetSlug(slug)) return {
1910
+ if (require_discovery.isPresetSlug(slug)) return {
2029
1911
  kind: "preset",
2030
1912
  slug
2031
1913
  };
2032
1914
  if (collection.schema.storage?.type === "firestore") {
2033
- require_store.log.warn("collections", "deleteCollection refused: a shared collection's records can be neither archived nor removed here", { slug });
1915
+ require_discovery.log.warn("collections", "deleteCollection refused: a shared collection's records can be neither archived nor removed here", { slug });
2034
1916
  return {
2035
1917
  kind: "unsupported-backend",
2036
1918
  slug
2037
1919
  };
2038
1920
  }
2039
1921
  if (!isDataDirSafe(collection.dataDir, slug, workspaceRoot)) {
2040
- require_store.log.warn("collections", "deleteCollection refused: dataDir is not under the per-collection root", {
1922
+ require_discovery.log.warn("collections", "deleteCollection refused: dataDir is not under the per-collection root", {
2041
1923
  slug,
2042
1924
  dataDir: collection.dataDir
2043
1925
  });
@@ -2046,19 +1928,19 @@ async function deleteCollection(collection, opts = {}) {
2046
1928
  slug
2047
1929
  };
2048
1930
  }
2049
- if (deleteTargets(collection, workspaceRoot).some((target) => !require_store.isContainedInRoot(target, workspaceRoot))) {
2050
- require_store.log.warn("collections", "deleteCollection refused: a target escapes the workspace", { slug });
1931
+ if (deleteTargets(collection, workspaceRoot).some((target) => !require_discovery.isContainedInRoot(target, workspaceRoot))) {
1932
+ require_discovery.log.warn("collections", "deleteCollection refused: a target escapes the workspace", { slug });
2051
1933
  return {
2052
1934
  kind: "path-escape",
2053
1935
  slug
2054
1936
  };
2055
1937
  }
2056
- const archiveRel = node_path.default.join(require_store.archiveDir(), `${opts.dateStamp ?? todayStamp()}-${(0, node_crypto.randomUUID)()}`);
1938
+ const archiveRel = node_path.default.join(require_discovery.archiveDir(), `${opts.dateStamp ?? todayStamp()}-${(0, node_crypto.randomUUID)()}`);
2057
1939
  const archiveDir$1 = node_path.default.join(workspaceRoot, archiveRel);
2058
1940
  await (0, node_fs_promises.mkdir)(archiveDir$1, { recursive: true });
2059
1941
  await writeArchive(collection, archiveDir$1, workspaceRoot);
2060
1942
  await removeLocations(collection, workspaceRoot);
2061
- require_store.log.info("collections", "collection deleted + archived", {
1943
+ require_discovery.log.info("collections", "collection deleted + archived", {
2062
1944
  slug,
2063
1945
  archive: archiveRel
2064
1946
  });
@@ -2088,7 +1970,7 @@ async function fileExists(target) {
2088
1970
  * deletes agree on both layouts. */
2089
1971
  async function canonicalBase(collection, workspaceRoot, safeSlug) {
2090
1972
  if (collection.source !== "project") return collection.skillDir;
2091
- const staging = require_store.stagingSkillDir(workspaceRoot, safeSlug);
1973
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, safeSlug);
2092
1974
  if (staging !== null && await fileExists(node_path.default.join(staging, "schema.json"))) return staging;
2093
1975
  return collection.skillDir;
2094
1976
  }
@@ -2098,11 +1980,11 @@ async function canonicalBase(collection, workspaceRoot, safeSlug) {
2098
1980
  * project collection (no staging mirror) doesn't have an empty staging tree
2099
1981
  * materialised by a side effect of the delete. */
2100
1982
  async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
2101
- const active = node_path.default.join(collection.skillDir, require_store.SCHEMA_FILE);
1983
+ const active = node_path.default.join(collection.skillDir, require_discovery.SCHEMA_FILE);
2102
1984
  if (collection.source !== "project") return [active];
2103
- const staging = require_store.stagingSkillDir(workspaceRoot, safeSlug);
1985
+ const staging = require_discovery.stagingSkillDir(workspaceRoot, safeSlug);
2104
1986
  const targets = [];
2105
- if (staging !== null && await fileExists(node_path.default.join(staging, "schema.json"))) targets.push(node_path.default.join(staging, require_store.SCHEMA_FILE));
1987
+ if (staging !== null && await fileExists(node_path.default.join(staging, "schema.json"))) targets.push(node_path.default.join(staging, require_discovery.SCHEMA_FILE));
2106
1988
  targets.push(active);
2107
1989
  return targets;
2108
1990
  }
@@ -2121,7 +2003,7 @@ async function unlinkIfPresent(target) {
2121
2003
  * preserved verbatim. */
2122
2004
  async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug) {
2123
2005
  const base = await canonicalBase(collection, workspaceRoot, safeSlug);
2124
- const canonical = node_path.default.join(base, require_store.SCHEMA_FILE);
2006
+ const canonical = node_path.default.join(base, require_discovery.SCHEMA_FILE);
2125
2007
  const parsed = JSON.parse(await (0, node_fs_promises.readFile)(canonical, "utf-8"));
2126
2008
  const next = require_dist.isRecord(parsed) && require_dist.isUnknownArray(parsed.views) ? {
2127
2009
  ...parsed,
@@ -2135,8 +2017,8 @@ async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug
2135
2017
  * refused (read-only / re-seeded on boot), consistent with `deleteCollection`. */
2136
2018
  async function deleteCustomView(collection, viewId, opts = {}) {
2137
2019
  if (collection.source === "user") return { kind: "user-scope" };
2138
- if (require_store.isPresetSlug(collection.slug)) return { kind: "preset" };
2139
- const safeSlug = require_store.safeSlugName(collection.slug);
2020
+ if (require_discovery.isPresetSlug(collection.slug)) return { kind: "preset" };
2021
+ const safeSlug = require_discovery.safeSlugName(collection.slug);
2140
2022
  if (safeSlug === null) return {
2141
2023
  kind: "unsafe-path",
2142
2024
  viewId
@@ -2147,8 +2029,8 @@ async function deleteCustomView(collection, viewId, opts = {}) {
2147
2029
  kind: "not-found",
2148
2030
  viewId
2149
2031
  };
2150
- const workspaceRoot = opts.workspaceRoot ?? require_store.getWorkspaceRoot();
2151
- const htmlPath = require_store.resolveTemplatePath(await canonicalBase(collection, workspaceRoot, safeSlug), view.file);
2032
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
2033
+ const htmlPath = require_discovery.resolveTemplatePath(await canonicalBase(collection, workspaceRoot, safeSlug), view.file);
2152
2034
  if (htmlPath === null) return {
2153
2035
  kind: "unsafe-path",
2154
2036
  viewId
@@ -2375,7 +2257,7 @@ var SCHEMA_DOCS_PROBE_SLUG = "_";
2375
2257
  /** Resolve the workspace root the same way every collections call does:
2376
2258
  * the injected override (tests) or the configured collection host. */
2377
2259
  function resolveBase(deps) {
2378
- return deps.workspaceRoot ?? require_store.getWorkspaceRoot();
2260
+ return deps.workspaceRoot ?? require_discovery.getWorkspaceRoot();
2379
2261
  }
2380
2262
  /** Where a collection skill is authored in THIS root, and therefore both which
2381
2263
  * authoring guide `schemaDocs` serves and where `getSchema` / `putSchema`
@@ -2389,7 +2271,7 @@ function resolveBase(deps) {
2389
2271
  * from there, so the tool would silently contradict its own documentation.
2390
2272
  * Staged requires BOTH to agree; anything else is direct. */
2391
2273
  function authoringTarget(deps, slug) {
2392
- const stagingDir = deps.stagedSkillAuthoring === false ? null : require_store.stagingSkillDir(resolveBase(deps), slug);
2274
+ const stagingDir = deps.stagedSkillAuthoring === false ? null : require_discovery.stagingSkillDir(resolveBase(deps), slug);
2393
2275
  return {
2394
2276
  variant: stagingDir === null ? "direct" : "staged",
2395
2277
  stagingDir
@@ -2432,7 +2314,7 @@ async function recordIssuesWarning(collection, deps) {
2432
2314
  return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
2433
2315
  }
2434
2316
  async function loadRequestedItems(collection, ids, deps) {
2435
- const store = require_store.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2317
+ const store = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2436
2318
  if (!ids) return {
2437
2319
  items: await store.list(),
2438
2320
  missing: []
@@ -2441,7 +2323,7 @@ async function loadRequestedItems(collection, ids, deps) {
2441
2323
  const missing = [];
2442
2324
  for (const recordId of ids) {
2443
2325
  const item = await store.read(recordId).catch((err) => {
2444
- if (require_store.isBackendUnavailable(err)) throw err;
2326
+ if (require_discovery.isBackendUnavailable(err)) throw err;
2445
2327
  return null;
2446
2328
  });
2447
2329
  if (item) items.push(item);
@@ -2472,7 +2354,7 @@ async function handleGetItems(collection, args, deps) {
2472
2354
  function computedKeyProblem(record, schema) {
2473
2355
  for (const key of Object.keys(record)) {
2474
2356
  const spec = schema.fields[key];
2475
- if (!spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type)) continue;
2357
+ if (!spec || !require_itemId.COMPUTED_TYPES.has(spec.type)) continue;
2476
2358
  if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
2477
2359
  return `'${key}' is ${{
2478
2360
  derived: "derived",
@@ -2506,13 +2388,13 @@ async function mergeWithExisting(collection, store, record, itemId) {
2506
2388
  try {
2507
2389
  existing = await store.read(itemId);
2508
2390
  } catch (err) {
2509
- if (require_store.isBackendUnavailable(err)) throw err;
2391
+ if (require_discovery.isBackendUnavailable(err)) throw err;
2510
2392
  return `'${itemId}' has a malformed stored file — mode "merge" needs to read it; fix the file (Read → correct → Write) or replace it whole with "upsert"`;
2511
2393
  }
2512
2394
  if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
2513
2395
  const stored = Object.entries(existing).filter(([key]) => {
2514
2396
  const spec = collection.schema.fields[key];
2515
- return !spec || !require_calendarGrid.COMPUTED_TYPES.has(spec.type);
2397
+ return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
2516
2398
  });
2517
2399
  return {
2518
2400
  ...Object.fromEntries(stored),
@@ -2521,7 +2403,7 @@ async function mergeWithExisting(collection, store, record, itemId) {
2521
2403
  }
2522
2404
  async function putOneItem(collection, store, write, record, mode, deps) {
2523
2405
  const { schema } = collection;
2524
- const itemId = require_store.resolveCreateItemId(schema, record);
2406
+ const itemId = require_discovery.resolveCreateItemId(schema, record);
2525
2407
  const reject = (about, problem) => ({ rejected: {
2526
2408
  id: require_promptSafety.defangForPrompt(about),
2527
2409
  problem: require_promptSafety.defangForPrompt(problem)
@@ -2552,7 +2434,7 @@ async function putOneItem(collection, store, write, record, mode, deps) {
2552
2434
  * `runCollectionQuery`, shared with the desktop custom view's `/query`
2553
2435
  * route so the two surfaces can never drift. */
2554
2436
  async function handleQueryItems(collection, queryArg, deps) {
2555
- const parsed = require_store.CollectionQueryZ.safeParse(queryArg);
2437
+ const parsed = require_discovery.CollectionQueryZ.safeParse(queryArg);
2556
2438
  if (!parsed.success) return `manageCollection: \`query\` rejected — fix and retry:\n${parsed.error.issues.slice(0, 20).map((issue) => `- ${issue.path.map(String).join(".") || "(root)"}: ${require_promptSafety.defangForPrompt(issue.message)}`).join("\n")}`;
2557
2439
  const rows = await runCollectionQuery(collection, parsed.data, { workspaceRoot: deps.workspaceRoot });
2558
2440
  return JSON.stringify({
@@ -2562,9 +2444,9 @@ async function handleQueryItems(collection, queryArg, deps) {
2562
2444
  });
2563
2445
  }
2564
2446
  async function handlePutItems(collection, args, deps) {
2565
- const store = require_store.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2447
+ const store = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2566
2448
  const { write } = store;
2567
- if (!write) return `manageCollection: ${require_store.readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2449
+ if (!write) return `manageCollection: ${require_discovery.readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2568
2450
  const written = [];
2569
2451
  const rejected = [];
2570
2452
  for (const record of args.items) {
@@ -2583,8 +2465,8 @@ async function handlePutItems(collection, args, deps) {
2583
2465
  * absent `delete` IS the refusal), and the same per-id result shape so a
2584
2466
  * partially-bad batch reports per id instead of failing whole. */
2585
2467
  async function handleDeleteItems(collection, ids, deps) {
2586
- const { delete: removeItem } = require_store.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2587
- if (!removeItem) return `manageCollection: ${require_store.readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2468
+ const { delete: removeItem } = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
2469
+ if (!removeItem) return `manageCollection: ${require_discovery.readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
2588
2470
  const deleted = [];
2589
2471
  const rejected = [];
2590
2472
  for (const itemId of ids) {
@@ -2653,32 +2535,6 @@ async function handleGetOntology(deps) {
2653
2535
  collections
2654
2536
  });
2655
2537
  }
2656
- /** Publish this repository's `app.json` + shared schemas to its Firestore app.
2657
- *
2658
- * Named as a whole-app action because it IS one: publish takes the repository
2659
- * as its unit (one roster, one public config, every shared collection), so it
2660
- * carries no `slug` and refusing one is not a limitation to work around.
2661
- *
2662
- * The reply is prose rather than a status code on purpose. This is the one
2663
- * operation in the collection surface that changes what every member sees the
2664
- * moment it lands, and the two things the caller has to relay — what will
2665
- * break, and that `confirm` is how it proceeds anyway — are sentences, not
2666
- * fields. */
2667
- async function handlePublishApp(deps, confirm) {
2668
- const result = await publishApp({
2669
- ...deps,
2670
- confirm
2671
- });
2672
- if (!result.ok) {
2673
- const bullets = result.problems.map((problem) => `- ${problem}`).join("\n");
2674
- return `${result.partial ? "publish FAILED PART-WAY — some documents are already live:" : "publish refused — nothing was written:"}\n${bullets}`;
2675
- }
2676
- const dirtyNote = result.dirty ? " (WORKING TREE DIRTY — the commit does not describe what was published)" : "";
2677
- const stamp = result.commit ? `commit ${result.commit.slice(0, 12)}${dirtyNote}` : "no commit (not a git repository, or no HEAD)";
2678
- const brokenCount = result.recordIssuesCapped ? `at least ${result.recordIssues}` : `${result.recordIssues}`;
2679
- const forced = result.recordIssues > 0 ? ` Published over ${brokenCount} record(s) that do not satisfy the new schema — repair them now; members are reading them.` : "";
2680
- 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}`;
2681
- }
2682
2538
  /** Return the collection-authoring reference (`collection-skills.md`),
2683
2539
  * rendered by `renderSchemaDocs` — the full doc overflows the agent's
2684
2540
  * per-result limit, so the default reply is the core guide + a table of
@@ -2699,7 +2555,7 @@ async function handleSchemaDocs(deps, topic) {
2699
2555
  * fallback for user-scope skills that have no staging copy. Raw text —
2700
2556
  * not the parsed schema — so the agent edits the true on-disk source. */
2701
2557
  async function handleGetSchema(slug, deps) {
2702
- const collection = await require_store.loadCollection(slug, deps);
2558
+ const collection = await require_discovery.loadCollection(slug, deps);
2703
2559
  if (!collection) return unknownCollection(slug);
2704
2560
  const { stagingDir } = authoringTarget(deps, collection.slug);
2705
2561
  const candidates = [...stagingDir === null ? [] : [node_path.default.join(stagingDir, SCHEMA_FILE)], node_path.default.join(collection.skillDir, SCHEMA_FILE)];
@@ -2761,11 +2617,11 @@ async function writeAndMirrorSchema(slug, skillDir, schema, deps) {
2761
2617
  * gate doesn't apply. Returns a one-line reason, or null when the schema
2762
2618
  * would be accepted. */
2763
2619
  function schemaDiscoveryGate(schema, base) {
2764
- const primaryField = require_store.resolvePrimaryField(schema.fields, schema.primaryKey);
2620
+ const primaryField = require_discovery.resolvePrimaryField(schema.fields, schema.primaryKey);
2765
2621
  if (!primaryField) return `primaryKey '${schema.primaryKey}' is not one of the declared fields`;
2766
2622
  if (primaryField.primary !== true) return `the primaryKey field '${schema.primaryKey}' must be flagged \`primary: true\``;
2767
- if (schema.dataPath !== void 0 && require_store.resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
2768
- if (schema.dataSource !== void 0 && require_store.resolveDataDir(schema.dataSource.path, base) === null) return `dataSource.path '${schema.dataSource.path}' escapes the workspace`;
2623
+ if (schema.dataPath !== void 0 && require_discovery.resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
2624
+ if (schema.dataSource !== void 0 && require_discovery.resolveDataDir(schema.dataSource.path, base) === null) return `dataSource.path '${schema.dataSource.path}' escapes the workspace`;
2769
2625
  return null;
2770
2626
  }
2771
2627
  /** Validate a schema against CollectionSchemaZ and, on success, persist it.
@@ -2773,11 +2629,11 @@ function schemaDiscoveryGate(schema, base) {
2773
2629
  * the root's authoring dir (the normal create flow), not through here. */
2774
2630
  async function handlePutSchema(slug, schemaArg, deps) {
2775
2631
  if (!schemaArg || typeof schemaArg !== "object" || Array.isArray(schemaArg)) return "manageCollection: `schema` is required for putSchema — the full collection schema object.";
2776
- const collection = await require_store.loadCollection(slug, deps);
2632
+ const collection = await require_discovery.loadCollection(slug, deps);
2777
2633
  if (!collection) return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under ${authoringDirLabel(deps, require_promptSafety.defangForPrompt(slug))}, then edit it here.`;
2778
2634
  const refusal = schemaEditRefusal(collection, slug, deps);
2779
2635
  if (refusal) return refusal;
2780
- const parsed = require_store.CollectionSchemaZ.safeParse(schemaArg);
2636
+ const parsed = require_discovery.CollectionSchemaZ.safeParse(schemaArg);
2781
2637
  if (!parsed.success) return formatSchemaIssues(parsed.error.issues);
2782
2638
  const gate = schemaDiscoveryGate(parsed.data, resolveBase(deps));
2783
2639
  if (gate) return `manageCollection: schema rejected — ${gate} (call schemaDocs for the field reference). It passes basic validation but discovery would skip it, hiding the collection.`;
@@ -2787,7 +2643,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
2787
2643
  written: true
2788
2644
  });
2789
2645
  }
2790
- 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.";
2646
+ 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.";
2791
2647
  /** Validate getItems' optional `ids`/`fields` args, then delegate. */
2792
2648
  async function dispatchGetItems(collection, args, deps) {
2793
2649
  const ids = optionalStringArray(args.ids, "ids");
@@ -2824,7 +2680,7 @@ async function manageCollectionHandler(deps, args) {
2824
2680
  try {
2825
2681
  return await dispatchManageCollection(deps, args);
2826
2682
  } catch (err) {
2827
- if (require_store.isBackendUnavailable(err)) return `manageCollection: ${err.message}`;
2683
+ if (require_discovery.isBackendUnavailable(err)) return `manageCollection: ${err.message}`;
2828
2684
  throw err;
2829
2685
  }
2830
2686
  }
@@ -2832,19 +2688,18 @@ async function dispatchManageCollection(deps, args) {
2832
2688
  const action = typeof args.action === "string" ? args.action : "";
2833
2689
  if (action === "schemaDocs") return handleSchemaDocs(deps, typeof args.topic === "string" ? args.topic : void 0);
2834
2690
  if (action === "getOntology") return handleGetOntology(deps);
2835
- if (action === "publishApp") return handlePublishApp(deps, args.confirm === true);
2836
2691
  const slug = typeof args.slug === "string" ? args.slug.trim() : "";
2837
2692
  if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
2838
2693
  if (action === "getSchema") return handleGetSchema(slug, deps);
2839
2694
  if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
2840
- if (!RECORD_ACTIONS.has(action)) return "manageCollection: `action` must be \"getItems\", \"putItems\", \"deleteItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", \"putSchema\", or \"publishApp\".";
2841
- const collection = await require_store.loadCollection(slug, deps);
2695
+ if (!RECORD_ACTIONS.has(action)) return "manageCollection: `action` must be \"getItems\", \"putItems\", \"deleteItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
2696
+ const collection = await require_discovery.loadCollection(slug, deps);
2842
2697
  if (!collection) return unknownCollection(slug);
2843
2698
  return dispatchRecordAction(action, collection, args, deps);
2844
2699
  }
2845
2700
  var MANAGE_COLLECTION_DEFINITION = {
2846
2701
  name: "manageCollection",
2847
- 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.",
2702
+ 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.",
2848
2703
  inputSchema: {
2849
2704
  type: "object",
2850
2705
  properties: {
@@ -2858,8 +2713,7 @@ var MANAGE_COLLECTION_DEFINITION = {
2858
2713
  "getOntology",
2859
2714
  "schemaDocs",
2860
2715
  "getSchema",
2861
- "putSchema",
2862
- "publishApp"
2716
+ "putSchema"
2863
2717
  ],
2864
2718
  description: "What to do."
2865
2719
  },
@@ -2895,10 +2749,6 @@ var MANAGE_COLLECTION_DEFINITION = {
2895
2749
  type: "object",
2896
2750
  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."
2897
2751
  },
2898
- confirm: {
2899
- type: "boolean",
2900
- 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."
2901
- },
2902
2752
  topic: {
2903
2753
  type: "string",
2904
2754
  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)."
@@ -2932,6 +2782,12 @@ Object.defineProperty(exports, "APP_ROLES", {
2932
2782
  return APP_ROLES;
2933
2783
  }
2934
2784
  });
2785
+ Object.defineProperty(exports, "APP_SLUGS_COLLECTION", {
2786
+ enumerable: true,
2787
+ get: function() {
2788
+ return APP_SLUGS_COLLECTION;
2789
+ }
2790
+ });
2935
2791
  Object.defineProperty(exports, "AuthoredAppZ", {
2936
2792
  enumerable: true,
2937
2793
  get: function() {
@@ -2992,6 +2848,18 @@ Object.defineProperty(exports, "appSchemasPath", {
2992
2848
  return appSchemasPath;
2993
2849
  }
2994
2850
  });
2851
+ Object.defineProperty(exports, "appSlugDoc", {
2852
+ enumerable: true,
2853
+ get: function() {
2854
+ return appSlugDoc;
2855
+ }
2856
+ });
2857
+ Object.defineProperty(exports, "appStagingPath", {
2858
+ enumerable: true,
2859
+ get: function() {
2860
+ return appStagingPath;
2861
+ }
2862
+ });
2995
2863
  Object.defineProperty(exports, "applyMutateAction", {
2996
2864
  enumerable: true,
2997
2865
  get: function() {
@@ -3118,16 +2986,28 @@ Object.defineProperty(exports, "projectApp", {
3118
2986
  return projectApp;
3119
2987
  }
3120
2988
  });
3121
- Object.defineProperty(exports, "promptPathsFor", {
2989
+ Object.defineProperty(exports, "projectDeploy", {
3122
2990
  enumerable: true,
3123
2991
  get: function() {
3124
- return promptPathsFor;
2992
+ return projectDeploy;
2993
+ }
2994
+ });
2995
+ Object.defineProperty(exports, "projectPublish", {
2996
+ enumerable: true,
2997
+ get: function() {
2998
+ return projectPublish;
2999
+ }
3000
+ });
3001
+ Object.defineProperty(exports, "promoteSchema", {
3002
+ enumerable: true,
3003
+ get: function() {
3004
+ return promoteSchema;
3125
3005
  }
3126
3006
  });
3127
- Object.defineProperty(exports, "publishApp", {
3007
+ Object.defineProperty(exports, "promptPathsFor", {
3128
3008
  enumerable: true,
3129
3009
  get: function() {
3130
- return publishApp;
3010
+ return promptPathsFor;
3131
3011
  }
3132
3012
  });
3133
3013
  Object.defineProperty(exports, "publishProblems", {
@@ -3184,6 +3064,12 @@ Object.defineProperty(exports, "schemaRelations", {
3184
3064
  return schemaRelations;
3185
3065
  }
3186
3066
  });
3067
+ Object.defineProperty(exports, "stagedRuleConfig", {
3068
+ enumerable: true,
3069
+ get: function() {
3070
+ return stagedRuleConfig;
3071
+ }
3072
+ });
3187
3073
  Object.defineProperty(exports, "successorId", {
3188
3074
  enumerable: true,
3189
3075
  get: function() {
@@ -3203,4 +3089,4 @@ Object.defineProperty(exports, "validateRecordObject", {
3203
3089
  }
3204
3090
  });
3205
3091
 
3206
- //# sourceMappingURL=server-CWZyg8fn.cjs.map
3092
+ //# sourceMappingURL=server-X_magqUa.cjs.map