@cavelang/query 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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @cavelang/query
2
+
3
+ CAVE-Q — the graph-pattern query layer (spec §12), compiled to SQL over an
4
+ open `@cavelang/store`.
5
+
6
+ ```ts
7
+ import { open } from '@cavelang/store'
8
+ import { query } from '@cavelang/query'
9
+
10
+ const store = open('knowledge.db')
11
+ query(store, '?x USES jwt')
12
+ // → [{ bindings: { x: 'auth/middleware' }, row: { … } }, …]
13
+
14
+ query(store, `?cause CAUSE app/crash
15
+ WHERE conf >= 0.7`)
16
+
17
+ query(store, '?x PART-OF monorepo') // inverse verb → same physical query
18
+ query(store, 'terrier EXTENDS+ animal') // transitive
19
+ ```
20
+
21
+ ## Pattern language (§12.1)
22
+
23
+ - `?name` variables bind subject, verb, object or attribute value;
24
+ a variable repeated in two positions forces equality (`?x NEEDS ?x`).
25
+ - `_` is a wildcard.
26
+ - `attr: ?v` matches attribute claims.
27
+ - `@ctx` and `#tag[:value]` on the pattern line filter contexts/tags
28
+ (flat `#tag` matches flat tags only, mirroring `store.byTag`).
29
+ - `VERB NOT` matches negated claims; patterns without `NOT` match
30
+ positive ones — direction is always explicit.
31
+ - **Inverse verbs** are valid: `?x PART-OF monorepo` compiles to
32
+ `verb = 'CONTAINS' AND subject = 'monorepo'` with `?x` binding on the
33
+ object side — the same physical query as the forward pattern.
34
+ - `VERB+` is transitive (one or more hops), compiled to a recursive CTE
35
+ over current, positive, non-retracted edges, depth-capped at 32.
36
+ Transitive works through inverses too (`packages/api PART-OF+ ?c` walks
37
+ `CONTAINS` upward from the object side).
38
+
39
+ ## Filters (§12.2)
40
+
41
+ ```
42
+ WHERE conf >= 0.8 (also accepts 80%)
43
+ WHERE tag = security (bare key matches any value; topic:auth is exact)
44
+ WHERE context = production
45
+ WHERE value > 1000 req/s (numeric column; unit equality when given)
46
+ WHERE tx > 2026-01-01 (dates are whole-day UTC intervals)
47
+ ```
48
+
49
+ `tx` filters compile a date to the interval `[day-start, next-day-start)`
50
+ in UUIDv7 space: `=` means "recorded that day", `<=` includes the boundary
51
+ day that `<` excludes, and `>` starts the day after. A timestamp value
52
+ covers one second.
53
+
54
+ ## Semantics
55
+
56
+ - Queries run over **current beliefs** (latest tx per claim key, §9.1) by
57
+ default, and skip retracted (`@ 0%`) ones — a retracted claim has no
58
+ current support (§9.3), and `VERB` must agree with `VERB+` on a one-hop
59
+ path. An explicit `WHERE conf …` filter or `{ all: true }` opts back in.
60
+ - Object-position variables bind relational rows only (`object IS NOT
61
+ NULL`) — `?x ?verb ?y` enumerates the relation graph, not attributes. A
62
+ *bound* date/number object additionally matches metric rows
63
+ (`latency IS 30ms` the pattern finds `latency IS 30ms` the claim).
64
+ - A repeated variable forces equality in transitive patterns too:
65
+ `?x EXTENDS+ ?x` finds nodes on cycles, not every reachable pair.
66
+ - Transitive patterns support endpoint slots only; tag/context/WHERE
67
+ filters on them are rejected rather than silently ignored.
68
+
69
+ ## Tests
70
+
71
+ ```
72
+ pnpm --filter @cavelang/query test
73
+ ```
74
+
75
+ Every §12.1 example pattern and every §12.2 filter runs against a live
76
+ in-memory store, including inverse and transitive-inverse cases,
77
+ current-vs-history semantics and negated patterns.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CAVE-Q → SQL compilation (spec §12).
3
+ *
4
+ * Patterns run over *current beliefs* by default — the latest transaction
5
+ * per claim key (spec §9.1) — pass `all` to match the full history.
6
+ * Inverse verbs compile to the same physical query as their primary
7
+ * (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
8
+ * identical SQL against canonical rows, with the pattern's subject binding
9
+ * on the object side.
10
+ *
11
+ * Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
12
+ * CTE over current, positive, non-retracted edges (depth-capped at 32).
13
+ */
14
+ import type { Row, Store } from '@cavelang/store';
15
+ /** One query solution: variable bindings plus the matched row (absent for transitive hops). */
16
+ export type Match = {
17
+ readonly bindings: Readonly<Record<string, string>>;
18
+ readonly row?: Row.t;
19
+ };
20
+ export type Options = {
21
+ /** Match all appended rows, not only current beliefs. */
22
+ readonly all?: boolean;
23
+ };
24
+ /**
25
+ * Runs a CAVE-Q query against a store.
26
+ *
27
+ * ```ts
28
+ * query(store, '?x USES jwt')
29
+ * query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
30
+ * query(store, 'terrier EXTENDS+ animal')
31
+ * ```
32
+ */
33
+ export declare const query: (store: Store, input: string, options?: Options) => Match[];
34
+ //# sourceMappingURL=compile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAGjD,+FAA+F;AAC/F,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,OAAO,GAAG;IACpB,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAyQD;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,KAAK,EAAE,OAAO,MAAM,EAAE,UAAS,OAAY,KAAG,KAAK,EAS/E,CAAA"}
@@ -0,0 +1,277 @@
1
+ /**
2
+ * CAVE-Q → SQL compilation (spec §12).
3
+ *
4
+ * Patterns run over *current beliefs* by default — the latest transaction
5
+ * per claim key (spec §9.1) — pass `all` to match the full history.
6
+ * Inverse verbs compile to the same physical query as their primary
7
+ * (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
8
+ * identical SQL against canonical rows, with the pattern's subject binding
9
+ * on the object side.
10
+ *
11
+ * Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
12
+ * CTE over current, positive, non-retracted edges (depth-capped at 32).
13
+ */
14
+ import { Uuidv7, Value } from '@cavelang/core';
15
+ import { Registry } from '@cavelang/canonical';
16
+ import * as Pattern from "./pattern.js";
17
+ const currentSql = `
18
+ SELECT c.* FROM cave_claim c
19
+ JOIN (
20
+ SELECT claim_key, MAX(tx) AS max_tx
21
+ FROM cave_claim GROUP BY claim_key
22
+ ) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
23
+ `;
24
+ /**
25
+ * UUIDv7 interval `[lo, hi)` for a tx filter value: a bare date covers the
26
+ * whole UTC day, a timestamp covers one second. Interval semantics keep
27
+ * adjacent operators distinguishable (`<=` includes the boundary day that
28
+ * `<` excludes) and make `WHERE tx = 2026-01-01` mean "recorded that day".
29
+ */
30
+ const txBounds = (text) => {
31
+ const hasTime = text.includes('T');
32
+ const start = Date.parse(hasTime ? text : `${text}T00:00:00Z`);
33
+ if (Number.isNaN(start)) {
34
+ throw new Error(`CAVE-Q: cannot parse tx date ${JSON.stringify(text)}`);
35
+ }
36
+ const end = start + (hasTime ? 1_000 : 86_400_000);
37
+ return { lo: Uuidv7.at(start, 0, new Uint8Array(8)), hi: Uuidv7.at(end, 0, new Uint8Array(8)) };
38
+ };
39
+ const compile = (pattern, registry, options) => {
40
+ // Inverse resolution (spec §12.1): swap the pattern's endpoint slots and
41
+ // query the primary verb.
42
+ let verb = pattern.verb;
43
+ let subjectSlot = pattern.subject;
44
+ let objectSlot = pattern.payload.kind === 'object' ? pattern.payload.object : undefined;
45
+ if (verb.kind === 'verb') {
46
+ const { primary, isInverse } = Registry.primaryOf(registry, verb.name);
47
+ if (isInverse) {
48
+ if (pattern.payload.kind === 'attribute') {
49
+ throw new Error(`CAVE-Q: inverse verb ${verb.name} cannot take an attribute pattern`);
50
+ }
51
+ const swapped = objectSlot ?? { kind: 'wildcard' };
52
+ objectSlot = subjectSlot;
53
+ subjectSlot = swapped;
54
+ verb = { kind: 'verb', name: primary, transitive: verb.transitive };
55
+ }
56
+ }
57
+ if (verb.kind === 'verb' && verb.transitive) {
58
+ return compileTransitive(pattern, verb.name, subjectSlot, objectSlot, options);
59
+ }
60
+ const conditions = [`c.negated = ${pattern.negated ? 1 : 0}`];
61
+ const params = [];
62
+ /** var name → row columns it binds; repeated vars add join conditions. */
63
+ const varColumns = new Map();
64
+ const slot = (value, column, requireNotNull) => {
65
+ switch (value.kind) {
66
+ case 'term':
67
+ // A date/number term in object position must also match metric
68
+ // rows, which store their value in value_text with object NULL
69
+ // (`latency IS 30ms` ⇒ pattern `latency IS 30ms` matches).
70
+ if (column === 'object') {
71
+ const parsed = Value.parse(value.text);
72
+ if (parsed.kind === 'number' || parsed.kind === 'date') {
73
+ conditions.push('(c.object = ? OR (c.object IS NULL AND c.value_text = ?))');
74
+ params.push(value.text, value.text);
75
+ return;
76
+ }
77
+ }
78
+ conditions.push(`c.${column} = ?`);
79
+ params.push(value.text);
80
+ return;
81
+ case 'var': {
82
+ const columns = varColumns.get(value.name) ?? [];
83
+ columns.push(column);
84
+ varColumns.set(value.name, columns);
85
+ if (requireNotNull) {
86
+ conditions.push(`c.${column} IS NOT NULL`);
87
+ }
88
+ return;
89
+ }
90
+ case 'wildcard':
91
+ if (requireNotNull) {
92
+ conditions.push(`c.${column} IS NOT NULL`);
93
+ }
94
+ return;
95
+ }
96
+ };
97
+ slot(subjectSlot, 'subject', false);
98
+ if (verb.kind === 'verb') {
99
+ conditions.push('c.verb = ?');
100
+ params.push(verb.name);
101
+ }
102
+ else if (verb.kind === 'var') {
103
+ const columns = varColumns.get(verb.name) ?? [];
104
+ columns.push('verb');
105
+ varColumns.set(verb.name, columns);
106
+ }
107
+ if (objectSlot !== undefined) {
108
+ slot(objectSlot, 'object', true);
109
+ }
110
+ if (pattern.payload.kind === 'attribute') {
111
+ conditions.push('c.attribute = ?');
112
+ params.push(pattern.payload.attribute);
113
+ slot(pattern.payload.value, 'value_text', true);
114
+ }
115
+ for (const columns of varColumns.values()) {
116
+ for (let i = 1; i < columns.length; i++) {
117
+ conditions.push(`c.${columns[0]} = c.${columns[i]}`);
118
+ }
119
+ }
120
+ for (const context of pattern.contexts) {
121
+ conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)');
122
+ params.push(context);
123
+ }
124
+ for (const tag of pattern.tags) {
125
+ if (tag.value === undefined) {
126
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value IS NULL)');
127
+ params.push(tag.key);
128
+ }
129
+ else {
130
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)');
131
+ params.push(tag.key, tag.value);
132
+ }
133
+ }
134
+ for (const filter of pattern.filters) {
135
+ switch (filter.field) {
136
+ case 'conf':
137
+ conditions.push(`c.conf ${filter.op} ?`);
138
+ params.push(filter.value);
139
+ break;
140
+ case 'tag':
141
+ if (filter.value === undefined) {
142
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ?)');
143
+ params.push(filter.key);
144
+ }
145
+ else {
146
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)');
147
+ params.push(filter.key, filter.value);
148
+ }
149
+ break;
150
+ case 'context':
151
+ conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)');
152
+ params.push(filter.value);
153
+ break;
154
+ case 'value':
155
+ conditions.push(`c.value_num ${filter.op} ?`);
156
+ params.push(filter.value);
157
+ if (filter.unit !== undefined) {
158
+ conditions.push('c.value_unit = ?');
159
+ params.push(filter.unit);
160
+ }
161
+ break;
162
+ case 'tx': {
163
+ const { lo, hi } = txBounds(filter.value);
164
+ switch (filter.op) {
165
+ case '>':
166
+ conditions.push('c.tx >= ?');
167
+ params.push(hi);
168
+ break;
169
+ case '>=':
170
+ conditions.push('c.tx >= ?');
171
+ params.push(lo);
172
+ break;
173
+ case '<':
174
+ conditions.push('c.tx < ?');
175
+ params.push(lo);
176
+ break;
177
+ case '<=':
178
+ conditions.push('c.tx < ?');
179
+ params.push(hi);
180
+ break;
181
+ case '=':
182
+ conditions.push('(c.tx >= ? AND c.tx < ?)');
183
+ params.push(lo, hi);
184
+ break;
185
+ case '!=':
186
+ conditions.push('(c.tx < ? OR c.tx >= ?)');
187
+ params.push(lo, hi);
188
+ break;
189
+ }
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ // Positive patterns match supported beliefs: a retracted (@ 0%) current
195
+ // belief has no current support (§9.3) and is skipped — mirroring the
196
+ // transitive CTE and store traversal — unless the query asks about
197
+ // confidence explicitly or runs over the full history.
198
+ if (options.all !== true && !pattern.filters.some(filter => filter.field === 'conf')) {
199
+ conditions.push('c.conf > 0');
200
+ }
201
+ const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql;
202
+ const sql = `SELECT c.* FROM (${base}) c WHERE ${conditions.join(' AND ')} ORDER BY c.tx`;
203
+ const bind = (row) => {
204
+ const bindings = {};
205
+ for (const [name, columns] of varColumns) {
206
+ bindings[name] = String(row[columns[0]]);
207
+ }
208
+ return bindings;
209
+ };
210
+ return { sql, params, bind, transitive: false };
211
+ };
212
+ const compileTransitive = (pattern, verb, subjectSlot, objectSlot, options) => {
213
+ if (pattern.negated || pattern.filters.length > 0 || pattern.contexts.length > 0 || pattern.tags.length > 0 ||
214
+ pattern.payload.kind === 'attribute') {
215
+ throw new Error('CAVE-Q: transitive patterns support subject/object slots only (spec §12.1)');
216
+ }
217
+ const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql;
218
+ const conditions = [];
219
+ const params = [verb];
220
+ if (subjectSlot.kind === 'term') {
221
+ conditions.push('h.src = ?');
222
+ params.push(subjectSlot.text);
223
+ }
224
+ if (objectSlot?.kind === 'term') {
225
+ conditions.push('h.dst = ?');
226
+ params.push(objectSlot.text);
227
+ }
228
+ // A repeated variable forces equality here just as in single-hop
229
+ // patterns: `?x EXTENDS+ ?x` asks for nodes on a cycle, not for every
230
+ // reachable pair.
231
+ if (subjectSlot.kind === 'var' && objectSlot?.kind === 'var' && subjectSlot.name === objectSlot.name) {
232
+ conditions.push('h.src = h.dst');
233
+ }
234
+ const sql = `
235
+ WITH RECURSIVE cur AS (
236
+ SELECT c.subject AS src, c.object AS dst
237
+ FROM (${base}) c
238
+ WHERE c.verb = ? AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
239
+ ), hops(src, dst, depth) AS (
240
+ SELECT src, dst, 1 FROM cur
241
+ UNION
242
+ SELECT h.src, cur.dst, h.depth + 1 FROM hops h JOIN cur ON cur.src = h.dst
243
+ WHERE h.depth < 32
244
+ )
245
+ SELECT DISTINCT h.src AS src, h.dst AS dst FROM hops h
246
+ ${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}
247
+ ORDER BY h.src, h.dst`;
248
+ const bind = (row) => {
249
+ const bindings = {};
250
+ if (subjectSlot.kind === 'var') {
251
+ bindings[subjectSlot.name] = String(row['src']);
252
+ }
253
+ if (objectSlot?.kind === 'var') {
254
+ bindings[objectSlot.name] = String(row['dst']);
255
+ }
256
+ return bindings;
257
+ };
258
+ return { sql, params, bind, transitive: true };
259
+ };
260
+ /**
261
+ * Runs a CAVE-Q query against a store.
262
+ *
263
+ * ```ts
264
+ * query(store, '?x USES jwt')
265
+ * query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
266
+ * query(store, 'terrier EXTENDS+ animal')
267
+ * ```
268
+ */
269
+ export const query = (store, input, options = {}) => {
270
+ const pattern = Pattern.parse(input);
271
+ const compiled = compile(pattern, store.registry(), options);
272
+ const rows = store.db.prepare(compiled.sql).all(...compiled.params);
273
+ return rows.map(row => compiled.transitive ?
274
+ { bindings: compiled.bind(row) } :
275
+ { bindings: compiled.bind(row), row: row });
276
+ };
277
+ //# sourceMappingURL=compile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
3
+ *
4
+ * ```ts
5
+ * import { query } from '@cavelang/query'
6
+ *
7
+ * query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
8
+ * query(store, '?x HAS bug: ?bug #security')
9
+ * query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
10
+ * query(store, 'terrier EXTENDS+ animal') // transitive
11
+ * ```
12
+ */
13
+ export * as Pattern from './pattern.ts';
14
+ export { query } from './compile.ts';
15
+ export type { Match, Options } from './compile.ts';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
3
+ *
4
+ * ```ts
5
+ * import { query } from '@cavelang/query'
6
+ *
7
+ * query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
8
+ * query(store, '?x HAS bug: ?bug #security')
9
+ * query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
10
+ * query(store, 'terrier EXTENDS+ animal') // transitive
11
+ * ```
12
+ */
13
+ export * as Pattern from "./pattern.js";
14
+ export { query } from "./compile.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CAVE-Q patterns (spec §12.1).
3
+ *
4
+ * ```cave
5
+ * ?x USES jwt ; all systems using jwt
6
+ * ?x HAS bug: ?bug #security ; all security bugs
7
+ * ?cause CAUSE app/crash ; candidate causes
8
+ * WHERE conf >= 0.7
9
+ * ?x ?verb ?y @production ; all production facts
10
+ * terrier EXTENDS+ animal ; transitive hops
11
+ * _ USES jwt ; wildcard
12
+ * ```
13
+ *
14
+ * A query is one pattern line followed by any number of `WHERE` filter
15
+ * lines (spec §12.2), indented or not.
16
+ */
17
+ /** A pattern slot: named variable, wildcard `_`, or a bound term. */
18
+ export type Slot = {
19
+ readonly kind: 'var';
20
+ readonly name: string;
21
+ } | {
22
+ readonly kind: 'wildcard';
23
+ } | {
24
+ readonly kind: 'term';
25
+ readonly text: string;
26
+ };
27
+ /** Verb position: concrete verb (possibly transitive `VERB+`), variable, or wildcard. */
28
+ export type VerbSlot = {
29
+ readonly kind: 'verb';
30
+ readonly name: string;
31
+ readonly transitive: boolean;
32
+ } | {
33
+ readonly kind: 'var';
34
+ readonly name: string;
35
+ } | {
36
+ readonly kind: 'wildcard';
37
+ };
38
+ export type PayloadPattern = {
39
+ readonly kind: 'object';
40
+ readonly object: Slot;
41
+ } | {
42
+ readonly kind: 'attribute';
43
+ readonly attribute: string;
44
+ readonly value: Slot;
45
+ } | {
46
+ readonly kind: 'any';
47
+ };
48
+ export type FilterOp = '=' | '!=' | '>' | '>=' | '<' | '<=';
49
+ export type Filter = {
50
+ readonly field: 'conf';
51
+ readonly op: FilterOp;
52
+ readonly value: number;
53
+ } | {
54
+ readonly field: 'tag';
55
+ readonly op: '=';
56
+ readonly key: string;
57
+ readonly value?: string;
58
+ } | {
59
+ readonly field: 'context';
60
+ readonly op: '=';
61
+ readonly value: string;
62
+ } | {
63
+ readonly field: 'value';
64
+ readonly op: FilterOp;
65
+ readonly value: number;
66
+ readonly unit?: string;
67
+ } | {
68
+ readonly field: 'tx';
69
+ readonly op: FilterOp;
70
+ readonly value: string;
71
+ };
72
+ export type Pattern = {
73
+ readonly subject: Slot;
74
+ readonly verb: VerbSlot;
75
+ readonly payload: PayloadPattern;
76
+ readonly negated: boolean;
77
+ /** `@ctx` filters on the pattern line. */
78
+ readonly contexts: readonly string[];
79
+ /** `#tag[:value]` filters on the pattern line. */
80
+ readonly tags: readonly {
81
+ key: string;
82
+ value?: string;
83
+ }[];
84
+ readonly filters: readonly Filter[];
85
+ };
86
+ export type t = Pattern;
87
+ /** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
88
+ export declare const parse: (input: string) => Pattern;
89
+ //# sourceMappingURL=pattern.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern.d.ts","sourceRoot":"","sources":["../../src/pattern.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * CAVE-Q patterns (spec §12.1).
3
+ *
4
+ * ```cave
5
+ * ?x USES jwt ; all systems using jwt
6
+ * ?x HAS bug: ?bug #security ; all security bugs
7
+ * ?cause CAUSE app/crash ; candidate causes
8
+ * WHERE conf >= 0.7
9
+ * ?x ?verb ?y @production ; all production facts
10
+ * terrier EXTENDS+ animal ; transitive hops
11
+ * _ USES jwt ; wildcard
12
+ * ```
13
+ *
14
+ * A query is one pattern line followed by any number of `WHERE` filter
15
+ * lines (spec §12.2), indented or not.
16
+ */
17
+ import { Confidence, Verb } from '@cavelang/core';
18
+ import { Token } from '@cavelang/parser';
19
+ const filterOps = ['>=', '<=', '!=', '=', '>', '<'];
20
+ const isFilterOp = (text) => filterOps.includes(text);
21
+ const slotOf = (token) => {
22
+ if (token.kind === 'word') {
23
+ if (token.text === '_') {
24
+ return { kind: 'wildcard' };
25
+ }
26
+ if (token.text.startsWith('?')) {
27
+ return { kind: 'var', name: token.text.slice(1) };
28
+ }
29
+ return { kind: 'term', text: token.text };
30
+ }
31
+ const delimiter = token.kind === 'code' ? '`' : '"';
32
+ return { kind: 'term', text: `${delimiter}${token.text}${delimiter}` };
33
+ };
34
+ const verbSlotOf = (token) => {
35
+ if (token.kind !== 'word') {
36
+ return undefined;
37
+ }
38
+ if (token.text === '_') {
39
+ return { kind: 'wildcard' };
40
+ }
41
+ if (token.text.startsWith('?')) {
42
+ return { kind: 'var', name: token.text.slice(1) };
43
+ }
44
+ const transitive = token.text.endsWith('+');
45
+ const name = transitive ? token.text.slice(0, -1) : token.text;
46
+ return Verb.isVerbToken(name) ? { kind: 'verb', name, transitive } : undefined;
47
+ };
48
+ const parseFilter = (tokens, lineNo) => {
49
+ const [field, op, ...valueTokens] = tokens;
50
+ const bad = (message) => {
51
+ throw new Error(`CAVE-Q line ${lineNo}: ${message}`);
52
+ };
53
+ if (field?.kind !== 'word' || op?.kind !== 'word' || valueTokens.length === 0) {
54
+ return bad('expected "WHERE <field> <op> <value>" (spec §12.2)');
55
+ }
56
+ if (!isFilterOp(op.text)) {
57
+ return bad(`unknown operator ${JSON.stringify(op.text)}`);
58
+ }
59
+ const valueText = valueTokens.map(token => token.text).join(' ');
60
+ switch (field.text) {
61
+ case 'conf': {
62
+ const conf = valueText.endsWith('%') ? Confidence.parse(valueText) : Number(valueText);
63
+ if (conf === undefined || Number.isNaN(conf)) {
64
+ return bad(`cannot parse confidence ${JSON.stringify(valueText)}`);
65
+ }
66
+ return { field: 'conf', op: op.text, value: conf };
67
+ }
68
+ case 'tag': {
69
+ if (op.text !== '=') {
70
+ return bad('tag filters support = only');
71
+ }
72
+ const colonAt = valueText.indexOf(':');
73
+ return colonAt === -1 ?
74
+ { field: 'tag', op: '=', key: valueText } :
75
+ { field: 'tag', op: '=', key: valueText.slice(0, colonAt), value: valueText.slice(colonAt + 1) };
76
+ }
77
+ case 'context': {
78
+ if (op.text !== '=') {
79
+ return bad('context filters support = only');
80
+ }
81
+ return { field: 'context', op: '=', value: valueText };
82
+ }
83
+ case 'value': {
84
+ const match = /^(-?\d+(?:\.\d+)?)(?:\s+(.+))?$/.exec(valueText);
85
+ if (!match) {
86
+ return bad(`cannot parse value filter ${JSON.stringify(valueText)}`);
87
+ }
88
+ const unit = match[2];
89
+ return { field: 'value', op: op.text, value: Number(match[1]), ...unit === undefined ? {} : { unit } };
90
+ }
91
+ case 'tx':
92
+ return { field: 'tx', op: op.text, value: valueText };
93
+ default:
94
+ return bad(`unknown filter field ${JSON.stringify(field.text)}`);
95
+ }
96
+ };
97
+ /** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
98
+ export const parse = (input) => {
99
+ const lines = input
100
+ .split(/\r?\n/)
101
+ .map(line => line.trim())
102
+ .filter(line => line !== '' && !line.startsWith(';'));
103
+ if (lines.length === 0) {
104
+ throw new Error('CAVE-Q: empty query');
105
+ }
106
+ const [patternLine, ...filterLines] = lines;
107
+ const { head } = Token.splitComment(patternLine);
108
+ const tokens = Token.tokenize(head);
109
+ if (tokens.length < 2) {
110
+ throw new Error('CAVE-Q: a pattern needs at least a subject and a verb (spec §12.1)');
111
+ }
112
+ const subject = slotOf(tokens[0]);
113
+ const verb = verbSlotOf(tokens[1]);
114
+ if (verb === undefined) {
115
+ throw new Error(`CAVE-Q: cannot parse verb position ${JSON.stringify(tokens[1].text)}`);
116
+ }
117
+ let rest = tokens.slice(2);
118
+ const negated = rest[0]?.kind === 'word' && rest[0].text === 'NOT';
119
+ if (negated) {
120
+ rest = rest.slice(1);
121
+ }
122
+ const contexts = [];
123
+ const tags = [];
124
+ const payloadTokens = [];
125
+ for (const token of rest) {
126
+ if (token.kind === 'word' && token.text.startsWith('@') && token.text.length > 1) {
127
+ contexts.push(token.text.slice(1));
128
+ }
129
+ else if (token.kind === 'word' && token.text.startsWith('#') && token.text.length > 1) {
130
+ const body = token.text.slice(1);
131
+ const colonAt = body.indexOf(':');
132
+ tags.push(colonAt === -1 ? { key: body } : { key: body.slice(0, colonAt), value: body.slice(colonAt + 1) });
133
+ }
134
+ else {
135
+ payloadTokens.push(token);
136
+ }
137
+ }
138
+ let payload;
139
+ if (payloadTokens.length === 0) {
140
+ payload = { kind: 'any' };
141
+ }
142
+ else if (payloadTokens[0].kind === 'word' && payloadTokens[0].text.endsWith(':') && payloadTokens[0].text.length > 1) {
143
+ if (payloadTokens.length !== 2) {
144
+ throw new Error('CAVE-Q: attribute patterns take exactly one value slot (spec §12.1)');
145
+ }
146
+ payload = { kind: 'attribute', attribute: payloadTokens[0].text.slice(0, -1), value: slotOf(payloadTokens[1]) };
147
+ }
148
+ else if (payloadTokens.length === 1) {
149
+ payload = { kind: 'object', object: slotOf(payloadTokens[0]) };
150
+ }
151
+ else {
152
+ throw new Error(`CAVE-Q: cannot parse payload ${JSON.stringify(payloadTokens.map(token => token.text).join(' '))}`);
153
+ }
154
+ const filters = filterLines.map((line, at) => {
155
+ const filterTokens = Token.tokenize(Token.splitComment(line).head);
156
+ const [keyword, ...rest_] = filterTokens;
157
+ if (keyword?.kind !== 'word' || keyword.text !== 'WHERE') {
158
+ throw new Error(`CAVE-Q line ${at + 2}: expected WHERE, got ${JSON.stringify(keyword?.text ?? '')}`);
159
+ }
160
+ return parseFilter(rest_, at + 2);
161
+ });
162
+ return { subject, verb, payload, negated, contexts, tags, filters };
163
+ };
164
+ //# sourceMappingURL=pattern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern.js","sourceRoot":"","sources":["../../src/pattern.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cavelang/query",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CAVE-Q — graph-pattern queries (?x vars, wildcards, inverse verbs, transitive hops, WHERE filters) compiled to SQL.",
6
+ "license": "CC0-1.0",
7
+ "author": "Mirek Rusin (https://github.com/mirek)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mirek/cave.git",
11
+ "directory": "packages/query"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.18"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/src/index.d.ts",
19
+ "default": "./dist/src/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/test",
25
+ "!dist/tsconfig.tsbuildinfo",
26
+ "src",
27
+ "!src/**/*.test.ts",
28
+ "README.md",
29
+ "License.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@cavelang/canonical": "0.1.0",
36
+ "@cavelang/core": "0.1.0",
37
+ "@cavelang/parser": "0.1.0",
38
+ "@cavelang/store": "0.1.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -b",
42
+ "test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
43
+ "typecheck": "tsc -b"
44
+ }
45
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * CAVE-Q → SQL compilation (spec §12).
3
+ *
4
+ * Patterns run over *current beliefs* by default — the latest transaction
5
+ * per claim key (spec §9.1) — pass `all` to match the full history.
6
+ * Inverse verbs compile to the same physical query as their primary
7
+ * (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
8
+ * identical SQL against canonical rows, with the pattern's subject binding
9
+ * on the object side.
10
+ *
11
+ * Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
12
+ * CTE over current, positive, non-retracted edges (depth-capped at 32).
13
+ */
14
+
15
+ import { Uuidv7, Value } from '@cavelang/core'
16
+ import { Registry } from '@cavelang/canonical'
17
+ import type { Row, Store } from '@cavelang/store'
18
+ import * as Pattern from './pattern.ts'
19
+
20
+ /** One query solution: variable bindings plus the matched row (absent for transitive hops). */
21
+ export type Match = {
22
+ readonly bindings: Readonly<Record<string, string>>
23
+ readonly row?: Row.t
24
+ }
25
+
26
+ export type Options = {
27
+ /** Match all appended rows, not only current beliefs. */
28
+ readonly all?: boolean
29
+ }
30
+
31
+ const currentSql = `
32
+ SELECT c.* FROM cave_claim c
33
+ JOIN (
34
+ SELECT claim_key, MAX(tx) AS max_tx
35
+ FROM cave_claim GROUP BY claim_key
36
+ ) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
37
+ `
38
+
39
+ /**
40
+ * UUIDv7 interval `[lo, hi)` for a tx filter value: a bare date covers the
41
+ * whole UTC day, a timestamp covers one second. Interval semantics keep
42
+ * adjacent operators distinguishable (`<=` includes the boundary day that
43
+ * `<` excludes) and make `WHERE tx = 2026-01-01` mean "recorded that day".
44
+ */
45
+ const txBounds = (text: string): { lo: string, hi: string } => {
46
+ const hasTime = text.includes('T')
47
+ const start = Date.parse(hasTime ? text : `${text}T00:00:00Z`)
48
+ if (Number.isNaN(start)) {
49
+ throw new Error(`CAVE-Q: cannot parse tx date ${JSON.stringify(text)}`)
50
+ }
51
+ const end = start + (hasTime ? 1_000 : 86_400_000)
52
+ return { lo: Uuidv7.at(start, 0, new Uint8Array(8)), hi: Uuidv7.at(end, 0, new Uint8Array(8)) }
53
+ }
54
+
55
+ type Compiled = {
56
+ readonly sql: string
57
+ readonly params: (string | number)[]
58
+ readonly bind: (row: Record<string, unknown>) => Record<string, string>
59
+ readonly transitive: boolean
60
+ }
61
+
62
+ const compile = (pattern: Pattern.t, registry: Registry.t, options: Options): Compiled => {
63
+ // Inverse resolution (spec §12.1): swap the pattern's endpoint slots and
64
+ // query the primary verb.
65
+ let verb = pattern.verb
66
+ let subjectSlot = pattern.subject
67
+ let objectSlot: undefined | Pattern.Slot =
68
+ pattern.payload.kind === 'object' ? pattern.payload.object : undefined
69
+ if (verb.kind === 'verb') {
70
+ const { primary, isInverse } = Registry.primaryOf(registry, verb.name)
71
+ if (isInverse) {
72
+ if (pattern.payload.kind === 'attribute') {
73
+ throw new Error(`CAVE-Q: inverse verb ${verb.name} cannot take an attribute pattern`)
74
+ }
75
+ const swapped = objectSlot ?? { kind: 'wildcard' as const }
76
+ objectSlot = subjectSlot
77
+ subjectSlot = swapped
78
+ verb = { kind: 'verb', name: primary, transitive: verb.transitive }
79
+ }
80
+ }
81
+
82
+ if (verb.kind === 'verb' && verb.transitive) {
83
+ return compileTransitive(pattern, verb.name, subjectSlot, objectSlot, options)
84
+ }
85
+
86
+ const conditions: string[] = [`c.negated = ${pattern.negated ? 1 : 0}`]
87
+ const params: (string | number)[] = []
88
+ /** var name → row columns it binds; repeated vars add join conditions. */
89
+ const varColumns = new Map<string, string[]>()
90
+ const slot = (value: Pattern.Slot, column: string, requireNotNull: boolean): void => {
91
+ switch (value.kind) {
92
+ case 'term':
93
+ // A date/number term in object position must also match metric
94
+ // rows, which store their value in value_text with object NULL
95
+ // (`latency IS 30ms` ⇒ pattern `latency IS 30ms` matches).
96
+ if (column === 'object') {
97
+ const parsed = Value.parse(value.text)
98
+ if (parsed.kind === 'number' || parsed.kind === 'date') {
99
+ conditions.push('(c.object = ? OR (c.object IS NULL AND c.value_text = ?))')
100
+ params.push(value.text, value.text)
101
+ return
102
+ }
103
+ }
104
+ conditions.push(`c.${column} = ?`)
105
+ params.push(value.text)
106
+ return
107
+ case 'var': {
108
+ const columns = varColumns.get(value.name) ?? []
109
+ columns.push(column)
110
+ varColumns.set(value.name, columns)
111
+ if (requireNotNull) {
112
+ conditions.push(`c.${column} IS NOT NULL`)
113
+ }
114
+ return
115
+ }
116
+ case 'wildcard':
117
+ if (requireNotNull) {
118
+ conditions.push(`c.${column} IS NOT NULL`)
119
+ }
120
+ return
121
+ }
122
+ }
123
+
124
+ slot(subjectSlot, 'subject', false)
125
+ if (verb.kind === 'verb') {
126
+ conditions.push('c.verb = ?')
127
+ params.push(verb.name)
128
+ } else if (verb.kind === 'var') {
129
+ const columns = varColumns.get(verb.name) ?? []
130
+ columns.push('verb')
131
+ varColumns.set(verb.name, columns)
132
+ }
133
+ if (objectSlot !== undefined) {
134
+ slot(objectSlot, 'object', true)
135
+ }
136
+ if (pattern.payload.kind === 'attribute') {
137
+ conditions.push('c.attribute = ?')
138
+ params.push(pattern.payload.attribute)
139
+ slot(pattern.payload.value, 'value_text', true)
140
+ }
141
+ for (const columns of varColumns.values()) {
142
+ for (let i = 1; i < columns.length; i++) {
143
+ conditions.push(`c.${columns[0]} = c.${columns[i]}`)
144
+ }
145
+ }
146
+ for (const context of pattern.contexts) {
147
+ conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)')
148
+ params.push(context)
149
+ }
150
+ for (const tag of pattern.tags) {
151
+ if (tag.value === undefined) {
152
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value IS NULL)')
153
+ params.push(tag.key)
154
+ } else {
155
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)')
156
+ params.push(tag.key, tag.value)
157
+ }
158
+ }
159
+ for (const filter of pattern.filters) {
160
+ switch (filter.field) {
161
+ case 'conf':
162
+ conditions.push(`c.conf ${filter.op} ?`)
163
+ params.push(filter.value)
164
+ break
165
+ case 'tag':
166
+ if (filter.value === undefined) {
167
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ?)')
168
+ params.push(filter.key)
169
+ } else {
170
+ conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)')
171
+ params.push(filter.key, filter.value)
172
+ }
173
+ break
174
+ case 'context':
175
+ conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)')
176
+ params.push(filter.value)
177
+ break
178
+ case 'value':
179
+ conditions.push(`c.value_num ${filter.op} ?`)
180
+ params.push(filter.value)
181
+ if (filter.unit !== undefined) {
182
+ conditions.push('c.value_unit = ?')
183
+ params.push(filter.unit)
184
+ }
185
+ break
186
+ case 'tx': {
187
+ const { lo, hi } = txBounds(filter.value)
188
+ switch (filter.op) {
189
+ case '>':
190
+ conditions.push('c.tx >= ?')
191
+ params.push(hi)
192
+ break
193
+ case '>=':
194
+ conditions.push('c.tx >= ?')
195
+ params.push(lo)
196
+ break
197
+ case '<':
198
+ conditions.push('c.tx < ?')
199
+ params.push(lo)
200
+ break
201
+ case '<=':
202
+ conditions.push('c.tx < ?')
203
+ params.push(hi)
204
+ break
205
+ case '=':
206
+ conditions.push('(c.tx >= ? AND c.tx < ?)')
207
+ params.push(lo, hi)
208
+ break
209
+ case '!=':
210
+ conditions.push('(c.tx < ? OR c.tx >= ?)')
211
+ params.push(lo, hi)
212
+ break
213
+ }
214
+ break
215
+ }
216
+ }
217
+ }
218
+
219
+ // Positive patterns match supported beliefs: a retracted (@ 0%) current
220
+ // belief has no current support (§9.3) and is skipped — mirroring the
221
+ // transitive CTE and store traversal — unless the query asks about
222
+ // confidence explicitly or runs over the full history.
223
+ if (options.all !== true && !pattern.filters.some(filter => filter.field === 'conf')) {
224
+ conditions.push('c.conf > 0')
225
+ }
226
+
227
+ const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql
228
+ const sql = `SELECT c.* FROM (${base}) c WHERE ${conditions.join(' AND ')} ORDER BY c.tx`
229
+ const bind = (row: Record<string, unknown>): Record<string, string> => {
230
+ const bindings: Record<string, string> = {}
231
+ for (const [name, columns] of varColumns) {
232
+ bindings[name] = String(row[columns[0]!])
233
+ }
234
+ return bindings
235
+ }
236
+ return { sql, params, bind, transitive: false }
237
+ }
238
+
239
+ const compileTransitive = (
240
+ pattern: Pattern.t,
241
+ verb: string,
242
+ subjectSlot: Pattern.Slot,
243
+ objectSlot: undefined | Pattern.Slot,
244
+ options: Options
245
+ ): Compiled => {
246
+ if (pattern.negated || pattern.filters.length > 0 || pattern.contexts.length > 0 || pattern.tags.length > 0 ||
247
+ pattern.payload.kind === 'attribute') {
248
+ throw new Error('CAVE-Q: transitive patterns support subject/object slots only (spec §12.1)')
249
+ }
250
+ const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql
251
+ const conditions: string[] = []
252
+ const params: (string | number)[] = [verb]
253
+ if (subjectSlot.kind === 'term') {
254
+ conditions.push('h.src = ?')
255
+ params.push(subjectSlot.text)
256
+ }
257
+ if (objectSlot?.kind === 'term') {
258
+ conditions.push('h.dst = ?')
259
+ params.push(objectSlot.text)
260
+ }
261
+ // A repeated variable forces equality here just as in single-hop
262
+ // patterns: `?x EXTENDS+ ?x` asks for nodes on a cycle, not for every
263
+ // reachable pair.
264
+ if (subjectSlot.kind === 'var' && objectSlot?.kind === 'var' && subjectSlot.name === objectSlot.name) {
265
+ conditions.push('h.src = h.dst')
266
+ }
267
+ const sql = `
268
+ WITH RECURSIVE cur AS (
269
+ SELECT c.subject AS src, c.object AS dst
270
+ FROM (${base}) c
271
+ WHERE c.verb = ? AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
272
+ ), hops(src, dst, depth) AS (
273
+ SELECT src, dst, 1 FROM cur
274
+ UNION
275
+ SELECT h.src, cur.dst, h.depth + 1 FROM hops h JOIN cur ON cur.src = h.dst
276
+ WHERE h.depth < 32
277
+ )
278
+ SELECT DISTINCT h.src AS src, h.dst AS dst FROM hops h
279
+ ${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}
280
+ ORDER BY h.src, h.dst`
281
+ const bind = (row: Record<string, unknown>): Record<string, string> => {
282
+ const bindings: Record<string, string> = {}
283
+ if (subjectSlot.kind === 'var') {
284
+ bindings[subjectSlot.name] = String(row['src'])
285
+ }
286
+ if (objectSlot?.kind === 'var') {
287
+ bindings[objectSlot.name] = String(row['dst'])
288
+ }
289
+ return bindings
290
+ }
291
+ return { sql, params, bind, transitive: true }
292
+ }
293
+
294
+ /**
295
+ * Runs a CAVE-Q query against a store.
296
+ *
297
+ * ```ts
298
+ * query(store, '?x USES jwt')
299
+ * query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
300
+ * query(store, 'terrier EXTENDS+ animal')
301
+ * ```
302
+ */
303
+ export const query = (store: Store, input: string, options: Options = {}): Match[] => {
304
+ const pattern = Pattern.parse(input)
305
+ const compiled = compile(pattern, store.registry(), options)
306
+ const rows = store.db.prepare(compiled.sql).all(...compiled.params) as Record<string, unknown>[]
307
+ return rows.map(row =>
308
+ compiled.transitive ?
309
+ { bindings: compiled.bind(row) } :
310
+ { bindings: compiled.bind(row), row: row as unknown as Row.t }
311
+ )
312
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
3
+ *
4
+ * ```ts
5
+ * import { query } from '@cavelang/query'
6
+ *
7
+ * query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
8
+ * query(store, '?x HAS bug: ?bug #security')
9
+ * query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
10
+ * query(store, 'terrier EXTENDS+ animal') // transitive
11
+ * ```
12
+ */
13
+
14
+ export * as Pattern from './pattern.ts'
15
+ export { query } from './compile.ts'
16
+ export type { Match, Options } from './compile.ts'
package/src/pattern.ts ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * CAVE-Q patterns (spec §12.1).
3
+ *
4
+ * ```cave
5
+ * ?x USES jwt ; all systems using jwt
6
+ * ?x HAS bug: ?bug #security ; all security bugs
7
+ * ?cause CAUSE app/crash ; candidate causes
8
+ * WHERE conf >= 0.7
9
+ * ?x ?verb ?y @production ; all production facts
10
+ * terrier EXTENDS+ animal ; transitive hops
11
+ * _ USES jwt ; wildcard
12
+ * ```
13
+ *
14
+ * A query is one pattern line followed by any number of `WHERE` filter
15
+ * lines (spec §12.2), indented or not.
16
+ */
17
+
18
+ import { Confidence, Verb } from '@cavelang/core'
19
+ import { Token } from '@cavelang/parser'
20
+
21
+ /** A pattern slot: named variable, wildcard `_`, or a bound term. */
22
+ export type Slot =
23
+ | { readonly kind: 'var', readonly name: string }
24
+ | { readonly kind: 'wildcard' }
25
+ | { readonly kind: 'term', readonly text: string }
26
+
27
+ /** Verb position: concrete verb (possibly transitive `VERB+`), variable, or wildcard. */
28
+ export type VerbSlot =
29
+ | { readonly kind: 'verb', readonly name: string, readonly transitive: boolean }
30
+ | { readonly kind: 'var', readonly name: string }
31
+ | { readonly kind: 'wildcard' }
32
+
33
+ export type PayloadPattern =
34
+ | { readonly kind: 'object', readonly object: Slot }
35
+ | { readonly kind: 'attribute', readonly attribute: string, readonly value: Slot }
36
+ | { readonly kind: 'any' }
37
+
38
+ export type FilterOp = '=' | '!=' | '>' | '>=' | '<' | '<='
39
+
40
+ export type Filter =
41
+ | { readonly field: 'conf', readonly op: FilterOp, readonly value: number }
42
+ | { readonly field: 'tag', readonly op: '=', readonly key: string, readonly value?: string }
43
+ | { readonly field: 'context', readonly op: '=', readonly value: string }
44
+ | { readonly field: 'value', readonly op: FilterOp, readonly value: number, readonly unit?: string }
45
+ | { readonly field: 'tx', readonly op: FilterOp, readonly value: string }
46
+
47
+ export type Pattern = {
48
+ readonly subject: Slot
49
+ readonly verb: VerbSlot
50
+ readonly payload: PayloadPattern
51
+ readonly negated: boolean
52
+ /** `@ctx` filters on the pattern line. */
53
+ readonly contexts: readonly string[]
54
+ /** `#tag[:value]` filters on the pattern line. */
55
+ readonly tags: readonly { key: string, value?: string }[]
56
+ readonly filters: readonly Filter[]
57
+ }
58
+
59
+ export type t = Pattern
60
+
61
+ const filterOps: readonly FilterOp[] = ['>=', '<=', '!=', '=', '>', '<']
62
+
63
+ const isFilterOp = (text: string): text is FilterOp =>
64
+ filterOps.includes(text as FilterOp)
65
+
66
+ const slotOf = (token: Token.t): Slot => {
67
+ if (token.kind === 'word') {
68
+ if (token.text === '_') {
69
+ return { kind: 'wildcard' }
70
+ }
71
+ if (token.text.startsWith('?')) {
72
+ return { kind: 'var', name: token.text.slice(1) }
73
+ }
74
+ return { kind: 'term', text: token.text }
75
+ }
76
+ const delimiter = token.kind === 'code' ? '`' : '"'
77
+ return { kind: 'term', text: `${delimiter}${token.text}${delimiter}` }
78
+ }
79
+
80
+ const verbSlotOf = (token: Token.t): undefined | VerbSlot => {
81
+ if (token.kind !== 'word') {
82
+ return undefined
83
+ }
84
+ if (token.text === '_') {
85
+ return { kind: 'wildcard' }
86
+ }
87
+ if (token.text.startsWith('?')) {
88
+ return { kind: 'var', name: token.text.slice(1) }
89
+ }
90
+ const transitive = token.text.endsWith('+')
91
+ const name = transitive ? token.text.slice(0, -1) : token.text
92
+ return Verb.isVerbToken(name) ? { kind: 'verb', name, transitive } : undefined
93
+ }
94
+
95
+ const parseFilter = (tokens: readonly Token.t[], lineNo: number): Filter => {
96
+ const [field, op, ...valueTokens] = tokens
97
+ const bad = (message: string): never => {
98
+ throw new Error(`CAVE-Q line ${lineNo}: ${message}`)
99
+ }
100
+ if (field?.kind !== 'word' || op?.kind !== 'word' || valueTokens.length === 0) {
101
+ return bad('expected "WHERE <field> <op> <value>" (spec §12.2)')
102
+ }
103
+ if (!isFilterOp(op.text)) {
104
+ return bad(`unknown operator ${JSON.stringify(op.text)}`)
105
+ }
106
+ const valueText = valueTokens.map(token => token.text).join(' ')
107
+ switch (field.text) {
108
+ case 'conf': {
109
+ const conf = valueText.endsWith('%') ? Confidence.parse(valueText) : Number(valueText)
110
+ if (conf === undefined || Number.isNaN(conf)) {
111
+ return bad(`cannot parse confidence ${JSON.stringify(valueText)}`)
112
+ }
113
+ return { field: 'conf', op: op.text, value: conf }
114
+ }
115
+ case 'tag': {
116
+ if (op.text !== '=') {
117
+ return bad('tag filters support = only')
118
+ }
119
+ const colonAt = valueText.indexOf(':')
120
+ return colonAt === -1 ?
121
+ { field: 'tag', op: '=', key: valueText } :
122
+ { field: 'tag', op: '=', key: valueText.slice(0, colonAt), value: valueText.slice(colonAt + 1) }
123
+ }
124
+ case 'context': {
125
+ if (op.text !== '=') {
126
+ return bad('context filters support = only')
127
+ }
128
+ return { field: 'context', op: '=', value: valueText }
129
+ }
130
+ case 'value': {
131
+ const match = /^(-?\d+(?:\.\d+)?)(?:\s+(.+))?$/.exec(valueText)
132
+ if (!match) {
133
+ return bad(`cannot parse value filter ${JSON.stringify(valueText)}`)
134
+ }
135
+ const unit = match[2]
136
+ return { field: 'value', op: op.text, value: Number(match[1]), ...unit === undefined ? {} : { unit } }
137
+ }
138
+ case 'tx':
139
+ return { field: 'tx', op: op.text, value: valueText }
140
+ default:
141
+ return bad(`unknown filter field ${JSON.stringify(field.text)}`)
142
+ }
143
+ }
144
+
145
+ /** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
146
+ export const parse = (input: string): Pattern => {
147
+ const lines = input
148
+ .split(/\r?\n/)
149
+ .map(line => line.trim())
150
+ .filter(line => line !== '' && !line.startsWith(';'))
151
+ if (lines.length === 0) {
152
+ throw new Error('CAVE-Q: empty query')
153
+ }
154
+ const [patternLine, ...filterLines] = lines
155
+ const { head } = Token.splitComment(patternLine!)
156
+ const tokens = Token.tokenize(head)
157
+ if (tokens.length < 2) {
158
+ throw new Error('CAVE-Q: a pattern needs at least a subject and a verb (spec §12.1)')
159
+ }
160
+ const subject = slotOf(tokens[0]!)
161
+ const verb = verbSlotOf(tokens[1]!)
162
+ if (verb === undefined) {
163
+ throw new Error(`CAVE-Q: cannot parse verb position ${JSON.stringify(tokens[1]!.text)}`)
164
+ }
165
+ let rest = tokens.slice(2)
166
+ const negated = rest[0]?.kind === 'word' && rest[0].text === 'NOT'
167
+ if (negated) {
168
+ rest = rest.slice(1)
169
+ }
170
+ const contexts: string[] = []
171
+ const tags: { key: string, value?: string }[] = []
172
+ const payloadTokens: Token.t[] = []
173
+ for (const token of rest) {
174
+ if (token.kind === 'word' && token.text.startsWith('@') && token.text.length > 1) {
175
+ contexts.push(token.text.slice(1))
176
+ } else if (token.kind === 'word' && token.text.startsWith('#') && token.text.length > 1) {
177
+ const body = token.text.slice(1)
178
+ const colonAt = body.indexOf(':')
179
+ tags.push(colonAt === -1 ? { key: body } : { key: body.slice(0, colonAt), value: body.slice(colonAt + 1) })
180
+ } else {
181
+ payloadTokens.push(token)
182
+ }
183
+ }
184
+ let payload: PayloadPattern
185
+ if (payloadTokens.length === 0) {
186
+ payload = { kind: 'any' }
187
+ } else if (payloadTokens[0]!.kind === 'word' && payloadTokens[0]!.text.endsWith(':') && payloadTokens[0]!.text.length > 1) {
188
+ if (payloadTokens.length !== 2) {
189
+ throw new Error('CAVE-Q: attribute patterns take exactly one value slot (spec §12.1)')
190
+ }
191
+ payload = { kind: 'attribute', attribute: payloadTokens[0]!.text.slice(0, -1), value: slotOf(payloadTokens[1]!) }
192
+ } else if (payloadTokens.length === 1) {
193
+ payload = { kind: 'object', object: slotOf(payloadTokens[0]!) }
194
+ } else {
195
+ throw new Error(`CAVE-Q: cannot parse payload ${JSON.stringify(payloadTokens.map(token => token.text).join(' '))}`)
196
+ }
197
+ const filters = filterLines.map((line, at) => {
198
+ const filterTokens = Token.tokenize(Token.splitComment(line).head)
199
+ const [keyword, ...rest_] = filterTokens
200
+ if (keyword?.kind !== 'word' || keyword.text !== 'WHERE') {
201
+ throw new Error(`CAVE-Q line ${at + 2}: expected WHERE, got ${JSON.stringify(keyword?.text ?? '')}`)
202
+ }
203
+ return parseFilter(rest_, at + 2)
204
+ })
205
+ return { subject, verb, payload, negated, contexts, tags, filters }
206
+ }