@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.
- package/dist/src/core/ids.d.ts +34 -3
- package/dist/src/core/ids.js +79 -6
- package/dist/src/core/store/fts.d.ts +9 -0
- package/dist/src/core/store/fts.js +13 -0
- package/dist/src/core/store/store.d.ts +4 -0
- package/dist/src/core/store/store.js +40 -1
- package/dist/src/format/index.d.ts +12 -1
- package/dist/src/format/index.js +14 -1
- package/dist/src/format/markdown.d.ts +7 -0
- package/dist/src/format/markdown.js +7 -0
- package/dist/src/graph/history.d.ts +21 -0
- package/dist/src/graph/history.js +48 -6
- package/dist/src/graph/links.d.ts +2 -0
- package/dist/src/graph/links.js +22 -11
- package/dist/src/index.d.ts +8 -5
- package/dist/src/index.js +8 -5
- package/dist/src/mcp/errors.d.ts +8 -0
- package/dist/src/mcp/errors.js +17 -3
- package/dist/src/mcp/server.d.ts +8 -0
- package/dist/src/mcp/server.js +47 -50
- package/dist/src/mutate/apply.js +12 -4
- package/dist/src/mutate/doc-store.js +4 -1
- package/dist/src/mutate/docs.d.ts +19 -0
- package/dist/src/mutate/docs.js +64 -16
- package/dist/src/mutate/ops.js +25 -13
- package/dist/src/oqx-js/sql/translate.d.ts +36 -13
- package/dist/src/oqx-js/sql/translate.js +122 -33
- package/dist/src/search/tasks.d.ts +9 -0
- package/dist/src/search/tasks.js +23 -1
- package/dist/src/spec-versions.d.ts +14 -0
- package/dist/src/spec-versions.js +25 -0
- package/dist/src/sync/admin.d.ts +13 -0
- package/dist/src/sync/admin.js +11 -0
- package/dist/src/version.d.ts +39 -0
- package/dist/src/version.js +103 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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,
|
|
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
|
|
85
|
-
* bool P D D D P
|
|
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
|
|
88
|
-
* prop P D
|
|
109
|
+
* json P D T T P D D
|
|
110
|
+
* prop P D T T D D D
|
|
89
111
|
*
|
|
90
|
-
* and for `<` `<=` `>` `>=`
|
|
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
|
-
//
|
|
107
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
276
|
-
*
|
|
277
|
-
*
|
|
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,
|
|
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
|
|
286
|
-
* bool P D D D P
|
|
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
|
|
289
|
-
* prop P D
|
|
310
|
+
* json P D T T P D D
|
|
311
|
+
* prop P D T T D D D
|
|
290
312
|
*
|
|
291
|
-
* and for `<` `<=` `>` `>=`
|
|
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 (
|
|
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
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 +
|
package/dist/src/search/tasks.js
CHANGED
|
@@ -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
|
package/dist/src/sync/admin.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/sync/admin.js
CHANGED
|
@@ -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
|