@mulmoclaude/core 0.25.0 → 0.26.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.
- package/assets/helps/billing-clients-worklog.md +3 -2
- package/assets/helps/collection-skills.md +54 -1
- package/assets/helps/custom-view.md +37 -3
- package/assets/helps/error-recovery.md +30 -0
- package/assets/helps/todo-collection.md +2 -2
- package/dist/collection/core/fieldText.d.ts +11 -0
- package/dist/collection/core/project.d.ts +3 -0
- package/dist/collection/core/schema.d.ts +13 -1
- package/dist/collection/core/schemaZ.d.ts +16 -0
- package/dist/collection/index.cjs +18 -10
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +15 -11
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/csvStore.d.ts +8 -1
- package/dist/collection/server/discoveredCollection.d.ts +4 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/index.cjs +20 -17
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/queryRunner.d.ts +4 -0
- package/dist/collection/server/spawn.d.ts +2 -1
- package/dist/collection/server/sqliteStore.d.ts +14 -0
- package/dist/collection/server/store.d.ts +43 -8
- package/dist/collection/server/storePage.d.ts +32 -0
- package/dist/collection-watchers/index.cjs +167 -43
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.d.ts +1 -1
- package/dist/collection-watchers/index.js +168 -45
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +13 -11
- package/dist/collection-watchers/watcher.d.ts +4 -3
- package/dist/{discovery-3K_Wtkvn.js → discovery-QUi_7aNG.js} +882 -7
- package/dist/discovery-QUi_7aNG.js.map +1 -0
- package/dist/{discovery-BZ2ms1tE.cjs → discovery-cwtfyDxl.cjs} +994 -4
- package/dist/discovery-cwtfyDxl.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +26 -14
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +26 -14
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/google/index.cjs +9 -11
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +9 -11
- package/dist/google/index.js.map +1 -1
- package/dist/{ids-BR4U1dJD.cjs → ids-Dl9Ij63K.cjs} +14 -1
- package/dist/ids-Dl9Ij63K.cjs.map +1 -0
- package/dist/{ids-D4e10971.js → ids-P2T89Gou.js} +9 -2
- package/dist/ids-P2T89Gou.js.map +1 -0
- package/dist/{ingestTypes-BsGOjR2l.js → ingestTypes-C-IQB4nw.js} +2 -2
- package/dist/{ingestTypes-BsGOjR2l.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
- package/dist/{ingestTypes-CygUXsjf.cjs → ingestTypes-DZpAC3N_.cjs} +2 -2
- package/dist/{ingestTypes-CygUXsjf.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
- package/dist/project-BWI5w_BT.cjs +17 -0
- package/dist/project-BWI5w_BT.cjs.map +1 -0
- package/dist/project-bU98ycsy.js +12 -0
- package/dist/project-bU98ycsy.js.map +1 -0
- package/dist/{promptSafety-Boz5qOs-.js → promptSafety-B5KPkaNY.js} +32 -9
- package/dist/promptSafety-B5KPkaNY.js.map +1 -0
- package/dist/{promptSafety-Co4cYQ_Z.cjs → promptSafety-BOc1B3im.cjs} +43 -8
- package/dist/promptSafety-BOc1B3im.cjs.map +1 -0
- package/dist/remote-view/index.cjs +5 -3
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +3 -1
- package/dist/remote-view/index.js +5 -3
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-DLRz-U7F.cjs → server-B7mtx4Cd.cjs} +302 -785
- package/dist/server-B7mtx4Cd.cjs.map +1 -0
- package/dist/{server-DZH7OjOD.js → server-CNLZZONa.js} +292 -696
- package/dist/server-CNLZZONa.js.map +1 -0
- package/package.json +1 -1
- package/dist/discovery-3K_Wtkvn.js.map +0 -1
- package/dist/discovery-BZ2ms1tE.cjs.map +0 -1
- package/dist/ids-BR4U1dJD.cjs.map +0 -1
- package/dist/ids-D4e10971.js.map +0 -1
- package/dist/promptSafety-Boz5qOs-.js.map +0 -1
- package/dist/promptSafety-Co4cYQ_Z.cjs.map +0 -1
- package/dist/server-DLRz-U7F.cjs.map +0 -1
- package/dist/server-DZH7OjOD.js.map +0 -1
|
@@ -1,528 +1,13 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
|
|
2
|
-
const require_ids = require("./ids-
|
|
3
|
-
const require_promptSafety = require("./promptSafety-
|
|
4
|
-
const require_discovery = require("./discovery-
|
|
2
|
+
const require_ids = require("./ids-Dl9Ij63K.cjs");
|
|
3
|
+
const require_promptSafety = require("./promptSafety-BOc1B3im.cjs");
|
|
4
|
+
const require_discovery = require("./discovery-cwtfyDxl.cjs");
|
|
5
5
|
const require_skill_bridge_index = require("./skill-bridge/index.cjs");
|
|
6
6
|
let node_path = require("node:path");
|
|
7
7
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
|
-
let node_os = require("node:os");
|
|
11
|
-
let iconv_lite = require("iconv-lite");
|
|
12
|
-
iconv_lite = require_rolldown_runtime.__toESM(iconv_lite, 1);
|
|
13
10
|
let zod = require("zod");
|
|
14
|
-
//#region src/collection/core/queryZ.ts
|
|
15
|
-
/** Result-column aliases double as SQL identifiers and JSON keys — keep
|
|
16
|
-
* them to a conservative identifier charset so neither side needs
|
|
17
|
-
* escaping gymnastics. */
|
|
18
|
-
var SAFE_ALIAS_PATTERN = /^[A-Za-z_]\w{0,63}$/;
|
|
19
|
-
/** Hard ceiling on returned rows; `limit` clamps below it. A group-by on
|
|
20
|
-
* a near-unique column would otherwise return one row per source row —
|
|
21
|
-
* the exact materialization the aggregate path exists to avoid. */
|
|
22
|
-
var MAX_QUERY_ROWS = 1e4;
|
|
23
|
-
/** Default row cap when the query declares no `limit`. */
|
|
24
|
-
var DEFAULT_QUERY_ROWS = 1e3;
|
|
25
|
-
/** One aggregate column: `count` (rows; `column` optional to count
|
|
26
|
-
* non-null cells) or `sum`/`avg`/`min`/`max` over a named CSV column. */
|
|
27
|
-
var QueryAggregateZ = zod.z.object({
|
|
28
|
-
op: zod.z.enum([
|
|
29
|
-
"count",
|
|
30
|
-
"sum",
|
|
31
|
-
"avg",
|
|
32
|
-
"min",
|
|
33
|
-
"max"
|
|
34
|
-
]),
|
|
35
|
-
column: zod.z.string().min(1).optional()
|
|
36
|
-
}).refine((aggregate) => aggregate.op === "count" || aggregate.column !== void 0, {
|
|
37
|
-
message: "`column` is required for every aggregate op except `count`",
|
|
38
|
-
path: ["column"]
|
|
39
|
-
});
|
|
40
|
-
/** One filter condition. Same op vocabulary as the schema-level `where`
|
|
41
|
-
* (`core/where.ts`) so authors learn one set; values may be typed
|
|
42
|
-
* (number / boolean) since CSV columns are. `in` requires an array
|
|
43
|
-
* value, every other op a scalar. */
|
|
44
|
-
var QueryWhereZ = zod.z.object({
|
|
45
|
-
field: zod.z.string().min(1),
|
|
46
|
-
op: zod.z.enum([
|
|
47
|
-
"eq",
|
|
48
|
-
"ne",
|
|
49
|
-
"in",
|
|
50
|
-
"gt",
|
|
51
|
-
"gte",
|
|
52
|
-
"lt",
|
|
53
|
-
"lte",
|
|
54
|
-
"contains"
|
|
55
|
-
]),
|
|
56
|
-
value: zod.z.union([
|
|
57
|
-
zod.z.string(),
|
|
58
|
-
zod.z.number(),
|
|
59
|
-
zod.z.boolean(),
|
|
60
|
-
zod.z.array(zod.z.union([
|
|
61
|
-
zod.z.string(),
|
|
62
|
-
zod.z.number(),
|
|
63
|
-
zod.z.boolean()
|
|
64
|
-
])).min(1).max(100)
|
|
65
|
-
])
|
|
66
|
-
}).refine((cond) => cond.op === "in" === Array.isArray(cond.value), {
|
|
67
|
-
message: "`in` requires an array value (the allowed set); every other op requires a scalar value",
|
|
68
|
-
path: ["value"]
|
|
69
|
-
});
|
|
70
|
-
var QueryOrderZ = zod.z.object({
|
|
71
|
-
/** A `groupBy` column or an aggregate alias — membership enforced by
|
|
72
|
-
* the whole-query refine below. */
|
|
73
|
-
field: zod.z.string().min(1),
|
|
74
|
-
dir: zod.z.enum(["asc", "desc"]).optional()
|
|
75
|
-
});
|
|
76
|
-
/** The whole query. At least one of `groupBy` / `aggregates` must be
|
|
77
|
-
* present: bare `groupBy` is a DISTINCT listing, bare `aggregates` a
|
|
78
|
-
* whole-file scalar row, together a grouped aggregation. */
|
|
79
|
-
var CollectionQueryZ = zod.z.object({
|
|
80
|
-
groupBy: zod.z.array(zod.z.string().min(1)).max(8).refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { message: "`groupBy` columns must be unique (case-insensitively — SQL identifiers ignore case)" }).optional(),
|
|
81
|
-
aggregates: zod.z.record(zod.z.string().regex(SAFE_ALIAS_PATTERN, "aggregate aliases must be simple identifiers (letters/digits/underscore)"), QueryAggregateZ).optional(),
|
|
82
|
-
where: zod.z.array(QueryWhereZ).max(16).optional(),
|
|
83
|
-
orderBy: zod.z.array(QueryOrderZ).max(4).optional(),
|
|
84
|
-
limit: zod.z.number().int().min(1).max(MAX_QUERY_ROWS).optional()
|
|
85
|
-
}).refine((query) => (query.groupBy?.length ?? 0) > 0 || Object.keys(query.aggregates ?? {}).length > 0, {
|
|
86
|
-
message: "declare at least one of `groupBy` (columns to bucket by) or `aggregates` (values to compute)",
|
|
87
|
-
path: ["groupBy"]
|
|
88
|
-
}).refine((query) => Object.keys(query.aggregates ?? {}).length <= 32, {
|
|
89
|
-
message: `\`aggregates\` supports at most 32 entries`,
|
|
90
|
-
path: ["aggregates"]
|
|
91
|
-
}).refine((query) => {
|
|
92
|
-
const groupLower = new Set((query.groupBy ?? []).map((column) => column.toLowerCase()));
|
|
93
|
-
const seen = /* @__PURE__ */ new Set();
|
|
94
|
-
return Object.keys(query.aggregates ?? {}).every((alias) => {
|
|
95
|
-
const lower = alias.toLowerCase();
|
|
96
|
-
if (groupLower.has(lower) || seen.has(lower)) return false;
|
|
97
|
-
seen.add(lower);
|
|
98
|
-
return true;
|
|
99
|
-
});
|
|
100
|
-
}, {
|
|
101
|
-
message: "aggregate aliases must be unique and must not collide with `groupBy` column names (case-insensitively — SQL identifiers ignore case)",
|
|
102
|
-
path: ["aggregates"]
|
|
103
|
-
}).refine((query) => {
|
|
104
|
-
const sortable = /* @__PURE__ */ new Set([...query.groupBy ?? [], ...Object.keys(query.aggregates ?? {})]);
|
|
105
|
-
return (query.orderBy ?? []).every((order) => sortable.has(order.field));
|
|
106
|
-
}, {
|
|
107
|
-
message: "every `orderBy.field` must be a `groupBy` column or an aggregate alias",
|
|
108
|
-
path: ["orderBy"]
|
|
109
|
-
});
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/collection/server/csvQuery.ts
|
|
112
|
-
/** Double-quote a SQL identifier (CSV column name / result alias). */
|
|
113
|
-
function quoteIdent(name) {
|
|
114
|
-
return `"${name.replaceAll("\"", "\"\"")}"`;
|
|
115
|
-
}
|
|
116
|
-
/** Single-quote a SQL string literal (a `types={...}` struct key). */
|
|
117
|
-
function quoteLiteral(value) {
|
|
118
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
119
|
-
}
|
|
120
|
-
/** The `read_csv` argument list shared by every CSV query: the (prepared)
|
|
121
|
-
* path plus a `types` pin forcing the key column to VARCHAR — without it
|
|
122
|
-
* DuckDB's sniffer turns `001` into BIGINT 1, so leading zeros vanish
|
|
123
|
-
* and distinct keys collapse. */
|
|
124
|
-
function readCsvArgs(primaryKey) {
|
|
125
|
-
return `?, types={${quoteLiteral(primaryKey)}: 'VARCHAR'}`;
|
|
126
|
-
}
|
|
127
|
-
/** One aggregate's SQL expression. `sum`/`avg` TRY_CAST to DOUBLE so a
|
|
128
|
-
* column the sniffer kept as VARCHAR (mixed values) aggregates over its
|
|
129
|
-
* numeric cells instead of erroring; non-numeric cells become NULL and
|
|
130
|
-
* are skipped — standard BI tolerance. `min`/`max` stay native (they are
|
|
131
|
-
* meaningful on strings and dates too). */
|
|
132
|
-
function aggregateExpr(aggregate) {
|
|
133
|
-
const { op, column } = aggregate;
|
|
134
|
-
if (op === "count") return column === void 0 ? "count(*)" : `count(${quoteIdent(column)})`;
|
|
135
|
-
if (op === "sum" || op === "avg") return `${op}(TRY_CAST(${quoteIdent(column ?? "")} AS DOUBLE))`;
|
|
136
|
-
return `${op}(${quoteIdent(column ?? "")})`;
|
|
137
|
-
}
|
|
138
|
-
/** One where condition → SQL fragment + its bound parameters. String
|
|
139
|
-
* equality compares against `CAST(col AS VARCHAR)` so a sniffer-typed
|
|
140
|
-
* column still matches its textual value; numeric/boolean values compare
|
|
141
|
-
* natively (DuckDB coerces the column side). */
|
|
142
|
-
function whereFragment(cond) {
|
|
143
|
-
const column = quoteIdent(cond.field);
|
|
144
|
-
const asText = `CAST(${column} AS VARCHAR)`;
|
|
145
|
-
if (cond.op === "in") {
|
|
146
|
-
const values = cond.value;
|
|
147
|
-
return {
|
|
148
|
-
sql: `${values.every((value) => typeof value === "string") ? asText : column} IN (${values.map(() => "?").join(", ")})`,
|
|
149
|
-
params: values
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (cond.op === "contains") return {
|
|
153
|
-
sql: `contains(${asText}, ?)`,
|
|
154
|
-
params: [String(cond.value)]
|
|
155
|
-
};
|
|
156
|
-
const operator = {
|
|
157
|
-
eq: "=",
|
|
158
|
-
ne: "<>",
|
|
159
|
-
gt: ">",
|
|
160
|
-
gte: ">=",
|
|
161
|
-
lt: "<",
|
|
162
|
-
lte: "<="
|
|
163
|
-
}[cond.op];
|
|
164
|
-
return {
|
|
165
|
-
sql: `${typeof cond.value === "string" && (cond.op === "eq" || cond.op === "ne") ? asText : column} ${operator} ?`,
|
|
166
|
-
params: [cond.value]
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/** Compile a validated query against `fromSql` (a table-function call
|
|
170
|
-
* whose FIRST placeholder is the source path — the executor binds it).
|
|
171
|
-
* Returns the SQL and the where-value parameters that follow the path.
|
|
172
|
-
* Callers MUST have run `CollectionQueryZ` first; this function trusts
|
|
173
|
-
* the shape (aliases already charset-checked, orderBy membership already
|
|
174
|
-
* enforced). */
|
|
175
|
-
function compileQuery(query, fromSql) {
|
|
176
|
-
const groupBy = query.groupBy ?? [];
|
|
177
|
-
const aggregates = Object.entries(query.aggregates ?? {});
|
|
178
|
-
const selectList = [...groupBy.map(quoteIdent), ...aggregates.map(([alias, aggregate]) => `${aggregateExpr(aggregate)} AS ${quoteIdent(alias)}`)];
|
|
179
|
-
const where = (query.where ?? []).map(whereFragment);
|
|
180
|
-
const clauses = [`SELECT ${selectList.join(", ")}`, `FROM ${fromSql}`];
|
|
181
|
-
if (where.length > 0) clauses.push(`WHERE ${where.map((fragment) => fragment.sql).join(" AND ")}`);
|
|
182
|
-
if (groupBy.length > 0) clauses.push(`GROUP BY ${groupBy.map(quoteIdent).join(", ")}`);
|
|
183
|
-
const orderBy = (query.orderBy ?? []).map((order) => quoteIdent(order.field) + (order.dir === "desc" ? " DESC" : " ASC"));
|
|
184
|
-
if (orderBy.length > 0) clauses.push(`ORDER BY ${orderBy.join(", ")}`);
|
|
185
|
-
clauses.push(`LIMIT ${query.limit ?? 1e3}`);
|
|
186
|
-
return {
|
|
187
|
-
sql: clauses.join(" "),
|
|
188
|
-
params: where.flatMap((fragment) => fragment.params)
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/** Compile against a CSV file (the dataSource store's engine). */
|
|
192
|
-
function compileCsvQuery(query, primaryKey) {
|
|
193
|
-
return compileQuery(query, `read_csv(${readCsvArgs(primaryKey)})`);
|
|
194
|
-
}
|
|
195
|
-
/** Compile against a JSONL file of ENRICHED records — the file-backed
|
|
196
|
-
* collections' engine (see `jsonlQuery.ts`). No VARCHAR key pin needed:
|
|
197
|
-
* enriched record ids are already strings. `sample_size=-1` makes the
|
|
198
|
-
* schema inference scan EVERY line — with the default sample, a sparse
|
|
199
|
-
* optional/derived field first appearing past the sample would not be
|
|
200
|
-
* inferred as a column and the query would binder-error on it (Codex P2
|
|
201
|
-
* on #2165). The full scan costs nothing extra here: aggregation reads
|
|
202
|
-
* the whole file anyway. */
|
|
203
|
-
function compileJsonlQuery(query) {
|
|
204
|
-
return compileQuery(query, `read_json(?, format='newline_delimited', sample_size=-1)`);
|
|
205
|
-
}
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region src/collection/server/csvStore.ts
|
|
208
|
-
/** `list()` row cap. Over-cap files are truncated with a warn — the v1
|
|
209
|
-
* contract is "browse + per-record views", not full-table analytics. */
|
|
210
|
-
var MAX_CSV_ROWS = 5e3;
|
|
211
|
-
/** Record ids minted from non-safe key values: `id0x` + utf-8 hex. Raw key
|
|
212
|
-
* values that themselves match this pattern are ALSO encoded, so the
|
|
213
|
-
* encoded namespace never collides with a raw value (injective mapping). */
|
|
214
|
-
var ENCODED_ID_PATTERN = /^id0x([0-9a-f]+)$/;
|
|
215
|
-
/** A CSV key value → the record id it's addressed by. Safe values pass
|
|
216
|
-
* through untouched; everything else (and anything shaped like an encoded
|
|
217
|
-
* id) becomes `id0x<hex>`. Pure + exported for unit tests. */
|
|
218
|
-
function encodeCsvRecordId(rawKey) {
|
|
219
|
-
if (require_discovery.safeRecordId(rawKey) === rawKey && !ENCODED_ID_PATTERN.test(rawKey)) return rawKey;
|
|
220
|
-
return `id0x${Buffer.from(rawKey, "utf-8").toString("hex")}`;
|
|
221
|
-
}
|
|
222
|
-
/** A record id → the CSV key value to look up. Inverse of
|
|
223
|
-
* `encodeCsvRecordId` for encoded ids; anything else is already the raw
|
|
224
|
-
* value. Pure + exported for unit tests. */
|
|
225
|
-
function decodeCsvRecordId(itemId) {
|
|
226
|
-
const match = ENCODED_ID_PATTERN.exec(itemId);
|
|
227
|
-
if (!match) return itemId;
|
|
228
|
-
return Buffer.from(match[1], "hex").toString("utf-8");
|
|
229
|
-
}
|
|
230
|
-
/** Normalize one DuckDB JS value into a JSON-safe record value: BigInt →
|
|
231
|
-
* number (string beyond the safe range), DATE/TIMESTAMP → ISO string
|
|
232
|
-
* (date-only when the clock is exactly UTC midnight, matching the `date`
|
|
233
|
-
* field contract), exotic DuckDB values → their string form. Pure +
|
|
234
|
-
* exported for unit tests. */
|
|
235
|
-
function normalizeCsvValue(value) {
|
|
236
|
-
if (typeof value === "bigint") return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
237
|
-
if (value instanceof Date) {
|
|
238
|
-
const iso = value.toISOString();
|
|
239
|
-
return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
|
|
240
|
-
}
|
|
241
|
-
if (value !== null && typeof value === "object") return String(value);
|
|
242
|
-
return value;
|
|
243
|
-
}
|
|
244
|
-
/** One raw DuckDB row → a CollectionItem, or null when the key cell is
|
|
245
|
-
* missing/empty (the row can't be addressed). The primaryKey field is
|
|
246
|
-
* OVERWRITTEN with the (possibly encoded) record id so `item[primaryKey]`
|
|
247
|
-
* and the record's address never drift — same invariant the file store's
|
|
248
|
-
* write path enforces. Pure + exported for unit tests. */
|
|
249
|
-
function csvRowToItem(row, primaryKey) {
|
|
250
|
-
const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
|
|
251
|
-
const rawKey = normalized[primaryKey];
|
|
252
|
-
if (rawKey === null || rawKey === void 0 || String(rawKey) === "") return null;
|
|
253
|
-
return {
|
|
254
|
-
...normalized,
|
|
255
|
-
[primaryKey]: encodeCsvRecordId(String(rawKey))
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
/** Dedupe by record id, LAST row wins (matches `csvRead`'s last-match
|
|
259
|
-
* pick). Returns the surviving items in first-seen order. Pure +
|
|
260
|
-
* exported for unit tests. */
|
|
261
|
-
function dedupeByRecordId(items, primaryKey) {
|
|
262
|
-
const byId = /* @__PURE__ */ new Map();
|
|
263
|
-
for (const item of items) byId.set(String(item[primaryKey]), item);
|
|
264
|
-
return {
|
|
265
|
-
items: [...byId.values()],
|
|
266
|
-
duplicates: items.length - byId.size
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
/** True when a thrown DuckDB error is the `types` pin naming a column the
|
|
270
|
-
* CSV doesn't have — the schema/file-mismatch case the caller downgrades
|
|
271
|
-
* to "empty collection + warn" instead of a 500. */
|
|
272
|
-
function isMissingKeyColumnError(err) {
|
|
273
|
-
return String(err).includes("do not exist in the CSV");
|
|
274
|
-
}
|
|
275
|
-
/** Bytes sniffed for UTF-8 validity. The trailing 3 bytes of the sample
|
|
276
|
-
* are dropped so a multibyte char split at the boundary can't produce a
|
|
277
|
-
* false negative on a valid file. */
|
|
278
|
-
var SNIFF_BYTES = 1024 * 1024;
|
|
279
|
-
function isValidUtf8(buf) {
|
|
280
|
-
try {
|
|
281
|
-
new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
282
|
-
return true;
|
|
283
|
-
} catch {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
/** Detect the (best-effort) encoding of a non-UTF-8 buffer. BOMs decide
|
|
288
|
-
* UTF-16; otherwise cp932 (the Shift_JIS superset — Excel-exported
|
|
289
|
-
* Japanese CSVs are the primary non-UTF-8 case this feature serves). */
|
|
290
|
-
function fallbackEncoding(buf) {
|
|
291
|
-
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) return "utf-16le";
|
|
292
|
-
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) return "utf-16be";
|
|
293
|
-
return "cp932";
|
|
294
|
-
}
|
|
295
|
-
function cacheDir() {
|
|
296
|
-
return node_path.default.join((0, node_os.tmpdir)(), "mulmoclaude-csv-utf8");
|
|
297
|
-
}
|
|
298
|
-
/** Read only the first `bytes` of a file — the encoding sniff must not
|
|
299
|
-
* pull a multi-hundred-MB CSV into memory on the (common) UTF-8 path. */
|
|
300
|
-
async function readHead(absPath, bytes) {
|
|
301
|
-
const handle = await (0, node_fs_promises.open)(absPath, "r");
|
|
302
|
-
try {
|
|
303
|
-
const { size } = await handle.stat();
|
|
304
|
-
const buf = Buffer.alloc(Math.min(bytes, size));
|
|
305
|
-
await handle.read(buf, 0, buf.length, 0);
|
|
306
|
-
return buf;
|
|
307
|
-
} finally {
|
|
308
|
-
await handle.close();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/** Decode the whole file into a UTF-8 cache copy and return its path.
|
|
312
|
-
* Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
|
|
313
|
-
* unchanged one never does. */
|
|
314
|
-
async function pathExists$1(target) {
|
|
315
|
-
try {
|
|
316
|
-
await (0, node_fs_promises.stat)(target);
|
|
317
|
-
return true;
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/** Best-effort removal of older decode-cache entries for the same source
|
|
323
|
-
* path — a frequently-replaced large CSV would otherwise accumulate one
|
|
324
|
-
* full copy per (mtime, size) forever. Runs AFTER the current copy is
|
|
325
|
-
* published; a concurrent reader holding an old fd is unaffected
|
|
326
|
-
* (unlink-while-open is safe on POSIX). */
|
|
327
|
-
async function evictSupersededCache(key, keepBasename) {
|
|
328
|
-
try {
|
|
329
|
-
const entries = await (0, node_fs_promises.readdir)(cacheDir());
|
|
330
|
-
await Promise.all(entries.filter((name) => name.startsWith(`${key}-`) && name !== keepBasename).map((name) => (0, node_fs_promises.unlink)(node_path.default.join(cacheDir(), name)).catch(() => void 0)));
|
|
331
|
-
} catch {}
|
|
332
|
-
}
|
|
333
|
-
/** Decode the whole file into a UTF-8 cache copy and return its path.
|
|
334
|
-
* Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
|
|
335
|
-
* unchanged one never does; superseded copies are evicted. The cache
|
|
336
|
-
* lives in the SHARED OS tmpdir, so the dir is 0700 and files 0600 —
|
|
337
|
-
* decoded rows must not be readable by other local users. */
|
|
338
|
-
async function decodeToCache(absPath, info) {
|
|
339
|
-
const key = (0, node_crypto.createHash)("sha256").update(absPath).digest("hex").slice(0, 16);
|
|
340
|
-
const cached = node_path.default.join(cacheDir(), `${key}-${Math.trunc(info.mtimeMs)}-${info.size}.csv`);
|
|
341
|
-
if (!await pathExists$1(cached)) {
|
|
342
|
-
const whole = await (0, node_fs_promises.readFile)(absPath);
|
|
343
|
-
const encoding = fallbackEncoding(whole);
|
|
344
|
-
const text = iconv_lite.default.decode(whole, encoding);
|
|
345
|
-
await (0, node_fs_promises.mkdir)(cacheDir(), {
|
|
346
|
-
recursive: true,
|
|
347
|
-
mode: 448
|
|
348
|
-
});
|
|
349
|
-
const tmp = `${cached}.${(0, node_crypto.randomBytes)(4).toString("hex")}.tmp`;
|
|
350
|
-
await (0, node_fs_promises.writeFile)(tmp, text, {
|
|
351
|
-
encoding: "utf-8",
|
|
352
|
-
mode: 384
|
|
353
|
-
});
|
|
354
|
-
await (0, node_fs_promises.rename)(tmp, cached);
|
|
355
|
-
require_discovery.log.info("collections", "decoded non-UTF-8 dataSource file to cache", {
|
|
356
|
-
path: absPath,
|
|
357
|
-
encoding
|
|
358
|
-
});
|
|
359
|
-
await evictSupersededCache(key, node_path.default.basename(cached));
|
|
360
|
-
}
|
|
361
|
-
return cached;
|
|
362
|
-
}
|
|
363
|
-
/** Re-validate the dataSource file at READ time, mirroring the JSON
|
|
364
|
-
* store's per-read defenses: realpath containment (a symlink swapped in
|
|
365
|
-
* after discovery must not walk out of the workspace) and an lstat
|
|
366
|
-
* regular-file check (a symlink leaf is refused outright, even one
|
|
367
|
-
* pointing inside the workspace — same rule as `isRegularFile` on
|
|
368
|
-
* record files). Returns the stat info, or null for "no readable file"
|
|
369
|
-
* (ENOENT / refused), which callers render as an empty collection. */
|
|
370
|
-
async function safeCsvStat(absPath, workspaceRoot) {
|
|
371
|
-
if (!require_discovery.isContainedInRoot(absPath, workspaceRoot)) {
|
|
372
|
-
require_discovery.log.warn("collections", "dataSource read refused: path escapes workspace", { path: absPath });
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
let info;
|
|
376
|
-
try {
|
|
377
|
-
info = await (0, node_fs_promises.lstat)(absPath);
|
|
378
|
-
} catch (err) {
|
|
379
|
-
if (err.code === "ENOENT") return null;
|
|
380
|
-
throw err;
|
|
381
|
-
}
|
|
382
|
-
if (!info.isFile()) {
|
|
383
|
-
require_discovery.log.warn("collections", "dataSource read refused: not a regular file (symlink?)", { path: absPath });
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
return info;
|
|
387
|
-
}
|
|
388
|
-
/** Return a path DuckDB can read as UTF-8: the original file when it
|
|
389
|
-
* already is UTF-8 (the cheap, common case — only the head is sniffed),
|
|
390
|
-
* else a decoded cache copy (see `decodeToCache`). Returns null when
|
|
391
|
-
* there is no readable file (missing, symlink, or containment-refused —
|
|
392
|
-
* see `safeCsvStat`), which callers render as an empty collection. */
|
|
393
|
-
async function ensureUtf8CsvPath(absPath, workspaceRoot) {
|
|
394
|
-
const info = await safeCsvStat(absPath, workspaceRoot);
|
|
395
|
-
if (info === null) return null;
|
|
396
|
-
const head = await readHead(absPath, SNIFF_BYTES);
|
|
397
|
-
const sample = head.length === SNIFF_BYTES ? head.subarray(0, SNIFF_BYTES - 3) : head;
|
|
398
|
-
if (!(head.length >= 2 && (head[0] === 255 && head[1] === 254 || head[0] === 254 && head[1] === 255)) && isValidUtf8(sample)) return absPath;
|
|
399
|
-
return decodeToCache(absPath, info);
|
|
400
|
-
}
|
|
401
|
-
var instancePromise = null;
|
|
402
|
-
/** Lazily create one shared in-memory DuckDB instance. The dynamic import
|
|
403
|
-
* keeps the native module OUT of core's load path — a platform where the
|
|
404
|
-
* prebuilt binding is missing degrades to a per-query error on dataSource
|
|
405
|
-
* collections only, never a broken core. A failed init is retried on the
|
|
406
|
-
* next call (the promise is reset). */
|
|
407
|
-
async function duckDbInstance() {
|
|
408
|
-
if (instancePromise === null) instancePromise = import("@duckdb/node-api").then((mod) => mod.DuckDBInstance.create(":memory:"));
|
|
409
|
-
try {
|
|
410
|
-
return await instancePromise;
|
|
411
|
-
} catch (err) {
|
|
412
|
-
instancePromise = null;
|
|
413
|
-
throw new Error(`DuckDB is unavailable on this host (@duckdb/node-api failed to load: ${String(err)}) — dataSource collections cannot be read`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async function queryCsv(sql, params) {
|
|
417
|
-
const connection = await (await duckDbInstance()).connect();
|
|
418
|
-
try {
|
|
419
|
-
return (await connection.runAndReadAll(sql, params)).getRowObjectsJS();
|
|
420
|
-
} finally {
|
|
421
|
-
connection.disconnectSync();
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/** Every row of the CSV as records — capped, deduped, id-encoded. The
|
|
425
|
-
* key column is pinned to VARCHAR (see `readCsvArgs`). `workspaceRoot`
|
|
426
|
-
* drives the per-read containment check; omitted, the configured host
|
|
427
|
-
* root is used. */
|
|
428
|
-
async function csvList(absPath, primaryKey, workspaceRoot) {
|
|
429
|
-
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? require_discovery.getWorkspaceRoot());
|
|
430
|
-
if (utf8Path === null) return [];
|
|
431
|
-
let rows;
|
|
432
|
-
try {
|
|
433
|
-
rows = await queryCsv(`SELECT * FROM read_csv(${readCsvArgs(primaryKey)}) LIMIT 5001`, [utf8Path]);
|
|
434
|
-
} catch (err) {
|
|
435
|
-
if (!isMissingKeyColumnError(err)) throw err;
|
|
436
|
-
require_discovery.log.warn("collections", "dataSource CSV has no primaryKey column — every row is skipped", {
|
|
437
|
-
path: absPath,
|
|
438
|
-
primaryKey
|
|
439
|
-
});
|
|
440
|
-
return [];
|
|
441
|
-
}
|
|
442
|
-
if (rows.length > 5e3) {
|
|
443
|
-
require_discovery.log.warn("collections", "dataSource CSV truncated to row cap", {
|
|
444
|
-
path: absPath,
|
|
445
|
-
cap: MAX_CSV_ROWS
|
|
446
|
-
});
|
|
447
|
-
rows.length = MAX_CSV_ROWS;
|
|
448
|
-
}
|
|
449
|
-
const items = rows.map((row) => csvRowToItem(row, primaryKey)).filter((item) => item !== null);
|
|
450
|
-
const skipped = rows.length - items.length;
|
|
451
|
-
if (skipped > 0) require_discovery.log.warn("collections", "dataSource CSV rows skipped (empty key cell)", {
|
|
452
|
-
path: absPath,
|
|
453
|
-
skipped
|
|
454
|
-
});
|
|
455
|
-
const deduped = dedupeByRecordId(items, primaryKey);
|
|
456
|
-
if (deduped.duplicates > 0) require_discovery.log.warn("collections", "dataSource CSV has duplicate key values (last row wins)", {
|
|
457
|
-
path: absPath,
|
|
458
|
-
duplicates: deduped.duplicates
|
|
459
|
-
});
|
|
460
|
-
return deduped.items;
|
|
461
|
-
}
|
|
462
|
-
/** The scan-order ordinal column the last-match read adds. Underscore
|
|
463
|
-
* prefix keeps it out of any plausible CSV header namespace; it is
|
|
464
|
-
* stripped from the returned record either way. */
|
|
465
|
-
var ROW_ORDINAL = "__mc_row";
|
|
466
|
-
/** One record by id. The comparison value rides as a prepared-statement
|
|
467
|
-
* parameter, and the LAST matching row is selected IN DuckDB (scan-order
|
|
468
|
-
* ordinal + LIMIT 1) — a CSV with thousands of duplicate keys must not
|
|
469
|
-
* materialize them all for one detail read. Consistent with csvList's
|
|
470
|
-
* last-wins dedupe. */
|
|
471
|
-
async function csvRead(absPath, primaryKey, itemId, workspaceRoot) {
|
|
472
|
-
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? require_discovery.getWorkspaceRoot());
|
|
473
|
-
if (utf8Path === null) return null;
|
|
474
|
-
const rawKey = decodeCsvRecordId(itemId);
|
|
475
|
-
const last = (await queryCsv(`SELECT * FROM (SELECT *, row_number() OVER () AS ${quoteIdent(ROW_ORDINAL)} FROM read_csv(${readCsvArgs(primaryKey)})) WHERE CAST(${quoteIdent(primaryKey)} AS VARCHAR) = ? ORDER BY ${quoteIdent(ROW_ORDINAL)} DESC LIMIT 1`, [utf8Path, rawKey])).at(0);
|
|
476
|
-
if (last === void 0) return null;
|
|
477
|
-
const { [ROW_ORDINAL]: __ordinal, ...record } = last;
|
|
478
|
-
return csvRowToItem(record, primaryKey);
|
|
479
|
-
}
|
|
480
|
-
/** Run a validated aggregation query (the structured DSL — see
|
|
481
|
-
* `core/queryZ.ts`) over the WHOLE file: no row cap on the scan (a
|
|
482
|
-
* capped aggregate would be a wrong number), only the result-row LIMIT
|
|
483
|
-
* the compiler emits. Values are normalized like list/read rows so a
|
|
484
|
-
* chart consumer gets plain JSON scalars. */
|
|
485
|
-
async function csvRunQuery(absPath, primaryKey, query, workspaceRoot) {
|
|
486
|
-
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? require_discovery.getWorkspaceRoot());
|
|
487
|
-
if (utf8Path === null) return [];
|
|
488
|
-
const { sql, params } = compileCsvQuery(query, primaryKey);
|
|
489
|
-
return (await queryCsv(sql, [utf8Path, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
|
|
490
|
-
}
|
|
491
|
-
//#endregion
|
|
492
|
-
//#region src/collection/server/store.ts
|
|
493
|
-
/** True when the collection accepts UI/tool writes. A `dataSource`
|
|
494
|
-
* collection is read-only: updates happen by editing/replacing the
|
|
495
|
-
* data file itself. Every write entry point checks this BEFORE calling
|
|
496
|
-
* `writeItem`/`deleteItem` — server-enforced, not just UI-hidden. */
|
|
497
|
-
function collectionWritable(collection) {
|
|
498
|
-
return !require_ids.isReadOnlySchema(collection.schema);
|
|
499
|
-
}
|
|
500
|
-
/** The one-line refusal write paths surface (HTTP 405 / MCP error text). */
|
|
501
|
-
function readOnlyRefusal(slug) {
|
|
502
|
-
return `collection '${slug}' is read-only (backed by an external dataSource) — update the data file itself instead`;
|
|
503
|
-
}
|
|
504
|
-
/** Pick the store implementation for a discovered collection. A
|
|
505
|
-
* `dataSource` schema whose `dataSourceFile` failed to resolve yields a
|
|
506
|
-
* read-only EMPTY store rather than falling back to the (writable) file
|
|
507
|
-
* store — a half-loaded read-only collection must never become writable. */
|
|
508
|
-
function storeFor(collection, opts = {}) {
|
|
509
|
-
if (require_ids.isReadOnlySchema(collection.schema)) {
|
|
510
|
-
const file = collection.dataSourceFile;
|
|
511
|
-
const key = collection.schema.primaryKey;
|
|
512
|
-
return {
|
|
513
|
-
capabilities: { writable: false },
|
|
514
|
-
list: () => file === void 0 ? Promise.resolve([]) : csvList(file, key, opts.workspaceRoot),
|
|
515
|
-
read: (itemId) => file === void 0 ? Promise.resolve(null) : csvRead(file, key, itemId, opts.workspaceRoot),
|
|
516
|
-
query: (query) => file === void 0 ? Promise.resolve([]) : csvRunQuery(file, key, query, opts.workspaceRoot)
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
capabilities: { writable: true },
|
|
521
|
-
list: () => require_discovery.listItems(collection.dataDir, opts),
|
|
522
|
-
read: (itemId) => require_discovery.readItem(collection.dataDir, itemId, opts)
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
//#endregion
|
|
526
11
|
//#region src/collection/server/jsonlQuery.ts
|
|
527
12
|
/** SQL semantics for an aggregate-only query over ZERO rows: one scalar
|
|
528
13
|
* row (`count` = 0, everything else NULL) — the same shape the CSV path
|
|
@@ -559,11 +44,11 @@ function absentReferencedColumns(rows, query) {
|
|
|
559
44
|
/** Run a validated query over enriched collection rows. */
|
|
560
45
|
async function runQueryOverRows(rows, query) {
|
|
561
46
|
if (rows.length === 0) return emptyCollectionResult(query);
|
|
562
|
-
await (0, node_fs_promises.mkdir)(cacheDir(), {
|
|
47
|
+
await (0, node_fs_promises.mkdir)(require_discovery.cacheDir(), {
|
|
563
48
|
recursive: true,
|
|
564
49
|
mode: 448
|
|
565
50
|
});
|
|
566
|
-
const jsonlPath = node_path.default.join(cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
|
|
51
|
+
const jsonlPath = node_path.default.join(require_discovery.cacheDir(), `q-${(0, node_crypto.randomBytes)(8).toString("hex")}.jsonl`);
|
|
567
52
|
try {
|
|
568
53
|
const nullPads = Object.fromEntries(absentReferencedColumns(rows, query).map((column) => [column, null]));
|
|
569
54
|
const handle = await (0, node_fs_promises.open)(jsonlPath, "wx", 384);
|
|
@@ -579,8 +64,8 @@ async function runQueryOverRows(rows, query) {
|
|
|
579
64
|
} finally {
|
|
580
65
|
await handle.close();
|
|
581
66
|
}
|
|
582
|
-
const { sql, params } = compileJsonlQuery(query);
|
|
583
|
-
return (await queryCsv(sql, [jsonlPath, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
|
|
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)])));
|
|
584
69
|
} finally {
|
|
585
70
|
await (0, node_fs_promises.unlink)(jsonlPath).catch((err) => {
|
|
586
71
|
if (err.code !== "ENOENT") require_discovery.log.warn("collections", "temp JSONL cleanup failed", {
|
|
@@ -591,6 +76,132 @@ async function runQueryOverRows(rows, query) {
|
|
|
591
76
|
}
|
|
592
77
|
}
|
|
593
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
|
|
594
205
|
//#region src/collection/core/recordZ.ts
|
|
595
206
|
/** The emptiness rule shared by `required` and the "only check present
|
|
596
207
|
* values" gate. NOT a truthiness check — `0` and `false` are filled. */
|
|
@@ -708,6 +319,7 @@ async function listRecordFilenames(dataDir, workspaceRoot) {
|
|
|
708
319
|
}
|
|
709
320
|
async function validateCollectionRecords(collection, opts = {}) {
|
|
710
321
|
if (collection.schema.dataSource !== void 0) return [];
|
|
322
|
+
if (collection.schema.storage !== void 0) return validateStoreRecords(collection, opts);
|
|
711
323
|
const workspaceRoot = opts.workspaceRoot ?? require_discovery.getWorkspaceRoot();
|
|
712
324
|
const entries = await listRecordFilenames(collection.dataDir, workspaceRoot);
|
|
713
325
|
const issues = [];
|
|
@@ -719,6 +331,34 @@ async function validateCollectionRecords(collection, opts = {}) {
|
|
|
719
331
|
}
|
|
720
332
|
return issues;
|
|
721
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
|
+
}
|
|
722
362
|
async function readRecordText(fullPath, name) {
|
|
723
363
|
try {
|
|
724
364
|
if (!(await (0, node_fs_promises.lstat)(fullPath)).isFile()) return {
|
|
@@ -796,7 +436,13 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
|
|
|
796
436
|
status: "invalid-params",
|
|
797
437
|
problem: paramProblem
|
|
798
438
|
};
|
|
799
|
-
const
|
|
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);
|
|
800
446
|
if (!existing) return {
|
|
801
447
|
ok: false,
|
|
802
448
|
status: "not-found",
|
|
@@ -821,10 +467,7 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
|
|
|
821
467
|
status: "invalid-record",
|
|
822
468
|
problem: invalid
|
|
823
469
|
};
|
|
824
|
-
const result = await
|
|
825
|
-
workspaceRoot: opts.workspaceRoot,
|
|
826
|
-
slug: collection.slug
|
|
827
|
-
});
|
|
470
|
+
const result = await store.write(itemId, merged);
|
|
828
471
|
if (result.kind !== "ok") return {
|
|
829
472
|
ok: false,
|
|
830
473
|
status: "write-refused",
|
|
@@ -882,14 +525,14 @@ async function countRecordFiles(dataDir, workspaceRoot) {
|
|
|
882
525
|
}
|
|
883
526
|
}
|
|
884
527
|
/** A collection's record count for the ontology. File-backed: the cheap
|
|
885
|
-
* readdir count.
|
|
886
|
-
*
|
|
887
|
-
* readdir count would always misreport 0. Fail-soft to 0 like
|
|
888
|
-
* `countRecordFiles` (an unreadable file / missing
|
|
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
|
|
889
532
|
* break the whole ontology). */
|
|
890
533
|
async function countRecords(collection, workspaceRoot) {
|
|
891
|
-
if (
|
|
892
|
-
return (await storeFor(collection, { workspaceRoot }).
|
|
534
|
+
if (require_ids.storageKindFor(collection.schema) !== "file") try {
|
|
535
|
+
return (await require_discovery.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
|
|
893
536
|
} catch {
|
|
894
537
|
return 0;
|
|
895
538
|
}
|
|
@@ -916,125 +559,6 @@ async function buildWorkspaceOntology(opts = {}) {
|
|
|
916
559
|
return Promise.all(collections.map((collection) => toOntologyEntry(collection, workspaceRoot)));
|
|
917
560
|
}
|
|
918
561
|
//#endregion
|
|
919
|
-
//#region src/collection/server/derive.ts
|
|
920
|
-
/** Slugs of every collection referenced by a `ref` field — top-level
|
|
921
|
-
* and one level into `table` sub-fields (nested tables are
|
|
922
|
-
* schema-rejected). Mirrors the client's `uniqueRefTargets`. */
|
|
923
|
-
function uniqueRefTargets(schema) {
|
|
924
|
-
const targets = /* @__PURE__ */ new Set();
|
|
925
|
-
const walk = (fields) => {
|
|
926
|
-
for (const field of Object.values(fields)) {
|
|
927
|
-
if (field.type === "ref" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
928
|
-
if (field.type === "table" && field.of) walk(field.of);
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
walk(schema.fields);
|
|
932
|
-
return [...targets];
|
|
933
|
-
}
|
|
934
|
-
/** Slugs of every collection referenced by an `embed` field (top-level
|
|
935
|
-
* only, like the client). */
|
|
936
|
-
function uniqueEmbedTargets(schema) {
|
|
937
|
-
const targets = /* @__PURE__ */ new Set();
|
|
938
|
-
for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
939
|
-
return [...targets];
|
|
940
|
-
}
|
|
941
|
-
/** Slugs of every SOURCE collection a `backlinks` or `rollup` field
|
|
942
|
-
* reverses over — loaded exactly like ref/embed targets (whole
|
|
943
|
-
* collection, once; the two field kinds share one load). */
|
|
944
|
-
function uniqueBacklinkSources(schema) {
|
|
945
|
-
const sources = /* @__PURE__ */ new Set();
|
|
946
|
-
for (const field of Object.values(schema.fields)) if ((field.type === "backlinks" || field.type === "rollup") && field.from.length > 0) sources.add(field.from);
|
|
947
|
-
return [...sources];
|
|
948
|
-
}
|
|
949
|
-
async function loadTarget(slug, opts) {
|
|
950
|
-
const target = await require_discovery.loadCollection(slug, opts);
|
|
951
|
-
if (!target) return null;
|
|
952
|
-
const items = await storeFor(target, { workspaceRoot: opts.workspaceRoot }).list();
|
|
953
|
-
const byId = {};
|
|
954
|
-
for (const item of items) {
|
|
955
|
-
const itemId = item[target.schema.primaryKey];
|
|
956
|
-
if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_promptSafety.deriveAll(target.schema, item, {});
|
|
957
|
-
}
|
|
958
|
-
return {
|
|
959
|
-
schema: target.schema,
|
|
960
|
-
byId
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
/** Load every ref/embed target and backlink source collection once.
|
|
964
|
-
* Unknown / unloadable targets are simply absent — downstream derefs
|
|
965
|
-
* resolve to null (em-dash) and backlinks to an empty row set, the
|
|
966
|
-
* same fail-soft the UI renders. */
|
|
967
|
-
async function loadLinkedTargets(schema, opts) {
|
|
968
|
-
const slugs = [.../* @__PURE__ */ new Set([
|
|
969
|
-
...uniqueRefTargets(schema),
|
|
970
|
-
...uniqueEmbedTargets(schema),
|
|
971
|
-
...uniqueBacklinkSources(schema)
|
|
972
|
-
])];
|
|
973
|
-
const loaded = {};
|
|
974
|
-
for (const slug of slugs) {
|
|
975
|
-
const target = await loadTarget(slug, opts);
|
|
976
|
-
if (target) loaded[slug] = target;
|
|
977
|
-
}
|
|
978
|
-
return loaded;
|
|
979
|
-
}
|
|
980
|
-
function toRefRecords(linked) {
|
|
981
|
-
return Object.fromEntries(Object.entries(linked).map(([slug, target]) => [slug, target.byId]));
|
|
982
|
-
}
|
|
983
|
-
/** The matching source rows for one `backlinks` field, projected to the
|
|
984
|
-
* source primaryKey + `display` columns — so getItems on a
|
|
985
|
-
* heavily-referenced record stays a summary, not a dump of the source
|
|
986
|
-
* collection. Missing source ⇒ [] (fail-soft). The rows come from the
|
|
987
|
-
* DERIVED source records (`byId`), so `display`/`filter` on a derived
|
|
988
|
-
* source column (an invoice `total`) works. */
|
|
989
|
-
function projectBacklinks(field, schema, enriched, linked) {
|
|
990
|
-
const source = linked[field.from];
|
|
991
|
-
if (!source) return [];
|
|
992
|
-
return require_promptSafety.backlinkRows(field, String(enriched[schema.primaryKey] ?? ""), Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
|
|
993
|
-
}
|
|
994
|
-
/** Project the computed (never-stored) field kinds onto one derived
|
|
995
|
-
* record: `toggle` → boolean off its enum, `embed` → the target record
|
|
996
|
-
* (fixed `id` or per-record `idField`), or null when missing,
|
|
997
|
-
* `backlinks` → the matching source rows (see `projectBacklinks`). */
|
|
998
|
-
function projectComputed(schema, enriched, linked) {
|
|
999
|
-
for (const [key, field] of Object.entries(schema.fields)) {
|
|
1000
|
-
if (field.type === "toggle" && field.field) enriched[key] = String(enriched[field.field] ?? "") === field.onValue;
|
|
1001
|
-
if (field.type === "embed" && field.to) {
|
|
1002
|
-
const targetId = require_ids.embedTargetId(field, enriched);
|
|
1003
|
-
enriched[key] = targetId && linked[field.to]?.byId[targetId] || null;
|
|
1004
|
-
}
|
|
1005
|
-
if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
|
|
1006
|
-
}
|
|
1007
|
-
return enriched;
|
|
1008
|
-
}
|
|
1009
|
-
/** Resolve every rollup field onto a COPY of the record, BEFORE the
|
|
1010
|
-
* formula pass — a `derived` formula may reference rollup values as
|
|
1011
|
-
* plain identifiers (`played = homePlayed + awayPlayed`). Same reverse
|
|
1012
|
-
* machinery as backlinks, collapsed to a number: an unresolvable source
|
|
1013
|
-
* is null (a formula reading it fails soft to em-dash); an empty match
|
|
1014
|
-
* set is a real 0. Returns `record` unchanged when the schema declares
|
|
1015
|
-
* no rollups. */
|
|
1016
|
-
function projectRollups(schema, record, linked) {
|
|
1017
|
-
let out = record;
|
|
1018
|
-
for (const [key, field] of Object.entries(schema.fields)) {
|
|
1019
|
-
if (field.type !== "rollup") continue;
|
|
1020
|
-
if (out === record) out = { ...record };
|
|
1021
|
-
const source = linked[field.from];
|
|
1022
|
-
const selfId = String(record[schema.primaryKey] ?? "");
|
|
1023
|
-
out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
|
|
1024
|
-
}
|
|
1025
|
-
return out;
|
|
1026
|
-
}
|
|
1027
|
-
/** Enrich records with every host-computed field: derived formulas
|
|
1028
|
-
* evaluated (cross-collection derefs included), toggles projected,
|
|
1029
|
-
* embeds resolved. Loads each linked collection ONCE per call. Input
|
|
1030
|
-
* records are not mutated. */
|
|
1031
|
-
async function enrichItems(collection, items, opts = {}) {
|
|
1032
|
-
const { schema } = collection;
|
|
1033
|
-
const linked = await loadLinkedTargets(schema, opts);
|
|
1034
|
-
const refRecords = toRefRecords(linked);
|
|
1035
|
-
return items.map((item) => projectComputed(schema, require_promptSafety.deriveAll(schema, projectRollups(schema, item, linked), refRecords), linked));
|
|
1036
|
-
}
|
|
1037
|
-
//#endregion
|
|
1038
562
|
//#region src/collection/server/dynamicIcon.ts
|
|
1039
563
|
/** Index `items` by their `primaryKey` value — the `recordsById` map a
|
|
1040
564
|
* `valueFrom` reference (e.g. `_config.defaultCity`) resolves against.
|
|
@@ -1067,7 +591,7 @@ async function computeCollectionIcon(collection, opts = {}) {
|
|
|
1067
591
|
try {
|
|
1068
592
|
const source = await require_discovery.loadCollection(spec.source.collection, opts);
|
|
1069
593
|
if (!source) return spec.fallback ?? schema.icon;
|
|
1070
|
-
const ordered = sortByPrimaryKey(await storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
|
|
594
|
+
const ordered = sortByPrimaryKey(await require_discovery.storeFor(source, { workspaceRoot: opts.workspaceRoot }).list(), source.schema.primaryKey);
|
|
1071
595
|
const orderBy = spec.source.orderBy ?? require_promptSafety.firstDateField(source.schema);
|
|
1072
596
|
const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
|
|
1073
597
|
return require_promptSafety.resolveIcon(require_promptSafety.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
|
|
@@ -1255,7 +779,8 @@ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
|
|
|
1255
779
|
* predicate doesn't match, the trigger date is unparseable, or the
|
|
1256
780
|
* successor already exists (create-if-absent). Never overwrites an
|
|
1257
781
|
* existing successor — protects any edits the user made to it. */
|
|
1258
|
-
async function maybeSpawnSuccessor(
|
|
782
|
+
async function maybeSpawnSuccessor(collection, sourceItem, sourceId, ioOpts = {}) {
|
|
783
|
+
const { slug, schema } = collection;
|
|
1259
784
|
const { spawn } = schema;
|
|
1260
785
|
if (!spawn || !schema.triggerField) return;
|
|
1261
786
|
if (!matchesWhen(spawn.when, schema, sourceItem)) return;
|
|
@@ -1273,11 +798,15 @@ async function maybeSpawnSuccessor(slug, schema, dataDir, sourceItem, sourceId,
|
|
|
1273
798
|
return;
|
|
1274
799
|
}
|
|
1275
800
|
try {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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 });
|
|
1281
810
|
if (result.kind === "ok") require_discovery.log.info("collections", "spawned successor", {
|
|
1282
811
|
slug,
|
|
1283
812
|
sourceId,
|
|
@@ -1335,7 +864,8 @@ function deleteTargets(collection, workspaceRoot) {
|
|
|
1335
864
|
return [
|
|
1336
865
|
stagingSkillDir(workspaceRoot, collection.slug),
|
|
1337
866
|
collection.skillDir,
|
|
1338
|
-
collection.dataDir
|
|
867
|
+
collection.dataDir,
|
|
868
|
+
...collection.storageFile !== void 0 ? [collection.storageFile] : []
|
|
1339
869
|
];
|
|
1340
870
|
}
|
|
1341
871
|
/** The records directory the delete recursively archives + removes
|
|
@@ -1367,6 +897,11 @@ function isDataDirSafe(dataDir, slug, workspaceRoot) {
|
|
|
1367
897
|
* `dataSource` collection has no record files to copy (its rows live in
|
|
1368
898
|
* the external data file, which the delete never touches). */
|
|
1369
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.`;
|
|
1370
905
|
if (schema.dataPath === void 0) return `2. Records: nothing to copy. This is a \`dataSource\` collection —
|
|
1371
906
|
its records are the rows of \`${schema.dataSource?.path}\`, which the
|
|
1372
907
|
delete never touched.`;
|
|
@@ -1403,7 +938,7 @@ ${restoreRecordsStep(schema)}
|
|
|
1403
938
|
|
|
1404
939
|
- slug: \`${slug}\`
|
|
1405
940
|
- title: ${schema.title}
|
|
1406
|
-
- dataPath: \`${schema.dataPath ?? `(dataSource) ${schema.dataSource?.path}`}\`
|
|
941
|
+
- dataPath: \`${schema.dataPath ?? (schema.storage !== void 0 ? `(storage) ${schema.storage.path}` : `(dataSource) ${schema.dataSource?.path}`)}\`
|
|
1407
942
|
`;
|
|
1408
943
|
}
|
|
1409
944
|
/** Copy one skill copy + the records + RESTORE.md into `archiveDir`. */
|
|
@@ -1411,6 +946,18 @@ async function writeArchive(collection, archiveDir, workspaceRoot) {
|
|
|
1411
946
|
const staging = stagingSkillDir(workspaceRoot, collection.slug);
|
|
1412
947
|
await (0, node_fs_promises.cp)(await pathExists(staging) ? staging : collection.skillDir, node_path.default.join(archiveDir, "skill"), { recursive: true });
|
|
1413
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
|
+
}
|
|
1414
961
|
await (0, node_fs_promises.writeFile)(node_path.default.join(archiveDir, "RESTORE.md"), buildRestoreDoc(collection), "utf-8");
|
|
1415
962
|
}
|
|
1416
963
|
/** Remove all three locations. `rm -rf`-style (force) so a missing dir
|
|
@@ -1430,6 +977,12 @@ async function removeLocations(collection, workspaceRoot) {
|
|
|
1430
977
|
force: true
|
|
1431
978
|
});
|
|
1432
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
|
+
}
|
|
1433
986
|
}
|
|
1434
987
|
async function deleteCollection(collection, opts = {}) {
|
|
1435
988
|
const { slug } = collection;
|
|
@@ -1626,7 +1179,7 @@ async function recordIssuesWarning(collection, deps) {
|
|
|
1626
1179
|
return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
|
|
1627
1180
|
}
|
|
1628
1181
|
async function loadRequestedItems(collection, ids, deps) {
|
|
1629
|
-
const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
|
|
1182
|
+
const store = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
|
|
1630
1183
|
if (!ids) return {
|
|
1631
1184
|
items: await store.list(),
|
|
1632
1185
|
missing: []
|
|
@@ -1688,13 +1241,13 @@ function computedKeyProblem(record, schema) {
|
|
|
1688
1241
|
* `toggle` value, and re-writing it would perpetuate a forged
|
|
1689
1242
|
* host-computed value. A merge heals the record instead.
|
|
1690
1243
|
*
|
|
1691
|
-
*
|
|
1244
|
+
* the store read THROWS on a malformed stored file (only ENOENT is null) —
|
|
1692
1245
|
* downgraded to a per-row rejection here, like loadRequestedItems'
|
|
1693
1246
|
* `missing`, so one broken file can't abort the whole putItems batch. */
|
|
1694
|
-
async function mergeWithExisting(collection, record, itemId
|
|
1247
|
+
async function mergeWithExisting(collection, store, record, itemId) {
|
|
1695
1248
|
let existing;
|
|
1696
1249
|
try {
|
|
1697
|
-
existing = await
|
|
1250
|
+
existing = await store.read(itemId);
|
|
1698
1251
|
} catch {
|
|
1699
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"`;
|
|
1700
1253
|
}
|
|
@@ -1708,7 +1261,7 @@ async function mergeWithExisting(collection, record, itemId, deps) {
|
|
|
1708
1261
|
...record
|
|
1709
1262
|
};
|
|
1710
1263
|
}
|
|
1711
|
-
async function putOneItem(collection, record, mode, deps) {
|
|
1264
|
+
async function putOneItem(collection, store, write, record, mode, deps) {
|
|
1712
1265
|
const { schema } = collection;
|
|
1713
1266
|
const itemId = require_discovery.resolveCreateItemId(schema, record);
|
|
1714
1267
|
const reject = (about, problem) => ({ rejected: {
|
|
@@ -1720,7 +1273,7 @@ async function putOneItem(collection, record, mode, deps) {
|
|
|
1720
1273
|
if (computed) return reject(itemId, computed);
|
|
1721
1274
|
let toWrite = record;
|
|
1722
1275
|
if (mode === "merge") {
|
|
1723
|
-
const merged = await mergeWithExisting(collection, record, itemId
|
|
1276
|
+
const merged = await mergeWithExisting(collection, store, record, itemId);
|
|
1724
1277
|
if (typeof merged === "string") return reject(itemId, merged);
|
|
1725
1278
|
toWrite = merged;
|
|
1726
1279
|
}
|
|
@@ -1728,11 +1281,7 @@ async function putOneItem(collection, record, mode, deps) {
|
|
|
1728
1281
|
const invalid = validateRecordObject(toWrite, itemId, schema);
|
|
1729
1282
|
if (invalid) return reject(itemId, invalid);
|
|
1730
1283
|
}
|
|
1731
|
-
const result = await
|
|
1732
|
-
refuseOverwrite: mode === "create",
|
|
1733
|
-
workspaceRoot: deps.workspaceRoot,
|
|
1734
|
-
slug: collection.slug
|
|
1735
|
-
});
|
|
1284
|
+
const result = await write(itemId, toWrite, { refuseOverwrite: mode === "create" });
|
|
1736
1285
|
if (result.kind === "ok") return { written: result.itemId };
|
|
1737
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)`);
|
|
1738
1287
|
if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
|
|
@@ -1740,25 +1289,14 @@ async function putOneItem(collection, record, mode, deps) {
|
|
|
1740
1289
|
}
|
|
1741
1290
|
/** Aggregation over a collection via the structured query DSL
|
|
1742
1291
|
* (`core/queryZ.ts`) — the paved road for counts / sums / group-bys
|
|
1743
|
-
* that a row listing can't answer honestly.
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
*
|
|
1747
|
-
* `derived` / `rollup` / `toggle` — are real columns) through the same
|
|
1748
|
-
* compiled SQL over a temp JSONL (`runQueryOverRows`). */
|
|
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. */
|
|
1749
1296
|
async function handleQueryItems(collection, queryArg, deps) {
|
|
1750
|
-
const parsed = CollectionQueryZ.safeParse(queryArg);
|
|
1297
|
+
const parsed = require_discovery.CollectionQueryZ.safeParse(queryArg);
|
|
1751
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")}`;
|
|
1752
|
-
const
|
|
1753
|
-
if (store.query) {
|
|
1754
|
-
const rows = await store.query(parsed.data);
|
|
1755
|
-
return JSON.stringify({
|
|
1756
|
-
collection: collection.slug,
|
|
1757
|
-
count: rows.length,
|
|
1758
|
-
rows
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
const rows = await runQueryOverRows(await enrichItems(collection, await store.list(), deps), parsed.data);
|
|
1299
|
+
const rows = await runCollectionQuery(collection, parsed.data, { workspaceRoot: deps.workspaceRoot });
|
|
1762
1300
|
return JSON.stringify({
|
|
1763
1301
|
collection: collection.slug,
|
|
1764
1302
|
count: rows.length,
|
|
@@ -1766,11 +1304,13 @@ async function handleQueryItems(collection, queryArg, deps) {
|
|
|
1766
1304
|
});
|
|
1767
1305
|
}
|
|
1768
1306
|
async function handlePutItems(collection, args, deps) {
|
|
1769
|
-
|
|
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).`;
|
|
1770
1310
|
const written = [];
|
|
1771
1311
|
const rejected = [];
|
|
1772
1312
|
for (const record of args.items) {
|
|
1773
|
-
const outcome = await putOneItem(collection, record, args.mode, deps);
|
|
1313
|
+
const outcome = await putOneItem(collection, store, write, record, args.mode, deps);
|
|
1774
1314
|
if (outcome.written) written.push(outcome.written);
|
|
1775
1315
|
if (outcome.rejected) rejected.push(outcome.rejected);
|
|
1776
1316
|
}
|
|
@@ -1780,6 +1320,44 @@ async function handlePutItems(collection, args, deps) {
|
|
|
1780
1320
|
rejected
|
|
1781
1321
|
});
|
|
1782
1322
|
}
|
|
1323
|
+
/** Delete records by id, through the store — so it works on any writable
|
|
1324
|
+
* backend, not just file records. Same read-only refusal as putItems (an
|
|
1325
|
+
* absent `delete` IS the refusal), and the same per-id result shape so a
|
|
1326
|
+
* partially-bad batch reports per id instead of failing whole. */
|
|
1327
|
+
async function handleDeleteItems(collection, ids, deps) {
|
|
1328
|
+
const { delete: removeItem } = require_discovery.storeFor(collection, { workspaceRoot: deps.workspaceRoot });
|
|
1329
|
+
if (!removeItem) return `manageCollection: ${require_discovery.readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
|
|
1330
|
+
const deleted = [];
|
|
1331
|
+
const rejected = [];
|
|
1332
|
+
for (const itemId of ids) {
|
|
1333
|
+
const outcome = await deleteOneItem(removeItem, itemId);
|
|
1334
|
+
if (outcome.deleted) deleted.push(outcome.deleted);
|
|
1335
|
+
if (outcome.rejected) rejected.push(outcome.rejected);
|
|
1336
|
+
}
|
|
1337
|
+
return JSON.stringify({
|
|
1338
|
+
collection: collection.slug,
|
|
1339
|
+
deleted,
|
|
1340
|
+
rejected
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
/** A missing id is a rejection, not a silent success: deleting by a typo'd
|
|
1344
|
+
* id would otherwise report "done" while the real record survives. */
|
|
1345
|
+
async function deleteOneItem(removeItem, itemId) {
|
|
1346
|
+
const result = await removeItem(itemId);
|
|
1347
|
+
if (result.kind === "ok") return { deleted: result.itemId };
|
|
1348
|
+
const reject = (problem) => ({ rejected: {
|
|
1349
|
+
id: require_promptSafety.defangForPrompt(itemId),
|
|
1350
|
+
problem: require_promptSafety.defangForPrompt(problem)
|
|
1351
|
+
} });
|
|
1352
|
+
if (result.kind === "invalid-id") return reject(`'${itemId}' is not a valid record id (letters/digits at the ends; -, _, or . inside; no '..' or path characters)`);
|
|
1353
|
+
if (result.kind === "not-found") return reject(`'${itemId}' not found — nothing was deleted; confirm the id with getItems`);
|
|
1354
|
+
return reject("delete refused: the collection's data dir escapes the workspace");
|
|
1355
|
+
}
|
|
1356
|
+
function parseDeleteIds(args) {
|
|
1357
|
+
const { ids } = args;
|
|
1358
|
+
if (!(Array.isArray(ids) && ids.length > 0 && ids.every((entry) => typeof entry === "string" && entry.trim().length > 0))) return "manageCollection: `ids` is required for deleteItems — a non-empty array of record ids.";
|
|
1359
|
+
return ids;
|
|
1360
|
+
}
|
|
1783
1361
|
function parsePutItems(args, slug) {
|
|
1784
1362
|
const { items, mode } = args;
|
|
1785
1363
|
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.";
|
|
@@ -1888,7 +1466,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
|
|
|
1888
1466
|
written: true
|
|
1889
1467
|
});
|
|
1890
1468
|
}
|
|
1891
|
-
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.";
|
|
1469
|
+
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. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
|
|
1892
1470
|
/** Validate getItems' optional `ids`/`fields` args, then delegate. */
|
|
1893
1471
|
async function dispatchGetItems(collection, args, deps) {
|
|
1894
1472
|
const ids = optionalStringArray(args.ids, "ids");
|
|
@@ -1901,6 +1479,26 @@ async function dispatchGetItems(collection, args, deps) {
|
|
|
1901
1479
|
fields: fields.value
|
|
1902
1480
|
}, deps);
|
|
1903
1481
|
}
|
|
1482
|
+
/** Actions that operate on a collection's RECORDS — i.e. the ones that
|
|
1483
|
+
* need the collection loaded first. Schema/workspace actions don't. */
|
|
1484
|
+
var RECORD_ACTIONS = /* @__PURE__ */ new Set([
|
|
1485
|
+
"getItems",
|
|
1486
|
+
"putItems",
|
|
1487
|
+
"deleteItems",
|
|
1488
|
+
"queryItems"
|
|
1489
|
+
]);
|
|
1490
|
+
/** Record-action dispatch, split from `manageCollectionHandler` to keep
|
|
1491
|
+
* both within the cognitive-complexity budget. */
|
|
1492
|
+
async function dispatchRecordAction(action, collection, args, deps) {
|
|
1493
|
+
if (action === "getItems") return dispatchGetItems(collection, args, deps);
|
|
1494
|
+
if (action === "queryItems") return handleQueryItems(collection, args.query, deps);
|
|
1495
|
+
if (action === "deleteItems") {
|
|
1496
|
+
const ids = parseDeleteIds(args);
|
|
1497
|
+
return typeof ids === "string" ? ids : handleDeleteItems(collection, ids, deps);
|
|
1498
|
+
}
|
|
1499
|
+
const parsed = parsePutItems(args, collection.slug);
|
|
1500
|
+
return typeof parsed === "string" ? parsed : handlePutItems(collection, parsed, deps);
|
|
1501
|
+
}
|
|
1904
1502
|
async function manageCollectionHandler(deps, args) {
|
|
1905
1503
|
const action = typeof args.action === "string" ? args.action : "";
|
|
1906
1504
|
if (action === "schemaDocs") return handleSchemaDocs(deps);
|
|
@@ -1909,18 +1507,14 @@ async function manageCollectionHandler(deps, args) {
|
|
|
1909
1507
|
if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
|
|
1910
1508
|
if (action === "getSchema") return handleGetSchema(slug, deps);
|
|
1911
1509
|
if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
|
|
1912
|
-
if (action
|
|
1510
|
+
if (!RECORD_ACTIONS.has(action)) return "manageCollection: `action` must be \"getItems\", \"putItems\", \"deleteItems\", \"queryItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
|
|
1913
1511
|
const collection = await require_discovery.loadCollection(slug, deps);
|
|
1914
1512
|
if (!collection) return unknownCollection(slug);
|
|
1915
|
-
|
|
1916
|
-
if (action === "queryItems") return handleQueryItems(collection, args.query, deps);
|
|
1917
|
-
const parsed = parsePutItems(args, slug);
|
|
1918
|
-
if (typeof parsed === "string") return parsed;
|
|
1919
|
-
return handlePutItems(collection, parsed, deps);
|
|
1513
|
+
return dispatchRecordAction(action, collection, args, deps);
|
|
1920
1514
|
}
|
|
1921
1515
|
var MANAGE_COLLECTION_DEFINITION = {
|
|
1922
1516
|
name: "manageCollection",
|
|
1923
|
-
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.",
|
|
1517
|
+
description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing; deleteItems removes records by id. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference; getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
|
|
1924
1518
|
inputSchema: {
|
|
1925
1519
|
type: "object",
|
|
1926
1520
|
properties: {
|
|
@@ -1929,6 +1523,7 @@ var MANAGE_COLLECTION_DEFINITION = {
|
|
|
1929
1523
|
enum: [
|
|
1930
1524
|
"getItems",
|
|
1931
1525
|
"putItems",
|
|
1526
|
+
"deleteItems",
|
|
1932
1527
|
"queryItems",
|
|
1933
1528
|
"getOntology",
|
|
1934
1529
|
"schemaDocs",
|
|
@@ -1944,7 +1539,7 @@ var MANAGE_COLLECTION_DEFINITION = {
|
|
|
1944
1539
|
ids: {
|
|
1945
1540
|
type: "array",
|
|
1946
1541
|
items: { type: "string" },
|
|
1947
|
-
description: "getItems: only these record ids (primary-key values)
|
|
1542
|
+
description: "getItems: only these record ids (primary-key values); omit for all records. deleteItems: the record ids to delete — required, and never optional there."
|
|
1948
1543
|
},
|
|
1949
1544
|
fields: {
|
|
1950
1545
|
type: "array",
|
|
@@ -1986,30 +1581,6 @@ function makeManageCollectionTool(deps = {}) {
|
|
|
1986
1581
|
};
|
|
1987
1582
|
}
|
|
1988
1583
|
//#endregion
|
|
1989
|
-
Object.defineProperty(exports, "CollectionQueryZ", {
|
|
1990
|
-
enumerable: true,
|
|
1991
|
-
get: function() {
|
|
1992
|
-
return CollectionQueryZ;
|
|
1993
|
-
}
|
|
1994
|
-
});
|
|
1995
|
-
Object.defineProperty(exports, "DEFAULT_QUERY_ROWS", {
|
|
1996
|
-
enumerable: true,
|
|
1997
|
-
get: function() {
|
|
1998
|
-
return DEFAULT_QUERY_ROWS;
|
|
1999
|
-
}
|
|
2000
|
-
});
|
|
2001
|
-
Object.defineProperty(exports, "MAX_CSV_ROWS", {
|
|
2002
|
-
enumerable: true,
|
|
2003
|
-
get: function() {
|
|
2004
|
-
return MAX_CSV_ROWS;
|
|
2005
|
-
}
|
|
2006
|
-
});
|
|
2007
|
-
Object.defineProperty(exports, "MAX_QUERY_ROWS", {
|
|
2008
|
-
enumerable: true,
|
|
2009
|
-
get: function() {
|
|
2010
|
-
return MAX_QUERY_ROWS;
|
|
2011
|
-
}
|
|
2012
|
-
});
|
|
2013
1584
|
Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
|
|
2014
1585
|
enumerable: true,
|
|
2015
1586
|
get: function() {
|
|
@@ -2046,24 +1617,6 @@ Object.defineProperty(exports, "buildWorkspaceOntology", {
|
|
|
2046
1617
|
return buildWorkspaceOntology;
|
|
2047
1618
|
}
|
|
2048
1619
|
});
|
|
2049
|
-
Object.defineProperty(exports, "collectionWritable", {
|
|
2050
|
-
enumerable: true,
|
|
2051
|
-
get: function() {
|
|
2052
|
-
return collectionWritable;
|
|
2053
|
-
}
|
|
2054
|
-
});
|
|
2055
|
-
Object.defineProperty(exports, "compileCsvQuery", {
|
|
2056
|
-
enumerable: true,
|
|
2057
|
-
get: function() {
|
|
2058
|
-
return compileCsvQuery;
|
|
2059
|
-
}
|
|
2060
|
-
});
|
|
2061
|
-
Object.defineProperty(exports, "compileJsonlQuery", {
|
|
2062
|
-
enumerable: true,
|
|
2063
|
-
get: function() {
|
|
2064
|
-
return compileJsonlQuery;
|
|
2065
|
-
}
|
|
2066
|
-
});
|
|
2067
1620
|
Object.defineProperty(exports, "compileRecordZ", {
|
|
2068
1621
|
enumerable: true,
|
|
2069
1622
|
get: function() {
|
|
@@ -2082,30 +1635,12 @@ Object.defineProperty(exports, "computeSuccessor", {
|
|
|
2082
1635
|
return computeSuccessor;
|
|
2083
1636
|
}
|
|
2084
1637
|
});
|
|
2085
|
-
Object.defineProperty(exports, "csvRowToItem", {
|
|
2086
|
-
enumerable: true,
|
|
2087
|
-
get: function() {
|
|
2088
|
-
return csvRowToItem;
|
|
2089
|
-
}
|
|
2090
|
-
});
|
|
2091
1638
|
Object.defineProperty(exports, "daysInMonth", {
|
|
2092
1639
|
enumerable: true,
|
|
2093
1640
|
get: function() {
|
|
2094
1641
|
return daysInMonth;
|
|
2095
1642
|
}
|
|
2096
1643
|
});
|
|
2097
|
-
Object.defineProperty(exports, "decodeCsvRecordId", {
|
|
2098
|
-
enumerable: true,
|
|
2099
|
-
get: function() {
|
|
2100
|
-
return decodeCsvRecordId;
|
|
2101
|
-
}
|
|
2102
|
-
});
|
|
2103
|
-
Object.defineProperty(exports, "dedupeByRecordId", {
|
|
2104
|
-
enumerable: true,
|
|
2105
|
-
get: function() {
|
|
2106
|
-
return dedupeByRecordId;
|
|
2107
|
-
}
|
|
2108
|
-
});
|
|
2109
1644
|
Object.defineProperty(exports, "deleteCollection", {
|
|
2110
1645
|
enumerable: true,
|
|
2111
1646
|
get: function() {
|
|
@@ -2124,12 +1659,6 @@ Object.defineProperty(exports, "deleteCustomView", {
|
|
|
2124
1659
|
return deleteCustomView;
|
|
2125
1660
|
}
|
|
2126
1661
|
});
|
|
2127
|
-
Object.defineProperty(exports, "encodeCsvRecordId", {
|
|
2128
|
-
enumerable: true,
|
|
2129
|
-
get: function() {
|
|
2130
|
-
return encodeCsvRecordId;
|
|
2131
|
-
}
|
|
2132
|
-
});
|
|
2133
1662
|
Object.defineProperty(exports, "enrichItems", {
|
|
2134
1663
|
enumerable: true,
|
|
2135
1664
|
get: function() {
|
|
@@ -2172,24 +1701,12 @@ Object.defineProperty(exports, "maybeSpawnSuccessor", {
|
|
|
2172
1701
|
return maybeSpawnSuccessor;
|
|
2173
1702
|
}
|
|
2174
1703
|
});
|
|
2175
|
-
Object.defineProperty(exports, "normalizeCsvValue", {
|
|
2176
|
-
enumerable: true,
|
|
2177
|
-
get: function() {
|
|
2178
|
-
return normalizeCsvValue;
|
|
2179
|
-
}
|
|
2180
|
-
});
|
|
2181
1704
|
Object.defineProperty(exports, "parseCivil", {
|
|
2182
1705
|
enumerable: true,
|
|
2183
1706
|
get: function() {
|
|
2184
1707
|
return parseCivil;
|
|
2185
1708
|
}
|
|
2186
1709
|
});
|
|
2187
|
-
Object.defineProperty(exports, "readOnlyRefusal", {
|
|
2188
|
-
enumerable: true,
|
|
2189
|
-
get: function() {
|
|
2190
|
-
return readOnlyRefusal;
|
|
2191
|
-
}
|
|
2192
|
-
});
|
|
2193
1710
|
Object.defineProperty(exports, "recordFieldProblem", {
|
|
2194
1711
|
enumerable: true,
|
|
2195
1712
|
get: function() {
|
|
@@ -2202,22 +1719,22 @@ Object.defineProperty(exports, "resolveEvery", {
|
|
|
2202
1719
|
return resolveEvery;
|
|
2203
1720
|
}
|
|
2204
1721
|
});
|
|
2205
|
-
Object.defineProperty(exports, "
|
|
1722
|
+
Object.defineProperty(exports, "runCollectionQuery", {
|
|
2206
1723
|
enumerable: true,
|
|
2207
1724
|
get: function() {
|
|
2208
|
-
return
|
|
1725
|
+
return runCollectionQuery;
|
|
2209
1726
|
}
|
|
2210
1727
|
});
|
|
2211
|
-
Object.defineProperty(exports, "
|
|
1728
|
+
Object.defineProperty(exports, "runQueryOverRows", {
|
|
2212
1729
|
enumerable: true,
|
|
2213
1730
|
get: function() {
|
|
2214
|
-
return
|
|
1731
|
+
return runQueryOverRows;
|
|
2215
1732
|
}
|
|
2216
1733
|
});
|
|
2217
|
-
Object.defineProperty(exports, "
|
|
1734
|
+
Object.defineProperty(exports, "schemaRelations", {
|
|
2218
1735
|
enumerable: true,
|
|
2219
1736
|
get: function() {
|
|
2220
|
-
return
|
|
1737
|
+
return schemaRelations;
|
|
2221
1738
|
}
|
|
2222
1739
|
});
|
|
2223
1740
|
Object.defineProperty(exports, "successorId", {
|
|
@@ -2239,4 +1756,4 @@ Object.defineProperty(exports, "validateRecordObject", {
|
|
|
2239
1756
|
}
|
|
2240
1757
|
});
|
|
2241
1758
|
|
|
2242
|
-
//# sourceMappingURL=server-
|
|
1759
|
+
//# sourceMappingURL=server-B7mtx4Cd.cjs.map
|