@omgbase/core 0.1.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/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- package/package.json +80 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type TokenType = "ident" | "field" | "outer" | "string" | "int" | "double" | "bool" | "null" | "op" | "lparen" | "rparen" | "comma" | "dot" | "eof";
|
|
2
|
+
export interface Token {
|
|
3
|
+
type: TokenType;
|
|
4
|
+
value: string;
|
|
5
|
+
pos: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class LexError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export declare function lex(src: string): Token[];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Lexer for the CEL subset (query-language §3.1). Produces a flat token
|
|
2
|
+
// stream for the recursive-descent parser.
|
|
3
|
+
export class LexError extends Error {
|
|
4
|
+
}
|
|
5
|
+
const OPS_2 = new Set(["&&", "||", "==", "!=", "<=", ">="]);
|
|
6
|
+
// Arithmetic operators are lexed so the parser can reject them with a precise
|
|
7
|
+
// "arithmetic is not supported" message (10 §3.1) rather than a lex error.
|
|
8
|
+
const OPS_1 = new Set(["!", "<", ">", "+", "-", "*", "/"]);
|
|
9
|
+
export function lex(src) {
|
|
10
|
+
const tokens = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
const n = src.length;
|
|
13
|
+
const push = (type, value, pos) => {
|
|
14
|
+
tokens.push({ type, value, pos });
|
|
15
|
+
};
|
|
16
|
+
while (i < n) {
|
|
17
|
+
const c = src[i];
|
|
18
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (c === "(") {
|
|
23
|
+
push("lparen", c, i);
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (c === ")") {
|
|
28
|
+
push("rparen", c, i);
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (c === ",") {
|
|
33
|
+
push("comma", c, i);
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (c === ".") {
|
|
38
|
+
push("dot", c, i);
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// two-char operators
|
|
43
|
+
const two = src.slice(i, i + 2);
|
|
44
|
+
if (OPS_2.has(two)) {
|
|
45
|
+
push("op", two, i);
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (OPS_1.has(c)) {
|
|
50
|
+
push("op", c, i);
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// strings
|
|
55
|
+
if (c === '"' || c === "'") {
|
|
56
|
+
const quote = c;
|
|
57
|
+
const start = i;
|
|
58
|
+
i++;
|
|
59
|
+
let value = "";
|
|
60
|
+
while (i < n && src[i] !== quote) {
|
|
61
|
+
if (src[i] === "\\") {
|
|
62
|
+
i++;
|
|
63
|
+
const esc = src[i];
|
|
64
|
+
if (esc === undefined)
|
|
65
|
+
throw new LexError(`unterminated escape at ${i}`);
|
|
66
|
+
value += esc === "n" ? "\n" : esc === "t" ? "\t" : esc;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
value += src[i];
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (i >= n)
|
|
75
|
+
throw new LexError(`unterminated string starting at ${start}`);
|
|
76
|
+
i++; // closing quote
|
|
77
|
+
push("string", value, start);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// numbers
|
|
81
|
+
if (c >= "0" && c <= "9") {
|
|
82
|
+
const start = i;
|
|
83
|
+
while (i < n && src[i] >= "0" && src[i] <= "9")
|
|
84
|
+
i++;
|
|
85
|
+
let isDouble = false;
|
|
86
|
+
if (src[i] === ".") {
|
|
87
|
+
isDouble = true;
|
|
88
|
+
i++;
|
|
89
|
+
while (i < n && src[i] >= "0" && src[i] <= "9")
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
push(isDouble ? "double" : "int", src.slice(start, i), start);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// ^name — a one-scope-outward binding reference (the symmetric read form of
|
|
96
|
+
// the `^name:` lift). OQX resolves the name against the enclosing query
|
|
97
|
+
// scope's bindings; a bare `^` with no name is a lex error.
|
|
98
|
+
if (c === "^") {
|
|
99
|
+
const start = i;
|
|
100
|
+
i++;
|
|
101
|
+
const nameStart = i;
|
|
102
|
+
while (i < n && /[A-Za-z0-9_]/.test(src[i]))
|
|
103
|
+
i++;
|
|
104
|
+
if (i === nameStart)
|
|
105
|
+
throw new LexError(`expected a binding name after '^' at ${start}`);
|
|
106
|
+
push("outer", src.slice(nameStart, i), start);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// $-intrinsic: $ident(.ident)*
|
|
110
|
+
if (c === "$") {
|
|
111
|
+
const start = i;
|
|
112
|
+
i++;
|
|
113
|
+
while (i < n && /[A-Za-z0-9_]/.test(src[i]))
|
|
114
|
+
i++;
|
|
115
|
+
// allow dotted continuation only when followed by ident (handled by parser via dot tokens);
|
|
116
|
+
// here we capture just the leading $ident. Dots handled as separate tokens.
|
|
117
|
+
push("field", src.slice(start, i), start);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// identifiers / keywords
|
|
121
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
122
|
+
const start = i;
|
|
123
|
+
while (i < n && /[A-Za-z0-9_]/.test(src[i]))
|
|
124
|
+
i++;
|
|
125
|
+
const word = src.slice(start, i);
|
|
126
|
+
if (word === "true" || word === "false")
|
|
127
|
+
push("bool", word, start);
|
|
128
|
+
else if (word === "null")
|
|
129
|
+
push("null", word, start);
|
|
130
|
+
else
|
|
131
|
+
push("ident", word, start);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new LexError(`unexpected character '${c}' at ${i}`);
|
|
135
|
+
}
|
|
136
|
+
push("eof", "", n);
|
|
137
|
+
return tokens;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=lexer.js.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { lex, LexError } from "./lexer.js";
|
|
2
|
+
// Recursive-descent parser for the CEL subset (10 §3.1). Throws FilterInvalid
|
|
3
|
+
// with a reason + hint on anything outside the grammar.
|
|
4
|
+
export class FilterInvalid extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
hint;
|
|
7
|
+
constructor(reason, hint = "see query-language.md §3") {
|
|
8
|
+
super(reason);
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
this.hint = hint;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const RELOPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
14
|
+
export function parseFilter(src) {
|
|
15
|
+
let tokens;
|
|
16
|
+
try {
|
|
17
|
+
tokens = lex(src);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
throw new FilterInvalid(e instanceof LexError ? e.message : String(e));
|
|
21
|
+
}
|
|
22
|
+
const p = new Parser(tokens);
|
|
23
|
+
const node = p.parseExpr();
|
|
24
|
+
p.expect("eof");
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
class Parser {
|
|
28
|
+
tokens;
|
|
29
|
+
pos = 0;
|
|
30
|
+
constructor(tokens) {
|
|
31
|
+
this.tokens = tokens;
|
|
32
|
+
}
|
|
33
|
+
peek() { return this.tokens[this.pos]; }
|
|
34
|
+
next() { return this.tokens[this.pos++]; }
|
|
35
|
+
expect(type) {
|
|
36
|
+
const t = this.peek();
|
|
37
|
+
if (t.type !== type)
|
|
38
|
+
throw new FilterInvalid(`expected ${type} but found '${t.value || t.type}' at ${t.pos}`);
|
|
39
|
+
return this.next();
|
|
40
|
+
}
|
|
41
|
+
parseExpr() { return this.parseOr(); }
|
|
42
|
+
parseOr() {
|
|
43
|
+
let left = this.parseAnd();
|
|
44
|
+
while (this.peek().type === "op" && this.peek().value === "||") {
|
|
45
|
+
this.next();
|
|
46
|
+
left = { kind: "or", left, right: this.parseAnd() };
|
|
47
|
+
}
|
|
48
|
+
return left;
|
|
49
|
+
}
|
|
50
|
+
parseAnd() {
|
|
51
|
+
let left = this.parseUnary();
|
|
52
|
+
while (this.peek().type === "op" && this.peek().value === "&&") {
|
|
53
|
+
this.next();
|
|
54
|
+
left = { kind: "and", left, right: this.parseUnary() };
|
|
55
|
+
}
|
|
56
|
+
return left;
|
|
57
|
+
}
|
|
58
|
+
parseUnary() {
|
|
59
|
+
if (this.peek().type === "op" && this.peek().value === "!") {
|
|
60
|
+
this.next();
|
|
61
|
+
return { kind: "not", operand: this.parseUnary() };
|
|
62
|
+
}
|
|
63
|
+
return this.parseComparison();
|
|
64
|
+
}
|
|
65
|
+
// comparison | membership | (primary that may stand alone as boolean)
|
|
66
|
+
parseComparison() {
|
|
67
|
+
const left = this.parsePostfix(this.parsePrimary());
|
|
68
|
+
// membership: literal in list(field) — detect the `in` keyword
|
|
69
|
+
if (this.peek().type === "ident" && this.peek().value === "in") {
|
|
70
|
+
this.next();
|
|
71
|
+
// `<value> in ^name`: membership over a one-scope-outward collection
|
|
72
|
+
// binding (a lift). The left operand may be any scalar (field/value/lit).
|
|
73
|
+
if (this.peek().type === "outer") {
|
|
74
|
+
const ref = this.next();
|
|
75
|
+
return { kind: "outerMembership", value: left, collection: { kind: "outerref", name: ref.value } };
|
|
76
|
+
}
|
|
77
|
+
const listCall = this.parsePrimary();
|
|
78
|
+
if (listCall.kind !== "call" || listCall.name !== "list") {
|
|
79
|
+
throw new FilterInvalid("`in` is only allowed as `<literal> in list(field)`", "10 §4");
|
|
80
|
+
}
|
|
81
|
+
if (left.kind !== "literal") {
|
|
82
|
+
throw new FilterInvalid("left side of `in list()` must be a literal", "10 §4");
|
|
83
|
+
}
|
|
84
|
+
const fieldArg = listCall.args[0];
|
|
85
|
+
if (!fieldArg || fieldArg.kind !== "field") {
|
|
86
|
+
throw new FilterInvalid("list() takes a single field argument", "10 §4");
|
|
87
|
+
}
|
|
88
|
+
return { kind: "membership", value: left, field: fieldArg };
|
|
89
|
+
}
|
|
90
|
+
if (this.peek().type === "op" && RELOPS.has(this.peek().value)) {
|
|
91
|
+
const op = this.next().value;
|
|
92
|
+
const right = this.parsePostfix(this.parsePrimary());
|
|
93
|
+
return { kind: "comparison", op, left, right };
|
|
94
|
+
}
|
|
95
|
+
if (this.peek().type === "op" && ["+", "-", "*", "/"].includes(this.peek().value)) {
|
|
96
|
+
throw new FilterInvalid("arithmetic is not supported", "10 §3.1");
|
|
97
|
+
}
|
|
98
|
+
return left;
|
|
99
|
+
}
|
|
100
|
+
// handle method calls: receiver.name(args) and quantifiers .exists/.all
|
|
101
|
+
parsePostfix(node) {
|
|
102
|
+
let cur = node;
|
|
103
|
+
while (this.peek().type === "dot") {
|
|
104
|
+
this.next();
|
|
105
|
+
// A `$`-segment after a dot is an intrinsic reach-through continuation
|
|
106
|
+
// (`doc.$path`, `doc.$updated_at`): the lexer emits it as a `field` token
|
|
107
|
+
// (value keeps the `$`). It is never a method receiver, so append it as a
|
|
108
|
+
// field segment directly. The compiler routes `doc.$…` to docs intrinsics.
|
|
109
|
+
if (this.peek().type === "field") {
|
|
110
|
+
const seg = this.next().value;
|
|
111
|
+
if (cur.kind !== "field") {
|
|
112
|
+
throw new FilterInvalid(`unexpected '.${seg}' — intrinsic segment requires a field receiver`, "10 §2");
|
|
113
|
+
}
|
|
114
|
+
cur = { kind: "field", segments: [...cur.segments, seg], intrinsic: cur.intrinsic };
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const name = this.expect("ident").value;
|
|
118
|
+
if (this.peek().type !== "lparen") {
|
|
119
|
+
// dotted field continuation (only valid if receiver was a field)
|
|
120
|
+
if (cur.kind === "field") {
|
|
121
|
+
cur = { kind: "field", segments: [...cur.segments, name], intrinsic: cur.intrinsic };
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw new FilterInvalid(`unexpected '.${name}' — method call requires '('`);
|
|
125
|
+
}
|
|
126
|
+
const args = this.parseArgs();
|
|
127
|
+
if (name === "exists" || name === "all") {
|
|
128
|
+
// first arg must be an identifier (the bound var), rest is the predicate
|
|
129
|
+
const [varArg, pred] = args;
|
|
130
|
+
if (!varArg || varArg.kind !== "field" || varArg.segments.length !== 1 || varArg.intrinsic) {
|
|
131
|
+
throw new FilterInvalid(`${name}() first argument must be a bound variable name`, "10 §3.2");
|
|
132
|
+
}
|
|
133
|
+
if (!pred)
|
|
134
|
+
throw new FilterInvalid(`${name}() requires a predicate`, "10 §3.2");
|
|
135
|
+
cur = { kind: "quantifier", collection: cur, op: name, varName: varArg.segments[0], predicate: pred };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
cur = { kind: "method", receiver: cur, name, args };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return cur;
|
|
142
|
+
}
|
|
143
|
+
parseArgs() {
|
|
144
|
+
this.expect("lparen");
|
|
145
|
+
const args = [];
|
|
146
|
+
if (this.peek().type !== "rparen") {
|
|
147
|
+
args.push(this.parseExpr());
|
|
148
|
+
while (this.peek().type === "comma") {
|
|
149
|
+
this.next();
|
|
150
|
+
args.push(this.parseExpr());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.expect("rparen");
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
parsePrimary() {
|
|
157
|
+
const t = this.peek();
|
|
158
|
+
if (t.type === "lparen") {
|
|
159
|
+
this.next();
|
|
160
|
+
const e = this.parseExpr();
|
|
161
|
+
this.expect("rparen");
|
|
162
|
+
return e;
|
|
163
|
+
}
|
|
164
|
+
if (t.type === "string") {
|
|
165
|
+
this.next();
|
|
166
|
+
return lit(t.value, "string");
|
|
167
|
+
}
|
|
168
|
+
if (t.type === "int") {
|
|
169
|
+
this.next();
|
|
170
|
+
return { kind: "literal", value: parseInt(t.value, 10), type: "int" };
|
|
171
|
+
}
|
|
172
|
+
if (t.type === "double") {
|
|
173
|
+
this.next();
|
|
174
|
+
return { kind: "literal", value: parseFloat(t.value), type: "double" };
|
|
175
|
+
}
|
|
176
|
+
if (t.type === "bool") {
|
|
177
|
+
this.next();
|
|
178
|
+
return { kind: "literal", value: t.value === "true", type: "bool" };
|
|
179
|
+
}
|
|
180
|
+
if (t.type === "null") {
|
|
181
|
+
this.next();
|
|
182
|
+
return { kind: "literal", value: null, type: "null" };
|
|
183
|
+
}
|
|
184
|
+
if (t.type === "field") {
|
|
185
|
+
this.next();
|
|
186
|
+
return { kind: "field", segments: [t.value], intrinsic: true };
|
|
187
|
+
}
|
|
188
|
+
if (t.type === "outer") {
|
|
189
|
+
this.next();
|
|
190
|
+
return { kind: "outerref", name: t.value };
|
|
191
|
+
}
|
|
192
|
+
if (t.type === "ident") {
|
|
193
|
+
if (t.value === "in")
|
|
194
|
+
throw new FilterInvalid("unexpected keyword `in`");
|
|
195
|
+
this.next();
|
|
196
|
+
// function call?
|
|
197
|
+
if (this.peek().type === "lparen") {
|
|
198
|
+
const args = this.parseArgs();
|
|
199
|
+
rejectClock(t.value);
|
|
200
|
+
return { kind: "call", name: t.value, args };
|
|
201
|
+
}
|
|
202
|
+
// bare field (frontmatter key or bare block field)
|
|
203
|
+
return { kind: "field", segments: [t.value], intrinsic: false };
|
|
204
|
+
}
|
|
205
|
+
if (t.type === "op" && (t.value === "+" || t.value === "-" || t.value === "*" || t.value === "/")) {
|
|
206
|
+
throw new FilterInvalid("arithmetic is not supported", "10 §3.1");
|
|
207
|
+
}
|
|
208
|
+
throw new FilterInvalid(`unexpected token '${t.value || t.type}' at ${t.pos}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function lit(value, type) {
|
|
212
|
+
return { kind: "literal", value, type };
|
|
213
|
+
}
|
|
214
|
+
function rejectClock(name) {
|
|
215
|
+
if (name === "now" || name === "random" || name === "rand") {
|
|
216
|
+
throw new FilterInvalid(`${name}() is rejected — determinism is load-bearing`, "10 §3.1");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import { EmbeddingWorker } from "./embeddings.js";
|
|
3
|
+
export interface DrainerOptions {
|
|
4
|
+
/** Coalesce a burst of mutations into one drain (default 500ms). */
|
|
5
|
+
debounceMs?: number;
|
|
6
|
+
/** Fired after a drain that embedded ≥1 block (progress/log hook). */
|
|
7
|
+
onDrain?: (result: {
|
|
8
|
+
embedded: number;
|
|
9
|
+
cached: number;
|
|
10
|
+
}) => void;
|
|
11
|
+
/** A drain threw (provider down, etc.). The drainer stays alive. */
|
|
12
|
+
onError?: (err: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class EmbedDrainer {
|
|
15
|
+
private store;
|
|
16
|
+
private repoId;
|
|
17
|
+
private worker;
|
|
18
|
+
private opts;
|
|
19
|
+
private readonly debounceMs;
|
|
20
|
+
private timer;
|
|
21
|
+
private dirty;
|
|
22
|
+
private running;
|
|
23
|
+
private closed;
|
|
24
|
+
constructor(store: Store, repoId: string, worker: EmbeddingWorker, opts?: DrainerOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Mark the repo dirty and (re)arm the debounce timer. Cheap and synchronous —
|
|
27
|
+
* safe to call from a mutation-tool handler or a watcher checkpoint. Bursts
|
|
28
|
+
* within the debounce window collapse into a single drain. Does no DB work and
|
|
29
|
+
* never throws; the actual embedding happens later, off the caller's path.
|
|
30
|
+
*/
|
|
31
|
+
schedule(): void;
|
|
32
|
+
/** Cancel any pending debounce and drain now; resolves when the drain (and any
|
|
33
|
+
* re-run triggered by mutations arriving mid-drain) completes. */
|
|
34
|
+
flush(): Promise<void>;
|
|
35
|
+
/** Cancel the timer and await any in-flight drain; no drains start after. */
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
private kick;
|
|
38
|
+
private loop;
|
|
39
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { EmbeddingWorker } from "./embeddings.js";
|
|
2
|
+
import { buildEmbedTasks, buildDocEmbedTasks } from "./tasks.js";
|
|
3
|
+
export class EmbedDrainer {
|
|
4
|
+
store;
|
|
5
|
+
repoId;
|
|
6
|
+
worker;
|
|
7
|
+
opts;
|
|
8
|
+
debounceMs;
|
|
9
|
+
timer = null;
|
|
10
|
+
dirty = false;
|
|
11
|
+
running = null;
|
|
12
|
+
closed = false;
|
|
13
|
+
constructor(store, repoId, worker, opts = {}) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
this.repoId = repoId;
|
|
16
|
+
this.worker = worker;
|
|
17
|
+
this.opts = opts;
|
|
18
|
+
this.debounceMs = opts.debounceMs ?? 500;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Mark the repo dirty and (re)arm the debounce timer. Cheap and synchronous —
|
|
22
|
+
* safe to call from a mutation-tool handler or a watcher checkpoint. Bursts
|
|
23
|
+
* within the debounce window collapse into a single drain. Does no DB work and
|
|
24
|
+
* never throws; the actual embedding happens later, off the caller's path.
|
|
25
|
+
*/
|
|
26
|
+
schedule() {
|
|
27
|
+
if (this.closed || !this.worker.available)
|
|
28
|
+
return;
|
|
29
|
+
this.dirty = true;
|
|
30
|
+
if (this.timer)
|
|
31
|
+
clearTimeout(this.timer);
|
|
32
|
+
this.timer = setTimeout(() => {
|
|
33
|
+
this.timer = null;
|
|
34
|
+
void this.kick();
|
|
35
|
+
}, this.debounceMs);
|
|
36
|
+
// Don't keep the process alive solely for a pending drain (hosts own their
|
|
37
|
+
// own lifetime via the transport/lease).
|
|
38
|
+
this.timer.unref?.();
|
|
39
|
+
}
|
|
40
|
+
/** Cancel any pending debounce and drain now; resolves when the drain (and any
|
|
41
|
+
* re-run triggered by mutations arriving mid-drain) completes. */
|
|
42
|
+
async flush() {
|
|
43
|
+
if (this.timer) {
|
|
44
|
+
clearTimeout(this.timer);
|
|
45
|
+
this.timer = null;
|
|
46
|
+
}
|
|
47
|
+
if (this.closed || !this.worker.available)
|
|
48
|
+
return;
|
|
49
|
+
this.dirty = true;
|
|
50
|
+
await this.kick();
|
|
51
|
+
}
|
|
52
|
+
/** Cancel the timer and await any in-flight drain; no drains start after. */
|
|
53
|
+
async close() {
|
|
54
|
+
this.closed = true;
|
|
55
|
+
if (this.timer) {
|
|
56
|
+
clearTimeout(this.timer);
|
|
57
|
+
this.timer = null;
|
|
58
|
+
}
|
|
59
|
+
await this.running;
|
|
60
|
+
}
|
|
61
|
+
// Single-flight: at most one drain runs at a time. If mutations mark the repo
|
|
62
|
+
// dirty again while a drain is in flight, the loop re-runs once more after it,
|
|
63
|
+
// so the last edit is never left unembedded.
|
|
64
|
+
kick() {
|
|
65
|
+
if (this.running)
|
|
66
|
+
return this.running;
|
|
67
|
+
this.running = this.loop().finally(() => { this.running = null; });
|
|
68
|
+
return this.running;
|
|
69
|
+
}
|
|
70
|
+
async loop() {
|
|
71
|
+
while (this.dirty && !this.closed) {
|
|
72
|
+
this.dirty = false;
|
|
73
|
+
try {
|
|
74
|
+
// Blocks first: the pooled doc-embedding fallback reuses block vectors,
|
|
75
|
+
// so embedding blocks before docs means a large doc can pool from
|
|
76
|
+
// freshly-cached vectors in the same drain.
|
|
77
|
+
const tasks = buildEmbedTasks(this.store, this.repoId);
|
|
78
|
+
const result = await this.worker.process(tasks);
|
|
79
|
+
// Then docs: recompute any document whose content changed (a block edit
|
|
80
|
+
// shifts the whole-doc input, invalidating its cached vector), so doc
|
|
81
|
+
// vectors never drift from content.
|
|
82
|
+
const docTasks = buildDocEmbedTasks(this.store, this.repoId);
|
|
83
|
+
const docResult = await this.worker.processDocs(docTasks);
|
|
84
|
+
if (result.embedded > 0 || docResult.embedded > 0 || docResult.pooled > 0) {
|
|
85
|
+
this.opts.onDrain?.({ embedded: result.embedded + docResult.embedded, cached: result.cached });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
this.opts.onError?.(err);
|
|
90
|
+
// Swallow — a transient provider failure must not crash the host. The
|
|
91
|
+
// next schedule() (or flush at shutdown) retries.
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=drain.js.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
export interface EmbeddingProvider {
|
|
3
|
+
model: string;
|
|
4
|
+
dim: number;
|
|
5
|
+
/**
|
|
6
|
+
* Max input length (in tokens) the model accepts before it truncates. Used to
|
|
7
|
+
* decide the doc-embedding strategy: inputs within budget take the whole-doc
|
|
8
|
+
* path, larger ones fall back to pooling block vectors. Absent ⇒ the caller's
|
|
9
|
+
* configured/default budget applies (05 §6; don't hardcode the threshold).
|
|
10
|
+
*/
|
|
11
|
+
maxInputTokens?: number;
|
|
12
|
+
/** Embed a batch of input strings → float32 vectors. */
|
|
13
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
14
|
+
}
|
|
15
|
+
export declare class SemanticUnavailable extends Error {
|
|
16
|
+
code: "semantic_unavailable";
|
|
17
|
+
}
|
|
18
|
+
export declare function contextPrefix(input: {
|
|
19
|
+
docTitle: string;
|
|
20
|
+
path: string;
|
|
21
|
+
headingChain: string[];
|
|
22
|
+
blockType: string;
|
|
23
|
+
}): string;
|
|
24
|
+
export declare function embedInput(ctx: string, blockText: string): string;
|
|
25
|
+
export declare function ctxHashHex(ctx: string): string;
|
|
26
|
+
export interface EmbedTask {
|
|
27
|
+
blockId: string;
|
|
28
|
+
contentHashHex: string;
|
|
29
|
+
ctx: string;
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function shouldEmbed(text: string, minTokens?: number): boolean;
|
|
33
|
+
export declare function estimateTokens(text: string): number;
|
|
34
|
+
export declare const DEFAULT_DOC_TOKEN_BUDGET = 512;
|
|
35
|
+
export interface DocEmbedBlockRef {
|
|
36
|
+
contentHashHex: string;
|
|
37
|
+
ctx: string;
|
|
38
|
+
tokens: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DocEmbedTask {
|
|
41
|
+
docId: string;
|
|
42
|
+
input: string;
|
|
43
|
+
blocks: DocEmbedBlockRef[];
|
|
44
|
+
}
|
|
45
|
+
export type DocEmbedMethod = "whole" | "pooled";
|
|
46
|
+
export interface DocVectorRow {
|
|
47
|
+
vec: Float32Array;
|
|
48
|
+
method: DocEmbedMethod;
|
|
49
|
+
}
|
|
50
|
+
export declare class EmbeddingWorker {
|
|
51
|
+
private store;
|
|
52
|
+
private provider;
|
|
53
|
+
constructor(store: Store, provider: EmbeddingProvider | null);
|
|
54
|
+
get available(): boolean;
|
|
55
|
+
private cacheKey;
|
|
56
|
+
/** Look up a cached vector; null if absent (stale/missing). */
|
|
57
|
+
getCached(contentHashHex: string, ctx: string): Float32Array | null;
|
|
58
|
+
/**
|
|
59
|
+
* Embed a batch (skipping cache hits) and persist. Throws if no provider.
|
|
60
|
+
* Cache misses are embedded in chunks of `batchSize` (default:
|
|
61
|
+
* DEFAULT_EMBED_BATCH); `onProgress` fires after each chunk is persisted so
|
|
62
|
+
* callers can show live progress on a long drain.
|
|
63
|
+
*/
|
|
64
|
+
process(tasks: EmbedTask[], opts?: {
|
|
65
|
+
batchSize?: number;
|
|
66
|
+
onProgress?: (p: {
|
|
67
|
+
embedded: number;
|
|
68
|
+
total: number;
|
|
69
|
+
}) => void;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
embedded: number;
|
|
72
|
+
cached: number;
|
|
73
|
+
}>;
|
|
74
|
+
/** Embed a bare query string (no context prefix, 05 §6). */
|
|
75
|
+
embedQuery(query: string): Promise<Float32Array>;
|
|
76
|
+
private docTokenBudget;
|
|
77
|
+
/** The freshness key for a doc task: sha256 of the whole-doc embed input. */
|
|
78
|
+
private docInputHash;
|
|
79
|
+
/** Look up a cached doc vector; null if absent or stale (input changed). */
|
|
80
|
+
getCachedDoc(docId: string, input: string): DocVectorRow | null;
|
|
81
|
+
/** Doc ids whose current input has no fresh cached vector — the doc queue. */
|
|
82
|
+
staleDocs(tasks: DocEmbedTask[]): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Compute one vector per document and persist. Whole-doc path (primary):
|
|
85
|
+
* embed the doc's input directly. Pooled fallback: only when the input
|
|
86
|
+
* exceeds the provider's token budget, pool the doc's already-cached block
|
|
87
|
+
* vectors (token-weighted mean, L2-normalized) — zero embedding calls. Skips
|
|
88
|
+
* docs whose fresh vector is already cached. Throws if no provider.
|
|
89
|
+
*/
|
|
90
|
+
processDocs(tasks: DocEmbedTask[], opts?: {
|
|
91
|
+
batchSize?: number;
|
|
92
|
+
onProgress?: (p: {
|
|
93
|
+
embedded: number;
|
|
94
|
+
total: number;
|
|
95
|
+
}) => void;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
embedded: number;
|
|
98
|
+
cached: number;
|
|
99
|
+
pooled: number;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Token-weighted mean of a document's cached block vectors, L2-normalized.
|
|
103
|
+
* Pool = Σ(wᵢ·vᵢ)/Σ(wᵢ) with wᵢ = block token count, then normalized (pooling
|
|
104
|
+
* breaks unit norm; downstream cosine assumes unit vectors). Returns null when
|
|
105
|
+
* no referenced block has a cached vector yet.
|
|
106
|
+
*/
|
|
107
|
+
private poolBlockVectors;
|
|
108
|
+
/**
|
|
109
|
+
* Staleness: a heading rename changes descendants' context prefixes, so their
|
|
110
|
+
* cached vectors (keyed by the OLD ctx_hash) no longer match the current
|
|
111
|
+
* context. This returns the block ids in a doc whose current ctx has no cached
|
|
112
|
+
* vector — i.e. stale/pending — for the worker queue.
|
|
113
|
+
*/
|
|
114
|
+
staleBlocks(tasks: EmbedTask[]): string[];
|
|
115
|
+
/**
|
|
116
|
+
* Vectors left behind by a previous model. The cache and both search paths key
|
|
117
|
+
* on model name, so after switching models the old model's rows are dead
|
|
118
|
+
* weight: never read, never overwritten (the new model writes under its own
|
|
119
|
+
* key). Reports how many block + doc vectors belong to a model OTHER than the
|
|
120
|
+
* current provider's, so callers can offer to reclaim the space.
|
|
121
|
+
*/
|
|
122
|
+
foreignVectorCount(): {
|
|
123
|
+
blocks: number;
|
|
124
|
+
docs: number;
|
|
125
|
+
};
|
|
126
|
+
/** Delete every cached vector not produced by the current model. */
|
|
127
|
+
pruneForeignVectors(): {
|
|
128
|
+
blocks: number;
|
|
129
|
+
docs: number;
|
|
130
|
+
};
|
|
131
|
+
}
|