@mulmoclaude/core 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/assets/helps/collection-skills.md +39 -7
  2. package/assets/helps/custom-view.md +13 -9
  3. package/assets/skills-preset/mc-zenn/SKILL.md +31 -0
  4. package/dist/collection/core/completion.d.ts +18 -0
  5. package/dist/collection/core/deriveAll.d.ts +14 -9
  6. package/dist/collection/core/schema.d.ts +9 -4
  7. package/dist/collection/core/schemaZ.d.ts +97 -43
  8. package/dist/collection/index.cjs +2 -1
  9. package/dist/collection/index.cjs.map +1 -1
  10. package/dist/collection/index.d.ts +1 -0
  11. package/dist/collection/index.js +2 -2
  12. package/dist/collection/index.js.map +1 -1
  13. package/dist/collection/registry/server/index.cjs +1 -1
  14. package/dist/collection/registry/server/index.js +1 -1
  15. package/dist/collection/server/csvQuery.d.ts +13 -5
  16. package/dist/collection/server/csvStore.d.ts +2 -0
  17. package/dist/collection/server/index.cjs +4 -2
  18. package/dist/collection/server/index.d.ts +2 -1
  19. package/dist/collection/server/index.js +3 -3
  20. package/dist/collection/server/jsonlQuery.d.ts +4 -0
  21. package/dist/collection-watchers/index.cjs +5 -14
  22. package/dist/collection-watchers/index.cjs.map +1 -1
  23. package/dist/collection-watchers/index.js +2 -11
  24. package/dist/collection-watchers/index.js.map +1 -1
  25. package/dist/collection-watchers/reconciler.d.ts +2 -4
  26. package/dist/feeds/index.cjs +2 -2
  27. package/dist/feeds/index.js +2 -2
  28. package/dist/feeds/server/index.cjs +3 -3
  29. package/dist/feeds/server/index.js +3 -3
  30. package/dist/google/auth.d.ts +10 -1
  31. package/dist/google/authFlow.d.ts +1 -0
  32. package/dist/google/index.cjs +79 -38
  33. package/dist/google/index.cjs.map +1 -1
  34. package/dist/google/index.d.ts +1 -1
  35. package/dist/google/index.js +78 -39
  36. package/dist/google/index.js.map +1 -1
  37. package/dist/{ingestTypes-B-dXxUF9.js → ingestTypes-CFEgTItj.js} +2 -2
  38. package/dist/{ingestTypes-B-dXxUF9.js.map → ingestTypes-CFEgTItj.js.map} +1 -1
  39. package/dist/{ingestTypes-Cev9q77C.cjs → ingestTypes-Stc7iQyA.cjs} +2 -2
  40. package/dist/{ingestTypes-Cev9q77C.cjs.map → ingestTypes-Stc7iQyA.cjs.map} +1 -1
  41. package/dist/{promptSafety-rDWA9_JS.js → promptSafety-BLUcImzd.js} +54 -26
  42. package/dist/promptSafety-BLUcImzd.js.map +1 -0
  43. package/dist/{promptSafety-DRd15gQ6.cjs → promptSafety-DI_1Rvhk.cjs} +59 -25
  44. package/dist/promptSafety-DI_1Rvhk.cjs.map +1 -0
  45. package/dist/{server--FgDORd3.js → server-BpEo0FOd.js} +233 -57
  46. package/dist/server-BpEo0FOd.js.map +1 -0
  47. package/dist/{server-Q7ld-FlZ.cjs → server-CkysfLh4.cjs} +244 -56
  48. package/dist/server-CkysfLh4.cjs.map +1 -0
  49. package/package.json +1 -1
  50. package/dist/promptSafety-DRd15gQ6.cjs.map +0 -1
  51. package/dist/promptSafety-rDWA9_JS.js.map +0 -1
  52. package/dist/server--FgDORd3.js.map +0 -1
  53. package/dist/server-Q7ld-FlZ.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { B as isFieldDrivenEvery, C as backlinkRows, D as actionVisible, E as rollupValue, F as AGENT_INGEST_KIND, I as COMPUTED_TYPES, L as FEED_SCHEDULES, M as SAFE_SLUG_PATTERN, N as isSafeRecordId, P as isSafeSlug, R as INGEST_KINDS, T as projectBacklinkRow, V as isReadOnlySchema, b as resolveIcon, d as parseIsoDateTime, g as deriveAll, j as SAFE_RECORD_ID_PATTERN, t as defangForPrompt, u as parseIsoDate, w as coerceNumeric, x as selectDynamicRecord, y as firstDateField, z as embedTargetId } from "./promptSafety-rDWA9_JS.js";
1
+ import { B as embedTargetId, D as rollupValue, E as projectBacklinkRow, F as isSafeSlug, H as isReadOnlySchema, I as AGENT_INGEST_KIND, L as COMPUTED_TYPES, M as SAFE_RECORD_ID_PATTERN, N as SAFE_SLUG_PATTERN, O as actionVisible, P as isSafeRecordId, R as FEED_SCHEDULES, S as itemIsDone, T as coerceNumeric, V as isFieldDrivenEvery, b as resolveIcon, d as parseIsoDateTime, g as deriveAll, t as defangForPrompt, u as parseIsoDate, w as backlinkRows, x as selectDynamicRecord, y as firstDateField, z as INGEST_KINDS } from "./promptSafety-BLUcImzd.js";
2
2
  import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath } from "./collection/paths.js";
3
3
  import { dataSkillDir, mirrorSkillWrite } from "./skill-bridge/index.js";
4
4
  import path from "node:path";
@@ -792,17 +792,18 @@ function whereFragment(cond) {
792
792
  params: [cond.value]
793
793
  };
794
794
  }
795
- /** Compile a validated query. Returns the SQL (whose FIRST placeholder is
796
- * the CSV path — the executor binds it) and the where-value parameters
797
- * that follow it. Callers MUST have run `CollectionQueryZ` first; this
798
- * function trusts the shape (aliases already charset-checked, orderBy
799
- * membership already enforced). */
800
- function compileCsvQuery(query, primaryKey) {
795
+ /** Compile a validated query against `fromSql` (a table-function call
796
+ * whose FIRST placeholder is the source path — the executor binds it).
797
+ * Returns the SQL and the where-value parameters that follow the path.
798
+ * Callers MUST have run `CollectionQueryZ` first; this function trusts
799
+ * the shape (aliases already charset-checked, orderBy membership already
800
+ * enforced). */
801
+ function compileQuery(query, fromSql) {
801
802
  const groupBy = query.groupBy ?? [];
802
803
  const aggregates = Object.entries(query.aggregates ?? {});
803
804
  const selectList = [...groupBy.map(quoteIdent), ...aggregates.map(([alias, aggregate]) => `${aggregateExpr(aggregate)} AS ${quoteIdent(alias)}`)];
804
805
  const where = (query.where ?? []).map(whereFragment);
805
- const clauses = [`SELECT ${selectList.join(", ")}`, `FROM read_csv(${readCsvArgs(primaryKey)})`];
806
+ const clauses = [`SELECT ${selectList.join(", ")}`, `FROM ${fromSql}`];
806
807
  if (where.length > 0) clauses.push(`WHERE ${where.map((fragment) => fragment.sql).join(" AND ")}`);
807
808
  if (groupBy.length > 0) clauses.push(`GROUP BY ${groupBy.map(quoteIdent).join(", ")}`);
808
809
  const orderBy = (query.orderBy ?? []).map((order) => quoteIdent(order.field) + (order.dir === "desc" ? " DESC" : " ASC"));
@@ -813,6 +814,21 @@ function compileCsvQuery(query, primaryKey) {
813
814
  params: where.flatMap((fragment) => fragment.params)
814
815
  };
815
816
  }
817
+ /** Compile against a CSV file (the dataSource store's engine). */
818
+ function compileCsvQuery(query, primaryKey) {
819
+ return compileQuery(query, `read_csv(${readCsvArgs(primaryKey)})`);
820
+ }
821
+ /** Compile against a JSONL file of ENRICHED records — the file-backed
822
+ * collections' engine (see `jsonlQuery.ts`). No VARCHAR key pin needed:
823
+ * enriched record ids are already strings. `sample_size=-1` makes the
824
+ * schema inference scan EVERY line — with the default sample, a sparse
825
+ * optional/derived field first appearing past the sample would not be
826
+ * inferred as a column and the query would binder-error on it (Codex P2
827
+ * on #2165). The full scan costs nothing extra here: aggregation reads
828
+ * the whole file anyway. */
829
+ function compileJsonlQuery(query) {
830
+ return compileQuery(query, `read_json(?, format='newline_delimited', sample_size=-1)`);
831
+ }
816
832
  //#endregion
817
833
  //#region src/collection/server/csvStore.ts
818
834
  /** `list()` row cap. Over-cap files are truncated with a warn — the v1
@@ -1133,6 +1149,74 @@ function storeFor(collection, opts = {}) {
1133
1149
  };
1134
1150
  }
1135
1151
  //#endregion
1152
+ //#region src/collection/server/jsonlQuery.ts
1153
+ /** SQL semantics for an aggregate-only query over ZERO rows: one scalar
1154
+ * row (`count` = 0, everything else NULL) — the same shape the CSV path
1155
+ * produces for a header-only file, so callers reading `rows[0]` never
1156
+ * break on storage kind or emptiness. A grouped query over zero rows is
1157
+ * zero groups (`[]`), also matching SQL. Synthesized here because DuckDB
1158
+ * has no empty file to infer a schema from. */
1159
+ function emptyCollectionResult(query) {
1160
+ if ((query.groupBy?.length ?? 0) > 0) return [];
1161
+ const aggregates = Object.entries(query.aggregates ?? {});
1162
+ return [Object.fromEntries(aggregates.map(([alias, aggregate]) => [alias, aggregate.op === "count" ? 0 : null]))];
1163
+ }
1164
+ /** The SOURCE columns a query reads: groupBy columns, aggregate input
1165
+ * columns, and where fields. (orderBy resolves to groupBy columns or
1166
+ * aggregate ALIASES — never to a new source column — so it adds none.) */
1167
+ function referencedSourceColumns(query) {
1168
+ const columns = new Set(query.groupBy ?? []);
1169
+ for (const aggregate of Object.values(query.aggregates ?? {})) if (aggregate.column !== void 0) columns.add(aggregate.column);
1170
+ for (const cond of query.where ?? []) columns.add(cond.field);
1171
+ return [...columns];
1172
+ }
1173
+ /** Referenced columns that appear in NO row — a freshly-added optional
1174
+ * field, or a derived field whose inputs no record has yet. DuckDB only
1175
+ * infers columns from keys that occur in the JSONL, so without help a
1176
+ * valid query on such a field binder-errors here while the CSV path
1177
+ * (whose header always declares the column) returns NULLs. Padding the
1178
+ * FIRST line with `col: null` makes the column exist (full-scan
1179
+ * inference unions keys across lines) with matching NULL semantics. */
1180
+ function absentReferencedColumns(rows, query) {
1181
+ const present = /* @__PURE__ */ new Set();
1182
+ for (const row of rows) for (const key of Object.keys(row)) present.add(key);
1183
+ return referencedSourceColumns(query).filter((column) => !present.has(column));
1184
+ }
1185
+ /** Run a validated query over enriched collection rows. */
1186
+ async function runQueryOverRows(rows, query) {
1187
+ if (rows.length === 0) return emptyCollectionResult(query);
1188
+ await mkdir(cacheDir(), {
1189
+ recursive: true,
1190
+ mode: 448
1191
+ });
1192
+ const jsonlPath = path.join(cacheDir(), `q-${randomBytes(8).toString("hex")}.jsonl`);
1193
+ try {
1194
+ const nullPads = Object.fromEntries(absentReferencedColumns(rows, query).map((column) => [column, null]));
1195
+ const handle = await open(jsonlPath, "wx", 384);
1196
+ try {
1197
+ let first = true;
1198
+ for (const row of rows) {
1199
+ await handle.write(`${JSON.stringify(first ? {
1200
+ ...nullPads,
1201
+ ...row
1202
+ } : row)}\n`);
1203
+ first = false;
1204
+ }
1205
+ } finally {
1206
+ await handle.close();
1207
+ }
1208
+ const { sql, params } = compileJsonlQuery(query);
1209
+ return (await queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
1210
+ } finally {
1211
+ await unlink(jsonlPath).catch((err) => {
1212
+ if (err.code !== "ENOENT") log.warn("collections", "temp JSONL cleanup failed", {
1213
+ path: jsonlPath,
1214
+ error: String(err)
1215
+ });
1216
+ });
1217
+ }
1218
+ }
1219
+ //#endregion
1136
1220
  //#region src/collection/core/recordZ.ts
1137
1221
  /** The emptiness rule shared by `required` and the "only check present
1138
1222
  * values" gate. NOT a truthiness check — `0` and `false` are filled. */
@@ -1418,6 +1502,32 @@ var WhenZ = z.object({
1418
1502
  field: z.string().trim().min(1),
1419
1503
  in: z.array(z.string().trim().min(1)).min(1)
1420
1504
  });
1505
+ var ValueRefZ = z.object({
1506
+ record: z.string().trim().min(1).optional(),
1507
+ field: z.string().trim().min(1)
1508
+ });
1509
+ var WhereCondZ = z.object({
1510
+ field: z.string().trim().min(1),
1511
+ op: z.enum([
1512
+ "eq",
1513
+ "ne",
1514
+ "in",
1515
+ "gt",
1516
+ "gte",
1517
+ "lt",
1518
+ "lte",
1519
+ "contains"
1520
+ ]),
1521
+ value: z.union([z.string(), z.array(z.string())]).optional(),
1522
+ valueFrom: ValueRefZ.optional()
1523
+ }).refine((cond) => cond.value !== void 0 !== (cond.valueFrom !== void 0), {
1524
+ message: "a where condition must declare exactly one of `value` (a literal) or `valueFrom` (a reference to another record's field), never both or neither",
1525
+ path: ["value"]
1526
+ }).refine((cond) => cond.value === void 0 || cond.op === "in" === Array.isArray(cond.value), {
1527
+ message: "`in` requires an array `value` (the allowed set); every other op requires a single string `value`",
1528
+ path: ["value"]
1529
+ });
1530
+ var WhereZ = z.array(WhereCondZ);
1421
1531
  var fieldBase = {
1422
1532
  label: z.string().min(1),
1423
1533
  primary: z.boolean().optional(),
@@ -1628,6 +1738,30 @@ var ToggleFieldZ = z.object({
1628
1738
  onValue: z.string().trim().min(1),
1629
1739
  offValue: z.string().trim().min(1)
1630
1740
  });
1741
+ /** A computed boolean: the record matched against a `where` predicate
1742
+ * (the same AND-of-conditions shape `dynamicIcon` uses; evaluated by
1743
+ * `./where`'s `matchesWhere`). Generic state summaries — isDone,
1744
+ * isPassed, isQualified — become declared fields, so every
1745
+ * field-driven mechanism (table cells, detail view, completion via
1746
+ * `completionField`) picks them up without special cases. Never
1747
+ * stored; computed by `deriveAll` on both server and client in the
1748
+ * same saturation loop as `derived`, so a flag may read derived /
1749
+ * rollup values — and other flags (`matchesWhere` stringifies, so
1750
+ * `eq "true"` composes). Deliberately a STRUCTURED field, not formula
1751
+ * syntax — the derived evaluator's no-string-literals boundary stays
1752
+ * untouched (same reasoning as `rollup`). Cross-record
1753
+ * `valueFrom.record` is rejected here: per-record evaluation has no
1754
+ * `recordsById`, so it could only ever silently never-match.
1755
+ * `where` condition fields are validated to exist by a schema-level
1756
+ * refine below (a field can't see its siblings). */
1757
+ var FlagFieldZ = z.object({
1758
+ type: z.literal("flag"),
1759
+ ...fieldBase,
1760
+ where: WhereZ.min(1)
1761
+ }).refine((spec) => spec.where.every((cond) => cond.valueFrom?.record === void 0), {
1762
+ message: "a flag's `where` cannot use `valueFrom.record` (cross-record references are unresolvable in per-record evaluation); use a literal `value` or a same-record `valueFrom` (field-to-field)",
1763
+ path: ["where"]
1764
+ });
1631
1765
  var FieldSpecZ = z.discriminatedUnion("type", [
1632
1766
  ScalarFieldZ,
1633
1767
  RefFieldZ,
@@ -1638,7 +1772,8 @@ var FieldSpecZ = z.discriminatedUnion("type", [
1638
1772
  EmbedFieldZ,
1639
1773
  BacklinksFieldZ,
1640
1774
  RollupFieldZ,
1641
- ToggleFieldZ
1775
+ ToggleFieldZ,
1776
+ FlagFieldZ
1642
1777
  ]);
1643
1778
  var actionBase = {
1644
1779
  id: z.string().trim().min(1),
@@ -1787,32 +1922,6 @@ var AgentIngestZ = z.object({
1787
1922
  * discovered from `<workspace>/feeds/` are REQUIRED to carry it (gated by
1788
1923
  * `acceptParsedSchema`). */
1789
1924
  var IngestZ = z.discriminatedUnion("kind", [DeclarativeIngestZ, AgentIngestZ]);
1790
- var ValueRefZ = z.object({
1791
- record: z.string().trim().min(1).optional(),
1792
- field: z.string().trim().min(1)
1793
- });
1794
- var WhereCondZ = z.object({
1795
- field: z.string().trim().min(1),
1796
- op: z.enum([
1797
- "eq",
1798
- "ne",
1799
- "in",
1800
- "gt",
1801
- "gte",
1802
- "lt",
1803
- "lte",
1804
- "contains"
1805
- ]),
1806
- value: z.union([z.string(), z.array(z.string())]).optional(),
1807
- valueFrom: ValueRefZ.optional()
1808
- }).refine((cond) => cond.value !== void 0 !== (cond.valueFrom !== void 0), {
1809
- message: "a where condition must declare exactly one of `value` (a literal) or `valueFrom` (a reference to another record's field), never both or neither",
1810
- path: ["value"]
1811
- }).refine((cond) => cond.value === void 0 || cond.op === "in" === Array.isArray(cond.value), {
1812
- message: "`in` requires an array `value` (the allowed set); every other op requires a single string `value`",
1813
- path: ["value"]
1814
- });
1815
- var WhereZ = z.array(WhereCondZ);
1816
1925
  var DynamicIconSourceZ = z.object({
1817
1926
  collection: z.string().trim().min(1),
1818
1927
  from: z.enum([
@@ -1907,12 +2016,12 @@ function fieldDrivenFromFieldCarried(schema) {
1907
2016
  * workspace-relative and containment-checked exactly like `dataPath`. The
1908
2017
  * row-id column is the schema's existing `primaryKey` — there is
1909
2018
  * deliberately no second key concept here.
1910
- * See plans/feat-collection-csv-duckdb-source.md. */
2019
+ * See plans/done/feat-collection-csv-duckdb-source.md. */
1911
2020
  var DataSourceZ = z.object({
1912
2021
  type: z.literal("csv"),
1913
2022
  path: z.string().min(1)
1914
2023
  });
1915
- var CollectionSchemaZ = z.object({
2024
+ var BareCollectionSchemaZ = z.object({
1916
2025
  title: z.string().min(1),
1917
2026
  icon: z.string().min(1),
1918
2027
  dataPath: z.string().min(1).optional(),
@@ -1972,8 +2081,11 @@ var CollectionSchemaZ = z.object({
1972
2081
  }).refine((schema) => collectCurrencyFieldRefs(schema.fields).every((name) => CODE_FIELD_TYPES.has(schema.fields[name]?.type ?? "")), {
1973
2082
  message: "a money field's `currencyField` must name a top-level `string`, `text`, or `enum` field that holds the currency code",
1974
2083
  path: ["fields"]
1975
- }).refine((schema) => schema.completionField === void 0 === (schema.completionDoneValues === void 0), {
1976
- message: "schema `completionField` and `completionDoneValues` must be declared together (both set, or both omitted)",
2084
+ }).refine((schema) => {
2085
+ if (schema.completionField !== void 0 && schema.fields[schema.completionField]?.type === "flag") return schema.completionDoneValues === void 0;
2086
+ return schema.completionField === void 0 === (schema.completionDoneValues === void 0);
2087
+ }, {
2088
+ message: "schema `completionField` and `completionDoneValues` must be declared together (both set, or both omitted) — unless `completionField` names a `flag` field, in which case `completionDoneValues` must be omitted (done ⇔ the flag matches)",
1977
2089
  path: ["completionField"]
1978
2090
  }).refine((schema) => schema.completionField === void 0 || schema.fields[schema.completionField] !== void 0, {
1979
2091
  message: "schema `completionField` must name a top-level field declared in `fields`",
@@ -1984,6 +2096,22 @@ var CollectionSchemaZ = z.object({
1984
2096
  }).refine((schema) => Object.values(schema.fields).every((field) => field.when === void 0 || schema.fields[field.when.field] !== void 0), {
1985
2097
  message: "a field's `when.field` must name a top-level field declared in `fields`",
1986
2098
  path: ["fields"]
2099
+ }).refine((schema) => Object.values(schema.fields).every((field) => field.type !== "flag" || field.where.every((cond) => schema.fields[cond.field] !== void 0 && (cond.valueFrom === void 0 || schema.fields[cond.valueFrom.field] !== void 0))), {
2100
+ message: "a flag field's `where` conditions must name top-level fields declared in `fields` (both `field` and a same-record `valueFrom.field`)",
2101
+ path: ["fields"]
2102
+ }).refine((schema) => {
2103
+ const spec = schema.completionField === void 0 ? void 0 : schema.fields[schema.completionField];
2104
+ if (spec?.type !== "flag") return true;
2105
+ return spec.where.every((cond) => [cond.field, ...cond.valueFrom ? [cond.valueFrom.field] : []].every((name) => {
2106
+ const target = schema.fields[name];
2107
+ return target !== void 0 && !COMPUTED_TYPES.has(target.type);
2108
+ }));
2109
+ }, {
2110
+ message: "a `flag` named by `completionField` may only reference STORED fields in its `where` — completion is evaluated against the raw record (before deriveAll), where computed values (derived/rollup/toggle/flag/embed/backlinks) are absent",
2111
+ path: ["completionField"]
2112
+ }).refine((schema) => schema.spawn === void 0 || schema.spawn.when !== void 0 || schema.fields[schema.completionField ?? ""]?.type !== "flag", {
2113
+ message: "a schema whose `completionField` names a `flag` field must declare an explicit `spawn.when` (the spawn-inert check cannot statically evaluate a flag's `where`)",
2114
+ path: ["spawn"]
1987
2115
  }).refine((schema) => Object.values(schema.fields).every((field) => {
1988
2116
  if (field.type !== "embed" || field.idField === void 0) return true;
1989
2117
  const target = schema.fields[field.idField];
@@ -2058,6 +2186,46 @@ var CollectionSchemaZ = z.object({
2058
2186
  message: "schema `views` must have unique `id`s",
2059
2187
  path: ["views"]
2060
2188
  });
2189
+ var PROTOTYPE_KEYS = [
2190
+ "__proto__",
2191
+ "constructor",
2192
+ "prototype"
2193
+ ];
2194
+ /** The first own prototype-sensitive key of `value`, or null. */
2195
+ function ownPrototypeKey(value) {
2196
+ if (value === null || typeof value !== "object") return null;
2197
+ for (const key of PROTOTYPE_KEYS) if (Object.hasOwn(value, key)) return key;
2198
+ return null;
2199
+ }
2200
+ /** Dotted path of the first prototype-sensitive field name in the raw
2201
+ * schema input — top-level `fields`, each table field's `of`, and each
2202
+ * action's `params` (the three records that DEFINE names) — or null. */
2203
+ function prototypeFieldKeyPath(input) {
2204
+ if (input === null || typeof input !== "object") return null;
2205
+ const schema = input;
2206
+ const bad = ownPrototypeKey(schema.fields);
2207
+ if (bad !== null) return `fields.${bad}`;
2208
+ for (const [key, spec] of Object.entries(schema.fields ?? {})) {
2209
+ const badSub = ownPrototypeKey(spec?.of);
2210
+ if (badSub !== null) return `fields.${key}.of.${badSub}`;
2211
+ }
2212
+ for (const [listName, list] of [["actions", schema.actions], ["collectionActions", schema.collectionActions]]) for (const action of Array.isArray(list) ? list : []) {
2213
+ const badParam = ownPrototypeKey(action?.params);
2214
+ if (badParam !== null) return `${listName}.params.${badParam}`;
2215
+ }
2216
+ return null;
2217
+ }
2218
+ var CollectionSchemaZ = z.preprocess((input, ctx) => {
2219
+ const bad = prototypeFieldKeyPath(input);
2220
+ if (bad !== null) {
2221
+ ctx.addIssue({
2222
+ code: "custom",
2223
+ message: `'${bad}': field names must not be prototype-sensitive keys (\`__proto__\`, \`constructor\`, \`prototype\`)`
2224
+ });
2225
+ return z.NEVER;
2226
+ }
2227
+ return input;
2228
+ }, BareCollectionSchemaZ);
2061
2229
  //#endregion
2062
2230
  //#region src/collection/server/discovery.ts
2063
2231
  function applyFeedSchemaDefaults(parsed, slug) {
@@ -2618,17 +2786,15 @@ function successorId(sourceId, next) {
2618
2786
  }
2619
2787
  /** True iff `item` satisfies the spawn predicate. With an explicit
2620
2788
  * `when`, matches `String(item[when.field]) ∈ when.in`. Without one,
2621
- * defaults to the completion-done condition. Self-contained (no import
2622
- * from notifications.ts) to keep the module graph acyclic. */
2789
+ * defaults to the completion-done condition the shared, flag-aware
2790
+ * `itemIsDone` (core/completion; downhill import, so the module graph
2791
+ * stays acyclic). */
2623
2792
  function matchesWhen(when, schema, item) {
2624
2793
  if (when) {
2625
2794
  const raw = item[when.field];
2626
2795
  return raw !== void 0 && raw !== null && when.in.includes(String(raw));
2627
2796
  }
2628
- const { completionField, completionDoneValues } = schema;
2629
- if (!completionField || !completionDoneValues) return false;
2630
- const raw = item[completionField];
2631
- return raw !== void 0 && raw !== null && completionDoneValues.includes(String(raw));
2797
+ return itemIsDone(schema, item);
2632
2798
  }
2633
2799
  /** Resolve the literal `every` that applies to `sourceItem`. Literal-arm
2634
2800
  * `spawn.every` passes through unchanged. Field-driven `spawn.every` reads
@@ -3173,17 +3339,27 @@ async function putOneItem(collection, record, mode, deps) {
3173
3339
  if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
3174
3340
  return reject(itemId, "write refused: the collection's data dir escapes the workspace");
3175
3341
  }
3176
- /** Aggregation over a dataSource collection's WHOLE file via the
3177
- * structured query DSL (`core/queryZ.ts`) — the paved road for counts /
3178
- * sums / group-bys that `getItems` (row-capped, unaggregated) can't
3179
- * answer honestly. File-backed collections have no query engine yet:
3180
- * refuse with a pointer instead of silently emulating. */
3342
+ /** Aggregation over a collection via the structured query DSL
3343
+ * (`core/queryZ.ts`) — the paved road for counts / sums / group-bys
3344
+ * that a row listing can't answer honestly. Two engines behind one
3345
+ * shape: a dataSource collection queries its CSV natively through the
3346
+ * store (`store.query`, uncapped whole-file scan); a file-backed
3347
+ * collection aggregates its ENRICHED records (computed fields —
3348
+ * `derived` / `rollup` / `toggle` — are real columns) through the same
3349
+ * compiled SQL over a temp JSONL (`runQueryOverRows`). */
3181
3350
  async function handleQueryItems(collection, queryArg, deps) {
3182
- const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
3183
- if (!store.query) return `manageCollection: '${collection.slug}' is file-backed — queryItems currently supports only dataSource (CSV) collections; use getItems (with \`fields\`) instead.`;
3184
3351
  const parsed = CollectionQueryZ.safeParse(queryArg);
3185
3352
  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)"}: ${defangForPrompt(issue.message)}`).join("\n")}`;
3186
- const rows = await store.query(parsed.data);
3353
+ const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
3354
+ if (store.query) {
3355
+ const rows = await store.query(parsed.data);
3356
+ return JSON.stringify({
3357
+ collection: collection.slug,
3358
+ count: rows.length,
3359
+ rows
3360
+ });
3361
+ }
3362
+ const rows = await runQueryOverRows(await enrichItems(collection, await store.list(), deps), parsed.data);
3187
3363
  return JSON.stringify({
3188
3364
  collection: collection.slug,
3189
3365
  count: rows.length,
@@ -3313,7 +3489,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
3313
3489
  written: true
3314
3490
  });
3315
3491
  }
3316
- 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. On a dataSource (CSV) collection, answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` — it scans the WHOLE file, while getItems is row-capped, so an aggregate computed from getItems output can be silently wrong on large files.";
3492
+ 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.";
3317
3493
  /** Validate getItems' optional `ids`/`fields` args, then delegate. */
3318
3494
  async function dispatchGetItems(collection, args, deps) {
3319
3495
  const ids = optionalStringArray(args.ids, "ids");
@@ -3396,7 +3572,7 @@ var MANAGE_COLLECTION_DEFINITION = {
3396
3572
  },
3397
3573
  query: {
3398
3574
  type: "object",
3399
- description: "queryItems (dataSource/CSV collections only): a structured aggregation query over the WHOLE file — `{ 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. Runs uncapped over the full file (unlike getItems), so use it for counts / sums / group-bys on large CSVs."
3575
+ 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."
3400
3576
  }
3401
3577
  },
3402
3578
  required: ["action"]
@@ -3411,6 +3587,6 @@ function makeManageCollectionTool(deps = {}) {
3411
3587
  };
3412
3588
  }
3413
3589
  //#endregion
3414
- export { promptPathsFor as $, validateCollectionRecords as A, dedupeByRecordId as B, discoverCollections as C, CollectionSchemaZ as D, toSummary as E, readOnlyRefusal as F, DEFAULT_QUERY_ROWS as G, normalizeCsvValue as H, storeFor as I, buildCollectionActionSeedPrompt as J, MAX_QUERY_ROWS as K, MAX_CSV_ROWS as L, compileRecordZ as M, recordFieldProblem as N, applyMutateAction as O, collectionWritable as P, listItems as Q, csvRowToItem as R, acceptParsedSchema as S, toDetail as T, compileCsvQuery as U, encodeCsvRecordId as V, CollectionQueryZ as W, generateItemId as X, deleteItem as Y, isRegularFile as Z, errorMessage as _, getWorkspaceRoot as _t, deleteCollection as a, writeItem as at, buildWorkspaceOntology as b, setCollectionChangePublisher as bt, computeSuccessor as c, isContainedInRoot as ct, isTriggerDue as d, resolveDataDir as dt, readCustomViewHtml as et, maybeSpawnSuccessor as f, resolveTemplatePath as ft, ONE_SECOND_MS as g, configureCollectionHost as gt, successorId as h, collectionsRegistriesConfigPath as ht, deleteCustomView as i, resolveCreateItemId as it, validateRecordObject as j, firstMutateParamProblem as k, daysInMonth as l, isContainedInWorkspace as lt, resolveEvery as m, safeSlugName as mt, MAX_UNSELECTIVE_ITEMS as n, readItem as nt, deleteCollectionRefusalMessage as o, writeFileAtomic as ot, parseCivil as p, safeRecordId as pt, buildActionSeedPrompt as q, makeManageCollectionTool as r, readSkillTemplate as rt, advanceTriggerDate as s, SCHEMA_FILE$1 as st, MAX_SCHEMA_ISSUES as t, readCustomViewI18n as tt, formatCivil as u, itemFilePath as ut, computeCollectionIcon as v, log as vt, loadCollection as w, schemaRelations as x, enrichItems as y, publishCollectionChange as yt, decodeCsvRecordId as z };
3590
+ export { isRegularFile as $, validateCollectionRecords as A, decodeCsvRecordId as B, discoverCollections as C, CollectionSchemaZ as D, toSummary as E, collectionWritable as F, compileJsonlQuery as G, encodeCsvRecordId as H, readOnlyRefusal as I, MAX_QUERY_ROWS as J, CollectionQueryZ as K, storeFor as L, compileRecordZ as M, recordFieldProblem as N, applyMutateAction as O, runQueryOverRows as P, generateItemId as Q, MAX_CSV_ROWS as R, acceptParsedSchema as S, setCollectionChangePublisher as St, toDetail as T, normalizeCsvValue as U, dedupeByRecordId as V, compileCsvQuery as W, buildCollectionActionSeedPrompt as X, buildActionSeedPrompt as Y, deleteItem as Z, errorMessage as _, collectionsRegistriesConfigPath as _t, deleteCollection as a, readSkillTemplate as at, buildWorkspaceOntology as b, log as bt, computeSuccessor as c, writeFileAtomic as ct, isTriggerDue as d, isContainedInWorkspace as dt, listItems as et, maybeSpawnSuccessor as f, itemFilePath as ft, ONE_SECOND_MS as g, safeSlugName as gt, successorId as h, safeRecordId as ht, deleteCustomView as i, readItem as it, validateRecordObject as j, firstMutateParamProblem as k, daysInMonth as l, SCHEMA_FILE$1 as lt, resolveEvery as m, resolveTemplatePath as mt, MAX_UNSELECTIVE_ITEMS as n, readCustomViewHtml as nt, deleteCollectionRefusalMessage as o, resolveCreateItemId as ot, parseCivil as p, resolveDataDir as pt, DEFAULT_QUERY_ROWS as q, makeManageCollectionTool as r, readCustomViewI18n as rt, advanceTriggerDate as s, writeItem as st, MAX_SCHEMA_ISSUES as t, promptPathsFor as tt, formatCivil as u, isContainedInRoot as ut, computeCollectionIcon as v, configureCollectionHost as vt, loadCollection as w, schemaRelations as x, publishCollectionChange as xt, enrichItems as y, getWorkspaceRoot as yt, csvRowToItem as z };
3415
3591
 
3416
- //# sourceMappingURL=server--FgDORd3.js.map
3592
+ //# sourceMappingURL=server-BpEo0FOd.js.map