trellis 2.0.8 → 2.0.13

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.
Files changed (42) hide show
  1. package/README.md +279 -116
  2. package/dist/cli/index.js +655 -4
  3. package/dist/core/index.js +471 -2
  4. package/dist/embeddings/index.js +5 -1
  5. package/dist/{index-s603ev6w.js → index-5b01h414.js} +1 -1
  6. package/dist/index-5m0g9r0y.js +1100 -0
  7. package/dist/{index-zf6htvnm.js → index-7gvjxt27.js} +166 -2
  8. package/dist/index-hybgxe40.js +1174 -0
  9. package/dist/index.js +7 -2
  10. package/dist/transformers.node-bx3q9d7k.js +33130 -0
  11. package/package.json +9 -4
  12. package/src/cli/index.ts +939 -0
  13. package/src/core/agents/harness.ts +380 -0
  14. package/src/core/agents/index.ts +18 -0
  15. package/src/core/agents/types.ts +90 -0
  16. package/src/core/index.ts +85 -2
  17. package/src/core/kernel/trellis-kernel.ts +593 -0
  18. package/src/core/ontology/builtins.ts +248 -0
  19. package/src/core/ontology/index.ts +34 -0
  20. package/src/core/ontology/registry.ts +209 -0
  21. package/src/core/ontology/types.ts +124 -0
  22. package/src/core/ontology/validator.ts +382 -0
  23. package/src/core/persist/backend.ts +10 -0
  24. package/src/core/persist/sqlite-backend.ts +298 -0
  25. package/src/core/plugins/index.ts +17 -0
  26. package/src/core/plugins/registry.ts +322 -0
  27. package/src/core/plugins/types.ts +126 -0
  28. package/src/core/query/datalog.ts +188 -0
  29. package/src/core/query/engine.ts +370 -0
  30. package/src/core/query/index.ts +34 -0
  31. package/src/core/query/parser.ts +481 -0
  32. package/src/core/query/types.ts +200 -0
  33. package/src/embeddings/auto-embed.ts +248 -0
  34. package/src/embeddings/index.ts +7 -0
  35. package/src/embeddings/model.ts +21 -4
  36. package/src/embeddings/types.ts +8 -1
  37. package/src/index.ts +9 -0
  38. package/src/sync/http-transport.ts +144 -0
  39. package/src/sync/index.ts +11 -0
  40. package/src/sync/multi-repo.ts +200 -0
  41. package/src/sync/ws-transport.ts +145 -0
  42. package/dist/index-5bhe57y9.js +0 -326
@@ -0,0 +1,481 @@
1
+ /**
2
+ * EQL-S Query Parser — Parses a simple DSL into Query AST.
3
+ *
4
+ * Syntax overview:
5
+ * SELECT ?e ?name
6
+ * WHERE {
7
+ * [?e "type" "Project"]
8
+ * [?e "name" ?name]
9
+ * (?e "memberOf" ?org)
10
+ * }
11
+ * FILTER ?name != "archived"
12
+ * ORDER BY ?name ASC
13
+ * LIMIT 10
14
+ * OFFSET 5
15
+ *
16
+ * Fact patterns: [entity attr value]
17
+ * Link patterns: (source attr target)
18
+ * Not patterns: NOT [entity attr value]
19
+ * Or patterns: OR { branch1 } { branch2 }
20
+ * Rule calls: ruleName(?x, ?y)
21
+ *
22
+ * Variables start with `?`. Strings are double-quoted.
23
+ * Numbers are bare. Booleans: true/false.
24
+ *
25
+ * @module trellis/core/query
26
+ */
27
+
28
+ import type {
29
+ Query, Pattern, FactPattern, LinkPattern, NotPattern, OrPattern,
30
+ RuleApplication, Filter, FilterOp, Aggregate, AggregateOp, OrderBy,
31
+ Term, DatalogRule,
32
+ } from './types.js';
33
+ import { variable, literal } from './types.js';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Tokenizer
37
+ // ---------------------------------------------------------------------------
38
+
39
+ type TokenKind = 'word' | 'string' | 'number' | 'symbol' | 'eof';
40
+
41
+ interface Token {
42
+ kind: TokenKind;
43
+ value: string;
44
+ pos: number;
45
+ }
46
+
47
+ function tokenize(input: string): Token[] {
48
+ const tokens: Token[] = [];
49
+ let i = 0;
50
+ while (i < input.length) {
51
+ // Skip whitespace
52
+ if (/\s/.test(input[i])) { i++; continue; }
53
+
54
+ // Skip comments (// to end of line)
55
+ if (input[i] === '/' && input[i + 1] === '/') {
56
+ while (i < input.length && input[i] !== '\n') i++;
57
+ continue;
58
+ }
59
+
60
+ const pos = i;
61
+
62
+ // Symbols
63
+ if ('[](){},:'.includes(input[i])) {
64
+ tokens.push({ kind: 'symbol', value: input[i], pos });
65
+ i++;
66
+ continue;
67
+ }
68
+
69
+ // Multi-char operators
70
+ if (input[i] === '!' && input[i + 1] === '=') {
71
+ tokens.push({ kind: 'symbol', value: '!=', pos });
72
+ i += 2;
73
+ continue;
74
+ }
75
+ if (input[i] === '<' && input[i + 1] === '=') {
76
+ tokens.push({ kind: 'symbol', value: '<=', pos });
77
+ i += 2;
78
+ continue;
79
+ }
80
+ if (input[i] === '>' && input[i + 1] === '=') {
81
+ tokens.push({ kind: 'symbol', value: '>=', pos });
82
+ i += 2;
83
+ continue;
84
+ }
85
+ if ('<>='.includes(input[i])) {
86
+ tokens.push({ kind: 'symbol', value: input[i], pos });
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ // Strings
92
+ if (input[i] === '"') {
93
+ i++;
94
+ let s = '';
95
+ while (i < input.length && input[i] !== '"') {
96
+ if (input[i] === '\\' && i + 1 < input.length) {
97
+ s += input[i + 1]; i += 2;
98
+ } else {
99
+ s += input[i]; i++;
100
+ }
101
+ }
102
+ if (i < input.length) i++; // skip closing "
103
+ tokens.push({ kind: 'string', value: s, pos });
104
+ continue;
105
+ }
106
+
107
+ // Numbers (including negative)
108
+ if (/[0-9]/.test(input[i]) || (input[i] === '-' && i + 1 < input.length && /[0-9]/.test(input[i + 1]))) {
109
+ let n = input[i]; i++;
110
+ while (i < input.length && /[0-9.]/.test(input[i])) { n += input[i]; i++; }
111
+ tokens.push({ kind: 'number', value: n, pos });
112
+ continue;
113
+ }
114
+
115
+ // Words (identifiers, variables, keywords)
116
+ if (/[?a-zA-Z_]/.test(input[i])) {
117
+ let w = '';
118
+ while (i < input.length && /[?a-zA-Z0-9_.:/-]/.test(input[i])) { w += input[i]; i++; }
119
+ tokens.push({ kind: 'word', value: w, pos });
120
+ continue;
121
+ }
122
+
123
+ // Unknown char — skip
124
+ i++;
125
+ }
126
+
127
+ tokens.push({ kind: 'eof', value: '', pos: input.length });
128
+ return tokens;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Parser
133
+ // ---------------------------------------------------------------------------
134
+
135
+ class Parser {
136
+ private tokens: Token[];
137
+ private pos = 0;
138
+
139
+ constructor(tokens: Token[]) {
140
+ this.tokens = tokens;
141
+ }
142
+
143
+ private peek(): Token { return this.tokens[this.pos]; }
144
+ private advance(): Token { return this.tokens[this.pos++]; }
145
+
146
+ private expect(kind: TokenKind, value?: string): Token {
147
+ const t = this.advance();
148
+ if (t.kind !== kind || (value !== undefined && t.value !== value)) {
149
+ throw new Error(`Expected ${kind}${value ? ` "${value}"` : ''} at pos ${t.pos}, got ${t.kind} "${t.value}"`);
150
+ }
151
+ return t;
152
+ }
153
+
154
+ private match(kind: TokenKind, value?: string): boolean {
155
+ const t = this.peek();
156
+ if (t.kind === kind && (value === undefined || t.value === value)) {
157
+ this.pos++;
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+
163
+ private isAt(kind: TokenKind, value?: string): boolean {
164
+ const t = this.peek();
165
+ return t.kind === kind && (value === undefined || t.value === value);
166
+ }
167
+
168
+ // -----------------------------------------------------------------------
169
+ // Terms
170
+ // -----------------------------------------------------------------------
171
+
172
+ parseTerm(): Term {
173
+ const t = this.peek();
174
+ if (t.kind === 'word' && t.value.startsWith('?')) {
175
+ this.advance();
176
+ return variable(t.value.slice(1));
177
+ }
178
+ if (t.kind === 'string') {
179
+ this.advance();
180
+ return literal(t.value);
181
+ }
182
+ if (t.kind === 'number') {
183
+ this.advance();
184
+ const n = Number(t.value);
185
+ return literal(n);
186
+ }
187
+ if (t.kind === 'word') {
188
+ const v = t.value;
189
+ this.advance();
190
+ if (v === 'true') return literal(true);
191
+ if (v === 'false') return literal(false);
192
+ return literal(v);
193
+ }
194
+ throw new Error(`Unexpected token at pos ${t.pos}: ${t.kind} "${t.value}"`);
195
+ }
196
+
197
+ // -----------------------------------------------------------------------
198
+ // Patterns
199
+ // -----------------------------------------------------------------------
200
+
201
+ parsePattern(): Pattern {
202
+ const t = this.peek();
203
+
204
+ // NOT pattern
205
+ if (t.kind === 'word' && t.value.toUpperCase() === 'NOT') {
206
+ this.advance();
207
+ const inner = this.parsePattern();
208
+ return { kind: 'not', pattern: inner } as NotPattern;
209
+ }
210
+
211
+ // OR pattern
212
+ if (t.kind === 'word' && t.value.toUpperCase() === 'OR') {
213
+ this.advance();
214
+ const branches: Pattern[][] = [];
215
+ while (this.isAt('symbol', '{')) {
216
+ this.advance();
217
+ const branch: Pattern[] = [];
218
+ while (!this.isAt('symbol', '}') && !this.isAt('eof', undefined)) {
219
+ branch.push(this.parsePattern());
220
+ }
221
+ this.expect('symbol', '}');
222
+ branches.push(branch);
223
+ }
224
+ return { kind: 'or', branches } as OrPattern;
225
+ }
226
+
227
+ // Fact pattern: [e a v]
228
+ if (t.kind === 'symbol' && t.value === '[') {
229
+ this.advance();
230
+ const entity = this.parseTerm();
231
+ const attribute = this.parseTerm();
232
+ const value = this.parseTerm();
233
+ this.expect('symbol', ']');
234
+ return { kind: 'fact', entity, attribute, value } as FactPattern;
235
+ }
236
+
237
+ // Link pattern: (src a tgt)
238
+ if (t.kind === 'symbol' && t.value === '(') {
239
+ this.advance();
240
+ const source = this.parseTerm();
241
+ const attribute = this.parseTerm();
242
+ const target = this.parseTerm();
243
+ this.expect('symbol', ')');
244
+ return { kind: 'link', source, attribute, target } as LinkPattern;
245
+ }
246
+
247
+ // Rule application: ruleName(?x, ?y)
248
+ if (t.kind === 'word' && !t.value.startsWith('?')) {
249
+ const name = this.advance().value;
250
+ if (this.isAt('symbol', '(')) {
251
+ this.advance();
252
+ const args: Term[] = [];
253
+ while (!this.isAt('symbol', ')') && !this.isAt('eof', undefined)) {
254
+ args.push(this.parseTerm());
255
+ this.match('symbol', ',');
256
+ }
257
+ this.expect('symbol', ')');
258
+ return { kind: 'rule', name, args } as RuleApplication;
259
+ }
260
+ throw new Error(`Expected '(' after rule name "${name}" at pos ${t.pos}`);
261
+ }
262
+
263
+ throw new Error(`Cannot parse pattern at pos ${t.pos}: ${t.kind} "${t.value}"`);
264
+ }
265
+
266
+ // -----------------------------------------------------------------------
267
+ // Filter
268
+ // -----------------------------------------------------------------------
269
+
270
+ parseFilter(): Filter {
271
+ const left = this.parseTerm();
272
+ const op = this.advance().value as FilterOp;
273
+ const right = this.parseTerm();
274
+ return { kind: 'filter', left, op, right };
275
+ }
276
+
277
+ // -----------------------------------------------------------------------
278
+ // Full Query
279
+ // -----------------------------------------------------------------------
280
+
281
+ parseQuery(): Query {
282
+ const query: Query = {
283
+ select: [],
284
+ where: [],
285
+ filters: [],
286
+ aggregates: [],
287
+ orderBy: [],
288
+ limit: 0,
289
+ offset: 0,
290
+ };
291
+
292
+ // Parse clauses in any order
293
+ while (!this.isAt('eof', undefined)) {
294
+ const kw = this.peek();
295
+ if (kw.kind !== 'word') {
296
+ throw new Error(`Expected keyword at pos ${kw.pos}, got ${kw.kind} "${kw.value}"`);
297
+ }
298
+
299
+ switch (kw.value.toUpperCase()) {
300
+ case 'SELECT': {
301
+ this.advance();
302
+ while (this.peek().kind === 'word' && this.peek().value.startsWith('?')) {
303
+ query.select.push(this.advance().value.slice(1));
304
+ }
305
+ break;
306
+ }
307
+
308
+ case 'WHERE': {
309
+ this.advance();
310
+ this.expect('symbol', '{');
311
+ while (!this.isAt('symbol', '}') && !this.isAt('eof', undefined)) {
312
+ query.where.push(this.parsePattern());
313
+ }
314
+ this.expect('symbol', '}');
315
+ break;
316
+ }
317
+
318
+ case 'FILTER': {
319
+ this.advance();
320
+ query.filters.push(this.parseFilter());
321
+ break;
322
+ }
323
+
324
+ case 'AGGREGATE': {
325
+ this.advance();
326
+ const op = this.advance().value as AggregateOp;
327
+ this.expect('symbol', '(');
328
+ const varName = this.advance().value;
329
+ const varClean = varName.startsWith('?') ? varName.slice(1) : varName;
330
+ this.expect('symbol', ')');
331
+ this.expect('word', 'AS');
332
+ const asName = this.advance().value;
333
+ const asClean = asName.startsWith('?') ? asName.slice(1) : asName;
334
+ query.aggregates.push({ op, variable: varClean, as: asClean });
335
+ break;
336
+ }
337
+
338
+ case 'ORDER': {
339
+ this.advance();
340
+ this.expect('word', 'BY');
341
+ while (this.peek().kind === 'word' && this.peek().value.startsWith('?')) {
342
+ const v = this.advance().value.slice(1);
343
+ let dir: 'asc' | 'desc' = 'asc';
344
+ if (this.peek().kind === 'word' && ['ASC', 'DESC'].includes(this.peek().value.toUpperCase())) {
345
+ dir = this.advance().value.toLowerCase() as 'asc' | 'desc';
346
+ }
347
+ query.orderBy.push({ variable: v, direction: dir });
348
+ }
349
+ break;
350
+ }
351
+
352
+ case 'LIMIT': {
353
+ this.advance();
354
+ query.limit = Number(this.expect('number').value);
355
+ break;
356
+ }
357
+
358
+ case 'OFFSET': {
359
+ this.advance();
360
+ query.offset = Number(this.expect('number').value);
361
+ break;
362
+ }
363
+
364
+ default:
365
+ throw new Error(`Unknown keyword "${kw.value}" at pos ${kw.pos}`);
366
+ }
367
+ }
368
+
369
+ return query;
370
+ }
371
+
372
+ // -----------------------------------------------------------------------
373
+ // Datalog Rule
374
+ // -----------------------------------------------------------------------
375
+
376
+ parseRule(): DatalogRule {
377
+ // name(?x, ?y) :- body
378
+ const name = this.expect('word').value;
379
+ this.expect('symbol', '(');
380
+ const params: string[] = [];
381
+ while (!this.isAt('symbol', ')') && !this.isAt('eof', undefined)) {
382
+ const v = this.expect('word').value;
383
+ params.push(v.startsWith('?') ? v.slice(1) : v);
384
+ this.match('symbol', ',');
385
+ }
386
+ this.expect('symbol', ')');
387
+
388
+ // :- separator (colon + minus as two tokens, or we accept ":-" as word)
389
+ if (this.isAt('symbol', ':')) {
390
+ this.advance();
391
+ // Accept - as a word or skip
392
+ if (this.peek().kind === 'number' && this.peek().value.startsWith('-')) {
393
+ this.advance();
394
+ }
395
+ } else if (this.isAt('word', ':-')) {
396
+ this.advance();
397
+ }
398
+
399
+ const body: Pattern[] = [];
400
+ const filters: Filter[] = [];
401
+
402
+ while (!this.isAt('eof', undefined)) {
403
+ if (this.peek().kind === 'word' && this.peek().value.toUpperCase() === 'FILTER') {
404
+ this.advance();
405
+ filters.push(this.parseFilter());
406
+ } else {
407
+ body.push(this.parsePattern());
408
+ }
409
+ this.match('symbol', ',');
410
+ }
411
+
412
+ return { name, params, body, filters };
413
+ }
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Public API
418
+ // ---------------------------------------------------------------------------
419
+
420
+ export function parseQuery(input: string): Query {
421
+ const tokens = tokenize(input);
422
+ return new Parser(tokens).parseQuery();
423
+ }
424
+
425
+ export function parseRule(input: string): DatalogRule {
426
+ const tokens = tokenize(input);
427
+ return new Parser(tokens).parseRule();
428
+ }
429
+
430
+ /**
431
+ * Shorthand: parse a simple "find entities where" query.
432
+ *
433
+ * Example: `find ?e where type = "Project"`
434
+ * Becomes: SELECT ?e WHERE { [?e "type" "Project"] }
435
+ */
436
+ export function parseSimple(input: string): Query {
437
+ const trimmed = input.trim();
438
+
439
+ // Try to detect if it's already a full query (starts with SELECT/WHERE)
440
+ const upper = trimmed.toUpperCase();
441
+ if (upper.startsWith('SELECT') || upper.startsWith('WHERE')) {
442
+ return parseQuery(trimmed);
443
+ }
444
+
445
+ // Simple format: find ?vars where attr op value [and attr op value]*
446
+ const findMatch = trimmed.match(/^find\s+(.+?)\s+where\s+(.+)$/i);
447
+ if (findMatch) {
448
+ const vars = findMatch[1].trim().split(/\s+/);
449
+ const conditions = findMatch[2].trim();
450
+
451
+ const selectVars = vars.map((v) => v.startsWith('?') ? v : `?${v}`);
452
+ const entity = selectVars[0];
453
+
454
+ // Parse conditions: "attr op value [and attr op value]*"
455
+ const parts = conditions.split(/\s+and\s+/i);
456
+ const patterns: string[] = [];
457
+ const filters: string[] = [];
458
+
459
+ for (const part of parts) {
460
+ const eqMatch = part.match(/^(\S+)\s*(=|!=|<|<=|>|>=|contains|startsWith|endsWith|matches)\s*(.+)$/);
461
+ if (eqMatch) {
462
+ const [, attr, op, val] = eqMatch;
463
+ const valTrimmed = val.trim();
464
+ if (op === '=') {
465
+ // Direct fact pattern
466
+ patterns.push(`[${entity} "${attr}" ${valTrimmed}]`);
467
+ } else {
468
+ // Need a variable + filter
469
+ const tmpVar = `?_${attr.replace(/[^a-zA-Z0-9]/g, '_')}`;
470
+ patterns.push(`[${entity} "${attr}" ${tmpVar}]`);
471
+ filters.push(`FILTER ${tmpVar} ${op} ${valTrimmed}`);
472
+ }
473
+ }
474
+ }
475
+
476
+ const fullQuery = `SELECT ${selectVars.join(' ')}\nWHERE {\n ${patterns.join('\n ')}\n}\n${filters.join('\n')}`;
477
+ return parseQuery(fullQuery);
478
+ }
479
+
480
+ throw new Error(`Cannot parse query: "${trimmed}". Use full EQL-S syntax or "find ?e where attr = value".`);
481
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * EQL-S Query Types — Entity Query Language (Structured)
3
+ *
4
+ * Defines the AST for structured queries over the EAV store.
5
+ * Queries are composed of patterns (fact/link clauses) that bind
6
+ * variables, plus optional filters, aggregations, and projections.
7
+ *
8
+ * @module trellis/core/query
9
+ */
10
+
11
+ import type { Atom } from '../store/eav-store.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Variables & Terms
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * A query variable, prefixed with `?` in the DSL.
19
+ * e.g. `?e`, `?name`, `?type`
20
+ */
21
+ export interface Variable {
22
+ kind: 'variable';
23
+ name: string;
24
+ }
25
+
26
+ /**
27
+ * A literal constant value.
28
+ */
29
+ export interface Literal {
30
+ kind: 'literal';
31
+ value: Atom;
32
+ }
33
+
34
+ /**
35
+ * A term is either a variable or a literal.
36
+ */
37
+ export type Term = Variable | Literal;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Patterns (clauses)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * A fact pattern matches triples (e, a, v) in the EAV store.
45
+ *
46
+ * Example DSL: `[?e "type" "Project"]` or `[?e ?attr ?val]`
47
+ */
48
+ export interface FactPattern {
49
+ kind: 'fact';
50
+ entity: Term;
51
+ attribute: Term;
52
+ value: Term;
53
+ }
54
+
55
+ /**
56
+ * A link pattern matches links (e1, a, e2) in the graph.
57
+ *
58
+ * Example DSL: `(?src "memberOf" ?tgt)`
59
+ */
60
+ export interface LinkPattern {
61
+ kind: 'link';
62
+ source: Term;
63
+ attribute: Term;
64
+ target: Term;
65
+ }
66
+
67
+ /**
68
+ * A negation pattern — succeeds when the inner pattern has NO matches.
69
+ */
70
+ export interface NotPattern {
71
+ kind: 'not';
72
+ pattern: Pattern;
73
+ }
74
+
75
+ /**
76
+ * An or-pattern — succeeds when ANY branch matches.
77
+ */
78
+ export interface OrPattern {
79
+ kind: 'or';
80
+ branches: Pattern[][];
81
+ }
82
+
83
+ /**
84
+ * A rule application — invoke a named Datalog rule.
85
+ */
86
+ export interface RuleApplication {
87
+ kind: 'rule';
88
+ name: string;
89
+ args: Term[];
90
+ }
91
+
92
+ export type Pattern = FactPattern | LinkPattern | NotPattern | OrPattern | RuleApplication;
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Filters
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export type FilterOp = '=' | '!=' | '<' | '<=' | '>' | '>=' | 'contains' | 'startsWith' | 'endsWith' | 'matches';
99
+
100
+ export interface Filter {
101
+ kind: 'filter';
102
+ left: Term;
103
+ op: FilterOp;
104
+ right: Term;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Aggregation
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export type AggregateOp = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'collect';
112
+
113
+ export interface Aggregate {
114
+ op: AggregateOp;
115
+ /** Variable to aggregate over. */
116
+ variable: string;
117
+ /** Output variable name. */
118
+ as: string;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Ordering
123
+ // ---------------------------------------------------------------------------
124
+
125
+ export interface OrderBy {
126
+ variable: string;
127
+ direction: 'asc' | 'desc';
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Query
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * A complete EQL-S query.
136
+ */
137
+ export interface Query {
138
+ /** Variables to project in the result. Empty = return all bound variables. */
139
+ select: string[];
140
+ /** Pattern clauses that must all match (conjunction). */
141
+ where: Pattern[];
142
+ /** Post-match filters. */
143
+ filters: Filter[];
144
+ /** Aggregation functions. */
145
+ aggregates: Aggregate[];
146
+ /** Ordering. */
147
+ orderBy: OrderBy[];
148
+ /** Maximum number of results (0 = unlimited). */
149
+ limit: number;
150
+ /** Number of results to skip. */
151
+ offset: number;
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Bindings
156
+ // ---------------------------------------------------------------------------
157
+
158
+ /**
159
+ * A single set of variable bindings.
160
+ */
161
+ export type Bindings = Map<string, Atom>;
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Datalog Rules
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * A Datalog rule: `head :- body`.
169
+ *
170
+ * Example: `ancestor(?x, ?z) :- [?x "parent" ?y], ancestor(?y, ?z)`
171
+ */
172
+ export interface DatalogRule {
173
+ name: string;
174
+ /** Parameter variable names for the head. */
175
+ params: string[];
176
+ /** Body patterns (conjunction). */
177
+ body: Pattern[];
178
+ /** Body filters. */
179
+ filters: Filter[];
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ export function isVariable(t: Term): t is Variable {
187
+ return t.kind === 'variable';
188
+ }
189
+
190
+ export function isLiteral(t: Term): t is Literal {
191
+ return t.kind === 'literal';
192
+ }
193
+
194
+ export function variable(name: string): Variable {
195
+ return { kind: 'variable', name };
196
+ }
197
+
198
+ export function literal(value: Atom): Literal {
199
+ return { kind: 'literal', value };
200
+ }