@mulmoclaude/core 0.24.0 → 0.25.1

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 (103) hide show
  1. package/assets/helps/collection-skills.md +51 -0
  2. package/assets/helps/custom-view.md +37 -3
  3. package/assets/helps/error-recovery.md +30 -0
  4. package/assets/helps/google-calendar-collection.md +79 -0
  5. package/assets/helps/index.md +1 -0
  6. package/dist/collection/core/project.d.ts +3 -0
  7. package/dist/collection/core/schema.d.ts +13 -1
  8. package/dist/collection/core/schemaZ.d.ts +45 -0
  9. package/dist/collection/index.cjs +18 -14
  10. package/dist/collection/index.cjs.map +1 -1
  11. package/dist/collection/index.d.ts +1 -0
  12. package/dist/collection/index.js +4 -2
  13. package/dist/collection/index.js.map +1 -1
  14. package/dist/collection/paths.cjs +5 -55
  15. package/dist/collection/paths.js +1 -51
  16. package/dist/collection/registry/server/index.cjs +20 -19
  17. package/dist/collection/registry/server/index.cjs.map +1 -1
  18. package/dist/collection/registry/server/index.js +3 -2
  19. package/dist/collection/registry/server/index.js.map +1 -1
  20. package/dist/collection/server/csvStore.d.ts +8 -1
  21. package/dist/collection/server/discoveredCollection.d.ts +4 -0
  22. package/dist/collection/server/discovery.d.ts +1 -0
  23. package/dist/collection/server/index.cjs +58 -54
  24. package/dist/collection/server/index.d.ts +1 -0
  25. package/dist/collection/server/index.js +5 -4
  26. package/dist/collection/server/queryRunner.d.ts +4 -0
  27. package/dist/collection/server/spawn.d.ts +2 -1
  28. package/dist/collection/server/sqliteStore.d.ts +14 -0
  29. package/dist/collection/server/store.d.ts +43 -8
  30. package/dist/collection/server/storePage.d.ts +32 -0
  31. package/dist/collection-watchers/index.cjs +131 -44
  32. package/dist/collection-watchers/index.cjs.map +1 -1
  33. package/dist/collection-watchers/index.d.ts +1 -1
  34. package/dist/collection-watchers/index.js +125 -39
  35. package/dist/collection-watchers/index.js.map +1 -1
  36. package/dist/collection-watchers/reconciler.d.ts +13 -11
  37. package/dist/collection-watchers/watcher.d.ts +4 -3
  38. package/dist/{server-BpEo0FOd.js → discovery-QUi_7aNG.js} +414 -1495
  39. package/dist/discovery-QUi_7aNG.js.map +1 -0
  40. package/dist/{server-CkysfLh4.cjs → discovery-cwtfyDxl.cjs} +463 -1658
  41. package/dist/discovery-cwtfyDxl.cjs.map +1 -0
  42. package/dist/feeds/index.cjs +4 -4
  43. package/dist/feeds/index.js +2 -2
  44. package/dist/feeds/server/index.cjs +35 -22
  45. package/dist/feeds/server/index.cjs.map +1 -1
  46. package/dist/feeds/server/index.js +26 -13
  47. package/dist/feeds/server/index.js.map +1 -1
  48. package/dist/google/apiClient.d.ts +8 -1
  49. package/dist/google/calendar.d.ts +35 -0
  50. package/dist/google/calendarSyncStore.d.ts +7 -0
  51. package/dist/google/collectionSync.d.ts +70 -0
  52. package/dist/google/index.cjs +294 -4
  53. package/dist/google/index.cjs.map +1 -1
  54. package/dist/google/index.d.ts +4 -2
  55. package/dist/google/index.js +279 -5
  56. package/dist/google/index.js.map +1 -1
  57. package/dist/ids-Dl9Ij63K.cjs +163 -0
  58. package/dist/ids-Dl9Ij63K.cjs.map +1 -0
  59. package/dist/ids-P2T89Gou.js +92 -0
  60. package/dist/ids-P2T89Gou.js.map +1 -0
  61. package/dist/{ingestTypes-CFEgTItj.js → ingestTypes-C-IQB4nw.js} +2 -2
  62. package/dist/{ingestTypes-CFEgTItj.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
  63. package/dist/{ingestTypes-Stc7iQyA.cjs → ingestTypes-DZpAC3N_.cjs} +3 -3
  64. package/dist/{ingestTypes-Stc7iQyA.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
  65. package/dist/project-BWI5w_BT.cjs +17 -0
  66. package/dist/project-BWI5w_BT.cjs.map +1 -0
  67. package/dist/project-bU98ycsy.js +12 -0
  68. package/dist/project-bU98ycsy.js.map +1 -0
  69. package/dist/{promptSafety-BLUcImzd.js → promptSafety-Boz5qOs-.js} +2 -84
  70. package/dist/promptSafety-Boz5qOs-.js.map +1 -0
  71. package/dist/{promptSafety-DI_1Rvhk.cjs → promptSafety-Co4cYQ_Z.cjs} +1 -149
  72. package/dist/promptSafety-Co4cYQ_Z.cjs.map +1 -0
  73. package/dist/remote-host/server/index.cjs +6 -4
  74. package/dist/remote-host/server/index.cjs.map +1 -1
  75. package/dist/remote-host/server/index.js +6 -4
  76. package/dist/remote-host/server/index.js.map +1 -1
  77. package/dist/remote-view/index.cjs +5 -3
  78. package/dist/remote-view/index.cjs.map +1 -1
  79. package/dist/remote-view/index.d.ts +3 -1
  80. package/dist/remote-view/index.js +5 -3
  81. package/dist/remote-view/index.js.map +1 -1
  82. package/dist/scheduler/index.cjs +3 -1
  83. package/dist/scheduler/index.cjs.map +1 -1
  84. package/dist/scheduler/index.js +3 -1
  85. package/dist/scheduler/index.js.map +1 -1
  86. package/dist/server-BI3wnTwl.js +1529 -0
  87. package/dist/server-BI3wnTwl.js.map +1 -0
  88. package/dist/server-CKdWENZ-.cjs +1704 -0
  89. package/dist/server-CKdWENZ-.cjs.map +1 -0
  90. package/dist/skill-bridge/index.cjs +2 -2
  91. package/dist/skill-bridge/index.cjs.map +1 -1
  92. package/dist/skill-bridge/index.js +1 -1
  93. package/dist/templatePath-27vUZowm.cjs +75 -0
  94. package/dist/templatePath-27vUZowm.cjs.map +1 -0
  95. package/dist/templatePath-k_WNbL_Q.js +52 -0
  96. package/dist/templatePath-k_WNbL_Q.js.map +1 -0
  97. package/package.json +1 -1
  98. package/dist/collection/paths.cjs.map +0 -1
  99. package/dist/collection/paths.js.map +0 -1
  100. package/dist/promptSafety-BLUcImzd.js.map +0 -1
  101. package/dist/promptSafety-DI_1Rvhk.cjs.map +0 -1
  102. package/dist/server-BpEo0FOd.js.map +0 -1
  103. package/dist/server-CkysfLh4.cjs.map +0 -1
@@ -0,0 +1,1704 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
2
+ const require_ids = require("./ids-Dl9Ij63K.cjs");
3
+ const require_promptSafety = require("./promptSafety-Co4cYQ_Z.cjs");
4
+ const require_discovery = require("./discovery-cwtfyDxl.cjs");
5
+ const require_skill_bridge_index = require("./skill-bridge/index.cjs");
6
+ let node_path = require("node:path");
7
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
8
+ let node_fs_promises = require("node:fs/promises");
9
+ let node_crypto = require("node:crypto");
10
+ let zod = require("zod");
11
+ //#region src/collection/server/jsonlQuery.ts
12
+ /** SQL semantics for an aggregate-only query over ZERO rows: one scalar
13
+ * row (`count` = 0, everything else NULL) — the same shape the CSV path
14
+ * produces for a header-only file, so callers reading `rows[0]` never
15
+ * break on storage kind or emptiness. A grouped query over zero rows is
16
+ * zero groups (`[]`), also matching SQL. Synthesized here because DuckDB
17
+ * has no empty file to infer a schema from. */
18
+ function emptyCollectionResult(query) {
19
+ if ((query.groupBy?.length ?? 0) > 0) return [];
20
+ const aggregates = Object.entries(query.aggregates ?? {});
21
+ return [Object.fromEntries(aggregates.map(([alias, aggregate]) => [alias, aggregate.op === "count" ? 0 : null]))];
22
+ }
23
+ /** The SOURCE columns a query reads: groupBy columns, aggregate input
24
+ * columns, and where fields. (orderBy resolves to groupBy columns or
25
+ * aggregate ALIASES — never to a new source column — so it adds none.) */
26
+ function referencedSourceColumns(query) {
27
+ const columns = new Set(query.groupBy ?? []);
28
+ for (const aggregate of Object.values(query.aggregates ?? {})) if (aggregate.column !== void 0) columns.add(aggregate.column);
29
+ for (const cond of query.where ?? []) columns.add(cond.field);
30
+ return [...columns];
31
+ }
32
+ /** Referenced columns that appear in NO row — a freshly-added optional
33
+ * field, or a derived field whose inputs no record has yet. DuckDB only
34
+ * infers columns from keys that occur in the JSONL, so without help a
35
+ * valid query on such a field binder-errors here while the CSV path
36
+ * (whose header always declares the column) returns NULLs. Padding the
37
+ * FIRST line with `col: null` makes the column exist (full-scan
38
+ * inference unions keys across lines) with matching NULL semantics. */
39
+ function absentReferencedColumns(rows, query) {
40
+ const present = /* @__PURE__ */ new Set();
41
+ for (const row of rows) for (const key of Object.keys(row)) present.add(key);
42
+ return referencedSourceColumns(query).filter((column) => !present.has(column));
43
+ }
44
+ /** Run a validated query over enriched collection rows. */
45
+ async function runQueryOverRows(rows, query) {
46
+ if (rows.length === 0) return emptyCollectionResult(query);
47
+ await (0, node_fs_promises.mkdir)(require_discovery.cacheDir(), {
48
+ recursive: true,
49
+ mode: 448
50
+ });
51
+ const jsonlPath = node_path.default.join(require_discovery.cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
52
+ try {
53
+ const nullPads = Object.fromEntries(absentReferencedColumns(rows, query).map((column) => [column, null]));
54
+ const handle = await (0, node_fs_promises.open)(jsonlPath, "wx", 384);
55
+ try {
56
+ let first = true;
57
+ for (const row of rows) {
58
+ await handle.write(`${JSON.stringify(first ? {
59
+ ...nullPads,
60
+ ...row
61
+ } : row)}\n`);
62
+ first = false;
63
+ }
64
+ } finally {
65
+ await handle.close();
66
+ }
67
+ const { sql, params } = require_discovery.compileJsonlQuery(query);
68
+ return (await require_discovery.queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, require_discovery.normalizeCsvValue(value)])));
69
+ } finally {
70
+ await (0, node_fs_promises.unlink)(jsonlPath).catch((err) => {
71
+ if (err.code !== "ENOENT") require_discovery.log.warn("collections", "temp JSONL cleanup failed", {
72
+ path: jsonlPath,
73
+ error: String(err)
74
+ });
75
+ });
76
+ }
77
+ }
78
+ //#endregion
79
+ //#region src/collection/server/derive.ts
80
+ /** Slugs of every collection referenced by a `ref` field — top-level
81
+ * and one level into `table` sub-fields (nested tables are
82
+ * schema-rejected). Mirrors the client's `uniqueRefTargets`. */
83
+ function uniqueRefTargets(schema) {
84
+ const targets = /* @__PURE__ */ new Set();
85
+ const walk = (fields) => {
86
+ for (const field of Object.values(fields)) {
87
+ if (field.type === "ref" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
88
+ if (field.type === "table" && field.of) walk(field.of);
89
+ }
90
+ };
91
+ walk(schema.fields);
92
+ return [...targets];
93
+ }
94
+ /** Slugs of every collection referenced by an `embed` field (top-level
95
+ * only, like the client). */
96
+ function uniqueEmbedTargets(schema) {
97
+ const targets = /* @__PURE__ */ new Set();
98
+ for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
99
+ return [...targets];
100
+ }
101
+ /** Slugs of every SOURCE collection a `backlinks` or `rollup` field
102
+ * reverses over — loaded exactly like ref/embed targets (whole
103
+ * collection, once; the two field kinds share one load). */
104
+ function uniqueBacklinkSources(schema) {
105
+ const sources = /* @__PURE__ */ new Set();
106
+ for (const field of Object.values(schema.fields)) if ((field.type === "backlinks" || field.type === "rollup") && field.from.length > 0) sources.add(field.from);
107
+ return [...sources];
108
+ }
109
+ async function loadTarget(slug, opts) {
110
+ const target = await require_discovery.loadCollection(slug, opts);
111
+ if (!target) return null;
112
+ const items = await require_discovery.storeFor(target, { workspaceRoot: opts.workspaceRoot }).list();
113
+ const byId = {};
114
+ for (const item of items) {
115
+ const itemId = item[target.schema.primaryKey];
116
+ if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_promptSafety.deriveAll(target.schema, item, {});
117
+ }
118
+ return {
119
+ schema: target.schema,
120
+ byId
121
+ };
122
+ }
123
+ /** Load every ref/embed target and backlink source collection once.
124
+ * Unknown / unloadable targets are simply absent — downstream derefs
125
+ * resolve to null (em-dash) and backlinks to an empty row set, the
126
+ * same fail-soft the UI renders. */
127
+ async function loadLinkedTargets(schema, opts) {
128
+ const slugs = [.../* @__PURE__ */ new Set([
129
+ ...uniqueRefTargets(schema),
130
+ ...uniqueEmbedTargets(schema),
131
+ ...uniqueBacklinkSources(schema)
132
+ ])];
133
+ const loaded = {};
134
+ for (const slug of slugs) {
135
+ const target = await loadTarget(slug, opts);
136
+ if (target) loaded[slug] = target;
137
+ }
138
+ return loaded;
139
+ }
140
+ function toRefRecords(linked) {
141
+ return Object.fromEntries(Object.entries(linked).map(([slug, target]) => [slug, target.byId]));
142
+ }
143
+ /** The matching source rows for one `backlinks` field, projected to the
144
+ * source primaryKey + `display` columns — so getItems on a
145
+ * heavily-referenced record stays a summary, not a dump of the source
146
+ * collection. Missing source ⇒ [] (fail-soft). The rows come from the
147
+ * DERIVED source records (`byId`), so `display`/`filter` on a derived
148
+ * source column (an invoice `total`) works. */
149
+ function projectBacklinks(field, schema, enriched, linked) {
150
+ const source = linked[field.from];
151
+ if (!source) return [];
152
+ return require_promptSafety.backlinkRows(field, String(enriched[schema.primaryKey] ?? ""), Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
153
+ }
154
+ /** Project the computed (never-stored) field kinds onto one derived
155
+ * record: `toggle` → boolean off its enum, `embed` → the target record
156
+ * (fixed `id` or per-record `idField`), or null when missing,
157
+ * `backlinks` → the matching source rows (see `projectBacklinks`). */
158
+ function projectComputed(schema, enriched, linked) {
159
+ for (const [key, field] of Object.entries(schema.fields)) {
160
+ if (field.type === "toggle" && field.field) enriched[key] = String(enriched[field.field] ?? "") === field.onValue;
161
+ if (field.type === "embed" && field.to) {
162
+ const targetId = require_ids.embedTargetId(field, enriched);
163
+ enriched[key] = targetId && linked[field.to]?.byId[targetId] || null;
164
+ }
165
+ if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
166
+ }
167
+ return enriched;
168
+ }
169
+ /** Resolve every rollup field onto a COPY of the record, BEFORE the
170
+ * formula pass — a `derived` formula may reference rollup values as
171
+ * plain identifiers (`played = homePlayed + awayPlayed`). Same reverse
172
+ * machinery as backlinks, collapsed to a number: an unresolvable source
173
+ * is null (a formula reading it fails soft to em-dash); an empty match
174
+ * set is a real 0. Returns `record` unchanged when the schema declares
175
+ * no rollups. */
176
+ function projectRollups(schema, record, linked) {
177
+ let out = record;
178
+ for (const [key, field] of Object.entries(schema.fields)) {
179
+ if (field.type !== "rollup") continue;
180
+ if (out === record) out = { ...record };
181
+ const source = linked[field.from];
182
+ const selfId = String(record[schema.primaryKey] ?? "");
183
+ out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
184
+ }
185
+ return out;
186
+ }
187
+ /** Enrich records with every host-computed field: derived formulas
188
+ * evaluated (cross-collection derefs included), toggles projected,
189
+ * embeds resolved. Loads each linked collection ONCE per call. Input
190
+ * records are not mutated. */
191
+ async function enrichItems(collection, items, opts = {}) {
192
+ const { schema } = collection;
193
+ const linked = await loadLinkedTargets(schema, opts);
194
+ const refRecords = toRefRecords(linked);
195
+ return items.map((item) => projectComputed(schema, require_promptSafety.deriveAll(schema, projectRollups(schema, item, linked), refRecords), linked));
196
+ }
197
+ //#endregion
198
+ //#region src/collection/server/queryRunner.ts
199
+ async function runCollectionQuery(collection, query, opts = {}) {
200
+ const store = require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot });
201
+ if (store.query) return store.query(query);
202
+ return runQueryOverRows(await enrichItems(collection, await store.list(), opts), query);
203
+ }
204
+ //#endregion
205
+ //#region src/collection/core/recordZ.ts
206
+ /** The emptiness rule shared by `required` and the "only check present
207
+ * values" gate. NOT a truthiness check — `0` and `false` are filled. */
208
+ var isEmptyValue = (value) => value === void 0 || value === null || value === "";
209
+ /** The historical write-gate checks, verbatim: required non-empty, enum
210
+ * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
211
+ function enforcedProblem(key, spec, value) {
212
+ const empty = isEmptyValue(value);
213
+ if (spec.required && empty) return `missing required field '${key}'`;
214
+ if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
215
+ return null;
216
+ }
217
+ /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
218
+ * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
219
+ * the lint flags exactly the values the calendar / trigger / spawn code
220
+ * would silently drop — impossible days like `2026-02-30`, and datetimes
221
+ * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
222
+ * which the day view can't place). `string`-backed types accept anything
223
+ * stringifiable; `ref` existence is out of scope. */
224
+ function strictTypeProblem(key, spec, value) {
225
+ switch (spec.type) {
226
+ case "number":
227
+ case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
228
+ case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
229
+ case "date": return require_promptSafety.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
230
+ case "datetime": return require_promptSafety.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)`;
231
+ default: return null;
232
+ }
233
+ }
234
+ /** Strict check for a PRESENT `table` value: an array of row objects, each
235
+ * row conforming to the sub-schema (required / enum / typed sub-values).
236
+ * First row problem wins, prefixed with the row number so the fix is
237
+ * locatable. */
238
+ function strictTableProblem(key, spec, value) {
239
+ if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
240
+ for (let index = 0; index < value.length; index++) {
241
+ const row = value[index];
242
+ if (!row || typeof row !== "object" || Array.isArray(row)) return `'${key}' row ${index + 1} is not an object`;
243
+ for (const [subKey, subSpec] of Object.entries(spec.of)) {
244
+ const subValue = row[subKey];
245
+ const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
246
+ if (problem) return `'${key}' row ${index + 1}: ${problem}`;
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+ /** First problem for one field's stored value under `tier`, or null.
252
+ * Enforced checks always run (and their messages never vary by tier — the
253
+ * scan and the write gate must agree on them); strict adds the per-type
254
+ * layer on present values only. */
255
+ function recordFieldProblem(key, spec, value, tier) {
256
+ const enforced = enforcedProblem(key, spec, value);
257
+ if (enforced || tier === "enforced") return enforced;
258
+ if (isEmptyValue(value)) return null;
259
+ if (spec.type === "table") return strictTableProblem(key, spec, value);
260
+ return strictTypeProblem(key, spec, value);
261
+ }
262
+ var compiled = /* @__PURE__ */ new WeakMap();
263
+ /** Compile `schema.fields` into a zod validator for a stored record.
264
+ * Loose object: unknown keys are allowed and any declared key may be
265
+ * absent (records are user files, not parse-and-rewrite targets —
266
+ * callers validate, they never persist the parse output). The checks run
267
+ * as ONE object-level refine iterating fields in declaration order —
268
+ * per-key shape schemas can't express "key may be absent BUT its absence
269
+ * must still reach the required check", and the single loop keeps the
270
+ * first reported issue identical to the historical first-problem-wins
271
+ * contract. */
272
+ function compileRecordZ(schema, tier) {
273
+ const cached = compiled.get(schema)?.[tier];
274
+ if (cached) return cached;
275
+ const stored = Object.entries(schema.fields).filter(([, spec]) => !require_ids.COMPUTED_TYPES.has(spec.type));
276
+ const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
277
+ for (const [key, spec] of stored) {
278
+ const problem = recordFieldProblem(key, spec, record[key], tier);
279
+ if (problem) ctx.addIssue({
280
+ code: "custom",
281
+ message: problem,
282
+ path: [key]
283
+ });
284
+ }
285
+ });
286
+ const entry = compiled.get(schema) ?? {};
287
+ entry[tier] = validator;
288
+ compiled.set(schema, entry);
289
+ return validator;
290
+ }
291
+ /** First schema problem on an in-memory record under `tier`, or null. One
292
+ * issue per record keeps the report short and the fix obvious (the
293
+ * historical contract of `validateRecordObject`). */
294
+ function firstRecordProblem(record, schema, tier) {
295
+ const result = compileRecordZ(schema, tier).safeParse(record);
296
+ if (result.success) return null;
297
+ return result.error.issues[0]?.message ?? "record failed schema validation";
298
+ }
299
+ //#endregion
300
+ //#region src/collection/server/validate.ts
301
+ var MAX_ISSUES = 25;
302
+ /** Read every `<id>.json` under the collection's dataDir and report the
303
+ * ones that won't load or violate the schema. An empty list means every
304
+ * record is fine. */
305
+ /** List entries under the data dir, guarding realpath containment (against a
306
+ * symlinked dir swapped in after discovery, like `listItems`) and treating a
307
+ * missing dir as empty while surfacing real I/O faults. */
308
+ async function listRecordFilenames(dataDir, workspaceRoot) {
309
+ if (!require_discovery.isContainedInRoot(dataDir, workspaceRoot)) {
310
+ require_discovery.log.warn("collections", "validate refused: dataDir escapes workspace via symlink", { dataDir });
311
+ return [];
312
+ }
313
+ try {
314
+ return await (0, node_fs_promises.readdir)(dataDir);
315
+ } catch (err) {
316
+ if (err.code === "ENOENT") return [];
317
+ throw err;
318
+ }
319
+ }
320
+ async function validateCollectionRecords(collection, opts = {}) {
321
+ if (collection.schema.dataSource !== void 0) return [];
322
+ if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
323
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
324
+ const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
325
+ const issues = [];
326
+ for (const name of entries.sort()) {
327
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
328
+ if (issues.length >= MAX_ISSUES) break;
329
+ const issue = await inspectRecord(node_path.default.join(collection.dataDir, name), name, collection.schema);
330
+ if (issue) issues.push(issue);
331
+ }
332
+ return issues;
333
+ }
334
+ /** Store-backed twin of the file scan: list every record through the
335
+ * collection's store and lint it with the same "strict" report-only tier.
336
+ * A row the store can't even parse is invisible here (the store skips
337
+ * it), so the read/parse classifications of the file scan don't apply —
338
+ * schema violations are what this catches. `file` carries the record id
339
+ * (there is no per-record filename). */
340
+ async function validateStoreRecords(collection, opts) {
341
+ let items;
342
+ try {
343
+ items = await require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
344
+ } catch (err) {
345
+ return [{
346
+ file: "(store)",
347
+ problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
348
+ }];
349
+ }
350
+ const issues = [];
351
+ for (const item of items) {
352
+ if (issues.length >= MAX_ISSUES) break;
353
+ const itemId = String(item[collection.schema.primaryKey] ?? "");
354
+ const problem = validateRecordObject(item, itemId, collection.schema, "strict");
355
+ if (problem) issues.push({
356
+ file: itemId,
357
+ problem
358
+ });
359
+ }
360
+ return issues;
361
+ }
362
+ async function readRecordText(fullPath, name) {
363
+ try {
364
+ if (!(await (0, node_fs_promises.lstat)(fullPath)).isFile()) return {
365
+ file: name,
366
+ problem: "not a regular file (symlink?) — skipped, won't appear"
367
+ };
368
+ return { raw: await (0, node_fs_promises.readFile)(fullPath, "utf-8") };
369
+ } catch {
370
+ return {
371
+ file: name,
372
+ problem: "could not be read — skipped, won't appear"
373
+ };
374
+ }
375
+ }
376
+ /** Classify a single record file: unreadable / unparseable / non-object /
377
+ * schema violation, or null when it's fine. */
378
+ async function inspectRecord(fullPath, name, schema) {
379
+ const read = await readRecordText(fullPath, name);
380
+ if ("problem" in read) return read;
381
+ let parsed;
382
+ try {
383
+ parsed = JSON.parse(read.raw);
384
+ } catch (err) {
385
+ return {
386
+ file: name,
387
+ 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.`
388
+ };
389
+ }
390
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
391
+ file: name,
392
+ problem: "not a JSON object — skipped, won't appear"
393
+ };
394
+ const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
395
+ return problem ? {
396
+ file: name,
397
+ problem
398
+ } : null;
399
+ }
400
+ /** First schema problem on an in-memory record (primaryKey↔id mismatch,
401
+ * then the compiled per-field checks — see `../core/recordZ` for the two
402
+ * tiers), or null when it's fine. One issue per record keeps the report
403
+ * short and the fix obvious. Pure + exported so write paths
404
+ * (manageCollection putItems) can gate on the SAME enforced rules the
405
+ * post-hoc file scan reports — `itemId` is the id the record is (or
406
+ * would be) stored under. The default `"enforced"` tier keeps every
407
+ * write gate on the historical three checks; only pass `"strict"` from
408
+ * report-only surfaces. */
409
+ function validateRecordObject(record, itemId, schema, tier = "enforced") {
410
+ const idValue = record[schema.primaryKey];
411
+ if (typeof idValue !== "string" || idValue !== itemId) return `'${schema.primaryKey}' is '${String(idValue ?? "")}' but must equal the filename ('${itemId}'), or the record can't be opened`;
412
+ return firstRecordProblem(record, schema, tier);
413
+ }
414
+ //#endregion
415
+ //#region src/collection/server/mutate.ts
416
+ /** First problem with the submitted params, or null. Every declared param
417
+ * is checked by the shared record-field validator; keys the action never
418
+ * declared are rejected outright — a stray key would otherwise ride the
419
+ * resolved `set` semantics silently. */
420
+ function firstMutateParamProblem(action, params) {
421
+ const declared = action.params ?? {};
422
+ for (const key of Object.keys(params)) if (declared[key] === void 0) return `unknown param '${key}' — not declared by action '${action.id}'`;
423
+ for (const [key, spec] of Object.entries(declared)) {
424
+ const problem = recordFieldProblem(key, spec, params[key], "strict");
425
+ if (problem) return problem;
426
+ }
427
+ return null;
428
+ }
429
+ /** Apply one mutate action to one record. Never throws for data-shaped
430
+ * failures — the outcome's `problem` doubles as agent feedback when an
431
+ * LLM (not the user) pressed the button. */
432
+ async function applyMutateAction(collection, action, itemId, params, opts = {}) {
433
+ const paramProblem = firstMutateParamProblem(action, params);
434
+ if (paramProblem) return {
435
+ ok: false,
436
+ status: "invalid-params",
437
+ problem: paramProblem
438
+ };
439
+ const store = require_discovery.storeFor(collection, opts);
440
+ if (!store.write) return {
441
+ ok: false,
442
+ status: "write-refused",
443
+ problem: "write refused (read-only collection)"
444
+ };
445
+ const existing = await store.read(itemId);
446
+ if (!existing) return {
447
+ ok: false,
448
+ status: "not-found",
449
+ problem: `item '${itemId}' not found`
450
+ };
451
+ if (!require_promptSafety.actionVisible(action, existing)) return {
452
+ ok: false,
453
+ status: "require-unmet",
454
+ problem: `action '${action.id}' is not available for item '${itemId}' in its current state`
455
+ };
456
+ const stored = Object.entries(existing).filter(([key]) => {
457
+ const spec = collection.schema.fields[key];
458
+ return !spec || !require_ids.COMPUTED_TYPES.has(spec.type);
459
+ });
460
+ const merged = {
461
+ ...Object.fromEntries(stored),
462
+ ...require_discovery.resolveMutateSet(action.set, params)
463
+ };
464
+ const invalid = validateRecordObject(merged, itemId, collection.schema);
465
+ if (invalid) return {
466
+ ok: false,
467
+ status: "invalid-record",
468
+ problem: invalid
469
+ };
470
+ const result = await store.write(itemId, merged);
471
+ if (result.kind !== "ok") return {
472
+ ok: false,
473
+ status: "write-refused",
474
+ problem: `write refused (${result.kind})`
475
+ };
476
+ return {
477
+ ok: true,
478
+ item: merged
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/collection/server/ontology.ts
483
+ /** Extract the relations a schema declares, in field declaration order:
484
+ * top-level `ref` / `embed` / `backlinks` fields plus `ref` sub-fields
485
+ * inside `table` columns. Pure — exported so the phase-2 graph panel
486
+ * can reuse it on already-loaded schemas. */
487
+ function schemaRelations(schema) {
488
+ const relations = [];
489
+ for (const [key, spec] of Object.entries(schema.fields)) {
490
+ if (spec.type === "ref" || spec.type === "embed") relations.push({
491
+ field: key,
492
+ kind: spec.type,
493
+ to: spec.to
494
+ });
495
+ if (spec.type === "backlinks" || spec.type === "rollup") relations.push({
496
+ field: key,
497
+ kind: spec.type,
498
+ to: spec.from
499
+ });
500
+ if (spec.type !== "table") continue;
501
+ for (const [subKey, subSpec] of Object.entries(spec.of)) if (subSpec.type === "ref") relations.push({
502
+ field: `${key}.${subKey}`,
503
+ kind: "ref",
504
+ to: subSpec.to
505
+ });
506
+ }
507
+ return relations;
508
+ }
509
+ /** Count the record files in a collection's data dir — the same
510
+ * `<id>.json` entries `listItems` considers, WITHOUT parsing them (the
511
+ * ontology is a summary; a malformed record is still a record). Each
512
+ * candidate is classified by the SHARED `isRegularFile` lstat helper —
513
+ * the exact file-disclosure defense `listItems` applies — so a
514
+ * symlinked record never counts and the count can't diverge from the
515
+ * readable set on any filesystem (Codex review on PR #2099, twice).
516
+ * Fail-soft: a missing dir or a dataDir escaping the workspace via
517
+ * symlink counts 0. */
518
+ async function countRecordFiles(dataDir, workspaceRoot) {
519
+ if (!require_discovery.isContainedInRoot(dataDir, workspaceRoot)) return 0;
520
+ try {
521
+ const names = (await (0, node_fs_promises.readdir)(dataDir)).filter((name) => name.endsWith(".json") && !name.startsWith("."));
522
+ return (await Promise.all(names.map((name) => require_discovery.isRegularFile(node_path.default.join(dataDir, name))))).filter(Boolean).length;
523
+ } catch {
524
+ return 0;
525
+ }
526
+ }
527
+ /** A collection's record count for the ontology. File-backed: the cheap
528
+ * readdir count. Every other backend (`dataSource` CSV, `storage`
529
+ * sqlite): the store's page total — the dataDir is a phantom there, so
530
+ * the readdir count would always misreport 0. Fail-soft to 0 like
531
+ * `countRecordFiles` (an unreadable file / missing engine must not
532
+ * break the whole ontology). */
533
+ async function countRecords(collection, workspaceRoot) {
534
+ if (require_ids.storageKindFor(collection.schema) !== "file") try {
535
+ return (await require_discovery.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
536
+ } catch {
537
+ return 0;
538
+ }
539
+ return countRecordFiles(collection.dataDir, workspaceRoot);
540
+ }
541
+ async function toOntologyEntry(collection, workspaceRoot) {
542
+ const { schema } = collection;
543
+ return {
544
+ slug: collection.slug,
545
+ title: schema.title,
546
+ icon: schema.icon,
547
+ primaryKey: schema.primaryKey,
548
+ displayField: schema.displayField ?? schema.primaryKey,
549
+ recordCount: await countRecords(collection, workspaceRoot),
550
+ relations: schemaRelations(schema)
551
+ };
552
+ }
553
+ /** Build the workspace ontology: every discovered collection (slug-sorted,
554
+ * discovery's order), each with its outbound relations and a
555
+ * readdir-cheap record count — fine to call on demand. */
556
+ async function buildWorkspaceOntology(opts = {}) {
557
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
558
+ const collections = await require_discovery.discoverCollections(opts);
559
+ return Promise.all(collections.map((collection) => toOntologyEntry(collection, workspaceRoot)));
560
+ }
561
+ //#endregion
562
+ //#region src/collection/server/dynamicIcon.ts
563
+ /** Index `items` by their `primaryKey` value — the `recordsById` map a
564
+ * `valueFrom` reference (e.g. `_config.defaultCity`) resolves against.
565
+ * Items whose primary key isn't a string (shouldn't happen for a valid
566
+ * schema, but records are untyped storage) are skipped rather than
567
+ * coerced, so a broken key never silently shadows a real one. */
568
+ function buildRecordsById(items, primaryKey) {
569
+ const entries = items.filter((item) => typeof item[primaryKey] === "string").map((item) => [String(item[primaryKey]), item]);
570
+ return Object.fromEntries(entries);
571
+ }
572
+ /** Order records by their `primaryKey` so record selection is deterministic:
573
+ * `listItems` returns filesystem `readdir` order (arbitrary across machines),
574
+ * which would let `from: "first"`, the no-`orderBy` `latest`, and `orderBy`
575
+ * ties pick a different record — and thus a different icon — between
576
+ * reconciles. A stable id sort pins one answer. */
577
+ function sortByPrimaryKey(items, primaryKey) {
578
+ return [...items].sort((left, right) => String(left[primaryKey] ?? "").localeCompare(String(right[primaryKey] ?? "")));
579
+ }
580
+ /** Compute the effective launcher icon for `collection`: its static
581
+ * `schema.icon` when it declares no `dynamicIcon`, else the icon
582
+ * resolved from `dynamicIcon.source`'s RAW stored records (no
583
+ * derive/enrich — the icon rules match against stored values) via the
584
+ * pure resolver. Fails soft on any read/discovery error (missing source
585
+ * collection, filesystem error): falls back to `dynamicIcon.fallback ??
586
+ * schema.icon` rather than surfacing to the collections list. */
587
+ async function computeCollectionIcon(collection, opts = {}) {
588
+ const { schema } = collection;
589
+ const spec = schema.dynamicIcon;
590
+ if (!spec) return schema.icon;
591
+ try {
592
+ const source = await require_discovery.loadCollection(spec.source.collection, opts);
593
+ if (!source) return spec.fallback ?? schema.icon;
594
+ const ordered = sortByPrimaryKey(await require_discovery.storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
595
+ const orderBy = spec.source.orderBy ?? require_promptSafety.firstDateField(source.schema);
596
+ const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
597
+ return require_promptSafety.resolveIcon(require_promptSafety.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
598
+ } catch (err) {
599
+ require_discovery.log.warn("collections", "dynamic icon compute failed, falling back", {
600
+ slug: collection.slug,
601
+ source: spec.source.collection,
602
+ error: String(err)
603
+ });
604
+ return spec.fallback ?? schema.icon;
605
+ }
606
+ }
607
+ //#endregion
608
+ //#region src/collection/server/util.ts
609
+ /** Human-readable message from an unknown thrown value. */
610
+ function errorMessage(err) {
611
+ return err instanceof Error ? err.message : String(err);
612
+ }
613
+ var ONE_SECOND_MS = 1e3;
614
+ var ONE_DAY_MS = 1440 * 60 * 1e3;
615
+ //#endregion
616
+ //#region src/collection/server/spawn.ts
617
+ var YMD_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
618
+ function pad2(value) {
619
+ return String(value).padStart(2, "0");
620
+ }
621
+ function pad4(value) {
622
+ return String(value).padStart(4, "0");
623
+ }
624
+ /** Days in `month` (1-12) of `year`, leap-year-aware. `new Date(y, m, 0)`
625
+ * is day 0 of the *next* month = the last day of `month`; `.getDate()`
626
+ * reads the civil day regardless of timezone. */
627
+ function daysInMonth(year, month) {
628
+ return new Date(year, month, 0).getDate();
629
+ }
630
+ /** Parse a `YYYY-MM-DD` string into a CivilDate, or null when the value
631
+ * isn't a well-formed in-range calendar date. */
632
+ function parseCivil(raw) {
633
+ if (typeof raw !== "string") return null;
634
+ const match = YMD_PATTERN.exec(raw.trim());
635
+ if (!match) return null;
636
+ const year = Number(match[1]);
637
+ const month = Number(match[2]);
638
+ const day = Number(match[3]);
639
+ if (month < 1 || month > 12) return null;
640
+ if (day < 1 || day > daysInMonth(year, month)) return null;
641
+ return {
642
+ y: year,
643
+ m: month,
644
+ d: day
645
+ };
646
+ }
647
+ /** `YYYY-MM-DD` for storage in a `date` field. */
648
+ function formatCivil(date) {
649
+ return `${pad4(date.y)}-${pad2(date.m)}-${pad2(date.d)}`;
650
+ }
651
+ /** A monotonic integer key for a civil date (YYYYMMDD), for ordering /
652
+ * equality without timezone concerns. */
653
+ function ordinal(date) {
654
+ return date.y * 1e4 + date.m * 100 + date.d;
655
+ }
656
+ /** Add `n` whole days to a civil date. Uses UTC epoch arithmetic so it
657
+ * is DST-immune (we only ever read back the civil Y/M/D). */
658
+ function addDays(date, days) {
659
+ const shifted = new Date(Date.UTC(date.y, date.m - 1, date.d) + days * ONE_DAY_MS);
660
+ return {
661
+ y: shifted.getUTCFullYear(),
662
+ m: shifted.getUTCMonth() + 1,
663
+ d: shifted.getUTCDate()
664
+ };
665
+ }
666
+ /** Advance a civil date by one `every` step. Month/year units preserve
667
+ * the rule's day-of-month anchor, clamped to the target month's length
668
+ * (no drift); day/week units do civil day arithmetic. */
669
+ function advanceTriggerDate(source, every) {
670
+ const { unit, interval } = every;
671
+ if (unit === "day") return addDays(source, interval);
672
+ if (unit === "week") return addDays(source, interval * 7);
673
+ const monthsToAdd = interval * (unit === "year" ? 12 : 1);
674
+ const total = source.y * 12 + (source.m - 1) + monthsToAdd;
675
+ const nextYear = Math.floor(total / 12);
676
+ const nextMonth = total % 12 + 1;
677
+ const dim = daysInMonth(nextYear, nextMonth);
678
+ const anchor = every.dayOfMonth === "last" ? dim : every.dayOfMonth ?? source.d;
679
+ return {
680
+ y: nextYear,
681
+ m: nextMonth,
682
+ d: Math.min(anchor, dim)
683
+ };
684
+ }
685
+ /** True iff `now`'s civil date (local timezone) has reached the fire date
686
+ * for `triggerRaw` — i.e. the trigger date minus `leadDays` (so a 10-day
687
+ * lead fires 10 days early). Returns null when `triggerRaw` isn't a
688
+ * parseable date — callers treat that as "don't fire" and warn. */
689
+ function isTriggerDue(triggerRaw, now, leadDays = 0) {
690
+ const due = parseCivil(triggerRaw);
691
+ if (due === null) return null;
692
+ const fireDate = leadDays > 0 ? addDays(due, -leadDays) : due;
693
+ return ordinal({
694
+ y: now.getFullYear(),
695
+ m: now.getMonth() + 1,
696
+ d: now.getDate()
697
+ }) >= ordinal(fireDate);
698
+ }
699
+ var DATE_SUFFIX_PATTERN = /-\d{8}$/;
700
+ /** Deterministic successor id: `<stem>-<YYYYMMDD>`, where `<stem>` is the
701
+ * source id with a trailing `-YYYYMMDD` stripped if present. So a chain
702
+ * shares one stem and each instance is dated:
703
+ * `rent` → `rent-20260610`
704
+ * `rent-20260610` → `rent-20260710`
705
+ * Slug-safe (alphanumeric + hyphen) and a pure function of the inputs,
706
+ * which is what makes create-if-absent idempotent. */
707
+ function successorId(sourceId, next) {
708
+ return `${sourceId.replace(DATE_SUFFIX_PATTERN, "")}-${pad4(next.y)}${pad2(next.m)}${pad2(next.d)}`;
709
+ }
710
+ /** True iff `item` satisfies the spawn predicate. With an explicit
711
+ * `when`, matches `String(item[when.field]) ∈ when.in`. Without one,
712
+ * defaults to the completion-done condition — the shared, flag-aware
713
+ * `itemIsDone` (core/completion; downhill import, so the module graph
714
+ * stays acyclic). */
715
+ function matchesWhen(when, schema, item) {
716
+ if (when) {
717
+ const raw = item[when.field];
718
+ return raw !== void 0 && raw !== null && when.in.includes(String(raw));
719
+ }
720
+ return require_promptSafety.itemIsDone(schema, item);
721
+ }
722
+ /** Resolve the literal `every` that applies to `sourceItem`. Literal-arm
723
+ * `spawn.every` passes through unchanged. Field-driven `spawn.every` reads
724
+ * `sourceItem[fromField]` and looks it up in `map`; an empty field or a
725
+ * value with no map entry yields null (caller skips + logs — see plan §5).
726
+ * Discovery rejects a map that doesn't cover the enum's values, so null
727
+ * here means a record that predates a map/enum edit. */
728
+ function resolveEvery(every, sourceItem) {
729
+ if (!require_ids.isFieldDrivenEvery(every)) return every;
730
+ const raw = sourceItem[every.fromField];
731
+ if (raw === void 0 || raw === null || raw === "") return null;
732
+ return every.map[String(raw)] ?? null;
733
+ }
734
+ /** Build the successor record purely from (schema, source record, source
735
+ * id). Returns null when the schema has no spawn/triggerField or the
736
+ * source's trigger date is unparseable. */
737
+ function computeSuccessor(schema, sourceItem, sourceId) {
738
+ const { spawn, triggerField } = schema;
739
+ if (!spawn || !triggerField) return null;
740
+ const srcDate = parseCivil(sourceItem[triggerField]);
741
+ if (srcDate === null) return null;
742
+ const every = resolveEvery(spawn.every, sourceItem);
743
+ if (every === null) return null;
744
+ const next = advanceTriggerDate(srcDate, every);
745
+ const nextId = successorId(sourceId, next);
746
+ const record = {};
747
+ for (const field of spawn.carry ?? []) if (Object.prototype.hasOwnProperty.call(sourceItem, field)) record[field] = sourceItem[field];
748
+ Object.assign(record, spawn.set ?? {});
749
+ record[triggerField] = formatCivil(next);
750
+ record[schema.primaryKey] = nextId;
751
+ return {
752
+ id: nextId,
753
+ record
754
+ };
755
+ }
756
+ /** Warn precisely about which of `computeSuccessor`'s two null causes fired
757
+ * (plan §5): an unparseable source trigger date (the original cause), or a
758
+ * field-driven `every` whose record value has no `map` entry (a record that
759
+ * predates a map/enum edit — discovery rejects this statically otherwise). */
760
+ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
761
+ if (parseCivil(sourceItem[triggerField]) === null) {
762
+ require_discovery.log.warn("collections", "spawn skipped: source trigger date unparseable", {
763
+ slug,
764
+ sourceId,
765
+ triggerField
766
+ });
767
+ return;
768
+ }
769
+ const fromField = require_ids.isFieldDrivenEvery(every) ? every.fromField : void 0;
770
+ require_discovery.log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
771
+ slug,
772
+ sourceId,
773
+ fromField,
774
+ value: fromField === void 0 ? void 0 : sourceItem[fromField]
775
+ });
776
+ }
777
+ /** Idempotently create the successor for `sourceItem` when it matches the
778
+ * spawn predicate. No-op when the schema declares no spawn, the
779
+ * predicate doesn't match, the trigger date is unparseable, or the
780
+ * successor already exists (create-if-absent). Never overwrites an
781
+ * existing successor — protects any edits the user made to it. */
782
+ async function maybeSpawnSuccessor(collection, sourceItem, sourceId, ioOpts = {}) {
783
+ const { slug, schema } = collection;
784
+ const { spawn } = schema;
785
+ if (!spawn || !schema.triggerField) return;
786
+ if (!matchesWhen(spawn.when, schema, sourceItem)) return;
787
+ const computed = computeSuccessor(schema, sourceItem, sourceId);
788
+ if (computed === null) {
789
+ logSpawnSkip(slug, schema.triggerField, spawn.every, sourceItem, sourceId);
790
+ return;
791
+ }
792
+ if (matchesWhen(spawn.when, schema, computed.record)) {
793
+ require_discovery.log.warn("collections", "spawn skipped: successor would be born matching its own predicate (unbounded respawn)", {
794
+ slug,
795
+ sourceId,
796
+ successorId: computed.id
797
+ });
798
+ return;
799
+ }
800
+ try {
801
+ const { write } = require_discovery.storeFor(collection, ioOpts);
802
+ if (!write) {
803
+ require_discovery.log.warn("collections", "spawn skipped: collection store is read-only", {
804
+ slug,
805
+ sourceId
806
+ });
807
+ return;
808
+ }
809
+ const result = await write(computed.id, computed.record, { refuseOverwrite: true });
810
+ if (result.kind === "ok") require_discovery.log.info("collections", "spawned successor", {
811
+ slug,
812
+ sourceId,
813
+ successorId: computed.id
814
+ });
815
+ else if (result.kind !== "conflict") require_discovery.log.warn("collections", "spawn write failed", {
816
+ slug,
817
+ sourceId,
818
+ successorId: computed.id,
819
+ kind: result.kind
820
+ });
821
+ } catch (err) {
822
+ require_discovery.log.warn("collections", "spawn write threw", {
823
+ slug,
824
+ sourceId,
825
+ successorId: computed.id,
826
+ error: errorMessage(err)
827
+ });
828
+ }
829
+ }
830
+ //#endregion
831
+ //#region src/collection/server/delete.ts
832
+ /** Human-readable reason for a non-`ok` delete result. Exported so the
833
+ * route maps `kind` → message without inlining the switch (keeps the
834
+ * handler short and the mapping unit-testable). The `Record` is
835
+ * exhaustive — a new refusal kind won't compile until it's added. */
836
+ function deleteCollectionRefusalMessage(result) {
837
+ const { slug } = result;
838
+ return {
839
+ "user-scope": `collection '${slug}' is user-scope (~/.claude/skills/) and is read-only from MulmoClaude`,
840
+ preset: `collection '${slug}' is a preset (mc-*) and re-seeds on restart; unstar it from the catalog instead`,
841
+ "unsafe-data-path": `collection '${slug}' declares a dataPath outside its own data/${slug}/ subtree; refusing to delete`,
842
+ "path-escape": `a directory for collection '${slug}' escapes the workspace`
843
+ }[result.kind];
844
+ }
845
+ /** The canonical staging dir for a slug: `data/skills/<slug>`. */
846
+ function stagingSkillDir(workspaceRoot, slug) {
847
+ return node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), slug);
848
+ }
849
+ async function pathExists(target) {
850
+ try {
851
+ await (0, node_fs_promises.stat)(target);
852
+ return true;
853
+ } catch {
854
+ return false;
855
+ }
856
+ }
857
+ /** UTC `YYYY-MM-DD` — keeps the archive folder human-sortable. */
858
+ function todayStamp() {
859
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
860
+ }
861
+ /** Every directory the delete will touch must resolve under the
862
+ * workspace root — guards against a symlinked ancestor escaping it. */
863
+ function deleteTargets(collection, workspaceRoot) {
864
+ return [
865
+ stagingSkillDir(workspaceRoot, collection.slug),
866
+ collection.skillDir,
867
+ collection.dataDir,
868
+ ...collection.storageFile !== void 0 ? [collection.storageFile] : []
869
+ ];
870
+ }
871
+ /** The records directory the delete recursively archives + removes
872
+ * (`collection.dataDir`) must live in this collection's OWN per-slug
873
+ * subtree. `dataDir` is normally derived from `schema.dataPath`, but
874
+ * `deleteCollection` accepts a `LoadedCollection` whose fields could be
875
+ * inconsistent — so we validate the RESOLVED target the destructive ops
876
+ * actually touch, not the schema string. A shared root like `data` or
877
+ * `data/skills` would otherwise turn the recursive removal into a
878
+ * workspace-wide wipe whose archive captures only this collection.
879
+ * `path.resolve` collapses any `..` before the prefix test (symlink
880
+ * escapes are caught separately by the realpath containment check in
881
+ * `deleteTargets`).
882
+ *
883
+ * TWO acceptable per-slug subtrees:
884
+ * - `data/<slug>/...` — the convention authored collections default to.
885
+ * - `data/collections/<slug>/...` — what `normalizedDataPath` in the
886
+ * registry-import path stamps onto every imported schema (so imported
887
+ * collections never collide with `data/<slug>/`-shaped authored ones).
888
+ * Both are equally per-collection and equally safe; a delete that targets
889
+ * anything else (e.g. raw `data/`, `data/wiki/`, a sibling slug) is
890
+ * refused. */
891
+ function isDataDirSafe(dataDir, slug, workspaceRoot) {
892
+ const acceptableRoots = [node_path.default.resolve(workspaceRoot, "data", slug), node_path.default.resolve(workspaceRoot, "data", "collections", slug)];
893
+ const resolved = node_path.default.resolve(dataDir);
894
+ return acceptableRoots.some((root) => resolved === root || resolved.startsWith(root + node_path.default.sep));
895
+ }
896
+ /** Step 2 of the restore doc — how to bring the records back. A
897
+ * `dataSource` collection has no record files to copy (its rows live in
898
+ * the external data file, which the delete never touches). */
899
+ function restoreRecordsStep(schema) {
900
+ if (schema.storage !== void 0) return `2. Records: copy the archived database file
901
+ \`${node_path.default.basename(schema.storage.path)}\` (next to this document) back to
902
+ \`${schema.storage.path}\` (workspace-relative, \`cp\`). It holds every
903
+ record. If \`-wal\`/\`-journal\` sidecar files were archived alongside
904
+ it, copy them back too.`;
905
+ if (schema.dataPath === void 0) return `2. Records: nothing to copy. This is a \`dataSource\` collection —
906
+ its records are the rows of \`${schema.dataSource?.path}\`, which the
907
+ delete never touched.`;
908
+ return `2. Copy the item data: \`cp\` every file under \`records/\` into
909
+ \`${schema.dataPath}/\`. The records are part of the collection and
910
+ must be restored. They are plain data files (NOT bridged), so use
911
+ \`cp\` — the Write-tool rule in step 1 applies ONLY to the skill
912
+ files, not to these records (there may be many; copy them, do not
913
+ Write them one by one).`;
914
+ }
915
+ function buildRestoreDoc(collection) {
916
+ const { slug, schema } = collection;
917
+ return `# Restore "${schema.title}" (collection \`${slug}\`)
918
+
919
+ This folder is an automatic backup made when the collection was deleted.
920
+ Follow these steps to restore it.
921
+
922
+ 1. Recreate the skill files in \`data/skills/${slug}/\` using the **Write
923
+ tool**: read each file under \`skill/\` and Write it to the matching
924
+ path — \`SKILL.md\`, \`schema.json\`, and any \`templates/*\`.
925
+
926
+ IMPORTANT — use the Write tool, NOT \`cp\` / \`mv\` / a shell redirect.
927
+ The skill-bridge hook mirrors \`data/skills/${slug}/\` into
928
+ \`.claude/skills/${slug}/\`, and that mirror is what actually registers
929
+ the collection. The hook only fires on Write/Edit tool calls, so a
930
+ \`cp\` would leave the files in staging with no \`.claude/skills/\`
931
+ mirror — the collection would stay invisible. (Writing
932
+ \`.claude/skills/\` directly is not an option either: that path is
933
+ permission-gated.)
934
+
935
+ ${restoreRecordsStep(schema)}
936
+
937
+ 3. Confirm the collection reappears at \`/collections/${slug}\`.
938
+
939
+ - slug: \`${slug}\`
940
+ - title: ${schema.title}
941
+ - dataPath: \`${schema.dataPath ?? (schema.storage !== void 0 ? `(storage) ${schema.storage.path}` : `(dataSource) ${schema.dataSource?.path}`)}\`
942
+ `;
943
+ }
944
+ /** Copy one skill copy + the records + RESTORE.md into `archiveDir`. */
945
+ async function writeArchive(collection, archiveDir, workspaceRoot) {
946
+ const staging = stagingSkillDir(workspaceRoot, collection.slug);
947
+ await (0, node_fs_promises.cp)(await pathExists(staging) ? staging : collection.skillDir, node_path.default.join(archiveDir, "skill"), { recursive: true });
948
+ if (await pathExists(collection.dataDir)) await (0, node_fs_promises.cp)(collection.dataDir, node_path.default.join(archiveDir, "records"), { recursive: true });
949
+ if (collection.storageFile !== void 0 && await pathExists(collection.storageFile)) {
950
+ const checkpointed = await require_discovery.checkpointSqliteDatabase(collection.storageFile);
951
+ await (0, node_fs_promises.cp)(collection.storageFile, node_path.default.join(archiveDir, node_path.default.basename(collection.storageFile)));
952
+ if (!checkpointed) for (const suffix of [
953
+ "-wal",
954
+ "-journal",
955
+ "-shm"
956
+ ]) {
957
+ const sidecar = `${collection.storageFile}${suffix}`;
958
+ if (await pathExists(sidecar)) await (0, node_fs_promises.cp)(sidecar, node_path.default.join(archiveDir, node_path.default.basename(sidecar)));
959
+ }
960
+ }
961
+ await (0, node_fs_promises.writeFile)(node_path.default.join(archiveDir, "RESTORE.md"), buildRestoreDoc(collection), "utf-8");
962
+ }
963
+ /** Remove all three locations. `rm -rf`-style (force) so a missing dir
964
+ * is a no-op; the now-empty data parent (`data/<slug>/` after its
965
+ * `items/` is gone) is swept too, but only when empty. */
966
+ async function removeLocations(collection, workspaceRoot) {
967
+ await (0, node_fs_promises.rm)(stagingSkillDir(workspaceRoot, collection.slug), {
968
+ recursive: true,
969
+ force: true
970
+ });
971
+ await (0, node_fs_promises.rm)(collection.skillDir, {
972
+ recursive: true,
973
+ force: true
974
+ });
975
+ await (0, node_fs_promises.rm)(collection.dataDir, {
976
+ recursive: true,
977
+ force: true
978
+ });
979
+ await (0, node_fs_promises.rmdir)(node_path.default.dirname(collection.dataDir)).catch(() => void 0);
980
+ if (collection.storageFile !== void 0) {
981
+ await (0, node_fs_promises.rm)(collection.storageFile, { force: true });
982
+ await (0, node_fs_promises.rm)(`${collection.storageFile}-wal`, { force: true });
983
+ await (0, node_fs_promises.rm)(`${collection.storageFile}-journal`, { force: true });
984
+ await (0, node_fs_promises.rm)(`${collection.storageFile}-shm`, { force: true });
985
+ }
986
+ }
987
+ async function deleteCollection(collection, opts = {}) {
988
+ const { slug } = collection;
989
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
990
+ if (collection.source === "user") return {
991
+ kind: "user-scope",
992
+ slug
993
+ };
994
+ if (require_discovery.isPresetSlug(slug)) return {
995
+ kind: "preset",
996
+ slug
997
+ };
998
+ if (!isDataDirSafe(collection.dataDir, slug, workspaceRoot)) {
999
+ require_discovery.log.warn("collections", "deleteCollection refused: dataDir is not under the per-collection root", {
1000
+ slug,
1001
+ dataDir: collection.dataDir
1002
+ });
1003
+ return {
1004
+ kind: "unsafe-data-path",
1005
+ slug
1006
+ };
1007
+ }
1008
+ if (deleteTargets(collection, workspaceRoot).some((target) => !require_discovery.isContainedInRoot(target, workspaceRoot))) {
1009
+ require_discovery.log.warn("collections", "deleteCollection refused: a target escapes the workspace", { slug });
1010
+ return {
1011
+ kind: "path-escape",
1012
+ slug
1013
+ };
1014
+ }
1015
+ const archiveRel = node_path.default.join(require_discovery.archiveDir(), `${opts.dateStamp ?? todayStamp()}-${(0, node_crypto.randomUUID)()}`);
1016
+ const archiveDir$1 = node_path.default.join(workspaceRoot, archiveRel);
1017
+ await (0, node_fs_promises.mkdir)(archiveDir$1, { recursive: true });
1018
+ await writeArchive(collection, archiveDir$1, workspaceRoot);
1019
+ await removeLocations(collection, workspaceRoot);
1020
+ require_discovery.log.info("collections", "collection deleted + archived", {
1021
+ slug,
1022
+ archive: archiveRel
1023
+ });
1024
+ return {
1025
+ kind: "ok",
1026
+ slug,
1027
+ archivePath: archiveRel
1028
+ };
1029
+ }
1030
+ //#endregion
1031
+ //#region src/collection/server/views.ts
1032
+ async function fileExists(target) {
1033
+ try {
1034
+ await (0, node_fs_promises.stat)(target);
1035
+ return true;
1036
+ } catch (err) {
1037
+ const { code } = err;
1038
+ if (code === "ENOENT" || code === "ENOTDIR") return false;
1039
+ throw err;
1040
+ }
1041
+ }
1042
+ /** The authoritative base dir for a collection's schema.json + view HTML.
1043
+ * For a project collection, prefer the staging tree when its schema.json is
1044
+ * present (authoring layout); otherwise fall back to the active skill dir
1045
+ * (imported layout — staging never materialised). For feed / user, it's
1046
+ * always the discovered skillDir. Matches `readCustomViewHtml` so reads and
1047
+ * deletes agree on both layouts. */
1048
+ async function canonicalBase(collection, workspaceRoot, safeSlug) {
1049
+ if (collection.source !== "project") return collection.skillDir;
1050
+ const staging = node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), safeSlug);
1051
+ if (await fileExists(node_path.default.join(staging, "schema.json"))) return staging;
1052
+ return collection.skillDir;
1053
+ }
1054
+ /** Every on-disk schema.json that must reflect the removal. The active
1055
+ * `<skillDir>/schema.json` is the discovery anchor and is always present.
1056
+ * The staging copy is included only when it actually exists, so an imported
1057
+ * project collection (no staging mirror) doesn't have an empty staging tree
1058
+ * materialised by a side effect of the delete. */
1059
+ async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
1060
+ const active = node_path.default.join(collection.skillDir, require_discovery.SCHEMA_FILE);
1061
+ if (collection.source !== "project") return [active];
1062
+ const stagingSchema = node_path.default.join(require_discovery.skillsStagingDir(workspaceRoot), safeSlug, require_discovery.SCHEMA_FILE);
1063
+ const targets = [];
1064
+ if (await fileExists(stagingSchema)) targets.push(stagingSchema);
1065
+ targets.push(active);
1066
+ return targets;
1067
+ }
1068
+ /** Idempotent unlink — a missing file is fine (the schema entry still gets
1069
+ * cleaned), but a real error (permissions, etc.) propagates. */
1070
+ async function unlinkIfPresent(target) {
1071
+ try {
1072
+ await (0, node_fs_promises.unlink)(target);
1073
+ } catch (err) {
1074
+ if (err.code !== "ENOENT") throw err;
1075
+ }
1076
+ }
1077
+ /** Re-read the canonical schema.json, drop the `views[]` entry, and write the
1078
+ * result back to every on-disk copy so staging + active stay identical. Reads
1079
+ * raw (not `collection.schema`) so fields the typed schema doesn't model are
1080
+ * preserved verbatim. */
1081
+ async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug) {
1082
+ const base = await canonicalBase(collection, workspaceRoot, safeSlug);
1083
+ const canonical = node_path.default.join(base, require_discovery.SCHEMA_FILE);
1084
+ const parsed = JSON.parse(await (0, node_fs_promises.readFile)(canonical, "utf-8"));
1085
+ if (Array.isArray(parsed.views)) parsed.views = parsed.views.filter((entry) => entry?.id !== viewId);
1086
+ const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
1087
+ for (const target of await schemaWriteTargets(collection, workspaceRoot, safeSlug)) await require_discovery.writeFileAtomic(target, serialized);
1088
+ }
1089
+ /** Delete one custom view from `collection`: unlink its HTML file and drop it
1090
+ * from every schema.json copy. User-scope and preset (mc-*) collections are
1091
+ * refused (read-only / re-seeded on boot), consistent with `deleteCollection`. */
1092
+ async function deleteCustomView(collection, viewId, opts = {}) {
1093
+ if (collection.source === "user") return { kind: "user-scope" };
1094
+ if (require_discovery.isPresetSlug(collection.slug)) return { kind: "preset" };
1095
+ const safeSlug = require_discovery.safeSlugName(collection.slug);
1096
+ if (safeSlug === null) return {
1097
+ kind: "unsafe-path",
1098
+ viewId
1099
+ };
1100
+ const views = collection.schema.views ?? [];
1101
+ const view = views.find((entry) => entry.id === viewId);
1102
+ if (!view) return {
1103
+ kind: "not-found",
1104
+ viewId
1105
+ };
1106
+ const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
1107
+ const htmlPath = require_discovery.resolveTemplatePath(await canonicalBase(collection, workspaceRoot, safeSlug), view.file);
1108
+ if (htmlPath === null) return {
1109
+ kind: "unsafe-path",
1110
+ viewId
1111
+ };
1112
+ await removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug);
1113
+ if (!views.some((entry) => entry.id !== viewId && entry.file === view.file)) await unlinkIfPresent(htmlPath);
1114
+ return {
1115
+ kind: "ok",
1116
+ viewId
1117
+ };
1118
+ }
1119
+ /** True for a launcher-managed preset slug (`mc-<something>`). The sync logic uses
1120
+ * this to bound which active skills it may refresh/prune, so user-authored skills are
1121
+ * never touched. */
1122
+ function isPresetSlug(slug) {
1123
+ return slug.startsWith("mc-") && slug.length > 3;
1124
+ }
1125
+ //#endregion
1126
+ //#region src/collection/server/manageTool.ts
1127
+ /** Refuse an unselective getItems beyond this many records — a silent
1128
+ * truncation would read as "covered everything", and an unbounded dump
1129
+ * of a large collection is a token bomb. `ids` or `fields` lifts it. */
1130
+ var MAX_UNSELECTIVE_ITEMS = 200;
1131
+ /** schema.json basename under a skill dir (canonical staging + active mirror). */
1132
+ var SCHEMA_FILE = "schema.json";
1133
+ /** The collection-authoring reference, served by the `schemaDocs` action. */
1134
+ var SCHEMA_DOCS_FILE = "collection-skills.md";
1135
+ /** Cap the rejected-schema issue list so a deeply-broken schema can't flood the result. */
1136
+ var MAX_SCHEMA_ISSUES = 20;
1137
+ /** The workspace help-docs dir both hosts seed (`@mulmoclaude/core/workspace-setup`
1138
+ * syncs the bundled assets here) — the user-editable copy schemaDocs prefers. */
1139
+ var HELPS_DIR = "config/helps";
1140
+ /** Resolve the workspace root the same way every collections call does:
1141
+ * the injected override (tests) or the configured collection host. */
1142
+ function resolveBase(deps) {
1143
+ return deps.workspaceRoot ?? require_discovery.getWorkspaceRoot();
1144
+ }
1145
+ /** Shared "unknown collection" message — its schema.json is missing or
1146
+ * failed validation, so discovery skipped it. */
1147
+ function unknownCollection(slug) {
1148
+ return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — its schema.json is missing or failed validation.`;
1149
+ }
1150
+ function optionalStringArray(value, name) {
1151
+ if (value === void 0) return { ok: true };
1152
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.length > 0)) return {
1153
+ ok: false,
1154
+ error: `manageCollection: \`${name}\` must be an array of non-empty strings when present.`
1155
+ };
1156
+ return {
1157
+ ok: true,
1158
+ value
1159
+ };
1160
+ }
1161
+ /** Project a record down to the requested fields. The primary key is
1162
+ * always kept so every returned record stays addressable for a
1163
+ * follow-up getItems/putItems. */
1164
+ function projectFields(record, fields, primaryKey) {
1165
+ const keys = fields.includes(primaryKey) ? fields : [primaryKey, ...fields];
1166
+ return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
1167
+ }
1168
+ /** The validation warning appended to a getItems result when stored
1169
+ * record files are malformed (they're silently skipped at read time,
1170
+ * so without this they'd just look missing). Issue strings quote
1171
+ * record-controlled text → defanged, mirroring the presentCollection
1172
+ * dispatch. The record VALUES in `items` ride verbatim, like a raw
1173
+ * file Read — only host-composed report strings are defanged. */
1174
+ async function recordIssuesWarning(collection, deps) {
1175
+ if (deps.ablateValidation) return void 0;
1176
+ const issues = await validateCollectionRecords(collection, { workspaceRoot: deps.workspaceRoot });
1177
+ if (issues.length === 0) return void 0;
1178
+ const lines = issues.map((issue) => `- ${require_promptSafety.defangForPrompt(issue.file)}: ${require_promptSafety.defangForPrompt(issue.problem)}`).join("\n");
1179
+ return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
1180
+ }
1181
+ async function loadRequestedItems(collection, ids, deps) {
1182
+ const store = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
1183
+ if (!ids) return {
1184
+ items: await store.list(),
1185
+ missing: []
1186
+ };
1187
+ const items = [];
1188
+ const missing = [];
1189
+ for (const recordId of ids) {
1190
+ const item = await store.read(recordId).catch(() => null);
1191
+ if (item) items.push(item);
1192
+ else missing.push(recordId);
1193
+ }
1194
+ return {
1195
+ items,
1196
+ missing
1197
+ };
1198
+ }
1199
+ async function handleGetItems(collection, args, deps) {
1200
+ const { items, missing } = await loadRequestedItems(collection, args.ids, deps);
1201
+ if (!args.ids && !args.fields && items.length > 200) return `manageCollection: refused — '${collection.slug}' has ${items.length} records, over the unselective limit of 200. Pass \`ids\` for specific records or \`fields\` to project only the columns you need.`;
1202
+ const enriched = await enrichItems(collection, items, deps);
1203
+ const projected = args.fields ? enriched.map((item) => projectFields(item, args.fields, collection.schema.primaryKey)) : enriched;
1204
+ const warning = !args.ids || missing.length > 0 ? await recordIssuesWarning(collection, deps) : void 0;
1205
+ return JSON.stringify({
1206
+ collection: collection.slug,
1207
+ count: projected.length,
1208
+ items: projected,
1209
+ ...missing.length > 0 ? { missing: missing.map((recordId) => require_promptSafety.defangForPrompt(recordId)) } : {},
1210
+ ...warning ? { warning } : {}
1211
+ });
1212
+ }
1213
+ /** Reject writes that set host-computed keys, with a pointer at the
1214
+ * writable source of truth (the toggle's enum) where one exists. */
1215
+ function computedKeyProblem(record, schema) {
1216
+ for (const key of Object.keys(record)) {
1217
+ const spec = schema.fields[key];
1218
+ if (!spec || !require_ids.COMPUTED_TYPES.has(spec.type)) continue;
1219
+ if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
1220
+ return `'${key}' is ${{
1221
+ derived: "derived",
1222
+ embed: "an embed",
1223
+ backlinks: "a backlinks view",
1224
+ rollup: "a rollup"
1225
+ }[spec.type] ?? "computed"} — computed by the host, remove it from the record`;
1226
+ }
1227
+ return null;
1228
+ }
1229
+ /** `mode: "merge"` resolves the row against the EXISTING record —
1230
+ * a partial row updates just the fields it carries, instead of a
1231
+ * whole-record upsert silently erasing the optional fields it omits
1232
+ * (an upsert of `{id, status}` would pass validation yet drop
1233
+ * `notes`/`lesson`/…). Merge is a partial UPDATE by definition, so a
1234
+ * missing record is a reject, not an implicit create — a merged-over-
1235
+ * nothing partial record is exactly the data shape this mode exists
1236
+ * to prevent.
1237
+ *
1238
+ * Computed keys found in the STORED record are stripped before the
1239
+ * merge: the caller's own row was already computed-key-rejected, but a
1240
+ * raw-written / legacy record can carry a stale `derived`/`embed`/
1241
+ * `toggle` value, and re-writing it would perpetuate a forged
1242
+ * host-computed value. A merge heals the record instead.
1243
+ *
1244
+ * the store read THROWS on a malformed stored file (only ENOENT is null) —
1245
+ * downgraded to a per-row rejection here, like loadRequestedItems'
1246
+ * `missing`, so one broken file can't abort the whole putItems batch. */
1247
+ async function mergeWithExisting(collection, store, record, itemId) {
1248
+ let existing;
1249
+ try {
1250
+ existing = await store.read(itemId);
1251
+ } catch {
1252
+ 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"`;
1253
+ }
1254
+ if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
1255
+ const stored = Object.entries(existing).filter(([key]) => {
1256
+ const spec = collection.schema.fields[key];
1257
+ return !spec || !require_ids.COMPUTED_TYPES.has(spec.type);
1258
+ });
1259
+ return {
1260
+ ...Object.fromEntries(stored),
1261
+ ...record
1262
+ };
1263
+ }
1264
+ async function putOneItem(collection, store, write, record, mode, deps) {
1265
+ const { schema } = collection;
1266
+ const itemId = require_discovery.resolveCreateItemId(schema, record);
1267
+ const reject = (about, problem) => ({ rejected: {
1268
+ id: require_promptSafety.defangForPrompt(about),
1269
+ problem: require_promptSafety.defangForPrompt(problem)
1270
+ } });
1271
+ if (itemId === null) return reject("(no id)", `record has no '${schema.primaryKey}' value — set it (it doubles as the filename)`);
1272
+ const computed = computedKeyProblem(record, schema);
1273
+ if (computed) return reject(itemId, computed);
1274
+ let toWrite = record;
1275
+ if (mode === "merge") {
1276
+ const merged = await mergeWithExisting(collection, store, record, itemId);
1277
+ if (typeof merged === "string") return reject(itemId, merged);
1278
+ toWrite = merged;
1279
+ }
1280
+ if (!deps.ablateValidation) {
1281
+ const invalid = validateRecordObject(toWrite, itemId, schema);
1282
+ if (invalid) return reject(itemId, invalid);
1283
+ }
1284
+ const result = await write(itemId, toWrite, { refuseOverwrite: mode === "create" });
1285
+ if (result.kind === "ok") return { written: result.itemId };
1286
+ if (result.kind === "invalid-id") return reject(itemId, `'${itemId}' is not a valid record id (letters/digits at the ends; -, _, or . inside; no '..' or path characters)`);
1287
+ if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
1288
+ return reject(itemId, "write refused: the collection's data dir escapes the workspace");
1289
+ }
1290
+ /** Aggregation over a collection via the structured query DSL
1291
+ * (`core/queryZ.ts`) — the paved road for counts / sums / group-bys
1292
+ * that a row listing can't answer honestly. The engine choice (native
1293
+ * store query vs enrich-then-JSONL fallback) lives in
1294
+ * `runCollectionQuery`, shared with the desktop custom view's `/query`
1295
+ * route so the two surfaces can never drift. */
1296
+ async function handleQueryItems(collection, queryArg, deps) {
1297
+ const parsed = require_discovery.CollectionQueryZ.safeParse(queryArg);
1298
+ 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")}`;
1299
+ const rows = await runCollectionQuery(collection, parsed.data, { workspaceRoot: deps.workspaceRoot });
1300
+ return JSON.stringify({
1301
+ collection: collection.slug,
1302
+ count: rows.length,
1303
+ rows
1304
+ });
1305
+ }
1306
+ async function handlePutItems(collection, args, deps) {
1307
+ const store = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
1308
+ const { write } = store;
1309
+ 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).`;
1310
+ const written = [];
1311
+ const rejected = [];
1312
+ for (const record of args.items) {
1313
+ const outcome = await putOneItem(collection, store, write, record, args.mode, deps);
1314
+ if (outcome.written) written.push(outcome.written);
1315
+ if (outcome.rejected) rejected.push(outcome.rejected);
1316
+ }
1317
+ return JSON.stringify({
1318
+ collection: collection.slug,
1319
+ written,
1320
+ rejected
1321
+ });
1322
+ }
1323
+ function parsePutItems(args, slug) {
1324
+ const { items, mode } = args;
1325
+ if (!(Array.isArray(items) && items.length > 0 && items.every((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)))) return "manageCollection: `items` is required for putItems — a non-empty array of record objects.";
1326
+ if (mode !== void 0 && mode !== "upsert" && mode !== "create" && mode !== "merge") return "manageCollection: `mode` must be \"upsert\" (default), \"create\", or \"merge\".";
1327
+ return {
1328
+ slug,
1329
+ items,
1330
+ mode: mode ?? "upsert"
1331
+ };
1332
+ }
1333
+ /** The machine-readable workspace ontology: every collection with its
1334
+ * identity, record count, and outbound `ref`/`embed` relations. Slugs
1335
+ * are discovery-sanitized; titles/labels are workspace-authored schema
1336
+ * text and ride verbatim — the same trust class as the record values
1337
+ * getItems returns. */
1338
+ async function handleGetOntology(deps) {
1339
+ const collections = await buildWorkspaceOntology(deps);
1340
+ return JSON.stringify({
1341
+ count: collections.length,
1342
+ collections
1343
+ });
1344
+ }
1345
+ /** Return the collection-authoring reference (`collection-skills.md`).
1346
+ * Workspace copy first (reflects user edits), bundled asset as the
1347
+ * always-present fallback. Both reads guarded; if neither resolves the
1348
+ * agent still gets an actionable message instead of a thrown call. */
1349
+ async function handleSchemaDocs(deps) {
1350
+ const candidates = [node_path.default.join(resolveBase(deps), HELPS_DIR, SCHEMA_DOCS_FILE), ...deps.bundledHelpsDir ? [node_path.default.join(deps.bundledHelpsDir(), SCHEMA_DOCS_FILE)] : []];
1351
+ for (const candidate of candidates) try {
1352
+ return await (0, node_fs_promises.readFile)(candidate, "utf-8");
1353
+ } catch {}
1354
+ return `manageCollection: could not read the collection-authoring reference (${SCHEMA_DOCS_FILE}).`;
1355
+ }
1356
+ /** Return the raw schema.json of an existing collection, for editing.
1357
+ * Staging (the canonical writable copy) first, the active mirror as a
1358
+ * fallback for user-scope skills that have no staging copy. Raw text —
1359
+ * not the parsed schema — so the agent edits the true on-disk source. */
1360
+ async function handleGetSchema(slug, deps) {
1361
+ const collection = await require_discovery.loadCollection(slug, deps);
1362
+ if (!collection) return unknownCollection(slug);
1363
+ const candidates = [node_path.default.join(require_skill_bridge_index.dataSkillDir(resolveBase(deps), collection.slug), SCHEMA_FILE), node_path.default.join(collection.skillDir, SCHEMA_FILE)];
1364
+ for (const candidate of candidates) try {
1365
+ return await (0, node_fs_promises.readFile)(candidate, "utf-8");
1366
+ } catch {}
1367
+ return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' has no readable ${SCHEMA_FILE}.`;
1368
+ }
1369
+ /** Refuse a schema edit the host can't honour: user-scope/feed collections
1370
+ * are read-only, and presets (mc-*) re-seed on restart so an edit is lost. */
1371
+ function schemaEditRefusal(collection, slug) {
1372
+ if (collection.source !== "project") return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is ${collection.source}-scope and read-only here — only project collections (data/skills/) can be edited.`;
1373
+ if (isPresetSlug(slug)) return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is a preset (mc-*) and re-seeds on restart — copy it under a different slug to customise.`;
1374
+ return null;
1375
+ }
1376
+ /** Turn a CollectionSchemaZ failure into a short, actionable list the
1377
+ * agent can fix, pointing back at the field reference. */
1378
+ function formatSchemaIssues(issues) {
1379
+ const shown = issues.slice(0, 20);
1380
+ const lines = shown.map((issue) => `- ${issue.path.map(String).join(".") || "(root)"}: ${require_promptSafety.defangForPrompt(issue.message)}`).join("\n");
1381
+ const omitted = issues.length - shown.length;
1382
+ return `manageCollection: schema rejected — fix and retry (call schemaDocs for the field reference):\n${lines}${omitted > 0 ? `\n- …and ${omitted} more issue(s); fix these first and retry.` : ""}`;
1383
+ }
1384
+ /** Write the validated schema to the canonical staging copy, then mirror
1385
+ * it into the active `.claude/skills/` tree discovery reads — an internal
1386
+ * write doesn't fire the skill-bridge hook, so we mirror explicitly. */
1387
+ async function writeAndMirrorSchema(slug, schema, deps) {
1388
+ const base = resolveBase(deps);
1389
+ await require_discovery.writeFileAtomic(node_path.default.join(require_skill_bridge_index.dataSkillDir(base, slug), SCHEMA_FILE), `${JSON.stringify(schema, null, 2)}\n`);
1390
+ require_skill_bridge_index.mirrorSkillWrite(base, {
1391
+ slug,
1392
+ relSegments: [SCHEMA_FILE]
1393
+ });
1394
+ try {
1395
+ await deps.refreshAfterWrite?.();
1396
+ } catch {}
1397
+ }
1398
+ /** The post-Zod acceptance gates discovery applies before a parsed schema
1399
+ * becomes a live collection. Mirrors discovery's checks (`loadOneCollection`)
1400
+ * so a write can't pass here yet be silently skipped on the next load.
1401
+ * putSchema only runs for project-scope collections, so the feed-`ingest`
1402
+ * gate doesn't apply. Returns a one-line reason, or null when the schema
1403
+ * would be accepted. */
1404
+ function schemaDiscoveryGate(schema, base) {
1405
+ const primaryField = schema.fields[schema.primaryKey];
1406
+ if (!primaryField) return `primaryKey '${schema.primaryKey}' is not one of the declared fields`;
1407
+ if (primaryField.primary !== true) return `the primaryKey field '${schema.primaryKey}' must be flagged \`primary: true\``;
1408
+ if (schema.dataPath !== void 0 && require_discovery.resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
1409
+ if (schema.dataSource !== void 0 && require_discovery.resolveDataDir(schema.dataSource.path, base) === null) return `dataSource.path '${schema.dataSource.path}' escapes the workspace`;
1410
+ return null;
1411
+ }
1412
+ /** Validate a schema against CollectionSchemaZ and, on success, persist it.
1413
+ * Edit-only: a new collection is created by writing SKILL.md + schema.json
1414
+ * under data/skills/<slug>/ (the normal create flow), not through here. */
1415
+ async function handlePutSchema(slug, schemaArg, deps) {
1416
+ if (!schemaArg || typeof schemaArg !== "object" || Array.isArray(schemaArg)) return "manageCollection: `schema` is required for putSchema — the full collection schema object.";
1417
+ const collection = await require_discovery.loadCollection(slug, deps);
1418
+ if (!collection) return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under data/skills/${require_promptSafety.defangForPrompt(slug)}/, then edit it here.`;
1419
+ const refusal = schemaEditRefusal(collection, slug);
1420
+ if (refusal) return refusal;
1421
+ const parsed = require_discovery.CollectionSchemaZ.safeParse(schemaArg);
1422
+ if (!parsed.success) return formatSchemaIssues(parsed.error.issues);
1423
+ const gate = schemaDiscoveryGate(parsed.data, resolveBase(deps));
1424
+ 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.`;
1425
+ await writeAndMirrorSchema(collection.slug, parsed.data, deps);
1426
+ return JSON.stringify({
1427
+ collection: collection.slug,
1428
+ written: true
1429
+ });
1430
+ }
1431
+ 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, 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. 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.";
1432
+ /** Validate getItems' optional `ids`/`fields` args, then delegate. */
1433
+ async function dispatchGetItems(collection, args, deps) {
1434
+ const ids = optionalStringArray(args.ids, "ids");
1435
+ if (!ids.ok) return ids.error;
1436
+ const fields = optionalStringArray(args.fields, "fields");
1437
+ if (!fields.ok) return fields.error;
1438
+ return handleGetItems(collection, {
1439
+ slug: collection.slug,
1440
+ ids: ids.value,
1441
+ fields: fields.value
1442
+ }, deps);
1443
+ }
1444
+ async function manageCollectionHandler(deps, args) {
1445
+ const action = typeof args.action === "string" ? args.action : "";
1446
+ if (action === "schemaDocs") return handleSchemaDocs(deps);
1447
+ if (action === "getOntology") return handleGetOntology(deps);
1448
+ const slug = typeof args.slug === "string" ? args.slug.trim() : "";
1449
+ if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
1450
+ if (action === "getSchema") return handleGetSchema(slug, deps);
1451
+ if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
1452
+ if (action !== "getItems" && action !== "putItems" && action !== "queryItems") return "manageCollection: `action` must be \"getItems\", \"putItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
1453
+ const collection = await require_discovery.loadCollection(slug, deps);
1454
+ if (!collection) return unknownCollection(slug);
1455
+ if (action === "getItems") return dispatchGetItems(collection, args, deps);
1456
+ if (action === "queryItems") return handleQueryItems(collection, args.query, deps);
1457
+ const parsed = parsePutItems(args, slug);
1458
+ if (typeof parsed === "string") return parsed;
1459
+ return handlePutItems(collection, parsed, deps);
1460
+ }
1461
+ var MANAGE_COLLECTION_DEFINITION = {
1462
+ name: "manageCollection",
1463
+ 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. 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; getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
1464
+ inputSchema: {
1465
+ type: "object",
1466
+ properties: {
1467
+ action: {
1468
+ type: "string",
1469
+ enum: [
1470
+ "getItems",
1471
+ "putItems",
1472
+ "queryItems",
1473
+ "getOntology",
1474
+ "schemaDocs",
1475
+ "getSchema",
1476
+ "putSchema"
1477
+ ],
1478
+ description: "What to do."
1479
+ },
1480
+ slug: {
1481
+ type: "string",
1482
+ description: "The collection's slug (its directory name, e.g. `stock-quotes`). Required for everything except schemaDocs and getOntology."
1483
+ },
1484
+ ids: {
1485
+ type: "array",
1486
+ items: { type: "string" },
1487
+ description: "getItems: only these record ids (primary-key values). Omit for all records."
1488
+ },
1489
+ fields: {
1490
+ type: "array",
1491
+ items: { type: "string" },
1492
+ description: "getItems: only these fields per record (the primary key is always included). Omit for all fields. Use on large collections."
1493
+ },
1494
+ items: {
1495
+ type: "array",
1496
+ items: { type: "object" },
1497
+ description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
1498
+ },
1499
+ mode: {
1500
+ type: "string",
1501
+ enum: [
1502
+ "upsert",
1503
+ "create",
1504
+ "merge"
1505
+ ],
1506
+ description: "putItems: \"upsert\" (default) replaces existing records WHOLE; \"create\" rejects rows whose id already exists; \"merge\" updates only the fields a row carries, keeping the rest of the existing record (rejects unknown ids). Use \"merge\" when changing a few fields."
1507
+ },
1508
+ schema: {
1509
+ type: "object",
1510
+ 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."
1511
+ },
1512
+ query: {
1513
+ type: "object",
1514
+ description: "queryItems: a structured aggregation query — `{ groupBy?: [\"col\"], aggregates?: { alias: { op: \"count\"|\"sum\"|\"avg\"|\"min\"|\"max\", column? } }, where?: [{ field, op, value }], orderBy?: [{ field, dir? }], limit? }`. At least one of groupBy/aggregates. On a dataSource (CSV) collection it scans the WHOLE file uncapped (unlike getItems); on a file-backed collection it aggregates the ENRICHED records, so computed fields (derived/rollup/toggle) are queryable columns. Use it for counts / sums / averages / group-bys instead of arithmetic over getItems output."
1515
+ }
1516
+ },
1517
+ required: ["action"]
1518
+ }
1519
+ };
1520
+ function makeManageCollectionTool(deps = {}) {
1521
+ return {
1522
+ definition: MANAGE_COLLECTION_DEFINITION,
1523
+ alwaysActive: true,
1524
+ prompt: MANAGE_COLLECTION_PROMPT,
1525
+ handler: (args) => manageCollectionHandler(deps, args)
1526
+ };
1527
+ }
1528
+ //#endregion
1529
+ Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
1530
+ enumerable: true,
1531
+ get: function() {
1532
+ return MAX_SCHEMA_ISSUES;
1533
+ }
1534
+ });
1535
+ Object.defineProperty(exports, "MAX_UNSELECTIVE_ITEMS", {
1536
+ enumerable: true,
1537
+ get: function() {
1538
+ return MAX_UNSELECTIVE_ITEMS;
1539
+ }
1540
+ });
1541
+ Object.defineProperty(exports, "ONE_SECOND_MS", {
1542
+ enumerable: true,
1543
+ get: function() {
1544
+ return ONE_SECOND_MS;
1545
+ }
1546
+ });
1547
+ Object.defineProperty(exports, "advanceTriggerDate", {
1548
+ enumerable: true,
1549
+ get: function() {
1550
+ return advanceTriggerDate;
1551
+ }
1552
+ });
1553
+ Object.defineProperty(exports, "applyMutateAction", {
1554
+ enumerable: true,
1555
+ get: function() {
1556
+ return applyMutateAction;
1557
+ }
1558
+ });
1559
+ Object.defineProperty(exports, "buildWorkspaceOntology", {
1560
+ enumerable: true,
1561
+ get: function() {
1562
+ return buildWorkspaceOntology;
1563
+ }
1564
+ });
1565
+ Object.defineProperty(exports, "compileRecordZ", {
1566
+ enumerable: true,
1567
+ get: function() {
1568
+ return compileRecordZ;
1569
+ }
1570
+ });
1571
+ Object.defineProperty(exports, "computeCollectionIcon", {
1572
+ enumerable: true,
1573
+ get: function() {
1574
+ return computeCollectionIcon;
1575
+ }
1576
+ });
1577
+ Object.defineProperty(exports, "computeSuccessor", {
1578
+ enumerable: true,
1579
+ get: function() {
1580
+ return computeSuccessor;
1581
+ }
1582
+ });
1583
+ Object.defineProperty(exports, "daysInMonth", {
1584
+ enumerable: true,
1585
+ get: function() {
1586
+ return daysInMonth;
1587
+ }
1588
+ });
1589
+ Object.defineProperty(exports, "deleteCollection", {
1590
+ enumerable: true,
1591
+ get: function() {
1592
+ return deleteCollection;
1593
+ }
1594
+ });
1595
+ Object.defineProperty(exports, "deleteCollectionRefusalMessage", {
1596
+ enumerable: true,
1597
+ get: function() {
1598
+ return deleteCollectionRefusalMessage;
1599
+ }
1600
+ });
1601
+ Object.defineProperty(exports, "deleteCustomView", {
1602
+ enumerable: true,
1603
+ get: function() {
1604
+ return deleteCustomView;
1605
+ }
1606
+ });
1607
+ Object.defineProperty(exports, "enrichItems", {
1608
+ enumerable: true,
1609
+ get: function() {
1610
+ return enrichItems;
1611
+ }
1612
+ });
1613
+ Object.defineProperty(exports, "errorMessage", {
1614
+ enumerable: true,
1615
+ get: function() {
1616
+ return errorMessage;
1617
+ }
1618
+ });
1619
+ Object.defineProperty(exports, "firstMutateParamProblem", {
1620
+ enumerable: true,
1621
+ get: function() {
1622
+ return firstMutateParamProblem;
1623
+ }
1624
+ });
1625
+ Object.defineProperty(exports, "formatCivil", {
1626
+ enumerable: true,
1627
+ get: function() {
1628
+ return formatCivil;
1629
+ }
1630
+ });
1631
+ Object.defineProperty(exports, "isTriggerDue", {
1632
+ enumerable: true,
1633
+ get: function() {
1634
+ return isTriggerDue;
1635
+ }
1636
+ });
1637
+ Object.defineProperty(exports, "makeManageCollectionTool", {
1638
+ enumerable: true,
1639
+ get: function() {
1640
+ return makeManageCollectionTool;
1641
+ }
1642
+ });
1643
+ Object.defineProperty(exports, "maybeSpawnSuccessor", {
1644
+ enumerable: true,
1645
+ get: function() {
1646
+ return maybeSpawnSuccessor;
1647
+ }
1648
+ });
1649
+ Object.defineProperty(exports, "parseCivil", {
1650
+ enumerable: true,
1651
+ get: function() {
1652
+ return parseCivil;
1653
+ }
1654
+ });
1655
+ Object.defineProperty(exports, "recordFieldProblem", {
1656
+ enumerable: true,
1657
+ get: function() {
1658
+ return recordFieldProblem;
1659
+ }
1660
+ });
1661
+ Object.defineProperty(exports, "resolveEvery", {
1662
+ enumerable: true,
1663
+ get: function() {
1664
+ return resolveEvery;
1665
+ }
1666
+ });
1667
+ Object.defineProperty(exports, "runCollectionQuery", {
1668
+ enumerable: true,
1669
+ get: function() {
1670
+ return runCollectionQuery;
1671
+ }
1672
+ });
1673
+ Object.defineProperty(exports, "runQueryOverRows", {
1674
+ enumerable: true,
1675
+ get: function() {
1676
+ return runQueryOverRows;
1677
+ }
1678
+ });
1679
+ Object.defineProperty(exports, "schemaRelations", {
1680
+ enumerable: true,
1681
+ get: function() {
1682
+ return schemaRelations;
1683
+ }
1684
+ });
1685
+ Object.defineProperty(exports, "successorId", {
1686
+ enumerable: true,
1687
+ get: function() {
1688
+ return successorId;
1689
+ }
1690
+ });
1691
+ Object.defineProperty(exports, "validateCollectionRecords", {
1692
+ enumerable: true,
1693
+ get: function() {
1694
+ return validateCollectionRecords;
1695
+ }
1696
+ });
1697
+ Object.defineProperty(exports, "validateRecordObject", {
1698
+ enumerable: true,
1699
+ get: function() {
1700
+ return validateRecordObject;
1701
+ }
1702
+ });
1703
+
1704
+ //# sourceMappingURL=server-CKdWENZ-.cjs.map