@omgbase/core 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,12 @@
28
28
  // KINDS of its two operands are provably compared the same way — see
29
29
  // `comparable` (the spec/surface §1 decline (a)) — and a name that is a relation, reach-through
30
30
  // or source handle is never read as a property (decline (b),
31
- // `NON_PROPERTY_NAMES`).
31
+ // `NON_PROPERTY_NAMES`). A bool or num literal against a JSON or property
32
+ // read is a TYPED push (the 1.2 patch): the stored type is tested in SQL
33
+ // before the value (`json_type(x) = 'true'`, `p.type = 'number' AND
34
+ // p.val_num >= ?`) and the whole test is wrapped `(…) IS 1` / `IS NOT 1`,
35
+ // so an absent or differently typed value compares as in memory —
36
+ // unequal, never ordered. See `typedComparison`.
32
37
  //
33
38
  // Only forms that are faithful in a POSITIVE, AND-composed context are
34
39
  // translated (that is the only context `partitionPushable` pushes into). `||`,
@@ -103,38 +108,46 @@ function intrinsicSql(name, ctx) {
103
108
  * unplanned when one is residual: spec/surface §1 decline (c)). */
104
109
  export const RESERVED_DOC_BASENAMES = new Set(["id", "path", "updated_at", "content_hash", "body"]);
105
110
  const SEG = /^[A-Za-z_][A-Za-z0-9_]*$/; // injection-safe inlined identifier
106
- // A single-valued document property (the CEL scalar-in-scope rule): the scalar
107
- // value only when the key has exactly one row in scope and it is card='scalar',
111
+ // The single-scalar-row scope of a document property (the CEL scalar-in-scope
112
+ // rule): the row for `key` on the document only when the key has exactly one
113
+ // row in scope and it is card='scalar' — a list-valued or nested key has none.
114
+ // `select` is the projected expression over the alias `p`.
115
+ function propSubquery(docAlias, key, select) {
116
+ return `(SELECT ${select} FROM properties p
117
+ WHERE p.doc_id = ${docAlias}.doc_id AND p.key = '${key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL
118
+ AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${docAlias}.doc_id AND p2.key = '${key}' AND p2.deleted_commit IS NULL) = 1
119
+ LIMIT 1)`;
120
+ }
121
+ // A single-valued document property: the scalar value of its one row in scope,
108
122
  // else NULL — matching the store context's `docProp` for a scalar read.
109
123
  function propScalar(docAlias, key) {
110
124
  if (!SEG.test(key))
111
125
  return null;
112
126
  return {
113
- sql: `(SELECT COALESCE(p.val_text, p.val_num, p.val_bool) FROM properties p
114
- WHERE p.doc_id = ${docAlias}.doc_id AND p.key = '${key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL
115
- AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${docAlias}.doc_id AND p2.key = '${key}' AND p2.deleted_commit IS NULL) = 1
116
- LIMIT 1)`,
127
+ sql: propSubquery(docAlias, key, "COALESCE(p.val_text, p.val_num, p.val_bool)"),
117
128
  params: [],
118
129
  kind: "prop",
130
+ prop: { docAlias, key },
119
131
  };
120
132
  }
121
133
  // json_extract path from validated segments, or null if any segment is unsafe.
122
134
  function jsonExtract(col, segs) {
123
135
  if (segs.some((s) => !SEG.test(s)))
124
136
  return null;
125
- return { sql: `json_extract(${col}, '$.${segs.join(".")}')`, params: [], kind: "json" };
137
+ const path = `'$.${segs.join(".")}'`;
138
+ return { sql: `json_extract(${col}, ${path})`, params: [], kind: "json", json: { col, path } };
126
139
  }
127
140
  // A literal or binding value as a bound `?` with its kind. better-sqlite3 binds
128
141
  // only number/string/bigint/buffer/null — booleans are coerced to 1/0 (how
129
- // json_extract surfaces JSON booleans); anything else (an array binding, say) is
130
- // not a scalar and declines.
142
+ // json_extract and `val_bool` surface booleans); anything else (an array
143
+ // binding, say) is not a scalar and declines.
131
144
  function constOperand(v) {
132
145
  switch (typeof v) {
133
- case "string": return { sql: "?", params: [v], kind: "text" };
134
- case "number": return { sql: "?", params: [v], kind: "num" };
135
- case "boolean": return { sql: "?", params: [v ? 1 : 0], kind: "bool" };
136
- case "undefined": return { sql: "?", params: [null], kind: "null" };
137
- default: return v === null ? { sql: "?", params: [null], kind: "null" } : null;
146
+ case "string": return { sql: "?", params: [v], kind: "text", value: v };
147
+ case "number": return { sql: "?", params: [v], kind: "num", value: v };
148
+ case "boolean": return { sql: "?", params: [v ? 1 : 0], kind: "bool", value: v };
149
+ case "undefined": return { sql: "?", params: [null], kind: "null", value: null };
150
+ default: return v === null ? { sql: "?", params: [null], kind: "null", value: null } : null;
138
151
  }
139
152
  }
140
153
  // The dotted `attrs.a.b` / `doc.x` receiver chain as segments, or null if it is
@@ -262,35 +275,49 @@ const IS_OP = {
262
275
  * comparison is pushed only when its two operand kinds are provably compared
263
276
  * the same way by SQLite and by the in-memory `equals`/`compare`.
264
277
  *
265
- * Equality (`==`, `!=`) pushes iff
278
+ * Equality (`==`, `!=`) pushes PLAINLY (`IS` / `IS NOT`) iff
266
279
  * • one operand is text — SQLite's typed `IS` and strict `===` agree that a
267
280
  * string equals nothing but an equal string (`5 IS '5'` is false);
268
281
  * • or one operand is null and the other is NOT a property read — the same
269
282
  * absence both ways, except that a list-valued or nested property has no
270
283
  * scalar row (SQL NULL) where in memory the value is an array or object;
271
284
  * • or both operands are numeric (int or num) — an exact integer comparison.
272
- * Relational (`<`, `<=`, `>`, `>=`) pushes iff both operands are text or both
273
- * are numeric.
285
+ * Relational (`<`, `<=`, `>`, `>=`) pushes plainly iff both operands are text
286
+ * or both are numeric.
287
+ * TYPED pushes (the 1.2 patch, `typedComparison`): a bool literal/binding
288
+ * against a json or prop read (`==`, `!=` only) and a num literal/binding
289
+ * against a json or prop read (every op) push with the stored type tested
290
+ * first — `json_type(x) = 'true'`, `json_type(x) IN ('integer','real') AND
291
+ * json_extract(x) <op> ?`, `p.type = 'bool' AND p.val_bool = ?`, `p.type =
292
+ * 'number' AND p.val_num <op> ?` — wrapped `(…) IS 1` (or `IS NOT 1` for
293
+ * `!=`), so an absent or differently typed value is unequal and never
294
+ * ordered, exactly as in memory.
274
295
  * Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
275
- * `val_num` alike (`checked == 1`, `$ordinal == checked`, `verified == 1`),
276
- * orders every integer before every text (`$ordinal < "3"`, `level < "x"`,
277
- * `$path > 5`), and two JSON or property reads carry no type at plan time
296
+ * `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
297
+ * integer before every text (`$ordinal < "3"`, `level < "x"`, `$path > 5`),
298
+ * and two JSON or property reads carry no type at plan time
278
299
  * (`attrs.a == attrs.b`, `era == stages`).
279
300
  *
280
- * The resulting grid (P = pushed, D = declined; symmetric) for `==` / `!=`:
301
+ * The resulting grid (P = pushed plainly, T = typed push, D = declined;
302
+ * symmetric) for `==` / `!=`:
281
303
  *
282
304
  * text int num bool null json prop
283
305
  * text P P P P P P P
284
306
  * int P P P D P D D
285
- * num P P P D P D D
286
- * bool P D D D P D D
307
+ * num P P P D P T T
308
+ * bool P D D D P T T
287
309
  * null P P P P P P D
288
- * json P D D D P D D
289
- * prop P D D D D D D
310
+ * json P D T T P D D
311
+ * prop P D T T D D D
290
312
  *
291
- * and for `<` `<=` `>` `>=` only text×text, int×int, int×num, num×num push.
313
+ * and for `<` `<=` `>` `>=` text×text, int×int, int×num, num×num push plainly
314
+ * and num×json, num×prop push typed.
292
315
  */
293
316
  export function comparable(op, a, b) {
317
+ return plainComparable(op, a, b) || typedPair(op, a, b) !== null;
318
+ }
319
+ // The plain cells of the grid (P): the two operands compare the same way as-is.
320
+ function plainComparable(op, a, b) {
294
321
  const numeric = (k) => k === "int" || k === "num";
295
322
  const bothNumeric = numeric(a) && numeric(b);
296
323
  if (op === "==" || op === "!=") {
@@ -302,6 +329,18 @@ export function comparable(op, a, b) {
302
329
  }
303
330
  return (a === "text" && b === "text") || bothNumeric;
304
331
  }
332
+ // The typed cells of the grid (T): which side is the constant (`"left"` /
333
+ // `"right"`), or null when the pair is not a typed push. A bool constant pushes
334
+ // typed only under `==` / `!=` (booleans never order); a num constant under any op.
335
+ function typedPair(op, a, b) {
336
+ const read = (k) => k === "json" || k === "prop";
337
+ const constant = (k) => k === "num" || (k === "bool" && (op === "==" || op === "!="));
338
+ if (constant(a) && read(b))
339
+ return "left";
340
+ if (constant(b) && read(a))
341
+ return "right";
342
+ return null;
343
+ }
305
344
  /** The complement of `comparable` — kept for readers of the earlier decline-list form. */
306
345
  export function declinedPair(a, b, op = "==") {
307
346
  return !comparable(op, a, b);
@@ -333,13 +372,63 @@ function translateComparison(e, ctx) {
333
372
  const r = translateOperand(e.right, ctx);
334
373
  if (!l || !r)
335
374
  return null;
336
- if (!comparable(e.op, l.kind, r.kind))
375
+ if (plainComparable(e.op, l.kind, r.kind)) {
376
+ // `==`/`!=` → null-safe IS / IS NOT (absence-normalized equality, faithful in
377
+ // any context). Relational ops → plain SQL: a NULL operand yields NULL, which
378
+ // is excluded in the positive AND context these fragments are pushed into,
379
+ // matching `relate`'s absent-operand ⇒ false rule.
380
+ return { sql: `(${l.sql} ${op} ${r.sql})`, params: [...l.params, ...r.params] };
381
+ }
382
+ const side = typedPair(e.op, l.kind, r.kind);
383
+ if (side === null)
337
384
  return null; // decline (a): the positive comparison rule
338
- // `==`/`!=` → null-safe IS / IS NOT (absence-normalized equality, faithful in
339
- // any context). Relational ops → plain SQL: a NULL operand yields NULL, which
340
- // is excluded in the positive AND context these fragments are pushed into,
341
- // matching `relate`'s absent-operand ⇒ false rule.
342
- return { sql: `(${l.sql} ${op} ${r.sql})`, params: [...l.params, ...r.params] };
385
+ // Normalize to <read> <op> <constant>, flipping a relational op when the
386
+ // constant is on the left (`800 < era` ⇔ `era > 800`).
387
+ return side === "right" ? typedComparison(e.op, l, r) : typedComparison(FLIP[e.op] ?? e.op, r, l);
388
+ }
389
+ const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" };
390
+ /**
391
+ * The typed push (spec/surface §1, 1.2 patch): `read <op> constant` where the
392
+ * read is a JSON or property read and the constant a bool or num literal/binding.
393
+ * The stored type is tested before the value, and the whole test is wrapped
394
+ * `(…) IS 1` for `==` and the relational ops, `(…) IS NOT 1` for `!=` — so a
395
+ * NULL (absent path, no scalar row) or a `0` (other type, unequal value) reads
396
+ * as "not equal" / "not ordered", which is the in-memory answer: `checked == 1`
397
+ * finds nothing, `checked != 1` everything, `level >= 2` only numeric levels.
398
+ *
399
+ * json × bool (json_type(col, path) = 'true'|'false') IS [NOT] 1 no bind: the type string IS the value
400
+ * json × num (json_type(col, path) IN ('integer','real') AND json_extract(col, path) <op> ?) IS [NOT] 1
401
+ * prop × bool (SELECT p.type = 'bool' AND p.val_bool = ? FROM properties p WHERE <scope> LIMIT 1) IS [NOT] 1 ? = 1|0
402
+ * prop × num (SELECT p.type = 'number' AND p.val_num <op> ? FROM properties p WHERE <scope> LIMIT 1) IS [NOT] 1
403
+ *
404
+ * where <op> is `=` for both `==` and `!=` (the `!=` case negates the equality
405
+ * by its `IS NOT 1` wrap) and the relational operator itself otherwise.
406
+ *
407
+ * `<scope>` is `propSubquery`'s single-scalar-row condition, so a list-valued or
408
+ * nested key yields NULL → unequal/unordered, as in memory.
409
+ */
410
+ function typedComparison(op, read, constant) {
411
+ // `!=` is `!equals`: the EQUALITY test, wrapped IS NOT 1 (never `<>` inside —
412
+ // `(type AND val <> ?) IS NOT 1` would drop every row of the right type with a
413
+ // different value, the differential caught it).
414
+ const wrap = op === "!=" ? "IS NOT 1" : "IS 1";
415
+ const cmp = op === "==" || op === "!=" ? "=" : op;
416
+ if (read.json) {
417
+ const { col, path } = read.json;
418
+ if (constant.kind === "bool") {
419
+ return { sql: `((json_type(${col}, ${path}) = '${constant.value ? "true" : "false"}') ${wrap})`, params: [] };
420
+ }
421
+ return {
422
+ sql: `((json_type(${col}, ${path}) IN ('integer', 'real') AND json_extract(${col}, ${path}) ${cmp} ?) ${wrap})`,
423
+ params: [...constant.params],
424
+ };
425
+ }
426
+ if (read.prop) {
427
+ const { docAlias, key } = read.prop;
428
+ const test = constant.kind === "bool" ? `p.type = 'bool' AND p.val_bool = ?` : `p.type = 'number' AND p.val_num ${cmp} ?`;
429
+ return { sql: `(${propSubquery(docAlias, key, test)} ${wrap})`, params: [...constant.params] };
430
+ }
431
+ return null;
343
432
  }
344
433
  // startsWith / contains / endsWith — CASE-SENSITIVE, via substr/instr (never
345
434
  // LIKE). `matches` (regex) is declined until a `regexp` UDF is registered.
@@ -35,6 +35,15 @@ export declare function makeBlockContextResolver(store: Store, scope: {
35
35
  * `shouldEmbed` floor.
36
36
  */
37
37
  export declare function buildEmbedTasks(store: Store, repoId: string): EmbedTask[];
38
+ /**
39
+ * The embedding queue depth without a provider process (spec/search §2.3): the
40
+ * embeddable blocks (`buildEmbedTasks`) whose current `(content_hash, ctx_hash)`
41
+ * has no cached vector — for `model` when one is named (the `embedding.model`
42
+ * setting), else for any model. `omg status` reports it as `embedQueue`;
43
+ * `EmbeddingWorker.staleBlocks` is the same test keyed by the connected
44
+ * provider's model.
45
+ */
46
+ export declare function staleEmbedCount(store: Store, repoId: string, model?: string): number;
38
47
  /**
39
48
  * Build DocEmbedTask[] for every live document in the repo (doc-grain semantic
40
49
  * retrieval). Each task carries the whole-document embed input (header +
@@ -1,7 +1,7 @@
1
1
  import { reconstructContent } from "../core/read/document.js";
2
2
  import { docPropertiesMerged } from "../core/store/properties.js";
3
3
  import { LIVE_LEAF_SQL } from "../core/store/fts.js";
4
- import { contextPrefix, shouldEmbed, estimateTokens } from "./embeddings.js";
4
+ import { contextPrefix, ctxHashHex, shouldEmbed, estimateTokens } from "./embeddings.js";
5
5
  function docTitle(titleByDoc, firstHeadingByDoc, docId, path) {
6
6
  const t = titleByDoc.get(docId);
7
7
  if (t && t.trim())
@@ -85,6 +85,28 @@ export function buildEmbedTasks(store, repoId) {
85
85
  }
86
86
  return tasks;
87
87
  }
88
+ /**
89
+ * The embedding queue depth without a provider process (spec/search §2.3): the
90
+ * embeddable blocks (`buildEmbedTasks`) whose current `(content_hash, ctx_hash)`
91
+ * has no cached vector — for `model` when one is named (the `embedding.model`
92
+ * setting), else for any model. `omg status` reports it as `embedQueue`;
93
+ * `EmbeddingWorker.staleBlocks` is the same test keyed by the connected
94
+ * provider's model.
95
+ */
96
+ export function staleEmbedCount(store, repoId, model) {
97
+ const tasks = buildEmbedTasks(store, repoId);
98
+ const cached = model
99
+ ? store.db.prepare("SELECT 1 FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?")
100
+ : store.db.prepare("SELECT 1 FROM embeddings WHERE content_hash = ? AND ctx_hash = ?");
101
+ let stale = 0;
102
+ for (const t of tasks) {
103
+ const key = [Buffer.from(t.contentHashHex, "hex"), Buffer.from(ctxHashHex(t.ctx), "hex")];
104
+ const hit = model ? cached.get(...key, model) : cached.get(...key);
105
+ if (!hit)
106
+ stale++;
107
+ }
108
+ return stale;
109
+ }
88
110
  // The doc-embedding header: a lightweight identifying line prepended to the
89
111
  // body so schema-y atomic notes (a two-line canon definition) embed with their
90
112
  // title + type/layer context rather than as near-empty bodies. Kept minimal and
@@ -57,3 +57,16 @@ export interface SyncStatus {
57
57
  * re-ingested — the exact stale-under-green-light failure this guards against.
58
58
  */
59
59
  export declare function syncStatus(store: Store, repoId: string, rootPath?: string): SyncStatus;
60
+ /** The `repos` tool's result: every repo of the workspace and whether an `fs` source feeds it. */
61
+ export interface ReposList {
62
+ repos: {
63
+ slug: string;
64
+ hasSource: boolean;
65
+ }[];
66
+ }
67
+ /**
68
+ * `repos` (spec/surface §4): the workspace's repos by slug, `hasSource` when an
69
+ * `fs` source is attached. Shared by the MCP tool and `omg repos --json` so the
70
+ * shape cannot drift; per-repo counts are `reposStatus`.
71
+ */
72
+ export declare function reposList(store: Store): ReposList;
@@ -50,4 +50,15 @@ export function syncStatus(store, repoId, rootPath) {
50
50
  disk: status.disk,
51
51
  };
52
52
  }
53
+ /**
54
+ * `repos` (spec/surface §4): the workspace's repos by slug, `hasSource` when an
55
+ * `fs` source is attached. Shared by the MCP tool and `omg repos --json` so the
56
+ * shape cannot drift; per-repo counts are `reposStatus`.
57
+ */
58
+ export function reposList(store) {
59
+ const rows = store.db
60
+ .prepare("SELECT r.slug AS slug, MAX(CASE WHEN s.adapter = 'fs' THEN 1 ELSE 0 END) AS has_fs FROM repos r LEFT JOIN attachments a ON a.repo_id = r.repo_id LEFT JOIN sources s ON s.source_id = a.source_id GROUP BY r.repo_id, r.slug ORDER BY r.slug")
61
+ .all();
62
+ return { repos: rows.map((r) => ({ slug: r.slug, hasSource: r.has_fs === 1 })) };
63
+ }
53
64
  //# sourceMappingURL=admin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omgbase/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Open Markdown Graph Base — embedded engine",
5
5
  "license": "MIT",
6
6
  "repository": {