@mulmoclaude/core 0.13.1 → 0.15.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 +15 -12
- package/assets/helps/collection-skills.md +67 -22
- package/assets/helps/todo-collection.md +13 -7
- package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
- package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
- package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
- package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
- package/dist/collection/core/backlinks.d.ts +20 -0
- package/dist/collection/core/ids.d.ts +12 -0
- package/dist/collection/core/recordZ.d.ts +23 -0
- package/dist/collection/core/schema.d.ts +72 -418
- package/dist/collection/core/schemaZ.d.ts +878 -0
- package/dist/collection/core/templatePath.d.ts +34 -0
- package/dist/collection/core/uiTypes.d.ts +22 -0
- package/dist/collection/core/where.d.ts +9 -15
- package/dist/collection/index.cjs +50 -330
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +14 -301
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/paths.cjs +1 -1
- package/dist/collection/paths.cjs.map +1 -1
- package/dist/collection/paths.js +1 -1
- package/dist/collection/paths.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +1 -1
- package/dist/collection/registry/server/index.js +1 -1
- package/dist/collection/server/discovery.d.ts +2 -244
- package/dist/collection/server/index.cjs +8 -2
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +3 -2
- package/dist/collection/server/io.d.ts +9 -0
- package/dist/collection/server/ontology.d.ts +35 -0
- package/dist/collection/server/templatePath.d.ts +1 -34
- package/dist/collection/server/validate.d.ts +11 -7
- package/dist/collection-watchers/index.cjs +5 -4
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +3 -2
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/ingestTypes.d.ts +15 -40
- package/dist/feeds/server/index.cjs +5 -5
- package/dist/feeds/server/index.js +3 -3
- package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
- package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
- package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
- package/dist/ingestTypes-V4upKeBx.js.map +1 -0
- package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
- package/dist/server-BNTR7ySK.js.map +1 -0
- package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
- package/dist/server-bsNrf2yW.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
- package/dist/deriveAll-CMFXDQ_G.js +0 -506
- package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
- package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
- package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
- package/dist/server-DKXXFTbw.js.map +0 -1
- package/dist/server-G6GtOdaW.cjs.map +0 -1
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
//#region src/collection/core/schema.ts
|
|
2
|
-
/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's
|
|
3
|
-
* feeds engine dispatches on these; they live here (with the schema contract)
|
|
4
|
-
* so the schema validator can enforce them. The host re-exports these from
|
|
5
|
-
* `server/workspace/feeds/ingestTypes.ts`. */
|
|
6
|
-
var INGEST_KINDS = [
|
|
7
|
-
"rss",
|
|
8
|
-
"atom",
|
|
9
|
-
"http-json"
|
|
10
|
-
];
|
|
11
|
-
/** The agent-performed ingest kind. Instead of a declarative fetch, the host
|
|
12
|
-
* dispatches a hidden background chat (origin `system`) in `ingest.role`,
|
|
13
|
-
* seeded with `ingest.template` + a summary of every record, on the
|
|
14
|
-
* `ingest.schedule` cadence; the worker edits records via the collections io
|
|
15
|
-
* layer. Kept separate from {@link INGEST_KINDS} (which the declarative
|
|
16
|
-
* retriever registry keys on) so the schema validator can model `ingest` as a
|
|
17
|
-
* discriminated union without the feeds engine gaining an "agent" retriever. */
|
|
18
|
-
var AGENT_INGEST_KIND = "agent";
|
|
19
|
-
/** Refresh cadences a Feed's `ingest.schedule` may declare. */
|
|
20
|
-
var FEED_SCHEDULES = [
|
|
21
|
-
"hourly",
|
|
22
|
-
"daily",
|
|
23
|
-
"weekly",
|
|
24
|
-
"on-demand"
|
|
25
|
-
];
|
|
26
|
-
/** Narrowing guard: true when `every` is the field-driven arm. */
|
|
27
|
-
function isFieldDrivenEvery(every) {
|
|
28
|
-
return "fromField" in every;
|
|
29
|
-
}
|
|
30
|
-
/** Resolve an `embed` field's target record id: the fixed `id`, or the value
|
|
31
|
-
* of the sibling `idField` on this record (empty string when neither applies
|
|
32
|
-
* — the caller renders that as "no record"). Pure + isomorphic so the server
|
|
33
|
-
* projection (`derive.ts`) and the client preview (`useCollectionRendering`)
|
|
34
|
-
* resolve embeds identically. */
|
|
35
|
-
function embedTargetId(field, record) {
|
|
36
|
-
if (field.id) return field.id;
|
|
37
|
-
if (field.idField && record) return String(record[field.idField] ?? "");
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/collection/core/where.ts
|
|
42
|
-
/** True when `record[field]` is absent (`undefined`/`null`) — the only case
|
|
43
|
-
* where `ne` and every other op disagree on the result. */
|
|
44
|
-
function isMissing(raw) {
|
|
45
|
-
return raw === void 0 || raw === null;
|
|
46
|
-
}
|
|
47
|
-
/** The effective comparison value for `cond`: its literal `value`, or — for
|
|
48
|
-
* a `valueFrom` reference — the target field read out of `recordsById`.
|
|
49
|
-
* `undefined` means UNRESOLVED (no such record, or the field on it is
|
|
50
|
-
* missing); the caller must treat that as "never matches", not as a
|
|
51
|
-
* literal `undefined` value to compare against. */
|
|
52
|
-
function resolveValue(cond, record, recordsById) {
|
|
53
|
-
if (!cond.valueFrom) return cond.value;
|
|
54
|
-
const { record: refRecord, field } = cond.valueFrom;
|
|
55
|
-
const raw = (refRecord === void 0 ? record : recordsById[refRecord])?.[field];
|
|
56
|
-
return isMissing(raw) ? void 0 : String(raw);
|
|
57
|
-
}
|
|
58
|
-
function matchesNumericOp(operator, left, right) {
|
|
59
|
-
if (operator === "gt") return left > right;
|
|
60
|
-
if (operator === "gte") return left >= right;
|
|
61
|
-
if (operator === "lt") return left < right;
|
|
62
|
-
return left <= right;
|
|
63
|
-
}
|
|
64
|
-
/** `Number("")` / `Number(" ")` are `0`, not `NaN`, so treat a blank string
|
|
65
|
-
* as non-numeric explicitly — an empty field must fail a numeric compare,
|
|
66
|
-
* not read as zero. */
|
|
67
|
-
function toNumber(raw) {
|
|
68
|
-
return raw.trim() === "" ? NaN : Number(raw);
|
|
69
|
-
}
|
|
70
|
-
function matchesNumeric(operator, raw, value) {
|
|
71
|
-
if (Array.isArray(value)) return false;
|
|
72
|
-
const left = toNumber(raw);
|
|
73
|
-
const right = toNumber(value);
|
|
74
|
-
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
75
|
-
return matchesNumericOp(operator, left, right);
|
|
76
|
-
}
|
|
77
|
-
/** True when the present string `raw` satisfies `operator` against the
|
|
78
|
-
* resolved `value` (field known to exist — MISSING is handled by the
|
|
79
|
-
* caller before this runs, and an UNRESOLVED `valueFrom` never reaches
|
|
80
|
-
* here either). */
|
|
81
|
-
function matchesPresent(operator, raw, value) {
|
|
82
|
-
switch (operator) {
|
|
83
|
-
case "eq": return raw === String(value);
|
|
84
|
-
case "ne": return raw !== String(value);
|
|
85
|
-
case "in": return Array.isArray(value) && value.includes(raw);
|
|
86
|
-
case "contains": return raw.includes(String(value));
|
|
87
|
-
case "gt":
|
|
88
|
-
case "gte":
|
|
89
|
-
case "lt":
|
|
90
|
-
case "lte": return matchesNumeric(operator, raw, value);
|
|
91
|
-
default: return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/** True when `record` satisfies one condition, given `recordsById` to
|
|
95
|
-
* resolve a `valueFrom` reference. Two independent MISSING cases, checked
|
|
96
|
-
* in order:
|
|
97
|
-
* - `record[cond.field]` absent (`undefined`/`null`) → matches only `ne`
|
|
98
|
-
* (vacuously true — "not equal to X" holds when there's no value at
|
|
99
|
-
* all); every other op is false. Unchanged from the literal-`value`
|
|
100
|
-
* behaviour, regardless of whether `valueFrom` would also resolve.
|
|
101
|
-
* - the resolved comparison value is UNRESOLVED (a `valueFrom` whose
|
|
102
|
-
* target record/field doesn't exist) → false for EVERY op, including
|
|
103
|
-
* `ne` — a broken reference must never spuriously match. */
|
|
104
|
-
function matchesCond(cond, record, recordsById) {
|
|
105
|
-
const raw = record[cond.field];
|
|
106
|
-
if (isMissing(raw)) return cond.op === "ne";
|
|
107
|
-
const value = resolveValue(cond, record, recordsById);
|
|
108
|
-
if (value === void 0) return false;
|
|
109
|
-
return matchesPresent(cond.op, String(raw), value);
|
|
110
|
-
}
|
|
111
|
-
/** True when `record` satisfies every condition in `where` (AND). An empty
|
|
112
|
-
* `where` matches everything. `recordsById` — the source collection's
|
|
113
|
-
* records keyed by primaryKey — resolves any `valueFrom` reference;
|
|
114
|
-
* omitted (default `{}`) for callers with no cross-record lookups, in
|
|
115
|
-
* which case every `valueFrom` condition is UNRESOLVED and so never
|
|
116
|
-
* matches. */
|
|
117
|
-
function matchesWhere(where, record, recordsById = {}) {
|
|
118
|
-
return where.every((cond) => matchesCond(cond, record, recordsById));
|
|
119
|
-
}
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/collection/core/dynamicIcon.ts
|
|
122
|
-
/** The record with the greatest `String(record[field])` (localeCompare) —
|
|
123
|
-
* ties keep the first-seen record (stable left-to-right `reduce`). */
|
|
124
|
-
function latestByField(pool, field) {
|
|
125
|
-
return pool.reduce((latest, candidate) => String(candidate[field] ?? "").localeCompare(String(latest[field] ?? "")) > 0 ? candidate : latest);
|
|
126
|
-
}
|
|
127
|
-
/** Reduce `records` to the one record that decides the effective icon, per
|
|
128
|
-
* `source`'s `where` filter + `from` strategy:
|
|
129
|
-
* - pool = `source.where`-filtered records, or every record when unset;
|
|
130
|
-
* - an empty pool resolves to `null` (no source record → fallback);
|
|
131
|
-
* - `from: "first"` / `"when"` → the first pool record (storage order);
|
|
132
|
-
* - `from: "latest"` (default), with `orderBy` given → the pool record
|
|
133
|
-
* whose `String(record[orderBy])` sorts highest;
|
|
134
|
-
* - `from: "latest"`, with no `orderBy` → the last pool record.
|
|
135
|
-
* `recordsById` (the source collection's records keyed by primaryKey)
|
|
136
|
-
* resolves any `valueFrom` reference inside `source.where`; omitted for
|
|
137
|
-
* callers with no cross-record lookups. */
|
|
138
|
-
function selectDynamicRecord(records, source, orderBy, recordsById = {}) {
|
|
139
|
-
const { where } = source;
|
|
140
|
-
const pool = where ? records.filter((record) => matchesWhere(where, record, recordsById)) : records;
|
|
141
|
-
if (pool.length === 0) return null;
|
|
142
|
-
if (source.from === "first" || source.from === "when") return pool[0];
|
|
143
|
-
return orderBy ? latestByField(pool, orderBy) : pool[pool.length - 1];
|
|
144
|
-
}
|
|
145
|
-
/** Map a resolved source record to the effective icon: `spec.fallback`
|
|
146
|
-
* (or the collection's own static `icon`) when there's no record or no
|
|
147
|
-
* rule matches; otherwise the `icon` of the first rule whose `where`
|
|
148
|
-
* matches the record. `recordsById` resolves any `valueFrom` reference
|
|
149
|
-
* inside a rule's `where`, same as `selectDynamicRecord`. */
|
|
150
|
-
function resolveIcon(record, spec, staticIcon, recordsById = {}) {
|
|
151
|
-
const fallback = spec.fallback ?? staticIcon;
|
|
152
|
-
if (!record) return fallback;
|
|
153
|
-
const matched = spec.rules.find((rule) => matchesWhere(rule.where, record, recordsById));
|
|
154
|
-
return matched ? matched.icon : fallback;
|
|
155
|
-
}
|
|
156
|
-
var isDateLikeField = (field) => field.type === "date" || field.type === "datetime";
|
|
157
|
-
/** The first field key (declaration order) whose type is `date` or
|
|
158
|
-
* `datetime` — the default `orderBy` for `from: "latest"` when a
|
|
159
|
-
* `DynamicIconSource` doesn't name one. `undefined` when the schema has
|
|
160
|
-
* no date-like field. */
|
|
161
|
-
function firstDateField(schema) {
|
|
162
|
-
return Object.entries(schema.fields).find(([, field]) => isDateLikeField(field))?.[0];
|
|
163
|
-
}
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region src/collection/core/derivedFormula.ts
|
|
166
|
-
function evaluateDerived(formula, ctx) {
|
|
167
|
-
let tokens;
|
|
168
|
-
try {
|
|
169
|
-
tokens = tokenize(formula);
|
|
170
|
-
} catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const parser = new Parser(tokens);
|
|
174
|
-
let ast;
|
|
175
|
-
try {
|
|
176
|
-
ast = parser.parseExpr();
|
|
177
|
-
if (!parser.atEnd()) return null;
|
|
178
|
-
} catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
const value = evaluate(ast, ctx);
|
|
182
|
-
return Number.isFinite(value) ? value : null;
|
|
183
|
-
}
|
|
184
|
-
var SINGLE_CHAR_PUNCT = /* @__PURE__ */ new Set([
|
|
185
|
-
"(",
|
|
186
|
-
")",
|
|
187
|
-
"+",
|
|
188
|
-
"-",
|
|
189
|
-
"*",
|
|
190
|
-
"/",
|
|
191
|
-
"."
|
|
192
|
-
]);
|
|
193
|
-
function consumeWhitespace(cur) {
|
|
194
|
-
const char = cur.input[cur.index];
|
|
195
|
-
if (char === " " || char === " " || char === "\n") {
|
|
196
|
-
cur.index++;
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
function consumeNumber(cur) {
|
|
202
|
-
const char = cur.input[cur.index] ?? "";
|
|
203
|
-
const next = cur.input[cur.index + 1] ?? "";
|
|
204
|
-
if (!isDigit(char) && !(char === "." && isDigit(next))) return null;
|
|
205
|
-
let raw = "";
|
|
206
|
-
while (cur.index < cur.input.length) {
|
|
207
|
-
const here = cur.input[cur.index] ?? "";
|
|
208
|
-
if (!isDigit(here) && here !== ".") break;
|
|
209
|
-
raw += here;
|
|
210
|
-
cur.index++;
|
|
211
|
-
}
|
|
212
|
-
const num = Number(raw);
|
|
213
|
-
if (!Number.isFinite(num)) throw new Error("bad number");
|
|
214
|
-
return {
|
|
215
|
-
kind: "number",
|
|
216
|
-
value: num
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
function consumeIdent(cur) {
|
|
220
|
-
if (!isIdentStart(cur.input[cur.index] ?? "")) return null;
|
|
221
|
-
let raw = "";
|
|
222
|
-
while (cur.index < cur.input.length && isIdentChar(cur.input[cur.index] ?? "")) {
|
|
223
|
-
raw += cur.input[cur.index];
|
|
224
|
-
cur.index++;
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
kind: "ident",
|
|
228
|
-
value: raw
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function consumePunct(cur) {
|
|
232
|
-
const char = cur.input[cur.index] ?? "";
|
|
233
|
-
if (char === "[" && cur.input[cur.index + 1] === "]") {
|
|
234
|
-
cur.index += 2;
|
|
235
|
-
return { kind: "[]" };
|
|
236
|
-
}
|
|
237
|
-
if (SINGLE_CHAR_PUNCT.has(char)) {
|
|
238
|
-
cur.index++;
|
|
239
|
-
return { kind: char };
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
function tokenize(input) {
|
|
244
|
-
const tokens = [];
|
|
245
|
-
const cur = {
|
|
246
|
-
input,
|
|
247
|
-
index: 0
|
|
248
|
-
};
|
|
249
|
-
while (cur.index < input.length) {
|
|
250
|
-
if (consumeWhitespace(cur)) continue;
|
|
251
|
-
const numTok = consumeNumber(cur);
|
|
252
|
-
if (numTok) {
|
|
253
|
-
tokens.push(numTok);
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
const punctTok = consumePunct(cur);
|
|
257
|
-
if (punctTok) {
|
|
258
|
-
tokens.push(punctTok);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
const identTok = consumeIdent(cur);
|
|
262
|
-
if (identTok) {
|
|
263
|
-
tokens.push(identTok);
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
throw new Error(`unexpected char ${input[cur.index]}`);
|
|
267
|
-
}
|
|
268
|
-
return tokens;
|
|
269
|
-
}
|
|
270
|
-
function isDigit(char) {
|
|
271
|
-
return char >= "0" && char <= "9";
|
|
272
|
-
}
|
|
273
|
-
function isIdentStart(char) {
|
|
274
|
-
return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_";
|
|
275
|
-
}
|
|
276
|
-
function isIdentChar(char) {
|
|
277
|
-
return isIdentStart(char) || isDigit(char);
|
|
278
|
-
}
|
|
279
|
-
var Parser = class {
|
|
280
|
-
tokens;
|
|
281
|
-
cursor = 0;
|
|
282
|
-
constructor(tokens) {
|
|
283
|
-
this.tokens = tokens;
|
|
284
|
-
}
|
|
285
|
-
atEnd() {
|
|
286
|
-
return this.cursor >= this.tokens.length;
|
|
287
|
-
}
|
|
288
|
-
peek() {
|
|
289
|
-
return this.tokens[this.cursor];
|
|
290
|
-
}
|
|
291
|
-
consume() {
|
|
292
|
-
const tok = this.tokens[this.cursor++];
|
|
293
|
-
if (!tok) throw new Error("unexpected end of input");
|
|
294
|
-
return tok;
|
|
295
|
-
}
|
|
296
|
-
expect(kind) {
|
|
297
|
-
const tok = this.consume();
|
|
298
|
-
if (tok.kind !== kind) throw new Error(`expected ${kind}, got ${tok.kind}`);
|
|
299
|
-
return tok;
|
|
300
|
-
}
|
|
301
|
-
parseExpr() {
|
|
302
|
-
let left = this.parseTerm();
|
|
303
|
-
while (this.peek()?.kind === "+" || this.peek()?.kind === "-") {
|
|
304
|
-
const operator = this.consume().kind;
|
|
305
|
-
const right = this.parseTerm();
|
|
306
|
-
left = {
|
|
307
|
-
kind: "binop",
|
|
308
|
-
operator,
|
|
309
|
-
left,
|
|
310
|
-
right
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
return left;
|
|
314
|
-
}
|
|
315
|
-
parseTerm() {
|
|
316
|
-
let left = this.parseFactor();
|
|
317
|
-
while (this.peek()?.kind === "*" || this.peek()?.kind === "/") {
|
|
318
|
-
const operator = this.consume().kind;
|
|
319
|
-
const right = this.parseFactor();
|
|
320
|
-
left = {
|
|
321
|
-
kind: "binop",
|
|
322
|
-
operator,
|
|
323
|
-
left,
|
|
324
|
-
right
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
return left;
|
|
328
|
-
}
|
|
329
|
-
parseFactor() {
|
|
330
|
-
const tok = this.peek();
|
|
331
|
-
if (!tok) throw new Error("unexpected end in factor");
|
|
332
|
-
if (tok.kind === "number") {
|
|
333
|
-
this.consume();
|
|
334
|
-
return {
|
|
335
|
-
kind: "num",
|
|
336
|
-
value: tok.value
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
if (tok.kind === "(") {
|
|
340
|
-
this.consume();
|
|
341
|
-
const inner = this.parseExpr();
|
|
342
|
-
this.expect(")");
|
|
343
|
-
return inner;
|
|
344
|
-
}
|
|
345
|
-
if (tok.kind === "ident") {
|
|
346
|
-
const name = tok.value ?? "";
|
|
347
|
-
if (name === "sum" && this.tokens[this.cursor + 1]?.kind === "(") {
|
|
348
|
-
this.consume();
|
|
349
|
-
this.expect("(");
|
|
350
|
-
const arg = this.parseSumArg();
|
|
351
|
-
this.expect(")");
|
|
352
|
-
return {
|
|
353
|
-
kind: "sum",
|
|
354
|
-
arg
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
this.consume();
|
|
358
|
-
if (this.peek()?.kind === ".") {
|
|
359
|
-
this.consume();
|
|
360
|
-
return {
|
|
361
|
-
kind: "ref",
|
|
362
|
-
field: name,
|
|
363
|
-
col: this.expect("ident").value
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
kind: "ident",
|
|
368
|
-
name
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
throw new Error(`unexpected token ${tok.kind} in factor`);
|
|
372
|
-
}
|
|
373
|
-
parseSumArg() {
|
|
374
|
-
const factors = [];
|
|
375
|
-
const operators = [];
|
|
376
|
-
factors.push(this.parseTableCol());
|
|
377
|
-
while (this.peek()?.kind === "*" || this.peek()?.kind === "/") {
|
|
378
|
-
const operator = this.consume().kind;
|
|
379
|
-
operators.push(operator);
|
|
380
|
-
factors.push(this.parseTableCol());
|
|
381
|
-
}
|
|
382
|
-
return {
|
|
383
|
-
factors,
|
|
384
|
-
operators
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
parseTableCol() {
|
|
388
|
-
const tableTok = this.expect("ident");
|
|
389
|
-
this.expect("[]");
|
|
390
|
-
this.expect(".");
|
|
391
|
-
const colTok = this.expect("ident");
|
|
392
|
-
return {
|
|
393
|
-
table: tableTok.value,
|
|
394
|
-
col: colTok.value
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
function evaluate(node, ctx) {
|
|
399
|
-
if (node.kind === "num") return node.value;
|
|
400
|
-
if (node.kind === "ident") {
|
|
401
|
-
const raw = ctx.record[node.name];
|
|
402
|
-
return toFiniteNumber(raw);
|
|
403
|
-
}
|
|
404
|
-
if (node.kind === "ref") {
|
|
405
|
-
const target = ctx.refs?.[node.field] ?? null;
|
|
406
|
-
if (!target) return NaN;
|
|
407
|
-
return toFiniteNumber(target[node.col]);
|
|
408
|
-
}
|
|
409
|
-
if (node.kind === "binop") {
|
|
410
|
-
const left = evaluate(node.left, ctx);
|
|
411
|
-
const right = evaluate(node.right, ctx);
|
|
412
|
-
return applyBinop(node.operator, left, right);
|
|
413
|
-
}
|
|
414
|
-
if (node.kind === "sum") return evaluateSum(node.arg, ctx);
|
|
415
|
-
throw new Error(`unknown node`);
|
|
416
|
-
}
|
|
417
|
-
function applyBinop(operator, left, right) {
|
|
418
|
-
if (!Number.isFinite(left) || !Number.isFinite(right)) return NaN;
|
|
419
|
-
if (operator === "+") return left + right;
|
|
420
|
-
if (operator === "-") return left - right;
|
|
421
|
-
if (operator === "*") return left * right;
|
|
422
|
-
if (right === 0) return NaN;
|
|
423
|
-
return left / right;
|
|
424
|
-
}
|
|
425
|
-
function evaluateSum(arg, ctx) {
|
|
426
|
-
if (arg.factors.length === 0) return 0;
|
|
427
|
-
const tableName = arg.factors[0].table;
|
|
428
|
-
for (const factor of arg.factors) if (factor.table !== tableName) return NaN;
|
|
429
|
-
const rows = ctx.record[tableName];
|
|
430
|
-
if (!Array.isArray(rows)) return 0;
|
|
431
|
-
let total = 0;
|
|
432
|
-
for (const row of rows) {
|
|
433
|
-
if (!row || typeof row !== "object") continue;
|
|
434
|
-
let product = toFiniteNumber(row[arg.factors[0].col]);
|
|
435
|
-
if (!Number.isFinite(product)) return NaN;
|
|
436
|
-
for (let i = 1; i < arg.factors.length; i++) {
|
|
437
|
-
const value = toFiniteNumber(row[arg.factors[i].col]);
|
|
438
|
-
if (!Number.isFinite(value)) return NaN;
|
|
439
|
-
product = applyBinop(arg.operators[i - 1], product, value);
|
|
440
|
-
}
|
|
441
|
-
total += product;
|
|
442
|
-
}
|
|
443
|
-
return total;
|
|
444
|
-
}
|
|
445
|
-
function toFiniteNumber(value) {
|
|
446
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
|
|
447
|
-
if (typeof value === "string" && value.length > 0) {
|
|
448
|
-
const num = Number(value);
|
|
449
|
-
return Number.isFinite(num) ? num : NaN;
|
|
450
|
-
}
|
|
451
|
-
return NaN;
|
|
452
|
-
}
|
|
453
|
-
//#endregion
|
|
454
|
-
//#region src/collection/core/deriveAll.ts
|
|
455
|
-
/** Map each `ref` field's stored slug to its loaded target record (or
|
|
456
|
-
* null when dangling / not loaded), keyed by the LOCAL field name —
|
|
457
|
-
* the shape `evaluateDerived` reads for `<field>.<col>` derefs. */
|
|
458
|
-
function resolveRowRefs(schema, record, refRecords) {
|
|
459
|
-
const refs = {};
|
|
460
|
-
for (const [key, field] of Object.entries(schema.fields)) {
|
|
461
|
-
if (field.type !== "ref" || !field.to) continue;
|
|
462
|
-
const slug = record[key];
|
|
463
|
-
refs[key] = typeof slug === "string" ? refRecords[field.to]?.[slug] ?? null : null;
|
|
464
|
-
}
|
|
465
|
-
return refs;
|
|
466
|
-
}
|
|
467
|
-
/** Evaluate every `derived` field against `base`, saturating so a
|
|
468
|
-
* derived field can read another derived field computed in an earlier
|
|
469
|
-
* pass (`subtotal → tax → total` converges in ≤ field-count passes).
|
|
470
|
-
* Cycles can't loop forever — passes are bounded by the number of
|
|
471
|
-
* derived fields and the loop breaks as soon as a pass changes
|
|
472
|
-
* nothing. Failed formulas stay ABSENT (the UI renders them as
|
|
473
|
-
* em-dash). Returns a copy; `base` is never mutated.
|
|
474
|
-
*
|
|
475
|
-
* Derived keys already present in `base` are stripped before
|
|
476
|
-
* evaluation: computed output is host-truth, never persisted-input
|
|
477
|
-
* fallback. A record JSON can carry a stale (or forged) derived value
|
|
478
|
-
* — raw Write/Edit, legacy data — and without the strip, a failing
|
|
479
|
-
* formula would silently surface that value as if the host computed
|
|
480
|
-
* it. */
|
|
481
|
-
function deriveAll(schema, base, refRecords) {
|
|
482
|
-
const derivedKeys = new Set(Object.keys(schema.fields).filter((key) => schema.fields[key]?.type === "derived"));
|
|
483
|
-
const enriched = Object.fromEntries(Object.entries(base).filter(([key]) => !derivedKeys.has(key)));
|
|
484
|
-
const refs = resolveRowRefs(schema, base, refRecords);
|
|
485
|
-
const maxPasses = Object.values(schema.fields).filter((field) => field.type === "derived").length;
|
|
486
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
487
|
-
let mutated = false;
|
|
488
|
-
for (const [key, field] of Object.entries(schema.fields)) {
|
|
489
|
-
if (field.type !== "derived" || !field.formula) continue;
|
|
490
|
-
const next = evaluateDerived(field.formula, {
|
|
491
|
-
record: enriched,
|
|
492
|
-
refs
|
|
493
|
-
});
|
|
494
|
-
if (next !== null && enriched[key] !== next) {
|
|
495
|
-
enriched[key] = next;
|
|
496
|
-
mutated = true;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (!mutated) break;
|
|
500
|
-
}
|
|
501
|
-
return enriched;
|
|
502
|
-
}
|
|
503
|
-
//#endregion
|
|
504
|
-
export { resolveIcon as a, AGENT_INGEST_KIND as c, embedTargetId as d, isFieldDrivenEvery as f, firstDateField as i, FEED_SCHEDULES as l, resolveRowRefs as n, selectDynamicRecord as o, evaluateDerived as r, matchesWhere as s, deriveAll as t, INGEST_KINDS as u };
|
|
505
|
-
|
|
506
|
-
//# sourceMappingURL=deriveAll-CMFXDQ_G.js.map
|