@odla-ai/kg 0.2.3 → 0.3.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.
@@ -0,0 +1,73 @@
1
+ // src/temporal/time.ts
2
+ var DAY = 864e5;
3
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
4
+ function parseDateMs(v) {
5
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
6
+ if (typeof v !== "string") return null;
7
+ const m = DATE_ONLY.exec(v.trim());
8
+ if (m) {
9
+ const t2 = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
10
+ return new Date(t2).toISOString().slice(0, 10) === m[0].trim() ? t2 : null;
11
+ }
12
+ const t = Date.parse(v);
13
+ return Number.isNaN(t) ? null : t;
14
+ }
15
+ function isoWeek(t) {
16
+ const d = new Date(t);
17
+ const midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
18
+ const dow = new Date(midnight).getUTCDay() || 7;
19
+ const thursday = midnight + (4 - dow) * DAY;
20
+ const year = new Date(thursday).getUTCFullYear();
21
+ const week = Math.floor((thursday - Date.UTC(year, 0, 1)) / DAY / 7) + 1;
22
+ return { year, week };
23
+ }
24
+ function isoWeekStart(year, week) {
25
+ const jan4 = Date.UTC(year, 0, 4);
26
+ const dow = new Date(jan4).getUTCDay() || 7;
27
+ return jan4 - (dow - 1) * DAY + (week - 1) * 7 * DAY;
28
+ }
29
+ var pad2 = (n) => String(n).padStart(2, "0");
30
+ function bucketKey(t, grain) {
31
+ const d = new Date(t);
32
+ if (grain === "month") return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}`;
33
+ if (grain === "quarter") return `${d.getUTCFullYear()}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
34
+ const { year, week } = isoWeek(t);
35
+ return `${year}-W${pad2(week)}`;
36
+ }
37
+ function bucketFor(key) {
38
+ let m = /^(\d{4})-(\d{2})$/.exec(key);
39
+ if (m) {
40
+ const [y, mo] = [Number(m[1]), Number(m[2])];
41
+ return { key, start: Date.UTC(y, mo - 1, 1), end: Date.UTC(y, mo, 1) };
42
+ }
43
+ m = /^(\d{4})-Q([1-4])$/.exec(key);
44
+ if (m) {
45
+ const [y, q] = [Number(m[1]), Number(m[2])];
46
+ return { key, start: Date.UTC(y, (q - 1) * 3, 1), end: Date.UTC(y, q * 3, 1) };
47
+ }
48
+ m = /^(\d{4})-W(\d{2})$/.exec(key);
49
+ if (m) {
50
+ const start = isoWeekStart(Number(m[1]), Number(m[2]));
51
+ return { key, start, end: start + 7 * DAY };
52
+ }
53
+ throw new Error(`not a bucket key: ${key}`);
54
+ }
55
+ function bucketRange(from, to, grain) {
56
+ const out = [];
57
+ if (from > to) return out;
58
+ let cur = bucketFor(bucketKey(from, grain));
59
+ const last = bucketKey(to, grain);
60
+ for (; ; ) {
61
+ out.push(cur);
62
+ if (cur.key === last) return out;
63
+ cur = bucketFor(bucketKey(cur.end, grain));
64
+ }
65
+ }
66
+
67
+ export {
68
+ parseDateMs,
69
+ bucketKey,
70
+ bucketFor,
71
+ bucketRange
72
+ };
73
+ //# sourceMappingURL=chunk-V3N2GRYU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/temporal/time.ts"],"sourcesContent":["// UTC-only time bucketing. Date-only strings are read as UTC midnight and all\n// bucket math uses Date.UTC accessors — results never depend on the host zone.\n\n/** Supported bucketing granularities. */\nexport type TimeGrain = \"week\" | \"month\" | \"quarter\";\n\n/** One half-open time bucket: `start` inclusive, `end` exclusive (epoch ms). */\nexport interface TimeBucket {\n key: string;\n start: number;\n end: number;\n}\n\nconst DAY = 86_400_000;\nconst DATE_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * Parse a date-ish value to epoch ms, or null if unparseable. Accepts\n * `\"YYYY-MM-DD\"` (UTC midnight, with real calendar validation — `2026-02-31`\n * is null), full ISO strings, and finite numbers (already epoch ms).\n */\nexport function parseDateMs(v: unknown): number | null {\n if (typeof v === \"number\") return Number.isFinite(v) ? v : null;\n if (typeof v !== \"string\") return null;\n const m = DATE_ONLY.exec(v.trim());\n if (m) {\n const t = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n // JS rolls invalid components (\"2026-02-31\" → Mar 3); a real date\n // round-trips to the same YYYY-MM-DD.\n return new Date(t).toISOString().slice(0, 10) === m[0].trim() ? t : null;\n }\n const t = Date.parse(v);\n return Number.isNaN(t) ? null : t;\n}\n\n// ISO-8601 week: week 1 contains the year's first Thursday; weeks run Mon–Sun.\nfunction isoWeek(t: number): { year: number; week: number } {\n const d = new Date(t);\n const midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());\n const dow = new Date(midnight).getUTCDay() || 7; // Mon=1 … Sun=7\n const thursday = midnight + (4 - dow) * DAY;\n const year = new Date(thursday).getUTCFullYear();\n const week = Math.floor((thursday - Date.UTC(year, 0, 1)) / DAY / 7) + 1;\n return { year, week };\n}\n\n// Monday (UTC midnight) of ISO week `week` of ISO year `year`: Jan 4 is\n// always inside week 1.\nfunction isoWeekStart(year: number, week: number): number {\n const jan4 = Date.UTC(year, 0, 4);\n const dow = new Date(jan4).getUTCDay() || 7;\n return jan4 - (dow - 1) * DAY + (week - 1) * 7 * DAY;\n}\n\nconst pad2 = (n: number) => String(n).padStart(2, \"0\");\n\n/** The bucket key containing epoch-ms `t`: `\"2026-07\"`, `\"2026-Q3\"`, or `\"2026-W29\"`. */\nexport function bucketKey(t: number, grain: TimeGrain): string {\n const d = new Date(t);\n if (grain === \"month\") return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}`;\n if (grain === \"quarter\") return `${d.getUTCFullYear()}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;\n const { year, week } = isoWeek(t);\n return `${year}-W${pad2(week)}`;\n}\n\n/**\n * Expand a bucket key back to its half-open [start, end) range. Throws on a\n * key that matches no known grain format.\n */\nexport function bucketFor(key: string): TimeBucket {\n let m = /^(\\d{4})-(\\d{2})$/.exec(key);\n if (m) {\n const [y, mo] = [Number(m[1]), Number(m[2])];\n return { key, start: Date.UTC(y, mo - 1, 1), end: Date.UTC(y, mo, 1) };\n }\n m = /^(\\d{4})-Q([1-4])$/.exec(key);\n if (m) {\n const [y, q] = [Number(m[1]), Number(m[2])];\n return { key, start: Date.UTC(y, (q - 1) * 3, 1), end: Date.UTC(y, q * 3, 1) };\n }\n m = /^(\\d{4})-W(\\d{2})$/.exec(key);\n if (m) {\n const start = isoWeekStart(Number(m[1]), Number(m[2]));\n return { key, start, end: start + 7 * DAY };\n }\n throw new Error(`not a bucket key: ${key}`);\n}\n\n/**\n * Every bucket from the one containing `from` through the one containing `to`,\n * in order. Returns [] when from > to.\n */\nexport function bucketRange(from: number, to: number, grain: TimeGrain): TimeBucket[] {\n const out: TimeBucket[] = [];\n if (from > to) return out;\n let cur = bucketFor(bucketKey(from, grain));\n const last = bucketKey(to, grain);\n for (;;) {\n out.push(cur);\n if (cur.key === last) return out;\n cur = bucketFor(bucketKey(cur.end, grain));\n }\n}\n"],"mappings":"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","names":["t"]}
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  createProvider: () => createProvider,
34
34
  emptyUsage: () => emptyUsage,
35
35
  enabledSources: () => enabledSources,
36
+ exportFragment: () => exportFragment,
36
37
  extractJson: () => extractJson,
37
38
  extractJsonArray: () => extractJsonArray,
38
39
  fetchText: () => fetchText,
@@ -207,6 +208,124 @@ async function writeFragment(db, fragment, ontology, mutationId) {
207
208
  return { txId, ops: ops.length };
208
209
  }
209
210
 
211
+ // src/temporal/time.ts
212
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
213
+ function parseDateMs(v) {
214
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
215
+ if (typeof v !== "string") return null;
216
+ const m = DATE_ONLY.exec(v.trim());
217
+ if (m) {
218
+ const t2 = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
219
+ return new Date(t2).toISOString().slice(0, 10) === m[0].trim() ? t2 : null;
220
+ }
221
+ const t = Date.parse(v);
222
+ return Number.isNaN(t) ? null : t;
223
+ }
224
+
225
+ // src/graph/export.ts
226
+ function planLinks(ontology, type, labels) {
227
+ return labels.map((label) => {
228
+ for (const edge of Object.values(ontology.edges)) {
229
+ if (edge.forwardLabel === label && edge.from === type) {
230
+ return { label, counterpart: edge.to, direction: "forward", forwardLabel: edge.forwardLabel };
231
+ }
232
+ if (edge.reverseLabel === label && edge.to === type) {
233
+ return { label, counterpart: edge.from, direction: "reverse", forwardLabel: edge.forwardLabel };
234
+ }
235
+ }
236
+ throw new Error(`exportFragment: no ontology edge matches link "${label}" on "${type}"`);
237
+ });
238
+ }
239
+ var isScalar = (v) => v === null || typeof v !== "object" && typeof v !== "function";
240
+ function pickAttrs(row, spec, links) {
241
+ const out = {};
242
+ for (const [k, v] of Object.entries(row)) {
243
+ if (spec.attrs ? !spec.attrs.includes(k) : k === "id" || links.includes(k) || !isScalar(v)) continue;
244
+ if (isScalar(v) && v !== null) out[k] = v;
245
+ }
246
+ return out;
247
+ }
248
+ var RETRYABLE_WITHOUT_WHERE = /query_scan_limit|query_too_complex/;
249
+ async function exportFragment(db, ontology, spec) {
250
+ const keys = naturalKeysOf(ontology);
251
+ const pageSize = Math.min(1e3, Math.max(1, spec.pageSize ?? 200));
252
+ const nodes = /* @__PURE__ */ new Map();
253
+ const edges = /* @__PURE__ */ new Map();
254
+ const putNode = (node) => {
255
+ const id = `${node.type}:${node.key}`;
256
+ const existing = nodes.get(id);
257
+ if (!existing || Object.keys(node.attrs).length > Object.keys(existing.attrs).length) nodes.set(id, node);
258
+ };
259
+ for (const [type, entity] of Object.entries(spec.entities)) {
260
+ const naturalKey = keys[type];
261
+ if (!naturalKey) throw new Error(`exportFragment: no ontology entity "${type}"`);
262
+ const links = planLinks(ontology, type, entity.links ?? []);
263
+ const limit = entity.limit ?? 1e3;
264
+ const fromMs = entity.window ? parseDateMs(entity.window.from) : null;
265
+ let useWhere = entity.window?.indexed === true;
266
+ let offset = 0;
267
+ let taken = 0;
268
+ while (taken < limit) {
269
+ const opts = {
270
+ limit: Math.min(pageSize, limit - taken),
271
+ offset,
272
+ order: { [naturalKey]: "asc" }
273
+ };
274
+ if (useWhere && entity.window) opts.where = { [entity.window.dateAttr]: { $gte: entity.window.from } };
275
+ const query = {
276
+ [type]: { $: opts, ...Object.fromEntries(links.map((l) => [l.label, {}])) }
277
+ };
278
+ let rows;
279
+ try {
280
+ rows = (await db.query(query))[type] ?? [];
281
+ } catch (err) {
282
+ if (useWhere && RETRYABLE_WITHOUT_WHERE.test(String(err.message ?? err))) {
283
+ useWhere = false;
284
+ offset = 0;
285
+ taken = 0;
286
+ continue;
287
+ }
288
+ throw err;
289
+ }
290
+ for (const row of rows) {
291
+ const key = row[naturalKey];
292
+ if (key === void 0 || key === null) continue;
293
+ if (entity.window && fromMs !== null) {
294
+ const t = parseDateMs(row[entity.window.dateAttr]);
295
+ if (t !== null && t < fromMs) continue;
296
+ }
297
+ const rootRef = { type, key: String(key) };
298
+ putNode({ ...rootRef, attrs: pickAttrs(row, entity, entity.links ?? []) });
299
+ for (const plan of links) {
300
+ const nestedKeyAttr = keys[plan.counterpart];
301
+ if (!nestedKeyAttr) throw new Error(`exportFragment: no ontology entity "${plan.counterpart}"`);
302
+ const raw = row[plan.label];
303
+ const nestedRows = Array.isArray(raw) ? raw : raw ? [raw] : [];
304
+ for (const nested of nestedRows) {
305
+ const nestedKey = nested[nestedKeyAttr];
306
+ if (nestedKey === void 0 || nestedKey === null) continue;
307
+ const nestedRef = { type: plan.counterpart, key: String(nestedKey) };
308
+ putNode({ ...nestedRef, attrs: pickAttrs(nested, {}, []) });
309
+ const edge = plan.direction === "forward" ? { from: rootRef, to: nestedRef, label: plan.forwardLabel } : { from: nestedRef, to: rootRef, label: plan.forwardLabel };
310
+ edges.set(`${edge.from.type}:${edge.from.key}|${edge.label}|${edge.to.type}:${edge.to.key}`, edge);
311
+ }
312
+ }
313
+ taken++;
314
+ if (taken >= limit) break;
315
+ }
316
+ if (rows.length < pageSize) break;
317
+ offset += rows.length;
318
+ }
319
+ }
320
+ const byRef = (a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
321
+ return {
322
+ nodes: [...nodes.values()].sort(byRef),
323
+ edges: [...edges.values()].sort(
324
+ (a, b) => byRef(a.from, b.from) || (a.label < b.label ? -1 : a.label > b.label ? 1 : 0) || byRef(a.to, b.to)
325
+ )
326
+ };
327
+ }
328
+
210
329
  // src/graph/resolve.ts
211
330
  async function searchEntities(db, type, q, limit = 5) {
212
331
  if (!q.trim()) return [];
@@ -826,6 +945,7 @@ function allJobs(sources) {
826
945
  // src/state/kv.ts
827
946
  var SEEN_TTL_S = 120 * 24 * 60 * 60;
828
947
  var ENRICHED_TTL_S = 30 * 24 * 60 * 60;
948
+ var TEMPORAL_TTL_S = 120 * 24 * 60 * 60;
829
949
  var Store = class {
830
950
  constructor(kv) {
831
951
  this.kv = kv;
@@ -872,6 +992,22 @@ var Store = class {
872
992
  async setConfigSources(list) {
873
993
  await this.kv.put("config:sources", JSON.stringify(list));
874
994
  }
995
+ /** Cache the latest temporal report (`temporal:latest`) plus a dated history copy. */
996
+ async setTemporalReport(report, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
997
+ const raw = JSON.stringify(report);
998
+ await this.kv.put("temporal:latest", raw);
999
+ await this.kv.put(`temporal:${date}`, raw, { expirationTtl: TEMPORAL_TTL_S });
1000
+ }
1001
+ /** The cached latest temporal report, or null when absent/unparseable. */
1002
+ async getTemporalReport() {
1003
+ const raw = await this.kv.get("temporal:latest");
1004
+ if (!raw) return null;
1005
+ try {
1006
+ return JSON.parse(raw);
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ }
875
1011
  };
876
1012
 
877
1013
  // src/workflow/helpers.ts