@omgbase/core 0.4.0 → 0.5.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.
@@ -32,9 +32,22 @@ export interface Frag {
32
32
  * prop a document property read: a bare key on docs or `doc.<k>` (the scalar-in-scope subquery)
33
33
  */
34
34
  export type OperandKind = "text" | "int" | "num" | "bool" | "null" | "json" | "prop";
35
- /** A value-position fragment with the kind of value it denotes. */
35
+ /** A value-position fragment with the kind of value it denotes, plus what the
36
+ * typed comparison (`typedComparison`) needs to re-shape it: the JSON column +
37
+ * path behind a `json` read, the document alias + key behind a `prop` read, and
38
+ * the constant behind a literal/binding (`value`; `bool`/`num`/`null`/text
39
+ * constants only — a column operand has none). */
36
40
  export interface Operand extends Frag {
37
41
  kind: OperandKind;
42
+ json?: {
43
+ col: string;
44
+ path: string;
45
+ };
46
+ prop?: {
47
+ docAlias: string;
48
+ key: string;
49
+ };
50
+ value?: unknown;
38
51
  }
39
52
  /**
40
53
  * Per target, the bare identifiers (and `doc.<k>` keys, via the docs set) that
@@ -61,33 +74,43 @@ export declare function translateValue(e: Expr, ctx: TranslateCtx): Frag | null;
61
74
  * comparison is pushed only when its two operand kinds are provably compared
62
75
  * the same way by SQLite and by the in-memory `equals`/`compare`.
63
76
  *
64
- * Equality (`==`, `!=`) pushes iff
77
+ * Equality (`==`, `!=`) pushes PLAINLY (`IS` / `IS NOT`) iff
65
78
  * • one operand is text — SQLite's typed `IS` and strict `===` agree that a
66
79
  * string equals nothing but an equal string (`5 IS '5'` is false);
67
80
  * • or one operand is null and the other is NOT a property read — the same
68
81
  * absence both ways, except that a list-valued or nested property has no
69
82
  * scalar row (SQL NULL) where in memory the value is an array or object;
70
83
  * • or both operands are numeric (int or num) — an exact integer comparison.
71
- * Relational (`<`, `<=`, `>`, `>=`) pushes iff both operands are text or both
72
- * are numeric.
84
+ * Relational (`<`, `<=`, `>`, `>=`) pushes plainly iff both operands are text
85
+ * or both are numeric.
86
+ * TYPED pushes (the 1.2 patch, `typedComparison`): a bool literal/binding
87
+ * against a json or prop read (`==`, `!=` only) and a num literal/binding
88
+ * against a json or prop read (every op) push with the stored type tested
89
+ * first — `json_type(x) = 'true'`, `json_type(x) IN ('integer','real') AND
90
+ * json_extract(x) <op> ?`, `p.type = 'bool' AND p.val_bool = ?`, `p.type =
91
+ * 'number' AND p.val_num <op> ?` — wrapped `(…) IS 1` (or `IS NOT 1` for
92
+ * `!=`), so an absent or differently typed value is unequal and never
93
+ * ordered, exactly as in memory.
73
94
  * Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
74
- * `val_num` alike (`checked == 1`, `$ordinal == checked`, `verified == 1`),
75
- * orders every integer before every text (`$ordinal < "3"`, `level < "x"`,
76
- * `$path > 5`), and two JSON or property reads carry no type at plan time
95
+ * `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
96
+ * integer before every text (`$ordinal < "3"`, `level < "x"`, `$path > 5`),
97
+ * and two JSON or property reads carry no type at plan time
77
98
  * (`attrs.a == attrs.b`, `era == stages`).
78
99
  *
79
- * The resulting grid (P = pushed, D = declined; symmetric) for `==` / `!=`:
100
+ * The resulting grid (P = pushed plainly, T = typed push, D = declined;
101
+ * symmetric) for `==` / `!=`:
80
102
  *
81
103
  * text int num bool null json prop
82
104
  * text P P P P P P P
83
105
  * int P P P D P D D
84
- * num P P P D P D D
85
- * bool P D D D P D D
106
+ * num P P P D P T T
107
+ * bool P D D D P T T
86
108
  * null P P P P P P D
87
- * json P D D D P D D
88
- * prop P D D D D D D
109
+ * json P D T T P D D
110
+ * prop P D T T D D D
89
111
  *
90
- * and for `<` `<=` `>` `>=` only text×text, int×int, int×num, num×num push.
112
+ * and for `<` `<=` `>` `>=` text×text, int×int, int×num, num×num push plainly
113
+ * and num×json, num×prop push typed.
91
114
  */
92
115
  export declare function comparable(op: string, a: OperandKind, b: OperandKind): boolean;
93
116
  /** The complement of `comparable` — kept for readers of the earlier decline-list form. */
@@ -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
@@ -0,0 +1,14 @@
1
+ export declare const SPEC_VERSIONS: {
2
+ readonly oqx: "0.13";
3
+ readonly format: "0.2";
4
+ readonly reconcile: "2.3";
5
+ readonly store: "13.5";
6
+ readonly properties: "1.1";
7
+ readonly graph: "1.1";
8
+ readonly search: "1.2";
9
+ readonly mutate: "1.2";
10
+ readonly sync: "1.3";
11
+ readonly surface: "1.4";
12
+ readonly cli: "1.1";
13
+ };
14
+ export type SpecName = keyof typeof SPEC_VERSIONS;
@@ -0,0 +1,25 @@
1
+ import { LANGUAGE_VERSION } from "@omgbase/oqx";
2
+ // The spec versions this reference implements (spec/surface §4, the `version`
3
+ // tool's `specs`): one compile-time constant per `spec/<x>/VERSION`, NOT a file
4
+ // read — a published package has no `spec/` next to it. Hand-maintained; the
5
+ // test beside this module (`spec-versions.test.ts`) asserts every entry equals
6
+ // the corresponding `spec/<x>/VERSION` file and that no spec is missing, so a
7
+ // spec bump that forgets this map fails the build. `oqx` is the one entry with
8
+ // a constant of its own: `@omgbase/oqx`'s `LANGUAGE_VERSION`.
9
+ //
10
+ // Key order is the order spec/surface §4 lists them (the CLI prints `specs` in
11
+ // this order).
12
+ export const SPEC_VERSIONS = {
13
+ oqx: LANGUAGE_VERSION,
14
+ format: "0.2",
15
+ reconcile: "2.3",
16
+ store: "13.5",
17
+ properties: "1.1",
18
+ graph: "1.1",
19
+ search: "1.2",
20
+ mutate: "1.2",
21
+ sync: "1.3",
22
+ surface: "1.4",
23
+ cli: "1.1",
24
+ };
25
+ //# sourceMappingURL=spec-versions.js.map
@@ -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
@@ -0,0 +1,39 @@
1
+ import type { Store } from "./core/store/store.js";
2
+ import { SPEC_VERSIONS } from "./spec-versions.js";
3
+ export interface HostInfo {
4
+ /** the serving binary's own version — `omgbase`'s package.json (what `--version` prints) */
5
+ version: string;
6
+ /** the build's short git revision, from `build-info.json`; null when unknown */
7
+ commit: string | null;
8
+ /** the build's RFC 3339 time, from `build-info.json`; null when unknown */
9
+ built: string | null;
10
+ /** the host module's `import.meta.url`: the component packages are resolved from its dependency graph */
11
+ resolveFrom?: string;
12
+ }
13
+ export interface VersionInfo {
14
+ engine: "typescript";
15
+ version: string;
16
+ /** every omgbase package the binary is built from, keys sorted bytewise */
17
+ components: Record<string, string>;
18
+ specs: typeof SPEC_VERSIONS;
19
+ /** `PRAGMA user_version` of the open store; null without one */
20
+ schema: number | null;
21
+ mcp: {
22
+ protocol: string;
23
+ sdk: string;
24
+ };
25
+ runtime: string;
26
+ commit: string | null;
27
+ built: string | null;
28
+ }
29
+ /** The omgbase packages the reference binary is built from (spec/surface §4). */
30
+ export declare const COMPONENT_PACKAGES: readonly ["omgbase", "@omgbase/core", "@omgbase/oqx", "@omgbase/sync", "@omgbase/fs-adapter"];
31
+ /** The nearest `package.json` named `name` at or above `fromUrl`'s directory, or null. */
32
+ export declare function nearestPackageVersion(fromUrl: string, name: string): string | null;
33
+ /** The engine's own package version (`@omgbase/core`), from the manifest above this module. */
34
+ export declare function coreVersion(): string;
35
+ /**
36
+ * The `version` result. `store` supplies `schema`; `host` the binary's own
37
+ * version, build info and resolution anchor (see the module comment).
38
+ */
39
+ export declare function versionInfo(store?: Store | null, host?: HostInfo): VersionInfo;
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
5
+ import { SPEC_VERSIONS } from "./spec-versions.js";
6
+ /** The omgbase packages the reference binary is built from (spec/surface §4). */
7
+ export const COMPONENT_PACKAGES = ["omgbase", "@omgbase/core", "@omgbase/oqx", "@omgbase/sync", "@omgbase/fs-adapter"];
8
+ const MCP_SDK = "@modelcontextprotocol/sdk";
9
+ /** The nearest `package.json` named `name` at or above `fromUrl`'s directory, or null. */
10
+ export function nearestPackageVersion(fromUrl, name) {
11
+ let dir = dirname(fileURLToPath(fromUrl));
12
+ for (;;) {
13
+ const candidate = join(dir, "package.json");
14
+ if (existsSync(candidate)) {
15
+ const pkg = readManifest(candidate);
16
+ if (pkg?.name === name && typeof pkg.version === "string")
17
+ return pkg.version;
18
+ }
19
+ const parent = dirname(dir);
20
+ if (parent === dir)
21
+ return null;
22
+ dir = parent;
23
+ }
24
+ }
25
+ function readManifest(path) {
26
+ try {
27
+ return JSON.parse(readFileSync(path, "utf8"));
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * `name`'s installed version as seen from `anchors` (module URLs). Node's own
35
+ * resolver is no use here: the omgbase packages are ESM-only (`exports` with an
36
+ * `import` condition alone), which a CJS `require.resolve` cannot see, and no
37
+ * package exports its `package.json`. So this walks what the algorithm would
38
+ * absent `exports`: the anchor's own package first (a package cannot name
39
+ * itself), then `node_modules/<name>/package.json` at each ancestor directory —
40
+ * pnpm's symlinked layout, npm's hoisted global install and a plain checkout all
41
+ * put the manifest there. Anchors are tried in turn; null when none has it.
42
+ */
43
+ function resolvePackageVersion(name, anchors) {
44
+ for (const anchor of anchors) {
45
+ const own = nearestPackageVersion(anchor, name);
46
+ if (own !== null)
47
+ return own;
48
+ let dir = dirname(fileURLToPath(anchor));
49
+ for (;;) {
50
+ const pkg = readManifest(join(dir, "node_modules", name, "package.json"));
51
+ if (pkg?.name === name && typeof pkg.version === "string")
52
+ return pkg.version;
53
+ const parent = dirname(dir);
54
+ if (parent === dir)
55
+ break;
56
+ dir = parent;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ /** The MCP SDK's version: from the module core itself imports, walking up to its manifest (its `package.json` is not exported). */
62
+ function mcpSdkVersion() {
63
+ try {
64
+ return nearestPackageVersion(import.meta.resolve(`${MCP_SDK}/types.js`), MCP_SDK);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /** The engine's own package version (`@omgbase/core`), from the manifest above this module. */
71
+ export function coreVersion() {
72
+ return nearestPackageVersion(import.meta.url, "@omgbase/core") ?? "0.0.0";
73
+ }
74
+ /**
75
+ * The `version` result. `store` supplies `schema`; `host` the binary's own
76
+ * version, build info and resolution anchor (see the module comment).
77
+ */
78
+ export function versionInfo(store, host) {
79
+ const anchors = [...(host?.resolveFrom ? [host.resolveFrom] : []), import.meta.url];
80
+ const components = {};
81
+ for (const name of COMPONENT_PACKAGES) {
82
+ // The host IS `omgbase`: its version is what it says it is (the resolver
83
+ // would find the same manifest above `resolveFrom`, but the host is explicit).
84
+ const v = name === "omgbase" && host ? host.version : resolvePackageVersion(name, anchors);
85
+ if (v !== null)
86
+ components[name] = v;
87
+ }
88
+ const sorted = Object.fromEntries(Object.entries(components).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
89
+ const version = host?.version ?? components["omgbase"] ?? coreVersion();
90
+ const schema = store ? store.db.pragma("user_version", { simple: true }) : null;
91
+ return {
92
+ engine: "typescript",
93
+ version,
94
+ components: sorted,
95
+ specs: SPEC_VERSIONS,
96
+ schema,
97
+ mcp: { protocol: LATEST_PROTOCOL_VERSION, sdk: mcpSdkVersion() ?? "unknown" },
98
+ runtime: `node ${process.versions.node}`,
99
+ commit: host?.commit ?? null,
100
+ built: host?.built ?? null,
101
+ };
102
+ }
103
+ //# sourceMappingURL=version.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.5.0",
4
4
  "description": "Open Markdown Graph Base — embedded engine",
5
5
  "license": "MIT",
6
6
  "repository": {