trellis 1.0.8 → 2.0.6

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -83
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -1,3321 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __defProp = Object.defineProperty;
4
- var __export = (target, all) => {
5
- for (var name in all)
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true,
9
- configurable: true,
10
- set: (newValue) => all[name] = () => newValue
11
- });
12
- };
13
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
-
16
- // src/ai/orchestrator.ts
17
- import { openai } from "@ai-sdk/openai";
18
- import { generateText, streamText, generateObject } from "ai";
19
- import { z as z2 } from "zod";
20
- function identifyEntityType(query) {
21
- const normalizedQuery = query.toLowerCase();
22
- const entityPatterns = [
23
- /(?:find|show|list|get|search)\s+([a-z]+s?)\b/i,
24
- /\b([a-z]+s?)\s+(?:that|with|containing|having)\b/i,
25
- /\b([a-z]+s?)'?s?\s+[a-z]+/i
26
- ];
27
- for (const pattern of entityPatterns) {
28
- const match = normalizedQuery.match(pattern);
29
- if (match && match[1]) {
30
- let entityType = match[1].toLowerCase();
31
- if (entityType.endsWith("ies")) {
32
- entityType = entityType.slice(0, -3) + "y";
33
- } else if (entityType.endsWith("es")) {
34
- entityType = entityType.slice(0, -2);
35
- } else if (entityType.endsWith("s") && entityType.length > 3) {
36
- entityType = entityType.slice(0, -1);
37
- }
38
- const excludeWords = ["this", "that", "the", "and", "or", "but", "with", "from", "by", "at", "in", "on", "to", "for", "of", "as", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", "all", "any", "some", "more", "most", "less", "least", "first", "last", "next", "previous"];
39
- if (!excludeWords.includes(entityType) && entityType.length > 2) {
40
- return entityType;
41
- }
42
- }
43
- }
44
- return "item";
45
- }
46
- function handleCommonPatterns(query) {
47
- let normalizedQuery = query.trim().toLowerCase();
48
- if (normalizedQuery.includes("start with") || normalizedQuery.includes("starts with") || normalizedQuery.includes("beginning with") || normalizedQuery.includes("begins with")) {
49
- const pattern = /(?:starts?|begins?|beginning) with\s+(?:the letter\s+)?['"]?([a-zA-Z0-9]+)['"]?/i;
50
- const match = query.match(pattern);
51
- if (match && match[1]) {
52
- const prefix = match[1];
53
- normalizedQuery = normalizedQuery.replace(/(?:starts?|begins?|beginning) with\s+(?:the letter\s+)?['"]?([a-z0-9]+)['"]?/i, `matches /^${prefix}/`);
54
- }
55
- }
56
- if (normalizedQuery.includes("end with") || normalizedQuery.includes("ends with") || normalizedQuery.includes("ending with")) {
57
- const pattern = /(?:ends?|ending) with\s+(?:the letter\s+)?['"]?([a-zA-Z0-9]+)['"]?/i;
58
- const match = query.match(pattern);
59
- if (match && match[1]) {
60
- const suffix = match[1];
61
- normalizedQuery = normalizedQuery.replace(/(?:ends?|ending) with\s+(?:the letter\s+)?['"]?([a-z0-9]+)['"]?/i, `matches /${suffix}$/`);
62
- }
63
- }
64
- if (normalizedQuery.includes("contain") || normalizedQuery.includes("having") || normalizedQuery.includes("with") && normalizedQuery.includes("in the name")) {
65
- const patterns = [
66
- /containing\s+(?:the text\s+)?['"]([^'"]+)['"]/i,
67
- /contains\s+(?:the text\s+)?['"]([^'"]+)['"]/i,
68
- /with\s+['"]([^'"]+)['"]\s+in the (?:name|title|text)/i,
69
- /having\s+['"]([^'"]+)['"]\s+in/i
70
- ];
71
- for (const pattern of patterns) {
72
- const match = query.match(pattern);
73
- if (match && match[1]) {
74
- const text = match[1];
75
- normalizedQuery = normalizedQuery.replace(pattern, `matches /${text}/`);
76
- break;
77
- }
78
- }
79
- }
80
- return normalizedQuery;
81
- }
82
- var IntentSchema, ActionSchema, TaskTypeSchema, ComplexitySchema, SentimentSchema, ToneSchema, MetaSchema, PlanSchema, PlanVoteSchema, OrchestratorOptionsSchema, OrchestratorOk, OrchestratorErr, OrchestratorResultSchema, DEFAULT_MODEL, CLASSIFICATION_PROMPT, processQuery = async (query, context) => {
83
- try {
84
- const processedQuery = handleCommonPatterns(query);
85
- const entityType = identifyEntityType(processedQuery);
86
- const attributes = Array.isArray(context.catalog) ? context.catalog.map((a) => a.attribute).filter(Boolean) : [];
87
- const aliasPairs = [];
88
- const findAttr = (needle) => attributes.find((a) => needle.test(a));
89
- const questionAttr = findAttr(/question$/i);
90
- const responseAttr = findAttr(/response$/i);
91
- const cotAttr = findAttr(/(complex[_\-.]?cot|chain[_\-.]?of[_\-.]?thought)/i);
92
- if (questionAttr)
93
- aliasPairs.push({ alias: "question", attr: questionAttr });
94
- if (responseAttr)
95
- aliasPairs.push({ alias: "response", attr: responseAttr });
96
- if (cotAttr)
97
- aliasPairs.push({ alias: "reasoning", attr: cotAttr });
98
- const aliasInfo = aliasPairs.map(({ alias, attr }) => `- ${alias} ⇒ ${attr}`).join(`
99
- `);
100
- const catalogInfo = context.catalog.map((attr) => `- ${attr.attribute}:${attr.type} e.g. ${attr.examples.join(", ")}`).join(`
101
- `);
102
- const prompt = `You are an expert at converting natural language queries to EQL-S (EAV Query Language - Strict).
103
-
104
- Available attributes in the dataset:
105
- ${catalogInfo}
106
-
107
- Attribute aliases you MUST use if the user mentions the alias term:
108
- ${aliasInfo || "- (none)"}
109
-
110
- Data statistics: ${JSON.stringify(context.dataStats, null, 2)}
111
-
112
- EQL-S Grammar Rules:
113
- - Use FIND <type> AS ?var to specify entity type and variable
114
- - Use WHERE clause for conditions
115
- - Use RETURN clause to specify output fields
116
- - Use ORDER BY for sorting, LIMIT for result limits
117
- - Operators: = != > >= < <= BETWEEN ... AND ... CONTAINS MATCHES
118
- - For string pattern matching use MATCHES with regex patterns: MATCHES /pattern/
119
- - For "starts with", use MATCHES /^prefix/
120
- - For "ends with", use MATCHES /suffix$/
121
- - For "contains", use MATCHES /text/ or CONTAINS "text"
122
- - Regex literals must use forward slashes: /pattern/
123
-
124
- STRICT formatting rules for this dataset:
125
- - Entity type MUST be "default" unless explicitly specified. Use: FIND default AS ?e
126
- - Only use attribute names EXACTLY as listed above (e.g., ${attributes.slice(0, 3).join(", ")} ...). Do not invent attributes.
127
- - NEVER use numeric property access like ?e.0 or ?e.6 — that is invalid.
128
- - If the user refers to "question", "answer/response", or "reasoning/chain of thought",
129
- use these attributes if available: ${[
130
- questionAttr,
131
- responseAttr,
132
- cotAttr
133
- ].filter(Boolean).join(", ") || "(none)"}
134
-
135
- Entity type detected: ${entityType || "item"}
136
- - Avoid using the IN operator for string matching
137
- - Variables must start with ? (e.g., ?p, ?user)
138
- - Strings must be in double quotes
139
- - Regex patterns should be in /pattern/ format but PREFER CONTAINS when possible
140
-
141
- Query Pattern Examples (adapt to available attributes):
142
- - "show me [entities] with more than X [numeric_attribute]" → FIND <type> AS ?e WHERE ?e.<attribute> > X RETURN ?e
143
- - "find [entities] containing [text]" → FIND <type> AS ?e WHERE ?e.<text_attribute> CONTAINS "text" RETURN ?e
144
- - "[entities] tagged with [value]" → FIND <type> AS ?e WHERE ?e.<tag_attribute> = "value" RETURN ?e
145
- - "[entities] between X and Y [units]" → FIND <type> AS ?e WHERE ?e.<numeric_attribute> BETWEEN X AND Y RETURN ?e
146
- - "[entities] that start with [prefix]" → FIND <type> AS ?e WHERE ?e.<text_attribute> MATCHES /^prefix/ RETURN ?e.<text_attribute>
147
- - "list [category] [entities]" → FIND <type> AS ?e WHERE ?e.<category_attribute> = "category" RETURN ?e
148
-
149
- Convert this natural language query to EQL-S: "${processedQuery}"
150
-
151
- Output ONLY the EQL-S query, no explanations or additional text.`;
152
- const result = await generateText({
153
- model: openai("gpt-4o-mini"),
154
- prompt,
155
- temperature: 0.1
156
- });
157
- let eqlsQuery = result.text.trim();
158
- if (eqlsQuery.startsWith("```") && eqlsQuery.endsWith("```")) {
159
- eqlsQuery = eqlsQuery.slice(3, -3).trim();
160
- const firstLine = eqlsQuery.split(`
161
- `)[0];
162
- if (firstLine && !firstLine.includes(" ") && firstLine.length < 20) {
163
- eqlsQuery = eqlsQuery.split(`
164
- `).slice(1).join(`
165
- `).trim();
166
- }
167
- }
168
- if (!eqlsQuery.startsWith("FIND")) {
169
- return { error: "Generated query does not start with FIND" };
170
- }
171
- eqlsQuery = eqlsQuery.replace(/^(FIND)\s+([A-Za-z_][A-Za-z0-9_-]*)\s+AS\s+\?[A-Za-z_][A-Za-z0-9_]*/i, (m, findKw) => {
172
- const varMatch = eqlsQuery.match(/AS\s+(\?[A-Za-z_][A-Za-z0-9_]*)/i);
173
- const v = varMatch ? varMatch[1] : "?e";
174
- return `${findKw} default AS ${v}`;
175
- });
176
- if (/\?[a-zA-Z_]\w*\.\d+/.test(eqlsQuery)) {
177
- const wantsQuestion = /hallucinat|question|prompt/i.test(processedQuery);
178
- const replacementAttr = wantsQuestion ? questionAttr : responseAttr || questionAttr || attributes[0];
179
- if (replacementAttr) {
180
- eqlsQuery = eqlsQuery.replace(/(\?[a-zA-Z_]\w*)\.\d+/g, `$1.${replacementAttr}`);
181
- } else {
182
- return { error: "Generated query used numeric property access (invalid)" };
183
- }
184
- }
185
- const attrRefs = Array.from(eqlsQuery.matchAll(/\?[a-zA-Z_]\w*\.([A-Za-z0-9_.-]+)/g)).map((m) => m[1]).filter((a) => typeof a === "string");
186
- const unknown = attrRefs.filter((a) => !attributes.includes(a));
187
- if (unknown.length) {
188
- let repaired = eqlsQuery;
189
- for (const unk of unknown) {
190
- const lower = unk.toLowerCase();
191
- const alias = aliasPairs.find(({ alias: alias2 }) => lower.includes(alias2));
192
- if (alias) {
193
- repaired = repaired.replace(new RegExp(`(?[a-zA-Z_]\\w*.)${unk.replace(/[-/\\.^$*+?()|[\]{}]/g, "\\$&")}`, "g"), `$1${alias.attr}`);
194
- }
195
- }
196
- if (repaired !== eqlsQuery) {
197
- eqlsQuery = repaired;
198
- }
199
- }
200
- return { eqlsQuery };
201
- } catch (error) {
202
- return {
203
- error: `Failed to process query: ${error instanceof Error ? error.message : "Unknown error"}`
204
- };
205
- }
206
- };
207
- var init_orchestrator = __esm(() => {
208
- IntentSchema = z2.enum([
209
- "conversation",
210
- "question",
211
- "task",
212
- "query",
213
- "command",
214
- "help",
215
- "error"
216
- ]);
217
- ActionSchema = z2.enum([
218
- "streamResponse",
219
- "generateResponse",
220
- "executeTask",
221
- "runQuery",
222
- "showHelp",
223
- "none"
224
- ]);
225
- TaskTypeSchema = z2.enum([
226
- "creative_writing",
227
- "analysis",
228
- "calculation",
229
- "code_generation",
230
- "data_processing",
231
- "general"
232
- ]);
233
- ComplexitySchema = z2.enum(["simple", "moderate", "complex"]);
234
- SentimentSchema = z2.enum(["positive", "negative", "neutral"]).optional();
235
- ToneSchema = z2.enum(["formal", "casual", "urgent", "friendly", "technical"]).optional();
236
- MetaSchema = z2.object({
237
- intent: IntentSchema,
238
- reason: z2.string(),
239
- action: ActionSchema,
240
- confidence: z2.number().min(0).max(1),
241
- sentiment: SentimentSchema,
242
- tone: ToneSchema,
243
- taskType: TaskTypeSchema.optional(),
244
- complexity: ComplexitySchema.optional(),
245
- estimatedTime: z2.number().optional()
246
- });
247
- PlanSchema = z2.object({
248
- rationale: z2.string(),
249
- steps: z2.array(z2.string()).min(1),
250
- assumptions: z2.array(z2.string()).optional(),
251
- risks: z2.array(z2.string()).optional(),
252
- revisedPrompt: z2.string().optional(),
253
- toolHints: z2.array(z2.string()).optional()
254
- });
255
- PlanVoteSchema = z2.object({
256
- winningIndex: z2.number().int().min(0),
257
- reason: z2.string()
258
- });
259
- OrchestratorOptionsSchema = z2.object({
260
- stream: z2.boolean().optional(),
261
- maxRetries: z2.number().min(0).max(5).default(2),
262
- timeoutMs: z2.number().positive().default(30000),
263
- includeAnalysis: z2.boolean().default(true),
264
- fallbackToConversation: z2.boolean().default(true),
265
- useToT: z2.boolean().default(false),
266
- plannerModel: z2.string().default("gpt-4o-mini"),
267
- numThoughts: z2.number().int().min(2).max(8).default(3)
268
- }).passthrough();
269
- OrchestratorOk = z2.object({
270
- status: z2.literal("ok"),
271
- meta: MetaSchema,
272
- payload: z2.object({
273
- requestId: z2.string().uuid(),
274
- prompt: z2.string(),
275
- options: z2.record(z2.string(), z2.unknown()),
276
- response: z2.any(),
277
- processingTimeMs: z2.number(),
278
- planning: z2.object({
279
- winningIndex: z2.number(),
280
- reason: z2.string(),
281
- plan: PlanSchema,
282
- allPlansCount: z2.number()
283
- }).optional()
284
- })
285
- });
286
- OrchestratorErr = z2.object({
287
- status: z2.literal("error"),
288
- meta: MetaSchema,
289
- payload: z2.object({
290
- requestId: z2.string().uuid(),
291
- prompt: z2.string(),
292
- options: z2.record(z2.string(), z2.unknown()),
293
- error: z2.string(),
294
- processingTimeMs: z2.number(),
295
- planning: z2.object({
296
- winningIndex: z2.number(),
297
- reason: z2.string(),
298
- plan: PlanSchema,
299
- allPlansCount: z2.number()
300
- }).optional()
301
- })
302
- });
303
- OrchestratorResultSchema = z2.union([OrchestratorOk, OrchestratorErr]);
304
- DEFAULT_MODEL = openai("gpt-4o-mini");
305
- CLASSIFICATION_PROMPT = `You are an intent classifier for a conversational AI orchestrator.
306
- Analyze the user prompt and return ONLY a JSON object with these fields:
307
-
308
- - intent: one of ${IntentSchema.options.join(", ")}
309
- - reason: brief explanation (max 50 chars)
310
- - action: one of ${ActionSchema.options.join(", ")}
311
- - confidence: number 0-1
312
- - sentiment: optional, one of positive/negative/neutral
313
- - tone: optional, one of formal/casual/urgent/friendly/technical
314
- - taskType: optional, one of ${TaskTypeSchema.options.join(", ")} (only for task intent)
315
- - complexity: optional, one of ${ComplexitySchema.options.join(", ")} (only for task intent)
316
- - estimatedTime: optional, rough time estimate in seconds (only for task intent)
317
-
318
- Guidelines:
319
- - "conversation": general chat, greetings, casual talk → streamResponse
320
- - "question": seeking information → streamResponse
321
- - "task": action requests → executeTask (include taskType, complexity, estimatedTime)
322
- - "query": data/search requests → runQuery
323
- - "command": system commands → executeTask
324
- - "help": assistance requests → showHelp
325
-
326
- Task type guidelines:
327
- - "creative_writing": stories, poems, creative content
328
- - "analysis": data analysis, research, evaluation
329
- - "calculation": math, computations, formulas
330
- - "code_generation": programming, scripts, technical solutions
331
- - "data_processing": data manipulation, transformation
332
- - "general": other tasks
333
-
334
- User prompt: """{{PROMPT}}"""
335
-
336
- Respond with valid JSON only:`;
337
- });
338
-
339
- // src/ai/nl-query-provider.ts
340
- var exports_nl_query_provider = {};
341
- __export(exports_nl_query_provider, {
342
- DefaultNLQueryProvider: () => DefaultNLQueryProvider
343
- });
344
-
345
- class DefaultNLQueryProvider {
346
- options;
347
- name = "default-orchestrator";
348
- constructor(options = {}) {
349
- this.options = options;
350
- }
351
- async translate(nl, context) {
352
- const catalog = context?.catalog || this.options.catalog || [];
353
- const dataStats = context?.dataStats || this.options.dataStats || { totalFacts: 0 };
354
- const result = await processQuery(nl, { catalog, dataStats });
355
- if (result.error) {
356
- throw new Error(`NL translation failed: ${result.error}`);
357
- }
358
- if (!result.eqlsQuery) {
359
- throw new Error("NL translation failed: no query generated");
360
- }
361
- return result.eqlsQuery;
362
- }
363
- }
364
- var init_nl_query_provider = __esm(() => {
365
- init_orchestrator();
366
- });
367
-
368
- // src/cli/server.ts
369
- import { join as join2 } from "node:path";
370
-
371
- // src/kernel/trellis-kernel.ts
372
- import { randomUUID } from "crypto";
373
-
374
- // src/store/eav-store.ts
375
- function* flatten(obj, base = "") {
376
- if (Array.isArray(obj)) {
377
- for (const v of obj) {
378
- yield* flatten(v, base);
379
- }
380
- } else if (obj && typeof obj === "object") {
381
- for (const [k, v] of Object.entries(obj)) {
382
- yield* flatten(v, base ? `${base}.${k}` : k);
383
- }
384
- } else {
385
- yield [base, obj];
386
- }
387
- }
388
- function jsonEntityFacts(entityId, root, type) {
389
- const facts = [{ e: entityId, a: "type", v: type }];
390
- for (const [a, v] of flatten(root)) {
391
- if (v === undefined || v === null)
392
- continue;
393
- if (Array.isArray(v)) {
394
- for (const el of v) {
395
- facts.push({ e: entityId, a, v: el });
396
- }
397
- } else if (typeof v === "object") {} else {
398
- facts.push({ e: entityId, a, v });
399
- }
400
- }
401
- return facts;
402
- }
403
-
404
- class EAVStore {
405
- facts = [];
406
- links = [];
407
- catalog = new Map;
408
- eavIndex = new Map;
409
- aevIndex = new Map;
410
- aveIndex = new Map;
411
- linkIndex = new Map;
412
- linkReverseIndex = new Map;
413
- linkAttrIndex = new Map;
414
- distinct = new Map;
415
- addFacts(facts) {
416
- for (let i = 0;i < facts.length; i++) {
417
- const fact = facts[i];
418
- if (fact) {
419
- this.facts.push(fact);
420
- this.updateIndexes(fact, this.facts.length - 1);
421
- this.updateCatalog(fact);
422
- }
423
- }
424
- }
425
- addLinks(links) {
426
- for (const link of links) {
427
- this.links.push(link);
428
- this.updateLinkIndexes(link);
429
- }
430
- }
431
- deleteFacts(factsToDelete) {
432
- for (const fact of factsToDelete) {
433
- const valueKey = this.valueKey(fact.v);
434
- const indices = this.aveIndex.get(fact.a)?.get(valueKey);
435
- if (!indices)
436
- continue;
437
- let foundIdx = -1;
438
- for (const idx of indices) {
439
- const storedFact = this.facts[idx];
440
- if (storedFact && storedFact.e === fact.e && storedFact.a === fact.a) {
441
- foundIdx = idx;
442
- break;
443
- }
444
- }
445
- if (foundIdx !== -1) {
446
- this.facts[foundIdx] = undefined;
447
- this.eavIndex.get(fact.e)?.get(fact.a)?.delete(foundIdx);
448
- this.aevIndex.get(fact.a)?.get(fact.e)?.delete(foundIdx);
449
- this.aveIndex.get(fact.a)?.get(valueKey)?.delete(foundIdx);
450
- const entry = this.catalog.get(fact.a);
451
- if (entry) {}
452
- }
453
- }
454
- }
455
- deleteLinks(linksToDelete) {
456
- for (const link of linksToDelete) {
457
- const initialLen = this.links.length;
458
- this.links = this.links.filter((l) => !(l.e1 === link.e1 && l.a === link.a && l.e2 === link.e2));
459
- if (this.links.length < initialLen) {
460
- this.linkIndex.get(link.e1)?.get(link.a)?.delete(link.e2);
461
- this.linkReverseIndex.get(link.e2)?.get(link.a)?.delete(link.e1);
462
- const attrPairs = this.linkAttrIndex.get(link.a);
463
- if (attrPairs) {
464
- for (const pair of attrPairs) {
465
- if (pair[0] === link.e1 && pair[1] === link.e2) {
466
- attrPairs.delete(pair);
467
- break;
468
- }
469
- }
470
- }
471
- }
472
- }
473
- }
474
- updateIndexes(fact, index) {
475
- if (!this.eavIndex.has(fact.e)) {
476
- this.eavIndex.set(fact.e, new Map);
477
- }
478
- if (!this.eavIndex.get(fact.e).has(fact.a)) {
479
- this.eavIndex.get(fact.e).set(fact.a, new Set);
480
- }
481
- this.eavIndex.get(fact.e).get(fact.a).add(index);
482
- if (!this.aevIndex.has(fact.a)) {
483
- this.aevIndex.set(fact.a, new Map);
484
- }
485
- if (!this.aevIndex.get(fact.a).has(fact.e)) {
486
- this.aevIndex.get(fact.a).set(fact.e, new Set);
487
- }
488
- this.aevIndex.get(fact.a).get(fact.e).add(index);
489
- if (!this.aveIndex.has(fact.a)) {
490
- this.aveIndex.set(fact.a, new Map);
491
- }
492
- const valueKey = this.valueKey(fact.v);
493
- if (!this.aveIndex.get(fact.a).has(valueKey)) {
494
- this.aveIndex.get(fact.a).set(valueKey, new Set);
495
- }
496
- this.aveIndex.get(fact.a).get(valueKey).add(index);
497
- }
498
- updateLinkIndexes(link) {
499
- if (!this.linkIndex.has(link.e1)) {
500
- this.linkIndex.set(link.e1, new Map);
501
- }
502
- const e1Attrs = this.linkIndex.get(link.e1);
503
- if (!e1Attrs.has(link.a)) {
504
- e1Attrs.set(link.a, new Set);
505
- }
506
- e1Attrs.get(link.a).add(link.e2);
507
- if (!this.linkReverseIndex.has(link.e2)) {
508
- this.linkReverseIndex.set(link.e2, new Map);
509
- }
510
- const e2Attrs = this.linkReverseIndex.get(link.e2);
511
- if (!e2Attrs.has(link.a)) {
512
- e2Attrs.set(link.a, new Set);
513
- }
514
- e2Attrs.get(link.a).add(link.e1);
515
- if (!this.linkAttrIndex.has(link.a)) {
516
- this.linkAttrIndex.set(link.a, new Set);
517
- }
518
- this.linkAttrIndex.get(link.a).add([link.e1, link.e2]);
519
- }
520
- valueKey(v) {
521
- if (v instanceof Date)
522
- return `date:${v.toISOString()}`;
523
- return `${typeof v}:${v}`;
524
- }
525
- updateCatalog(fact) {
526
- const entry = this.catalog.get(fact.a) || {
527
- attribute: fact.a,
528
- type: this.inferType(fact.v),
529
- cardinality: "one",
530
- distinctCount: 0,
531
- examples: []
532
- };
533
- const factType = this.inferType(fact.v);
534
- if (entry.type !== factType && entry.type !== "mixed") {
535
- entry.type = "mixed";
536
- }
537
- const entityAttrs = this.eavIndex.get(fact.e)?.get(fact.a);
538
- if (entityAttrs && entityAttrs.size > 1) {
539
- entry.cardinality = "many";
540
- }
541
- const k = this.valueKey(fact.v);
542
- const s = this.distinct.get(fact.a) || (this.distinct.set(fact.a, new Set), this.distinct.get(fact.a));
543
- s.add(k);
544
- entry.distinctCount = s.size;
545
- if (entry.examples.length < 5 && !entry.examples.includes(fact.v)) {
546
- entry.examples.push(fact.v);
547
- }
548
- if (typeof fact.v === "number") {
549
- entry.min = Math.min(entry.min ?? fact.v, fact.v);
550
- entry.max = Math.max(entry.max ?? fact.v, fact.v);
551
- }
552
- this.catalog.set(fact.a, entry);
553
- }
554
- inferType(v) {
555
- if (typeof v === "string")
556
- return "string";
557
- if (typeof v === "number")
558
- return "number";
559
- if (typeof v === "boolean")
560
- return "boolean";
561
- if (v instanceof Date)
562
- return "date";
563
- return "mixed";
564
- }
565
- getFactsByEntity(entity) {
566
- const indices = this.eavIndex.get(entity);
567
- if (!indices)
568
- return [];
569
- const result = [];
570
- for (const attrIndices of indices.values()) {
571
- for (const idx of attrIndices) {
572
- const fact = this.facts[idx];
573
- if (fact) {
574
- result.push(fact);
575
- }
576
- }
577
- }
578
- return result;
579
- }
580
- getFactsByAttribute(attribute) {
581
- const indices = this.aevIndex.get(attribute);
582
- if (!indices)
583
- return [];
584
- const result = [];
585
- for (const entityIndices of indices.values()) {
586
- for (const idx of entityIndices) {
587
- const fact = this.facts[idx];
588
- if (fact) {
589
- result.push(fact);
590
- }
591
- }
592
- }
593
- return result;
594
- }
595
- getFactsByValue(attribute, value) {
596
- const indices = this.aveIndex.get(attribute)?.get(this.valueKey(value));
597
- if (!indices)
598
- return [];
599
- return Array.from(indices).map((idx) => this.facts[idx]).filter((fact) => fact !== undefined);
600
- }
601
- getCatalog() {
602
- return Array.from(this.catalog.values());
603
- }
604
- getCatalogEntry(attribute) {
605
- return this.catalog.get(attribute);
606
- }
607
- getAllFacts() {
608
- return this.facts.filter((f) => f !== undefined);
609
- }
610
- getAllLinks() {
611
- return [...this.links];
612
- }
613
- getLinksByEntity(entity) {
614
- const results = [];
615
- const forwardLinks = this.linkIndex.get(entity);
616
- if (forwardLinks) {
617
- for (const [attr, targets] of forwardLinks) {
618
- for (const target of targets) {
619
- results.push({ e1: entity, a: attr, e2: target });
620
- }
621
- }
622
- }
623
- const reverseLinks = this.linkReverseIndex.get(entity);
624
- if (reverseLinks) {
625
- for (const [attr, sources] of reverseLinks) {
626
- for (const source of sources) {
627
- results.push({ e1: source, a: attr, e2: entity });
628
- }
629
- }
630
- }
631
- return results;
632
- }
633
- getLinksByAttribute(attribute) {
634
- const results = [];
635
- const links = this.linkAttrIndex.get(attribute);
636
- if (links) {
637
- for (const [e1, e2] of links) {
638
- results.push({ e1, a: attribute, e2 });
639
- }
640
- }
641
- return results;
642
- }
643
- getLinksByEntityAndAttribute(entity, attribute) {
644
- const results = [];
645
- const attrs = this.linkIndex.get(entity);
646
- if (attrs) {
647
- const targets = attrs.get(attribute);
648
- if (targets) {
649
- for (const target of targets) {
650
- results.push({ e1: entity, a: attribute, e2: target });
651
- }
652
- }
653
- }
654
- return results;
655
- }
656
- getStats() {
657
- return {
658
- totalFacts: this.facts.length,
659
- totalLinks: this.links.length,
660
- uniqueEntities: this.eavIndex.size,
661
- uniqueAttributes: this.aevIndex.size,
662
- catalogEntries: this.catalog.size
663
- };
664
- }
665
- snapshot() {
666
- return {
667
- facts: this.facts.filter((f) => f !== undefined),
668
- links: [...this.links],
669
- catalog: this.getCatalog()
670
- };
671
- }
672
- restore(snapshot) {
673
- this.facts = [];
674
- this.links = [];
675
- this.catalog.clear();
676
- this.eavIndex.clear();
677
- this.aevIndex.clear();
678
- this.aveIndex.clear();
679
- this.linkIndex.clear();
680
- this.linkReverseIndex.clear();
681
- this.linkAttrIndex.clear();
682
- this.distinct.clear();
683
- this.addFacts(snapshot.facts);
684
- this.addLinks(snapshot.links);
685
- if (snapshot.catalog) {
686
- for (const entry of snapshot.catalog) {
687
- this.catalog.set(entry.attribute, entry);
688
- }
689
- }
690
- }
691
- }
692
-
693
- // src/query/datalog-evaluator.ts
694
- class ExternalPredicates {
695
- static regex(str, pattern) {
696
- if (typeof pattern === "string") {
697
- try {
698
- const regexMatch = pattern.match(/^\/(.*)\/([gimuy]*)$/);
699
- if (regexMatch) {
700
- const [, regexPattern, flags] = regexMatch;
701
- const regex = new RegExp(regexPattern, flags || "i");
702
- return regex.test(str);
703
- }
704
- return new RegExp(pattern, "i").test(str);
705
- } catch (e) {
706
- console.warn(`Invalid regex pattern: ${pattern}`, e);
707
- return str.toLowerCase().includes(pattern.toLowerCase());
708
- }
709
- }
710
- return pattern.test(str);
711
- }
712
- static gt(a, b) {
713
- return a > b;
714
- }
715
- static lt(a, b) {
716
- return a < b;
717
- }
718
- static between(val, min, max) {
719
- return val >= min && val <= max;
720
- }
721
- static contains(str, substr) {
722
- return str.toLowerCase().includes(substr.toLowerCase());
723
- }
724
- static after(a, b) {
725
- return a > b;
726
- }
727
- static betweenDate(d, start, end) {
728
- return d >= start && d <= end;
729
- }
730
- static sum(values) {
731
- return values.reduce((a, b) => a + b, 0);
732
- }
733
- static count(values) {
734
- return values.length;
735
- }
736
- static avg(values) {
737
- return values.length > 0 ? this.sum(values) / values.length : 0;
738
- }
739
- }
740
-
741
- class DatalogEvaluator {
742
- store;
743
- rules = [];
744
- ws = new Map;
745
- constructor(store) {
746
- this.store = store;
747
- }
748
- addRule(rule) {
749
- this.rules.push(rule);
750
- }
751
- seedBaseFacts() {
752
- const attrRows = [];
753
- for (const f of this.store.getAllFacts()) {
754
- if (f) {
755
- attrRows.push([f.e, f.a, f.v]);
756
- }
757
- }
758
- this.ws.set("attr", attrRows);
759
- const linkRows = [];
760
- for (const link of this.store.getAllLinks()) {
761
- linkRows.push([link.e1, link.a, link.e2]);
762
- }
763
- this.ws.set("link", linkRows);
764
- }
765
- pushDerived(predicate, tuple) {
766
- const bucket = this.ws.get(predicate) || [];
767
- if (!this.ws.has(predicate)) {
768
- this.ws.set(predicate, bucket);
769
- }
770
- const key = JSON.stringify(tuple);
771
- if (!bucket._keys) {
772
- bucket._keys = new Set;
773
- }
774
- const keys = bucket._keys;
775
- if (keys.has(key)) {
776
- return false;
777
- }
778
- bucket.push(tuple);
779
- keys.add(key);
780
- return true;
781
- }
782
- evaluate(query, limit) {
783
- const startTime = performance.now();
784
- const trace = [];
785
- this.seedBaseFacts();
786
- let added = true;
787
- let iterations = 0;
788
- const maxIterations = 100;
789
- while (added && iterations < maxIterations) {
790
- added = false;
791
- for (const rule of this.rules) {
792
- const bindings2 = this.findBindingsOverWS(rule.body);
793
- for (const binding of bindings2) {
794
- const head = this.substitute(rule.head, binding);
795
- const tuple = head.terms.map((term) => this.resolveTerm(term, binding));
796
- if (this.pushDerived(head.predicate, tuple)) {
797
- added = true;
798
- }
799
- }
800
- }
801
- iterations++;
802
- }
803
- const bindings = this.findBindingsOverWS(query.goals, trace, limit);
804
- return {
805
- bindings,
806
- executionTime: performance.now() - startTime,
807
- plan: `Semi-naive evaluation: ${iterations} iterations, ${this.getTotalFacts()} facts`,
808
- trace
809
- };
810
- }
811
- getTotalFacts() {
812
- let total = 0;
813
- for (const tuples of this.ws.values()) {
814
- total += tuples.length;
815
- }
816
- return total;
817
- }
818
- findBindingsOverWS(goals, trace, limit) {
819
- if (goals.length === 0) {
820
- return [{}];
821
- }
822
- let bindings = [{}];
823
- for (const goal of goals) {
824
- const goalStartTime = performance.now();
825
- const newBindings = [];
826
- outer:
827
- for (const binding of bindings) {
828
- const goalBindings = this.evaluateGoal(goal, binding);
829
- for (const goalBinding of goalBindings) {
830
- const merged = { ...binding, ...goalBinding };
831
- let hasConflict = false;
832
- for (const key in merged) {
833
- if (binding[key] !== undefined && goalBinding[key] !== undefined && binding[key] !== goalBinding[key]) {
834
- hasConflict = true;
835
- break;
836
- }
837
- }
838
- if (!hasConflict) {
839
- newBindings.push(merged);
840
- if (limit !== undefined && newBindings.length >= limit)
841
- break outer;
842
- }
843
- }
844
- }
845
- bindings = newBindings;
846
- if (trace) {
847
- trace.push({
848
- goal: `${goal.predicate}(${goal.terms.join(", ")})`,
849
- bindingsCount: bindings.length,
850
- durationMs: performance.now() - goalStartTime
851
- });
852
- }
853
- }
854
- const uniqueBindings = new Map;
855
- for (const binding of bindings) {
856
- const key = JSON.stringify(binding);
857
- uniqueBindings.set(key, binding);
858
- }
859
- return Array.from(uniqueBindings.values());
860
- }
861
- evaluateGoal(goal, binding) {
862
- const { predicate, terms } = goal;
863
- if (predicate === "not") {
864
- const inner = goal.terms[0];
865
- const res = this.evaluateGoal(inner, binding);
866
- return res.length === 0 ? [binding] : [];
867
- }
868
- if (predicate === "attr") {
869
- return this.evaluateAttrPredicate(terms, binding);
870
- }
871
- if (predicate === "link") {
872
- return this.evaluateLinkPredicate(terms, binding);
873
- }
874
- if (predicate === "gt" || predicate === "lt" || predicate === "between" || predicate === ">" || predicate === "<" || predicate === ">=" || predicate === "<=" || predicate === "=" || predicate === "!=") {
875
- return this.evaluateComparisonPredicate(goal, binding);
876
- }
877
- if (predicate === "regex" || predicate === "contains") {
878
- return this.evaluateStringPredicate(goal, binding);
879
- }
880
- if (predicate === "after" || predicate === "betweenDate") {
881
- return this.evaluateDatePredicate(goal, binding);
882
- }
883
- if (predicate.startsWith("ext_")) {
884
- return this.evaluateExternalPredicate(goal, binding);
885
- }
886
- return this.evalPredicateFromWS(predicate, terms, binding);
887
- }
888
- evalPredicateFromWS(predicate, terms, binding) {
889
- const rows = this.ws.get(predicate) || [];
890
- const results = [];
891
- rowloop:
892
- for (const row of rows) {
893
- const newBinding = { ...binding };
894
- for (let i = 0;i < terms.length; i++) {
895
- const term = terms[i];
896
- const val = row[i];
897
- if (typeof term === "string" && term.startsWith("?")) {
898
- const bound = newBinding[term];
899
- if (bound !== undefined && bound !== val) {
900
- continue rowloop;
901
- }
902
- newBinding[term] = val;
903
- } else {
904
- if (term !== val) {
905
- continue rowloop;
906
- }
907
- }
908
- }
909
- results.push(newBinding);
910
- }
911
- return results;
912
- }
913
- evaluateAttrPredicate(terms, binding) {
914
- if (terms.length !== 3)
915
- return [];
916
- const [entity, attribute, value] = terms.map((term) => this.resolveTerm(term, binding));
917
- const results = [];
918
- if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?") && (typeof value !== "string" || !value.startsWith("?"))) {
919
- const facts = this.store.getFactsByValue(attribute, value);
920
- for (const fact of facts) {
921
- if (fact.e === entity) {
922
- results.push({});
923
- }
924
- }
925
- return results;
926
- }
927
- if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?")) {
928
- const facts = this.store.getFactsByEntity(entity);
929
- for (const fact of facts) {
930
- if (fact.a === attribute) {
931
- const newBinding = { ...binding };
932
- if (typeof value === "string" && value.startsWith("?")) {
933
- newBinding[value] = fact.v;
934
- results.push(newBinding);
935
- } else if (fact.v === value) {
936
- results.push(newBinding);
937
- }
938
- }
939
- }
940
- return results;
941
- }
942
- if (typeof attribute === "string" && !attribute.startsWith("?")) {
943
- const facts = this.store.getFactsByAttribute(attribute);
944
- for (const fact of facts) {
945
- const newBinding = { ...binding };
946
- if (typeof entity === "string" && !entity.startsWith("?") && fact.e !== entity) {
947
- continue;
948
- }
949
- if ((typeof value !== "string" || !value.startsWith("?")) && fact.v !== value) {
950
- continue;
951
- }
952
- if (typeof entity === "string" && entity.startsWith("?")) {
953
- newBinding[entity] = fact.e;
954
- }
955
- if (typeof value === "string" && value.startsWith("?")) {
956
- newBinding[value] = fact.v;
957
- }
958
- results.push(newBinding);
959
- }
960
- return results;
961
- }
962
- return [];
963
- }
964
- evaluateLinkPredicate(terms, binding) {
965
- if (terms.length !== 3)
966
- return [];
967
- const [e1, a, e2] = terms;
968
- const results = [];
969
- const links = this.store.getAllLinks();
970
- for (const link of links) {
971
- const newBinding = { ...binding };
972
- let matches = true;
973
- if (typeof e1 === "string" && !e1.startsWith("?")) {
974
- if (link.e1 !== e1)
975
- continue;
976
- } else if (typeof e1 === "string" && e1.startsWith("?")) {
977
- newBinding[e1] = link.e1;
978
- }
979
- if (typeof a === "string" && !a.startsWith("?")) {
980
- if (link.a !== a)
981
- continue;
982
- } else if (typeof a === "string" && a.startsWith("?")) {
983
- newBinding[a] = link.a;
984
- }
985
- if (typeof e2 === "string" && !e2.startsWith("?")) {
986
- if (link.e2 !== e2)
987
- continue;
988
- } else if (typeof e2 === "string" && e2.startsWith("?")) {
989
- newBinding[e2] = link.e2;
990
- }
991
- if (matches) {
992
- results.push(newBinding);
993
- }
994
- }
995
- return results;
996
- }
997
- evaluateComparisonPredicate(goal, binding) {
998
- const { predicate, terms } = goal;
999
- if (terms.length < 2)
1000
- return [];
1001
- const left = this.resolveTerm(terms[0], binding);
1002
- const right = this.resolveTerm(terms[1], binding);
1003
- let leftNum = left;
1004
- let rightNum = right;
1005
- if (typeof left === "string" && !isNaN(Number(left))) {
1006
- leftNum = Number(left);
1007
- }
1008
- if (typeof right === "string" && !isNaN(Number(right))) {
1009
- rightNum = Number(right);
1010
- }
1011
- if (typeof leftNum !== "number" || typeof rightNum !== "number")
1012
- return [];
1013
- let result = false;
1014
- switch (predicate) {
1015
- case "gt":
1016
- case ">":
1017
- result = ExternalPredicates.gt(leftNum, rightNum);
1018
- break;
1019
- case "lt":
1020
- case "<":
1021
- result = ExternalPredicates.lt(leftNum, rightNum);
1022
- break;
1023
- case ">=":
1024
- result = leftNum >= rightNum;
1025
- break;
1026
- case "<=":
1027
- result = leftNum <= rightNum;
1028
- break;
1029
- case "=":
1030
- result = leftNum === rightNum;
1031
- break;
1032
- case "!=":
1033
- result = leftNum !== rightNum;
1034
- break;
1035
- case "between":
1036
- if (terms.length >= 3) {
1037
- const max = this.resolveTerm(terms[2], binding);
1038
- let maxNum = max;
1039
- if (typeof max === "string" && !isNaN(Number(max))) {
1040
- maxNum = Number(max);
1041
- }
1042
- if (typeof maxNum === "number") {
1043
- result = ExternalPredicates.between(leftNum, rightNum, maxNum);
1044
- }
1045
- }
1046
- break;
1047
- }
1048
- return result ? [{}] : [];
1049
- }
1050
- evaluateStringPredicate(goal, binding) {
1051
- const { predicate, terms } = goal;
1052
- if (terms.length < 2)
1053
- return [];
1054
- const str = this.resolveTerm(terms[0], binding);
1055
- const pattern = this.resolveTerm(terms[1], binding);
1056
- if (typeof str !== "string" || typeof pattern !== "string")
1057
- return [];
1058
- let result = false;
1059
- switch (predicate) {
1060
- case "regex":
1061
- result = ExternalPredicates.regex(str, pattern);
1062
- break;
1063
- case "contains":
1064
- result = ExternalPredicates.contains(str, pattern);
1065
- break;
1066
- }
1067
- return result ? [{}] : [];
1068
- }
1069
- evaluateDatePredicate(goal, binding) {
1070
- const { predicate, terms } = goal;
1071
- if (terms.length < 2)
1072
- return [];
1073
- const left = this.resolveTerm(terms[0], binding);
1074
- const right = this.resolveTerm(terms[1], binding);
1075
- if (!(left instanceof Date) || !(right instanceof Date))
1076
- return [];
1077
- let result = false;
1078
- switch (predicate) {
1079
- case "after":
1080
- result = ExternalPredicates.after(left, right);
1081
- break;
1082
- case "betweenDate":
1083
- if (terms.length >= 3) {
1084
- const end = this.resolveTerm(terms[2], binding);
1085
- if (end instanceof Date) {
1086
- result = ExternalPredicates.betweenDate(left, right, end);
1087
- }
1088
- }
1089
- break;
1090
- }
1091
- return result ? [{}] : [];
1092
- }
1093
- evaluateExternalPredicate(goal, binding) {
1094
- const { predicate, terms } = goal;
1095
- const resolvedTerms = terms.map((term) => this.resolveTerm(term, binding));
1096
- let result = false;
1097
- switch (predicate) {
1098
- case "ext_regex":
1099
- if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
1100
- result = ExternalPredicates.regex(resolvedTerms[0], resolvedTerms[1]);
1101
- }
1102
- break;
1103
- case "ext_gt":
1104
- if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number") {
1105
- result = ExternalPredicates.gt(resolvedTerms[0], resolvedTerms[1]);
1106
- }
1107
- break;
1108
- case "ext_between":
1109
- if (resolvedTerms.length >= 3 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number" && typeof resolvedTerms[2] === "number") {
1110
- result = ExternalPredicates.between(resolvedTerms[0], resolvedTerms[1], resolvedTerms[2]);
1111
- }
1112
- break;
1113
- case "ext_contains":
1114
- if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
1115
- result = ExternalPredicates.contains(resolvedTerms[0], resolvedTerms[1]);
1116
- }
1117
- break;
1118
- }
1119
- return result ? [{}] : [];
1120
- }
1121
- resolveTerm(term, binding) {
1122
- if (typeof term === "string" && term.startsWith("?")) {
1123
- return binding[term] || term;
1124
- }
1125
- return term;
1126
- }
1127
- substitute(atom, binding) {
1128
- return {
1129
- predicate: atom.predicate,
1130
- terms: atom.terms.map((term) => this.resolveTerm(term, binding))
1131
- };
1132
- }
1133
- }
1134
-
1135
- // src/query/attribute-resolver.ts
1136
- class AttributeResolver {
1137
- schema = {};
1138
- buildSchema(catalog) {
1139
- this.schema = {};
1140
- for (const entry of catalog) {
1141
- const entityType = "default";
1142
- const attributeName = entry.attribute;
1143
- if (!this.schema[entityType]) {
1144
- this.schema[entityType] = {};
1145
- }
1146
- this.schema[entityType][attributeName] = {
1147
- type: entry.type,
1148
- distinctCount: entry.distinctCount,
1149
- examples: entry.examples
1150
- };
1151
- }
1152
- }
1153
- resolveAttribute(entityType, queryAttribute) {
1154
- const entitySchema = this.schema[entityType];
1155
- if (!entitySchema) {
1156
- return null;
1157
- }
1158
- const queryLower = queryAttribute.toLowerCase();
1159
- if (entitySchema[queryAttribute]) {
1160
- return queryAttribute;
1161
- }
1162
- for (const [actualAttribute] of Object.entries(entitySchema)) {
1163
- if (actualAttribute.toLowerCase() === queryLower) {
1164
- return actualAttribute;
1165
- }
1166
- }
1167
- return null;
1168
- }
1169
- validateQuery(entityType, attributes) {
1170
- const errors = [];
1171
- const resolved = new Map;
1172
- for (const attr of attributes) {
1173
- const resolvedAttr = this.resolveAttribute(entityType, attr);
1174
- if (resolvedAttr) {
1175
- resolved.set(attr, resolvedAttr);
1176
- } else {
1177
- errors.push(`Unknown attribute '${attr}' for entity type '${entityType}'. Available attributes: ${Object.keys(this.schema[entityType] || {}).join(", ")}`);
1178
- }
1179
- }
1180
- return {
1181
- valid: errors.length === 0,
1182
- errors,
1183
- resolved
1184
- };
1185
- }
1186
- getAvailableAttributes(entityType) {
1187
- return Object.keys(this.schema[entityType] || {});
1188
- }
1189
- getSchema() {
1190
- return this.schema;
1191
- }
1192
- }
1193
-
1194
- // src/query/query-optimizer.ts
1195
- class QueryOptimizer {
1196
- catalog;
1197
- constructor(catalog = []) {
1198
- this.catalog = catalog;
1199
- }
1200
- optimize(query) {
1201
- if (query.goals.length <= 1)
1202
- return query;
1203
- const optimizedGoals = [];
1204
- const remainingGoals = [...query.goals];
1205
- const boundVars = new Set;
1206
- const typeGoalIdx = remainingGoals.findIndex((g) => g.predicate === "attr" && g.terms[1] === "type");
1207
- if (typeGoalIdx !== -1) {
1208
- const typeGoal = remainingGoals.splice(typeGoalIdx, 1)[0];
1209
- optimizedGoals.push(typeGoal);
1210
- this.collectVars(typeGoal, boundVars);
1211
- }
1212
- while (remainingGoals.length > 0) {
1213
- const bestIdx = this.findBestNextGoal(remainingGoals, boundVars);
1214
- if (bestIdx === -1) {
1215
- const goal = remainingGoals.splice(0, 1)[0];
1216
- optimizedGoals.push(goal);
1217
- this.collectVars(goal, boundVars);
1218
- } else {
1219
- const goal = remainingGoals.splice(bestIdx, 1)[0];
1220
- optimizedGoals.push(goal);
1221
- this.collectVars(goal, boundVars);
1222
- }
1223
- let pushdownPossible = true;
1224
- while (pushdownPossible) {
1225
- const filterIdx = remainingGoals.findIndex((g) => this.isFilter(g) && this.isSatisfied(g, boundVars));
1226
- if (filterIdx !== -1) {
1227
- const filter = remainingGoals.splice(filterIdx, 1)[0];
1228
- optimizedGoals.push(filter);
1229
- } else {
1230
- pushdownPossible = false;
1231
- }
1232
- }
1233
- }
1234
- return {
1235
- ...query,
1236
- goals: optimizedGoals
1237
- };
1238
- }
1239
- findBestNextGoal(goals, boundVars) {
1240
- let bestIdx = -1;
1241
- let bestScore = -1;
1242
- const filterVars = new Set;
1243
- for (const goal of goals) {
1244
- if (this.isFilter(goal)) {
1245
- for (const term of goal.terms) {
1246
- if (typeof term === "string" && term.startsWith("?")) {
1247
- filterVars.add(term);
1248
- }
1249
- }
1250
- }
1251
- }
1252
- for (let i = 0;i < goals.length; i++) {
1253
- const goal = goals[i];
1254
- if (this.isFilter(goal))
1255
- continue;
1256
- let score = this.calculateRestrictiveness(goal, boundVars);
1257
- for (const term of goal.terms) {
1258
- if (typeof term === "string" && term.startsWith("?") && !boundVars.has(term) && filterVars.has(term)) {
1259
- score += 25;
1260
- }
1261
- }
1262
- if (score > bestScore) {
1263
- bestScore = score;
1264
- bestIdx = i;
1265
- }
1266
- }
1267
- return bestIdx;
1268
- }
1269
- calculateRestrictiveness(goal, boundVars) {
1270
- let score = 0;
1271
- const terms = goal.terms;
1272
- for (const term of terms) {
1273
- if (typeof term !== "string" || !term.startsWith("?")) {
1274
- score += 100;
1275
- } else if (boundVars.has(term)) {
1276
- score += 50;
1277
- }
1278
- }
1279
- if (goal.predicate === "attr" && typeof terms[1] === "string") {
1280
- const entry = this.catalog.find((e) => e.attribute === terms[1]);
1281
- if (entry) {
1282
- if (entry.cardinality === "one") {
1283
- score += 20;
1284
- }
1285
- score -= Math.min(10, entry.distinctCount / 100);
1286
- }
1287
- }
1288
- return score;
1289
- }
1290
- isFilter(goal) {
1291
- const filters = new Set([
1292
- "gt",
1293
- "lt",
1294
- "between",
1295
- "regex",
1296
- "contains",
1297
- ">",
1298
- "<",
1299
- ">=",
1300
- "<=",
1301
- "=",
1302
- "!=",
1303
- "after",
1304
- "betweenDate"
1305
- ]);
1306
- return filters.has(goal.predicate) || goal.predicate.startsWith("ext_");
1307
- }
1308
- isSatisfied(goal, boundVars) {
1309
- return goal.terms.every((term) => {
1310
- if (typeof term === "string" && term.startsWith("?")) {
1311
- return boundVars.has(term);
1312
- }
1313
- return true;
1314
- });
1315
- }
1316
- collectVars(goal, boundVars) {
1317
- for (const term of goal.terms) {
1318
- if (typeof term === "string" && term.startsWith("?")) {
1319
- boundVars.add(term);
1320
- }
1321
- }
1322
- }
1323
- }
1324
-
1325
- // src/query/eqls-parser.ts
1326
- class EQLSParser {
1327
- tokens = [];
1328
- current = 0;
1329
- errors = [];
1330
- static KEYWORDS = new Set([
1331
- "FIND",
1332
- "AS",
1333
- "WHERE",
1334
- "AND",
1335
- "OR",
1336
- "RETURN",
1337
- "ORDER",
1338
- "BY",
1339
- "LIMIT",
1340
- "ASC",
1341
- "DESC",
1342
- "BETWEEN",
1343
- "CONTAINS",
1344
- "MATCHES",
1345
- "IN"
1346
- ]);
1347
- static SINGLE_CHAR_OPERATORS = new Set(["=", ">", "<"]);
1348
- static MULTI_CHAR_OPERATORS = new Set([
1349
- "CONTAINS",
1350
- "MATCHES",
1351
- "BETWEEN",
1352
- "IN"
1353
- ]);
1354
- parse(query) {
1355
- this.tokens = this.tokenize(query);
1356
- this.current = 0;
1357
- this.errors = [];
1358
- try {
1359
- const parsed = this.parseQuery();
1360
- if (this.errors.length > 0) {
1361
- return { errors: this.errors };
1362
- }
1363
- return { query: parsed, errors: [] };
1364
- } catch (error) {
1365
- this.errors.push({
1366
- line: 1,
1367
- column: 1,
1368
- message: `Parse error: ${error instanceof Error ? error.message : "Unknown error"}`
1369
- });
1370
- return { errors: this.errors };
1371
- }
1372
- }
1373
- tokenize(input) {
1374
- const tokens = [];
1375
- const lines = input.split(`
1376
- `);
1377
- for (let lineNum = 0;lineNum < lines.length; lineNum++) {
1378
- const line = lines[lineNum];
1379
- const trimmed = line.trim();
1380
- if (!trimmed || trimmed.startsWith("--"))
1381
- continue;
1382
- let pos = 0;
1383
- while (pos < line.length) {
1384
- const char = line[pos];
1385
- if (char === " ") {
1386
- pos++;
1387
- continue;
1388
- }
1389
- if (char === '"') {
1390
- const start = pos;
1391
- pos++;
1392
- while (pos < line.length && line[pos] !== '"') {
1393
- if (line[pos] === "\\" && pos + 1 < line.length) {
1394
- pos += 2;
1395
- } else {
1396
- pos++;
1397
- }
1398
- }
1399
- if (pos < line.length) {
1400
- pos++;
1401
- const value = line.slice(start + 1, pos - 1);
1402
- tokens.push({
1403
- type: "STRING",
1404
- value,
1405
- line: lineNum + 1,
1406
- column: start + 1
1407
- });
1408
- } else {
1409
- this.errors.push({
1410
- line: lineNum + 1,
1411
- column: start + 1,
1412
- message: "Unterminated string literal"
1413
- });
1414
- break;
1415
- }
1416
- } else if (char === "/" && pos + 1 < line.length) {
1417
- const start = pos;
1418
- pos++;
1419
- while (pos < line.length && line[pos] !== "/") {
1420
- if (line[pos] === "\\" && pos + 1 < line.length) {
1421
- pos += 2;
1422
- } else {
1423
- pos++;
1424
- }
1425
- }
1426
- if (pos < line.length) {
1427
- pos++;
1428
- const pattern = line.slice(start, pos);
1429
- tokens.push({
1430
- type: "REGEX",
1431
- value: pattern,
1432
- line: lineNum + 1,
1433
- column: start + 1
1434
- });
1435
- } else {
1436
- this.errors.push({
1437
- line: lineNum + 1,
1438
- column: start + 1,
1439
- message: "Unterminated regex literal"
1440
- });
1441
- break;
1442
- }
1443
- } else if (char.match(/[A-Za-z_@]/)) {
1444
- const start = pos;
1445
- while (pos < line.length && line[pos].match(/[A-Za-z0-9_:@-]/)) {
1446
- pos++;
1447
- }
1448
- const value = line.slice(start, pos);
1449
- const upperValue = value.toUpperCase();
1450
- let type = "IDENTIFIER";
1451
- let tokenValue = value;
1452
- if (EQLSParser.KEYWORDS.has(upperValue)) {
1453
- type = upperValue;
1454
- tokenValue = upperValue;
1455
- } else if (EQLSParser.MULTI_CHAR_OPERATORS.has(upperValue)) {
1456
- type = "OPERATOR";
1457
- tokenValue = upperValue;
1458
- }
1459
- tokens.push({
1460
- type,
1461
- value: tokenValue,
1462
- line: lineNum + 1,
1463
- column: start + 1
1464
- });
1465
- } else if (char.match(/[0-9]/)) {
1466
- const start = pos;
1467
- let hasDecimal = false;
1468
- while (pos < line.length) {
1469
- const nextChar = line[pos];
1470
- if (nextChar.match(/[0-9]/)) {
1471
- pos++;
1472
- } else if (nextChar === "." && !hasDecimal && pos + 1 < line.length && line[pos + 1].match(/[0-9]/)) {
1473
- hasDecimal = true;
1474
- pos++;
1475
- } else {
1476
- break;
1477
- }
1478
- }
1479
- const value = line.slice(start, pos);
1480
- const numValue = value.includes(".") ? parseFloat(value) : parseInt(value, 10);
1481
- tokens.push({
1482
- type: "NUMBER",
1483
- value: numValue,
1484
- line: lineNum + 1,
1485
- column: start + 1
1486
- });
1487
- } else if (char === ".") {
1488
- tokens.push({
1489
- type: "DOT",
1490
- value: ".",
1491
- line: lineNum + 1,
1492
- column: pos + 1
1493
- });
1494
- pos++;
1495
- } else if (char === "?") {
1496
- const start = pos;
1497
- pos++;
1498
- while (pos < line.length && line[pos].match(/[A-Za-z0-9_]/)) {
1499
- pos++;
1500
- }
1501
- const value = line.slice(start, pos);
1502
- tokens.push({
1503
- type: "VARIABLE",
1504
- value,
1505
- line: lineNum + 1,
1506
- column: start + 1
1507
- });
1508
- } else if (EQLSParser.SINGLE_CHAR_OPERATORS.has(char) || char === "!" && pos + 1 < line.length && line[pos + 1] === "=" || char === ">" && pos + 1 < line.length && line[pos + 1] === "=" || char === "<" && pos + 1 < line.length && line[pos + 1] === "=" || char === "=" && pos + 1 < line.length && line[pos + 1] === "=") {
1509
- const start = pos;
1510
- if (char === "!" || char === ">" || char === "<" || char === "=") {
1511
- pos += 2;
1512
- } else {
1513
- pos++;
1514
- }
1515
- const value = line.slice(start, pos);
1516
- tokens.push({
1517
- type: "OPERATOR",
1518
- value,
1519
- line: lineNum + 1,
1520
- column: start + 1
1521
- });
1522
- } else if (char === ",") {
1523
- tokens.push({
1524
- type: "COMMA",
1525
- value: ",",
1526
- line: lineNum + 1,
1527
- column: pos + 1
1528
- });
1529
- pos++;
1530
- } else if (char === "(") {
1531
- tokens.push({
1532
- type: "LPAREN",
1533
- value: "(",
1534
- line: lineNum + 1,
1535
- column: pos + 1
1536
- });
1537
- pos++;
1538
- } else if (char === ")") {
1539
- tokens.push({
1540
- type: "RPAREN",
1541
- value: ")",
1542
- line: lineNum + 1,
1543
- column: pos + 1
1544
- });
1545
- pos++;
1546
- } else {
1547
- this.errors.push({
1548
- line: lineNum + 1,
1549
- column: pos + 1,
1550
- message: `Unexpected character '${char}'`,
1551
- expected: ["identifier", "string", "number", "operator"]
1552
- });
1553
- pos++;
1554
- }
1555
- }
1556
- }
1557
- return tokens;
1558
- }
1559
- parseQuery() {
1560
- this.expect("FIND");
1561
- const find = this.expect("IDENTIFIER").value;
1562
- this.expect("AS");
1563
- const as = this.expect("VARIABLE").value;
1564
- let where;
1565
- if (this.match("WHERE")) {
1566
- where = this.parseExpression();
1567
- }
1568
- let returnFields;
1569
- if (this.match("RETURN")) {
1570
- returnFields = this.parseReturnFields();
1571
- }
1572
- let orderBy;
1573
- if (this.match("ORDER")) {
1574
- this.expect("BY");
1575
- const field = this.parseAttributeReference();
1576
- const direction = this.match("DESC") ? "DESC" : this.match("ASC") ? "ASC" : "ASC";
1577
- orderBy = { field, direction };
1578
- }
1579
- let limit;
1580
- if (this.match("LIMIT")) {
1581
- limit = this.expect("NUMBER").value;
1582
- }
1583
- return { find, as, where, return: returnFields, orderBy, limit };
1584
- }
1585
- parseExpression() {
1586
- let left = this.parseTerm();
1587
- while (this.match("AND") || this.match("OR")) {
1588
- const op = this.previous().value;
1589
- const right = this.parseTerm();
1590
- left = { op, left, right };
1591
- }
1592
- return left;
1593
- }
1594
- parseTerm() {
1595
- if (this.match("LPAREN")) {
1596
- const expr = this.parseExpression();
1597
- this.expect("RPAREN");
1598
- return expr;
1599
- }
1600
- if ((this.check("STRING") || this.check("NUMBER") || this.check("IDENTIFIER")) && this.tokens[this.current + 1]?.type === "IN") {
1601
- const value = this.parseValue();
1602
- this.expect("IN");
1603
- const field = this.parseAttributeReference();
1604
- return { type: "MEMBERSHIP", value, field };
1605
- }
1606
- return this.parsePredicate();
1607
- }
1608
- parsePredicate() {
1609
- const field = this.parseAttributeReference();
1610
- if (this.match("BETWEEN")) {
1611
- const min = this.expect("NUMBER").value;
1612
- this.expect("AND");
1613
- const max = this.expect("NUMBER").value;
1614
- return { type: "BETWEEN", field, min, max };
1615
- }
1616
- if (this.match("CONTAINS")) {
1617
- const pattern = this.expect("STRING").value;
1618
- return { type: "CONTAINS", field, pattern };
1619
- }
1620
- if (this.match("MATCHES")) {
1621
- const regex = this.expect("REGEX").value;
1622
- return { type: "MATCHES", field, regex };
1623
- }
1624
- if (this.match("IN")) {
1625
- const value = this.parseValue();
1626
- return { type: "MEMBERSHIP", value, field };
1627
- }
1628
- const op = this.expect("OPERATOR").value.trim();
1629
- const right = this.parseValue();
1630
- if (op === "=" || op === "==") {
1631
- return { type: "EQUALS", field, value: right };
1632
- } else {
1633
- return { type: "COMP", left: field, op, right };
1634
- }
1635
- }
1636
- parseAttributeReference() {
1637
- const variable = this.expect("VARIABLE").value;
1638
- const attributeParts = [];
1639
- while (this.check("DOT")) {
1640
- this.advance();
1641
- const attributePart = this.expect("IDENTIFIER").value;
1642
- attributeParts.push(this.toCamelCase(attributePart));
1643
- }
1644
- if (attributeParts.length > 0) {
1645
- return `${variable}.${attributeParts.join(".")}`;
1646
- }
1647
- return variable;
1648
- }
1649
- toCamelCase(str) {
1650
- return str;
1651
- }
1652
- parseValue() {
1653
- if (this.match("STRING"))
1654
- return this.previous().value;
1655
- if (this.match("NUMBER"))
1656
- return this.previous().value;
1657
- if (this.match("IDENTIFIER")) {
1658
- const value = this.previous().value;
1659
- if (value === "true")
1660
- return true;
1661
- if (value === "false")
1662
- return false;
1663
- return value;
1664
- }
1665
- if (this.match("VARIABLE"))
1666
- return this.previous().value;
1667
- throw new Error(`Expected value, got ${this.peek().type}`);
1668
- }
1669
- parseReturnFields() {
1670
- const fields = [];
1671
- do {
1672
- const field = this.parseAttributeReference();
1673
- fields.push(field);
1674
- } while (this.match("COMMA"));
1675
- return fields;
1676
- }
1677
- extractContainsFields(expr) {
1678
- const fields = [];
1679
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
1680
- fields.push(...this.extractContainsFields(expr.left));
1681
- fields.push(...this.extractContainsFields(expr.right));
1682
- } else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
1683
- fields.push(expr.field);
1684
- }
1685
- return fields;
1686
- }
1687
- match(type) {
1688
- if (this.check(type)) {
1689
- this.advance();
1690
- return true;
1691
- }
1692
- return false;
1693
- }
1694
- check(type) {
1695
- if (this.isAtEnd())
1696
- return false;
1697
- return this.peek().type === type;
1698
- }
1699
- advance() {
1700
- if (!this.isAtEnd())
1701
- this.current++;
1702
- return this.previous();
1703
- }
1704
- isAtEnd() {
1705
- return this.peek().type === "EOF";
1706
- }
1707
- peek() {
1708
- return this.tokens[this.current] || {
1709
- type: "EOF",
1710
- value: "",
1711
- line: 0,
1712
- column: 0
1713
- };
1714
- }
1715
- previous() {
1716
- return this.tokens[this.current - 1] || {
1717
- type: "EOF",
1718
- value: "",
1719
- line: 0,
1720
- column: 0
1721
- };
1722
- }
1723
- expect(type) {
1724
- if (this.check(type)) {
1725
- return this.advance();
1726
- }
1727
- const token = this.peek();
1728
- this.errors.push({
1729
- line: token.line,
1730
- column: token.column,
1731
- message: `Expected ${type}, got ${token.type}`,
1732
- expected: [type]
1733
- });
1734
- throw new Error(`Expected ${type}, got ${token.type}`);
1735
- }
1736
- }
1737
-
1738
- class EQLSCompiler {
1739
- projectionMap = new Map;
1740
- tempCounter = 0;
1741
- compileAll(eqlsQuery) {
1742
- const baseGoals = [];
1743
- const baseVariables = new Set;
1744
- this.projectionMap.clear();
1745
- this.tempCounter = 0;
1746
- baseGoals.push({
1747
- predicate: "attr",
1748
- terms: [eqlsQuery.as, "type", eqlsQuery.find]
1749
- });
1750
- baseVariables.add(eqlsQuery.as.substring(1));
1751
- const returnGoals = [];
1752
- const returnVars = new Set;
1753
- if (eqlsQuery.return) {
1754
- for (const field of eqlsQuery.return) {
1755
- if (this.isAttributeReference(field)) {
1756
- const [entityVar, attributePath] = this.splitAttributeReference(field);
1757
- const outputVar = this.generateTempVar();
1758
- returnVars.add(outputVar);
1759
- returnGoals.push({
1760
- predicate: "attr",
1761
- terms: [entityVar, attributePath, `?${outputVar}`]
1762
- });
1763
- this.projectionMap.set(field, `?${outputVar}`);
1764
- } else {
1765
- returnVars.add(field.substring(1));
1766
- this.projectionMap.set(field, field);
1767
- }
1768
- }
1769
- }
1770
- const clauses = eqlsQuery.where ? this.toDNF(eqlsQuery.where) : [[]];
1771
- const compiledQueries = [];
1772
- for (const clause of clauses) {
1773
- const goals = [...baseGoals];
1774
- const variables = new Set(baseVariables);
1775
- for (const pred of clause) {
1776
- this.compilePredicate(pred, goals, variables);
1777
- }
1778
- for (const g of returnGoals)
1779
- goals.push(g);
1780
- for (const v of returnVars)
1781
- variables.add(v);
1782
- compiledQueries.push({ goals, variables });
1783
- }
1784
- return compiledQueries;
1785
- }
1786
- compile(eqlsQuery) {
1787
- const all = this.compileAll(eqlsQuery);
1788
- return all[0] || { goals: [], variables: new Set };
1789
- }
1790
- getProjectionMap() {
1791
- return this.projectionMap;
1792
- }
1793
- isAttributeReference(field) {
1794
- return field.includes(".") && field.startsWith("?");
1795
- }
1796
- splitAttributeReference(field) {
1797
- const parts = field.split(".");
1798
- if (parts.length < 2) {
1799
- throw new Error(`Invalid attribute reference: ${field}`);
1800
- }
1801
- const entityVar = parts[0];
1802
- const attributePath = parts.slice(1).join(".");
1803
- return [entityVar, attributePath];
1804
- }
1805
- compileExpression(expr, goals, variables) {
1806
- if (!expr || typeof expr !== "object") {
1807
- throw new Error(`Invalid expression: ${expr}`);
1808
- }
1809
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
1810
- this.compileExpression(expr.left, goals, variables);
1811
- this.compileExpression(expr.right, goals, variables);
1812
- } else {
1813
- this.compilePredicate(expr, goals, variables);
1814
- }
1815
- }
1816
- compilePredicate(pred, goals, variables) {
1817
- switch (pred.type) {
1818
- case "EQUALS":
1819
- goals.push({
1820
- predicate: "attr",
1821
- terms: [
1822
- this.extractEntityVar(pred.field),
1823
- this.extractAttributePath(pred.field),
1824
- pred.value
1825
- ]
1826
- });
1827
- break;
1828
- case "MEMBERSHIP":
1829
- goals.push({
1830
- predicate: "attr",
1831
- terms: [
1832
- this.extractEntityVar(pred.field),
1833
- this.extractAttributePath(pred.field),
1834
- pred.value
1835
- ]
1836
- });
1837
- break;
1838
- case "COMP":
1839
- const tempVar = this.generateTempVar();
1840
- variables.add(tempVar);
1841
- goals.push({
1842
- predicate: "attr",
1843
- terms: [
1844
- this.extractEntityVar(pred.left),
1845
- this.extractAttributePath(pred.left),
1846
- `?${tempVar}`
1847
- ]
1848
- });
1849
- goals.push({
1850
- predicate: pred.op.toLowerCase(),
1851
- terms: [`?${tempVar}`, pred.right]
1852
- });
1853
- break;
1854
- case "BETWEEN":
1855
- const tempVar2 = this.generateTempVar();
1856
- variables.add(tempVar2);
1857
- goals.push({
1858
- predicate: "attr",
1859
- terms: [
1860
- this.extractEntityVar(pred.field),
1861
- this.extractAttributePath(pred.field),
1862
- `?${tempVar2}`
1863
- ]
1864
- });
1865
- goals.push({
1866
- predicate: "between",
1867
- terms: [`?${tempVar2}`, pred.min, pred.max]
1868
- });
1869
- break;
1870
- case "CONTAINS":
1871
- const tempVar3 = this.generateTempVar();
1872
- variables.add(tempVar3);
1873
- goals.push({
1874
- predicate: "attr",
1875
- terms: [
1876
- this.extractEntityVar(pred.field),
1877
- this.extractAttributePath(pred.field),
1878
- `?${tempVar3}`
1879
- ]
1880
- });
1881
- goals.push({
1882
- predicate: "contains",
1883
- terms: [`?${tempVar3}`, pred.pattern]
1884
- });
1885
- break;
1886
- case "MATCHES":
1887
- const tempVar4 = this.generateTempVar();
1888
- variables.add(tempVar4);
1889
- const attributePath = this.extractAttributePath(pred.field);
1890
- const entityVar = this.extractEntityVar(pred.field);
1891
- goals.push({
1892
- predicate: "attr",
1893
- terms: [entityVar, attributePath, `?${tempVar4}`]
1894
- });
1895
- goals.push({
1896
- predicate: "regex",
1897
- terms: [`?${tempVar4}`, pred.regex]
1898
- });
1899
- break;
1900
- }
1901
- }
1902
- extractEntityVar(field) {
1903
- const parts = field.split(".");
1904
- return parts[0];
1905
- }
1906
- extractAttributePath(field) {
1907
- const parts = field.split(".");
1908
- if (parts.length > 1) {
1909
- return parts.slice(1).join(".");
1910
- }
1911
- return field.substring(1);
1912
- }
1913
- generateTempVar() {
1914
- this.tempCounter += 1;
1915
- return `temp${this.tempCounter}`;
1916
- }
1917
- toDNF(expr) {
1918
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
1919
- const left = this.toDNF(expr.left);
1920
- const right = this.toDNF(expr.right);
1921
- if (expr.op === "OR") {
1922
- return [...left, ...right];
1923
- }
1924
- const combined = [];
1925
- for (const l of left) {
1926
- for (const r of right) {
1927
- combined.push([...l, ...r]);
1928
- }
1929
- }
1930
- return combined;
1931
- }
1932
- return [[expr]];
1933
- }
1934
- }
1935
-
1936
- class EQLSProcessor {
1937
- parser = new EQLSParser;
1938
- compiler = new EQLSCompiler;
1939
- attributeResolver = new AttributeResolver;
1940
- catalog = [];
1941
- setSchema(catalog) {
1942
- this.catalog = catalog;
1943
- this.attributeResolver.buildSchema(catalog);
1944
- }
1945
- process(query) {
1946
- const parseResult = this.parser.parse(query);
1947
- if (parseResult.errors.length > 0) {
1948
- return { errors: parseResult.errors };
1949
- }
1950
- this.ensureFieldsInProjection(parseResult.query);
1951
- if (Object.keys(this.attributeResolver.getSchema()).length > 0) {
1952
- const entityType = "default";
1953
- const attributes = this.extractAttributes(parseResult.query);
1954
- const validation = this.attributeResolver.validateQuery(entityType, attributes);
1955
- if (!validation.valid) {
1956
- return {
1957
- errors: validation.errors.map((msg) => ({
1958
- message: msg,
1959
- line: 1,
1960
- column: 1
1961
- }))
1962
- };
1963
- }
1964
- this.resolveAttributesInQuery(parseResult.query, validation.resolved);
1965
- }
1966
- const compiledQueries = this.compiler.compileAll(parseResult.query);
1967
- const optimizer = new QueryOptimizer(this.catalog);
1968
- const optimizedQueries = compiledQueries.map((q) => optimizer.optimize(q));
1969
- const projectionMap = this.compiler.getProjectionMap();
1970
- return {
1971
- query: optimizedQueries[0],
1972
- queries: optimizedQueries,
1973
- errors: [],
1974
- projectionMap,
1975
- meta: {
1976
- orderBy: parseResult.query.orderBy,
1977
- limit: parseResult.query.limit
1978
- }
1979
- };
1980
- }
1981
- ensureFieldsInProjection(eqlsQuery) {
1982
- if (!eqlsQuery.return) {
1983
- eqlsQuery.return = [];
1984
- }
1985
- if (eqlsQuery.where) {
1986
- const matchesFields = this.extractMatchesFields(eqlsQuery.where);
1987
- for (const field of matchesFields) {
1988
- if (!eqlsQuery.return.includes(field)) {
1989
- eqlsQuery.return.push(field);
1990
- }
1991
- }
1992
- const containsFields = this.extractContainsFields(eqlsQuery.where);
1993
- for (const field of containsFields) {
1994
- if (!eqlsQuery.return.includes(field)) {
1995
- eqlsQuery.return.push(field);
1996
- }
1997
- }
1998
- }
1999
- if (eqlsQuery.orderBy?.field) {
2000
- const field = eqlsQuery.orderBy.field;
2001
- if (!eqlsQuery.return.includes(field)) {
2002
- eqlsQuery.return.push(field);
2003
- }
2004
- }
2005
- }
2006
- extractMatchesFields(expr) {
2007
- const fields = [];
2008
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
2009
- fields.push(...this.extractMatchesFields(expr.left));
2010
- fields.push(...this.extractMatchesFields(expr.right));
2011
- } else if ("type" in expr && expr.type === "MATCHES" && "field" in expr) {
2012
- fields.push(expr.field);
2013
- }
2014
- return fields;
2015
- }
2016
- extractContainsFields(expr) {
2017
- const fields = [];
2018
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
2019
- fields.push(...this.extractContainsFields(expr.left));
2020
- fields.push(...this.extractContainsFields(expr.right));
2021
- } else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
2022
- fields.push(expr.field);
2023
- }
2024
- return fields;
2025
- }
2026
- extractAttributes(eqlsQuery) {
2027
- const attributes = new Set;
2028
- if (eqlsQuery.where) {
2029
- this.extractAttributesFromExpression(eqlsQuery.where, attributes);
2030
- }
2031
- if (eqlsQuery.return) {
2032
- for (const field of eqlsQuery.return) {
2033
- if (this.isAttributeReference(field)) {
2034
- const [, attribute] = this.splitAttributeReference(field);
2035
- attributes.add(attribute);
2036
- }
2037
- }
2038
- }
2039
- return Array.from(attributes);
2040
- }
2041
- extractAttributesFromExpression(expr, attributes) {
2042
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
2043
- this.extractAttributesFromExpression(expr.left, attributes);
2044
- this.extractAttributesFromExpression(expr.right, attributes);
2045
- } else if ("field" in expr) {
2046
- if (this.isAttributeReference(expr.field)) {
2047
- const [, attribute] = this.splitAttributeReference(expr.field);
2048
- attributes.add(attribute);
2049
- }
2050
- } else if ("left" in expr && "right" in expr) {
2051
- if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
2052
- const [, attribute] = this.splitAttributeReference(expr.left);
2053
- attributes.add(attribute);
2054
- }
2055
- }
2056
- }
2057
- resolveAttributesInQuery(eqlsQuery, resolved) {
2058
- if (eqlsQuery.where) {
2059
- this.resolveAttributesInExpression(eqlsQuery.where, resolved);
2060
- }
2061
- if (eqlsQuery.return) {
2062
- for (let i = 0;i < eqlsQuery.return.length; i++) {
2063
- const field = eqlsQuery.return[i];
2064
- if (this.isAttributeReference(field)) {
2065
- const [entityVar, attribute] = this.splitAttributeReference(field);
2066
- const resolvedAttr = resolved.get(attribute);
2067
- if (resolvedAttr) {
2068
- eqlsQuery.return[i] = `${entityVar}.${resolvedAttr}`;
2069
- }
2070
- }
2071
- }
2072
- }
2073
- }
2074
- resolveAttributesInExpression(expr, resolved) {
2075
- if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
2076
- this.resolveAttributesInExpression(expr.left, resolved);
2077
- this.resolveAttributesInExpression(expr.right, resolved);
2078
- } else if ("field" in expr) {
2079
- if (this.isAttributeReference(expr.field)) {
2080
- const [entityVar, attribute] = this.splitAttributeReference(expr.field);
2081
- const resolvedAttr = resolved.get(attribute);
2082
- if (resolvedAttr) {
2083
- expr.field = `${entityVar}.${resolvedAttr}`;
2084
- }
2085
- }
2086
- } else if ("left" in expr && "right" in expr) {
2087
- if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
2088
- const [entityVar, attribute] = this.splitAttributeReference(expr.left);
2089
- const resolvedAttr = resolved.get(attribute);
2090
- if (resolvedAttr) {
2091
- expr.left = `${entityVar}.${resolvedAttr}`;
2092
- }
2093
- }
2094
- }
2095
- }
2096
- isAttributeReference(field) {
2097
- return field.includes(".");
2098
- }
2099
- splitAttributeReference(field) {
2100
- const parts = field.split(".");
2101
- return [parts[0], parts.slice(1).join(".")];
2102
- }
2103
- }
2104
-
2105
- // src/kernel/operations.ts
2106
- async function hashOp(op) {
2107
- const content = JSON.stringify({
2108
- kind: op.kind,
2109
- timestamp: op.timestamp,
2110
- agentId: op.agentId,
2111
- previousHash: op.previousHash,
2112
- facts: op.facts,
2113
- links: op.links
2114
- });
2115
- const msgUint8 = new TextEncoder().encode(content);
2116
- const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
2117
- const hashArray = Array.from(new Uint8Array(hashBuffer));
2118
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
2119
- return `trellis:op:${hashHex}`;
2120
- }
2121
- async function createOp(kind, params) {
2122
- const opBase = {
2123
- kind,
2124
- timestamp: new Date().toISOString(),
2125
- agentId: params.agentId,
2126
- previousHash: params.previousHash,
2127
- facts: params.facts,
2128
- links: params.links
2129
- };
2130
- const hash = await hashOp(opBase);
2131
- return { ...opBase, hash };
2132
- }
2133
-
2134
- // src/kernel/workspace.ts
2135
- import { z } from "zod";
2136
- var PropertyTypeSchema = z.enum([
2137
- "title",
2138
- "rich_text",
2139
- "number",
2140
- "select",
2141
- "multi_select",
2142
- "status",
2143
- "date",
2144
- "people",
2145
- "files",
2146
- "checkbox",
2147
- "url",
2148
- "email",
2149
- "phone_number",
2150
- "relation",
2151
- "rollup",
2152
- "formula",
2153
- "ai_generated"
2154
- ]);
2155
- var PropertyValueSpecificationSchema = z.object({
2156
- name: z.string(),
2157
- valueType: PropertyTypeSchema,
2158
- required: z.boolean().optional(),
2159
- description: z.string().optional(),
2160
- selectOptions: z.array(z.any()).optional(),
2161
- relation: z.object({
2162
- targetSchema: z.string().optional(),
2163
- cardinality: z.enum(["one", "many"]).optional(),
2164
- syncedProperty: z.string().optional()
2165
- }).optional(),
2166
- formula: z.string().optional(),
2167
- rollup: z.object({
2168
- relationProperty: z.string(),
2169
- targetProperty: z.string(),
2170
- aggregation: z.enum([
2171
- "count",
2172
- "sum",
2173
- "avg",
2174
- "min",
2175
- "max",
2176
- "median",
2177
- "mode"
2178
- ])
2179
- }).optional(),
2180
- aiGenerated: z.object({
2181
- prompt: z.string(),
2182
- model: z.string().optional()
2183
- }).optional()
2184
- });
2185
- var SchemaDefinitionSchema = z.object({
2186
- "@id": z.string(),
2187
- "@type": z.literal("trellis:Schema"),
2188
- version: z.string(),
2189
- fields: z.array(PropertyValueSpecificationSchema)
2190
- });
2191
- var ProjectionDefinitionSchema = z.object({
2192
- "@id": z.string(),
2193
- "@type": z.literal("trellis:Projection"),
2194
- name: z.string(),
2195
- type: z.enum([
2196
- "card-grid",
2197
- "table",
2198
- "timeline",
2199
- "dashboard",
2200
- "kanban",
2201
- "graph"
2202
- ]),
2203
- query: z.string(),
2204
- config: z.record(z.string(), z.any()).optional()
2205
- });
2206
- var WorkspaceConfigSchema = z.object({
2207
- workspace: z.object({
2208
- name: z.string().optional(),
2209
- description: z.string().optional(),
2210
- ontologies: z.record(z.string(), SchemaDefinitionSchema).optional(),
2211
- graph: z.object({
2212
- nodes: z.array(z.any()).optional(),
2213
- edges: z.array(z.any()).optional()
2214
- }).optional(),
2215
- projections: z.record(z.string(), ProjectionDefinitionSchema).optional()
2216
- })
2217
- });
2218
-
2219
- // src/kernel/trellis-kernel.ts
2220
- var stableCompare = (a, b) => {
2221
- if (a === b)
2222
- return 0;
2223
- if (a === undefined || a === null)
2224
- return 1;
2225
- if (b === undefined || b === null)
2226
- return -1;
2227
- if (typeof a === "number" && typeof b === "number")
2228
- return a - b;
2229
- if (a instanceof Date && b instanceof Date)
2230
- return a.getTime() - b.getTime();
2231
- return String(a).localeCompare(String(b));
2232
- };
2233
-
2234
- class TrellisKernel {
2235
- store;
2236
- evaluator;
2237
- eqls;
2238
- backend;
2239
- middleware = [];
2240
- sync;
2241
- opened = false;
2242
- ontologies = new Map;
2243
- projections = new Map;
2244
- constructor(storeOrOpts) {
2245
- const opts = storeOrOpts instanceof EAVStore ? { store: storeOrOpts } : storeOrOpts ?? {};
2246
- this.store = opts.store ?? new EAVStore;
2247
- this.evaluator = new DatalogEvaluator(this.store);
2248
- this.eqls = new EQLSProcessor;
2249
- this.backend = opts.backend;
2250
- this.middleware = opts.middleware ?? [];
2251
- this.sync = opts.sync;
2252
- if (this.sync) {
2253
- this.sync.onRemoteOp(async (op) => {
2254
- await this.applyRemoteOperation(op);
2255
- });
2256
- }
2257
- const autoReplay = opts.autoReplay ?? true;
2258
- if (this.backend && autoReplay) {
2259
- this.open();
2260
- }
2261
- }
2262
- open() {
2263
- if (!this.backend || this.opened)
2264
- return;
2265
- this.backend.init();
2266
- const snapshot = this.backend.loadLatestSnapshot();
2267
- let ops = [];
2268
- if (snapshot) {
2269
- this.store.restore(snapshot.data);
2270
- ops = this.backend.readAfter(snapshot.lastOpHash);
2271
- } else {
2272
- ops = this.backend.readAll();
2273
- }
2274
- for (const op of ops) {
2275
- this.applyOp(op, { system: true });
2276
- }
2277
- this.opened = true;
2278
- }
2279
- async checkpoint() {
2280
- if (!this.backend)
2281
- return;
2282
- const lastOp = this.backend.getLastOp();
2283
- if (!lastOp)
2284
- return;
2285
- const snapshotData = this.store.snapshot();
2286
- this.backend.saveSnapshot(lastOp.hash, snapshotData);
2287
- }
2288
- close() {
2289
- this.backend?.close?.();
2290
- }
2291
- getStore() {
2292
- return this.store;
2293
- }
2294
- async boot(data, opts) {
2295
- if (this.backend) {
2296
- this.open();
2297
- }
2298
- const wsParse = WorkspaceConfigSchema.safeParse(data);
2299
- if (wsParse.success) {
2300
- const config = wsParse.data;
2301
- await this.bootWorkspace(config);
2302
- return;
2303
- }
2304
- if (Array.isArray(data)) {
2305
- const entityType = opts?.entityType ?? "item";
2306
- const idKey = opts?.idKey ?? "id";
2307
- const results = [];
2308
- for (let i = 0;i < data.length; i++) {
2309
- const item = data[i];
2310
- const idVal = item && typeof item === "object" ? item[idKey] : undefined;
2311
- const entityId = `${entityType}:${idVal ?? i}`;
2312
- const facts2 = jsonEntityFacts(entityId, item, entityType);
2313
- results.push(this.appendFacts(facts2));
2314
- }
2315
- await Promise.all(results);
2316
- this.eqls.setSchema(this.store.getCatalog());
2317
- return;
2318
- }
2319
- const rootEntityId = opts?.rootEntityId ?? "root";
2320
- const rootEntityType = opts?.rootEntityType ?? "root";
2321
- const facts = jsonEntityFacts(rootEntityId, data, rootEntityType);
2322
- await this.appendFacts(facts);
2323
- this.eqls.setSchema(this.store.getCatalog());
2324
- }
2325
- async bootWorkspace(config) {
2326
- const ws = config.workspace;
2327
- if (ws.ontologies) {
2328
- for (const [id, schema] of Object.entries(ws.ontologies)) {
2329
- this.ontologies.set(id, schema);
2330
- }
2331
- }
2332
- if (ws.projections) {
2333
- for (const [id, projection] of Object.entries(ws.projections)) {
2334
- this.projections.set(id, projection);
2335
- }
2336
- }
2337
- if (ws.graph) {
2338
- const mutationPromises = [];
2339
- if (ws.graph.nodes) {
2340
- for (const node of ws.graph.nodes) {
2341
- const entityId = node["@id"] || `node:${randomUUID()}`;
2342
- const type = node["@type"] || "default";
2343
- mutationPromises.push(this.createNode(entityId, node, type));
2344
- }
2345
- }
2346
- if (ws.graph.edges) {
2347
- for (const edge of ws.graph.edges) {
2348
- const source = edge.source?.["@id"] || edge.source;
2349
- const target = edge.target?.["@id"] || edge.target;
2350
- const relation = edge.relationType || edge.relation;
2351
- if (source && target && relation) {
2352
- mutationPromises.push(this.link(source, relation, target));
2353
- }
2354
- }
2355
- }
2356
- await Promise.all(mutationPromises);
2357
- }
2358
- this.eqls.setSchema(this.store.getCatalog());
2359
- }
2360
- getProjection(id) {
2361
- return this.projections.get(id);
2362
- }
2363
- listProjections() {
2364
- return Array.from(this.projections.values());
2365
- }
2366
- getOntology(id) {
2367
- return this.ontologies.get(id);
2368
- }
2369
- async exportWorkspace() {
2370
- const ontologies = {};
2371
- for (const [id, schema] of this.ontologies.entries()) {
2372
- ontologies[id] = schema;
2373
- }
2374
- const projections = {};
2375
- for (const [id, proj] of this.projections.entries()) {
2376
- projections[id] = proj;
2377
- }
2378
- const nodes = [];
2379
- const entities = new Set;
2380
- for (const fact of this.store.getAllFacts()) {
2381
- if (fact)
2382
- entities.add(fact.e);
2383
- }
2384
- for (const entityId of entities) {
2385
- const facts = this.store.getFactsByEntity(entityId);
2386
- const node = { "@id": entityId };
2387
- for (const f of facts) {
2388
- if (f.a === "type") {
2389
- node["@type"] = f.v;
2390
- } else {
2391
- node[f.a] = f.v;
2392
- }
2393
- }
2394
- nodes.push(node);
2395
- }
2396
- const edges = [];
2397
- for (const link of this.store.getAllLinks()) {
2398
- edges.push({
2399
- source: { "@id": link.e1 },
2400
- target: { "@id": link.e2 },
2401
- relationType: link.a
2402
- });
2403
- }
2404
- return {
2405
- workspace: {
2406
- ontologies: Object.keys(ontologies).length > 0 ? ontologies : undefined,
2407
- projections: Object.keys(projections).length > 0 ? projections : undefined,
2408
- graph: {
2409
- nodes: nodes.length > 0 ? nodes : undefined,
2410
- edges: edges.length > 0 ? edges : undefined
2411
- }
2412
- }
2413
- };
2414
- }
2415
- async executeProjection(id, ctx = {}) {
2416
- const projection = this.getProjection(id);
2417
- if (!projection) {
2418
- throw new Error(`Projection ${id} not found`);
2419
- }
2420
- return this.query(projection.query, ctx);
2421
- }
2422
- async appendFacts(facts, ctx = {}) {
2423
- const lastOp = this.backend?.getLastOp();
2424
- const op = await createOp("addFacts", {
2425
- agentId: ctx.agentId || "system",
2426
- facts,
2427
- previousHash: lastOp?.hash
2428
- });
2429
- if (this.backend) {
2430
- this.open();
2431
- this.backend.append(op);
2432
- }
2433
- return this.applyOp(op, ctx);
2434
- }
2435
- applyOp(op, ctx = {}) {
2436
- const runMiddleware = (index) => {
2437
- if (index >= this.middleware.length) {
2438
- if (op.kind === "addFacts" && op.facts) {
2439
- this.store.addFacts(op.facts);
2440
- } else if (op.kind === "addLinks" && op.links) {
2441
- this.store.addLinks(op.links);
2442
- } else if (op.kind === "deleteFacts" && op.facts) {
2443
- this.store.deleteFacts(op.facts);
2444
- } else if (op.kind === "deleteLinks" && op.links) {
2445
- this.store.deleteLinks(op.links);
2446
- }
2447
- return;
2448
- }
2449
- const mw = this.middleware[index];
2450
- if (mw && mw.handleOp) {
2451
- return mw.handleOp(op, ctx, (nextOp, nextCtx) => runMiddleware(index + 1));
2452
- } else {
2453
- return runMiddleware(index + 1);
2454
- }
2455
- };
2456
- return runMiddleware(0);
2457
- }
2458
- async mutate(op, ctx = {}) {
2459
- if (this.backend) {
2460
- this.open();
2461
- this.backend.append(op);
2462
- }
2463
- await this.applyOp(op, ctx);
2464
- if (this.sync && !ctx.remote && !ctx.system) {
2465
- await this.sync.broadcast(op);
2466
- }
2467
- }
2468
- async applyRemoteOperation(op, ctx = {}) {
2469
- return this.mutate(op, { ...ctx, remote: true });
2470
- }
2471
- async _mutate(kind, params, ctx) {
2472
- const lastOp = this.backend?.getLastOp();
2473
- const op = await createOp(kind, {
2474
- agentId: ctx.agentId || "system",
2475
- facts: params.facts,
2476
- links: params.links,
2477
- previousHash: lastOp?.hash
2478
- });
2479
- return this.mutate(op, ctx);
2480
- }
2481
- async createNode(entityId, data, type, ctx = {}) {
2482
- const facts = jsonEntityFacts(entityId, data, type);
2483
- await this._mutate("addFacts", { facts }, ctx);
2484
- }
2485
- async updateNode(entityId, data, type, ctx = {}) {
2486
- const existingFacts = this.store.getFactsByEntity(entityId);
2487
- if (existingFacts.length > 0) {
2488
- await this._mutate("deleteFacts", { facts: existingFacts }, ctx);
2489
- }
2490
- const facts = jsonEntityFacts(entityId, data, type);
2491
- await this._mutate("addFacts", { facts }, ctx);
2492
- }
2493
- async deleteNode(entityId, ctx = {}) {
2494
- const facts = this.store.getFactsByEntity(entityId);
2495
- if (facts.length > 0) {
2496
- await this._mutate("deleteFacts", { facts }, ctx);
2497
- }
2498
- }
2499
- async link(e1, a, e2, ctx = {}) {
2500
- await this._mutate("addLinks", { links: [{ e1, a, e2 }] }, ctx);
2501
- }
2502
- query(eqlsQuery, ctx = {}) {
2503
- return this.runQueryMiddleware(eqlsQuery, ctx);
2504
- }
2505
- async queryNatural(nl, opts) {
2506
- const eqlsQuery = await opts.provider.translate(nl, opts.context);
2507
- return this.query(eqlsQuery, opts.context);
2508
- }
2509
- async queryDatalog(query, ctx = {}) {
2510
- return this.runQueryMiddleware(query, ctx);
2511
- }
2512
- runQueryMiddleware(query, ctx) {
2513
- if (this.backend) {
2514
- this.open();
2515
- }
2516
- const runMiddleware = (index, currentQuery, currentCtx) => {
2517
- if (index >= this.middleware.length) {
2518
- if (this.backend && (currentCtx.atHash || currentCtx.atTimestamp)) {
2519
- const ephemeralStore = new EAVStore;
2520
- const ops = currentCtx.atHash ? this.backend.readUntil(currentCtx.atHash) : this.backend.readUntilTimestamp(currentCtx.atTimestamp);
2521
- for (const op of ops) {
2522
- if (op.kind === "addFacts" && op.facts) {
2523
- ephemeralStore.addFacts(op.facts);
2524
- } else if (op.kind === "addLinks" && op.links) {
2525
- ephemeralStore.addLinks(op.links);
2526
- } else if (op.kind === "deleteFacts" && op.facts) {
2527
- ephemeralStore.deleteFacts(op.facts);
2528
- } else if (op.kind === "deleteLinks" && op.links) {
2529
- ephemeralStore.deleteLinks(op.links);
2530
- }
2531
- }
2532
- return this.executeBaseQuery(currentQuery, ephemeralStore);
2533
- }
2534
- return this.executeBaseQuery(currentQuery);
2535
- }
2536
- const mw = this.middleware[index];
2537
- if (mw && mw.handleQuery) {
2538
- return mw.handleQuery(currentQuery, currentCtx, (q, c) => runMiddleware(index + 1, q, c));
2539
- } else {
2540
- return runMiddleware(index + 1, currentQuery, currentCtx);
2541
- }
2542
- };
2543
- return runMiddleware(0, query, ctx);
2544
- }
2545
- executeBaseQuery(queryOrEqls, storeOverride) {
2546
- const store = storeOverride || this.store;
2547
- const evaluator = storeOverride ? new DatalogEvaluator(storeOverride) : this.evaluator;
2548
- this.eqls.setSchema(store.getCatalog());
2549
- if (typeof queryOrEqls !== "string") {
2550
- const exec = evaluator.evaluate(queryOrEqls);
2551
- return {
2552
- rows: exec.bindings,
2553
- executionTime: exec.executionTime,
2554
- plan: exec.plan,
2555
- bindings: exec.bindings
2556
- };
2557
- }
2558
- const processed = this.eqls.process(queryOrEqls);
2559
- if (processed.errors.length > 0 || !processed.query) {
2560
- const message = processed.errors.map((e) => e.message).join("; ");
2561
- throw new Error(message || "Query parsing failed");
2562
- }
2563
- const compiledQueries = processed.queries && processed.queries.length > 0 ? processed.queries : [processed.query];
2564
- const projectionMap = processed.projectionMap || new Map;
2565
- const mergedRows = [];
2566
- const seen = new Set;
2567
- let totalTime = 0;
2568
- const plans = [];
2569
- const allTraces = [];
2570
- for (const q of compiledQueries) {
2571
- const exec = evaluator.evaluate(q);
2572
- totalTime += exec.executionTime;
2573
- if (exec.plan)
2574
- plans.push(exec.plan);
2575
- if (exec.trace)
2576
- allTraces.push(...exec.trace);
2577
- for (const binding of exec.bindings) {
2578
- const row = projectionMap.size === 0 ? binding : (() => {
2579
- const projected = {};
2580
- for (const [field, varName] of projectionMap.entries()) {
2581
- projected[field] = binding[varName];
2582
- }
2583
- return projected;
2584
- })();
2585
- const k = JSON.stringify(row);
2586
- if (!seen.has(k)) {
2587
- seen.add(k);
2588
- mergedRows.push(row);
2589
- }
2590
- }
2591
- }
2592
- const orderBy = processed.meta?.orderBy;
2593
- if (orderBy?.field) {
2594
- const dir = orderBy.direction === "DESC" ? -1 : 1;
2595
- mergedRows.sort((ra, rb) => dir * stableCompare(ra[orderBy.field], rb[orderBy.field]));
2596
- }
2597
- const limit = processed.meta?.limit;
2598
- const finalRows = typeof limit === "number" && limit >= 0 ? mergedRows.slice(0, limit) : mergedRows;
2599
- return {
2600
- rows: finalRows,
2601
- executionTime: totalTime,
2602
- plan: plans.length > 0 ? plans.join(" | ") : undefined,
2603
- trace: allTraces.length > 0 ? allTraces : undefined
2604
- };
2605
- }
2606
- }
2607
-
2608
- // src/persist/sqlite-backend.ts
2609
- import { createRequire as createRequire2 } from "module";
2610
- var require2 = createRequire2(import.meta.url);
2611
- var encodeAtom = (v) => {
2612
- if (v instanceof Date)
2613
- return { $type: "date", value: v.toISOString() };
2614
- return v;
2615
- };
2616
- var decodeAtom = (v) => {
2617
- if (v && typeof v === "object" && "$type" in v && v.$type === "date" && typeof v.value === "string") {
2618
- return new Date(v.value);
2619
- }
2620
- return v;
2621
- };
2622
- var encodeOp = (op) => {
2623
- const encoded = { ...op };
2624
- if (encoded.facts) {
2625
- encoded.facts = encoded.facts.map((f) => ({
2626
- ...f,
2627
- v: encodeAtom(f.v)
2628
- }));
2629
- }
2630
- return encoded;
2631
- };
2632
- var decodeOp = (raw) => {
2633
- const decoded = { ...raw };
2634
- if (decoded.facts) {
2635
- decoded.facts = decoded.facts.map((f) => ({
2636
- ...f,
2637
- v: decodeAtom(f.v)
2638
- }));
2639
- }
2640
- return decoded;
2641
- };
2642
-
2643
- class SqliteKernelBackend {
2644
- opts;
2645
- db;
2646
- constructor(opts) {
2647
- this.opts = opts;
2648
- const mod = require2(["bun", "sqlite"].join(":"));
2649
- const DatabaseCtor = mod?.Database;
2650
- if (!DatabaseCtor) {
2651
- throw new Error("bun:sqlite is not available in this runtime");
2652
- }
2653
- this.db = new DatabaseCtor(opts.filename);
2654
- }
2655
- init() {
2656
- this.db.exec(`
2657
- PRAGMA journal_mode = WAL;
2658
- CREATE TABLE IF NOT EXISTS ops (
2659
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2660
- hash TEXT NOT NULL UNIQUE,
2661
- ts INTEGER NOT NULL,
2662
- kind TEXT NOT NULL,
2663
- payload TEXT NOT NULL
2664
- );
2665
- CREATE INDEX IF NOT EXISTS idx_ops_hash ON ops(hash);
2666
-
2667
- CREATE TABLE IF NOT EXISTS snapshots (
2668
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2669
- last_op_hash TEXT NOT NULL,
2670
- ts INTEGER NOT NULL,
2671
- data TEXT NOT NULL
2672
- );
2673
- CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON snapshots(ts);
2674
- `);
2675
- }
2676
- append(op) {
2677
- const stmt = this.db.prepare("INSERT INTO ops (hash, ts, kind, payload) VALUES (?, ?, ?, ?)");
2678
- stmt.run(op.hash, new Date(op.timestamp).getTime(), op.kind, JSON.stringify(encodeOp(op)));
2679
- }
2680
- readAll() {
2681
- const rows = this.db.prepare("SELECT payload FROM ops ORDER BY id ASC").all();
2682
- return rows.map((r) => decodeOp(JSON.parse(r.payload)));
2683
- }
2684
- readUntil(hash) {
2685
- const target = this.db.prepare("SELECT id FROM ops WHERE hash = ?").get(hash);
2686
- if (!target) {
2687
- throw new Error(`Operation with hash ${hash} not found`);
2688
- }
2689
- const rows = this.db.prepare("SELECT payload FROM ops WHERE id <= ? ORDER BY id ASC").all(target.id);
2690
- return rows.map((r) => decodeOp(JSON.parse(r.payload)));
2691
- }
2692
- readAfter(hash) {
2693
- const target = this.db.prepare("SELECT id FROM ops WHERE hash = ?").get(hash);
2694
- if (!target) {
2695
- throw new Error(`Operation with hash ${hash} not found`);
2696
- }
2697
- const rows = this.db.prepare("SELECT payload FROM ops WHERE id > ? ORDER BY id ASC").all(target.id);
2698
- return rows.map((r) => decodeOp(JSON.parse(r.payload)));
2699
- }
2700
- readUntilTimestamp(isoTimestamp) {
2701
- const ts = new Date(isoTimestamp).getTime();
2702
- const rows = this.db.prepare("SELECT payload FROM ops WHERE ts <= ? ORDER BY id ASC").all(ts);
2703
- return rows.map((r) => decodeOp(JSON.parse(r.payload)));
2704
- }
2705
- getLastOp() {
2706
- const row = this.db.prepare("SELECT payload FROM ops ORDER BY id DESC LIMIT 1").get();
2707
- if (!row)
2708
- return;
2709
- return decodeOp(JSON.parse(row.payload));
2710
- }
2711
- saveSnapshot(lastOpHash, data) {
2712
- const stmt = this.db.prepare("INSERT INTO snapshots (last_op_hash, ts, data) VALUES (?, ?, ?)");
2713
- stmt.run(lastOpHash, Date.now(), JSON.stringify(data));
2714
- }
2715
- loadLatestSnapshot() {
2716
- const row = this.db.prepare("SELECT last_op_hash, data FROM snapshots ORDER BY ts DESC LIMIT 1").get();
2717
- if (!row)
2718
- return;
2719
- return {
2720
- lastOpHash: row.last_op_hash,
2721
- data: JSON.parse(row.data)
2722
- };
2723
- }
2724
- close() {
2725
- this.db.close();
2726
- }
2727
- }
2728
-
2729
- // src/server/tenant-registry.ts
2730
- import { createRequire as createRequire3 } from "module";
2731
- import { randomUUID as randomUUID2 } from "crypto";
2732
- import { join } from "node:path";
2733
- var require3 = createRequire3(import.meta.url);
2734
- function generateApiKey() {
2735
- return "tql_" + randomUUID2().replace(/-/g, "").slice(0, 32);
2736
- }
2737
-
2738
- class TenantRegistry {
2739
- db;
2740
- constructor(registryDbPath) {
2741
- const mod = require3(["bun", "sqlite"].join(":"));
2742
- this.db = new mod.Database(registryDbPath);
2743
- this.init();
2744
- }
2745
- init() {
2746
- this.db.run("PRAGMA journal_mode = WAL");
2747
- this.db.run(`
2748
- CREATE TABLE IF NOT EXISTS api_keys (
2749
- key TEXT PRIMARY KEY,
2750
- label TEXT NOT NULL,
2751
- db_path TEXT NOT NULL,
2752
- created_at TEXT NOT NULL
2753
- )
2754
- `);
2755
- }
2756
- provision(dataDir, label) {
2757
- const key = generateApiKey();
2758
- const dbPath = join(dataDir, `${key}.db`);
2759
- const createdAt = new Date().toISOString();
2760
- const effectiveLabel = label ?? key;
2761
- this.db.run("INSERT INTO api_keys (key, label, db_path, created_at) VALUES (?, ?, ?, ?)", [key, effectiveLabel, dbPath, createdAt]);
2762
- return { key, label: effectiveLabel, dbPath, createdAt };
2763
- }
2764
- lookup(key) {
2765
- const row = this.db.query("SELECT key, label, db_path, created_at FROM api_keys WHERE key = ?").get(key);
2766
- if (!row)
2767
- return;
2768
- return { key: row.key, label: row.label, dbPath: row.db_path, createdAt: row.created_at };
2769
- }
2770
- revoke(key) {
2771
- const existing = this.lookup(key);
2772
- if (!existing)
2773
- return false;
2774
- this.db.run("DELETE FROM api_keys WHERE key = ?", [key]);
2775
- return true;
2776
- }
2777
- list() {
2778
- const rows = this.db.query("SELECT key, label, db_path, created_at FROM api_keys ORDER BY created_at ASC").all();
2779
- return rows.map((r) => ({
2780
- key: r.key,
2781
- label: r.label,
2782
- dbPath: r.db_path,
2783
- createdAt: r.created_at
2784
- }));
2785
- }
2786
- close() {
2787
- this.db.close();
2788
- }
2789
- }
2790
-
2791
- // src/server/kernel-pool.ts
2792
- class KernelPool {
2793
- pool = new Map;
2794
- getOrCreate(dbPath) {
2795
- const existing = this.pool.get(dbPath);
2796
- if (existing)
2797
- return existing;
2798
- const backend = new SqliteKernelBackend({ filename: dbPath });
2799
- const kernel = new TrellisKernel({ backend });
2800
- this.pool.set(dbPath, kernel);
2801
- return kernel;
2802
- }
2803
- close(dbPath) {
2804
- const kernel = this.pool.get(dbPath);
2805
- if (kernel) {
2806
- kernel.close();
2807
- this.pool.delete(dbPath);
2808
- }
2809
- }
2810
- closeAll() {
2811
- for (const [dbPath] of this.pool) {
2812
- this.close(dbPath);
2813
- }
2814
- }
2815
- }
2816
-
2817
- // src/cli/server.ts
2818
- var DEFAULT_PORT = 8080;
2819
- function jsonResponse(data, status = 200) {
2820
- return new Response(JSON.stringify(data, null, 2), {
2821
- status,
2822
- headers: {
2823
- "Content-Type": "application/json",
2824
- "Access-Control-Allow-Origin": "*",
2825
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
2826
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
2827
- }
2828
- });
2829
- }
2830
- function errorResponse(message, status = 400) {
2831
- return jsonResponse({ error: message }, status);
2832
- }
2833
- async function parseBody(req) {
2834
- const body = await req.json();
2835
- return body;
2836
- }
2837
-
2838
- class TQLServer {
2839
- kernel;
2840
- port;
2841
- adminKey;
2842
- dataDir;
2843
- registry;
2844
- pool;
2845
- constructor(opts = {}) {
2846
- this.port = opts.port ?? DEFAULT_PORT;
2847
- this.adminKey = opts.adminKey;
2848
- this.dataDir = opts.dataDir;
2849
- if (opts.adminKey) {
2850
- if (!opts.dataDir) {
2851
- throw new Error("dataDir is required when adminKey is provided");
2852
- }
2853
- const registryDb = opts.registryDb ?? join2(opts.dataDir, "registry.db");
2854
- this.registry = new TenantRegistry(registryDb);
2855
- this.pool = new KernelPool;
2856
- this.kernel = null;
2857
- } else {
2858
- this.registry = null;
2859
- this.pool = null;
2860
- if (opts.db) {
2861
- const backend = new SqliteKernelBackend({ filename: opts.db });
2862
- this.kernel = new TrellisKernel({ backend });
2863
- } else {
2864
- this.kernel = new TrellisKernel;
2865
- }
2866
- }
2867
- }
2868
- getKernel() {
2869
- return this.kernel;
2870
- }
2871
- extractBearerToken(req) {
2872
- const auth = req.headers.get("Authorization") ?? req.headers.get("authorization");
2873
- if (!auth?.startsWith("Bearer "))
2874
- return null;
2875
- return auth.slice(7).trim();
2876
- }
2877
- getEntitiesFrom(kernel) {
2878
- const entities = new Set;
2879
- for (const fact of kernel.getStore().getAllFacts()) {
2880
- entities.add(fact.e);
2881
- }
2882
- return Array.from(entities).sort();
2883
- }
2884
- async handleRequest(req) {
2885
- const url = new URL(req.url);
2886
- const path = url.pathname;
2887
- const method = req.method;
2888
- if (method === "OPTIONS") {
2889
- return jsonResponse({ ok: true });
2890
- }
2891
- if (this.adminKey !== undefined) {
2892
- const token = this.extractBearerToken(req);
2893
- if (path === "/instances" || path.startsWith("/instances/")) {
2894
- if (token !== this.adminKey) {
2895
- return errorResponse("Unauthorized", 401);
2896
- }
2897
- return this.handleAdminRequest(req, path, method);
2898
- }
2899
- if (!token) {
2900
- return errorResponse("Unauthorized", 401);
2901
- }
2902
- const tenant = this.registry.lookup(token);
2903
- if (!tenant) {
2904
- return errorResponse("Unauthorized", 401);
2905
- }
2906
- const kernel = this.pool.getOrCreate(tenant.dbPath);
2907
- return this.handleTenantRequest(req, url, path, method, kernel);
2908
- }
2909
- return this.handleTenantRequest(req, url, path, method, this.kernel);
2910
- }
2911
- async handleAdminRequest(req, path, method) {
2912
- if (path === "/instances" && method === "POST") {
2913
- const body = await req.json();
2914
- const record = this.registry.provision(this.dataDir, body.label);
2915
- return jsonResponse({ apiKey: record.key, label: record.label, dbPath: record.dbPath, createdAt: record.createdAt }, 201);
2916
- }
2917
- if (path === "/instances" && method === "GET") {
2918
- const tenants = this.registry.list().map((r) => ({
2919
- apiKey: r.key,
2920
- label: r.label,
2921
- dbPath: r.dbPath,
2922
- createdAt: r.createdAt
2923
- }));
2924
- return jsonResponse({ tenants });
2925
- }
2926
- if (path.startsWith("/instances/") && method === "DELETE") {
2927
- const key = decodeURIComponent(path.slice("/instances/".length));
2928
- const tenant = this.registry.lookup(key);
2929
- if (!tenant) {
2930
- return errorResponse(`Not found: ${key}`, 404);
2931
- }
2932
- this.pool.close(tenant.dbPath);
2933
- this.registry.revoke(key);
2934
- return jsonResponse({ revoked: true });
2935
- }
2936
- return errorResponse(`Not found: ${method} ${path}`, 404);
2937
- }
2938
- async handleTenantRequest(req, url, path, method, kernel) {
2939
- try {
2940
- if (path === "/" && method === "GET") {
2941
- return jsonResponse({
2942
- name: "TQL HTTP API",
2943
- version: "1.0.0",
2944
- description: "REST API for the Trellis Query Language kernel",
2945
- endpoints: {
2946
- "GET /": "This index",
2947
- "GET /health": "Health check and uptime",
2948
- "GET /stats": "Fact/link/entity counts",
2949
- "GET /catalog": "Attribute catalog with types and examples",
2950
- "GET /nodes": "List all entity IDs",
2951
- "POST /nodes": "Create a node { id, data, type? }",
2952
- "GET /nodes/:id": "Get all facts for an entity",
2953
- "PUT /nodes/:id": "Update a node { data, type? }",
2954
- "DELETE /nodes/:id": "Delete a node and its facts",
2955
- "GET /links": "List all links",
2956
- "POST /links": "Create a link { source, relation, target }",
2957
- "POST /query": "Run an EQL-S query { q, natural?, context? }",
2958
- "GET /query?q=": "Run an EQL-S query via GET",
2959
- "POST /query/datalog": "Run a raw Datalog query { query }",
2960
- "GET /workspace": "Export workspace config",
2961
- "POST /workspace": "Boot from workspace config",
2962
- "POST /workspace/checkpoint": "Save a state snapshot",
2963
- "GET /openapi.json": "OpenAPI 3.0 spec"
2964
- },
2965
- query_syntax: "FIND <type> AS ?var [WHERE <conditions>] RETURN <fields>",
2966
- examples: {
2967
- create_node: 'POST /nodes {"id":"alice","data":{"name":"Alice","age":30},"type":"person"}',
2968
- create_link: 'POST /links {"source":"alice","relation":"works_at","target":"acme"}',
2969
- query: 'POST /query {"q":"FIND person AS ?e RETURN ?e.name, ?e.age"}',
2970
- query_filter: 'POST /query {"q":"FIND person AS ?e WHERE ?e.age > 25 RETURN ?e.name"}'
2971
- }
2972
- });
2973
- }
2974
- if (path === "/health" && method === "GET") {
2975
- return jsonResponse({ status: "ok", uptime: process.uptime() });
2976
- }
2977
- if (path === "/stats" && method === "GET") {
2978
- return jsonResponse(kernel.getStore().getStats());
2979
- }
2980
- if (path === "/catalog" && method === "GET") {
2981
- return jsonResponse(kernel.getStore().getCatalog());
2982
- }
2983
- if (path === "/nodes" && method === "GET") {
2984
- return jsonResponse({ entities: this.getEntitiesFrom(kernel) });
2985
- }
2986
- if (path === "/nodes" && method === "POST") {
2987
- const body = await parseBody(req);
2988
- const { id, data, type } = body;
2989
- if (!id || !data) {
2990
- return errorResponse("Missing required fields: id, data");
2991
- }
2992
- await kernel.createNode(id, data, type ?? "default");
2993
- return jsonResponse({ id, created: true }, 201);
2994
- }
2995
- if (path.startsWith("/nodes/") && method === "GET") {
2996
- const id = decodeURIComponent(path.slice(7));
2997
- const facts = kernel.getStore().getFactsByEntity(id);
2998
- return jsonResponse({ id, facts });
2999
- }
3000
- if (path.startsWith("/nodes/") && method === "PUT") {
3001
- const id = decodeURIComponent(path.slice(7));
3002
- const body = await parseBody(req);
3003
- const { data, type } = body;
3004
- if (!data) {
3005
- return errorResponse("Missing required field: data");
3006
- }
3007
- await kernel.updateNode(id, data, type ?? "default");
3008
- return jsonResponse({ id, updated: true });
3009
- }
3010
- if (path.startsWith("/nodes/") && method === "DELETE") {
3011
- const id = decodeURIComponent(path.slice(7));
3012
- await kernel.deleteNode(id);
3013
- return jsonResponse({ id, deleted: true });
3014
- }
3015
- if (path === "/links" && method === "GET") {
3016
- return jsonResponse({ links: kernel.getStore().getAllLinks() });
3017
- }
3018
- if (path === "/links" && method === "POST") {
3019
- const body = await parseBody(req);
3020
- const { source, target, relation } = body;
3021
- if (!source || !target || !relation) {
3022
- return errorResponse("Missing required fields: source, target, relation");
3023
- }
3024
- await kernel.link(source, relation, target);
3025
- return jsonResponse({ source, target, relation, created: true }, 201);
3026
- }
3027
- if (path === "/query" && method === "POST") {
3028
- const body = await parseBody(req);
3029
- const { q, natural, context } = body;
3030
- if (!q) {
3031
- return errorResponse("Missing required field: q");
3032
- }
3033
- const start = performance.now();
3034
- let result;
3035
- if (natural) {
3036
- const { DefaultNLQueryProvider: DefaultNLQueryProvider2 } = await Promise.resolve().then(() => (init_nl_query_provider(), exports_nl_query_provider));
3037
- const nlProvider = new DefaultNLQueryProvider2({
3038
- catalog: kernel.getStore().getCatalog(),
3039
- dataStats: kernel.getStore().getStats()
3040
- });
3041
- result = await kernel.queryNatural(q, { provider: nlProvider, context });
3042
- } else {
3043
- const queryResult = kernel.query(q);
3044
- result = queryResult instanceof Promise ? await queryResult : queryResult;
3045
- }
3046
- const elapsed = performance.now() - start;
3047
- return jsonResponse({
3048
- rows: result.rows,
3049
- executionTime: result.executionTime ?? elapsed,
3050
- plan: result.plan,
3051
- rowCount: result.rows.length
3052
- });
3053
- }
3054
- if (path === "/query" && method === "GET") {
3055
- const q = url.searchParams.get("q");
3056
- const natural = url.searchParams.get("natural") === "true";
3057
- if (!q) {
3058
- return errorResponse("Missing query parameter: q");
3059
- }
3060
- const start = performance.now();
3061
- let result;
3062
- if (natural) {
3063
- const { DefaultNLQueryProvider: DefaultNLQueryProvider2 } = await Promise.resolve().then(() => (init_nl_query_provider(), exports_nl_query_provider));
3064
- const nlProvider = new DefaultNLQueryProvider2({
3065
- catalog: kernel.getStore().getCatalog(),
3066
- dataStats: kernel.getStore().getStats()
3067
- });
3068
- result = await kernel.queryNatural(q, { provider: nlProvider });
3069
- } else {
3070
- const queryResult = kernel.query(q);
3071
- result = queryResult instanceof Promise ? await queryResult : queryResult;
3072
- }
3073
- const elapsed = performance.now() - start;
3074
- return jsonResponse({
3075
- rows: result.rows,
3076
- executionTime: result.executionTime ?? elapsed,
3077
- plan: result.plan,
3078
- rowCount: result.rows.length
3079
- });
3080
- }
3081
- if (path === "/query/datalog" && method === "POST") {
3082
- const body = await parseBody(req);
3083
- const { query } = body;
3084
- if (!query) {
3085
- return errorResponse("Missing required field: query");
3086
- }
3087
- const result = await kernel.queryDatalog(query);
3088
- return jsonResponse({
3089
- rows: result.rows,
3090
- executionTime: result.executionTime,
3091
- plan: result.plan,
3092
- rowCount: result.rows.length
3093
- });
3094
- }
3095
- if (path === "/workspace" && method === "POST") {
3096
- const body = await req.json();
3097
- await kernel.boot(body);
3098
- return jsonResponse({ booted: true });
3099
- }
3100
- if (path === "/workspace" && method === "GET") {
3101
- const workspace = await kernel.exportWorkspace();
3102
- return jsonResponse(workspace);
3103
- }
3104
- if (path === "/workspace/checkpoint" && method === "POST") {
3105
- await kernel.checkpoint();
3106
- return jsonResponse({ checkpoint: true });
3107
- }
3108
- if (path === "/openapi.json" && method === "GET") {
3109
- return jsonResponse(this.generateOpenAPI());
3110
- }
3111
- return errorResponse(`Not found: ${method} ${path}`, 404);
3112
- } catch (err) {
3113
- const message = err instanceof Error ? err.message : "Internal error";
3114
- console.error(`Error handling ${method} ${path}:`, err);
3115
- return errorResponse(message, 500);
3116
- }
3117
- }
3118
- generateOpenAPI() {
3119
- const securitySchemes = this.adminKey ? { BearerAuth: { type: "http", scheme: "bearer" } } : undefined;
3120
- const security = this.adminKey ? [{ BearerAuth: [] }] : undefined;
3121
- return {
3122
- openapi: "3.0.0",
3123
- info: {
3124
- title: "TQL HTTP API",
3125
- version: "1.0.0",
3126
- description: "REST API for Trellis Query Language kernel"
3127
- },
3128
- servers: [{ url: `http://localhost:${this.port}` }],
3129
- ...securitySchemes ? { components: { securitySchemes } } : {},
3130
- ...security ? { security } : {},
3131
- paths: {
3132
- "/instances": {
3133
- get: {
3134
- summary: "List all tenants (admin only)",
3135
- responses: { "200": { description: "Tenant list" } }
3136
- },
3137
- post: {
3138
- summary: "Provision a new tenant (admin only)",
3139
- requestBody: {
3140
- content: {
3141
- "application/json": {
3142
- schema: { type: "object", properties: { label: { type: "string" } } }
3143
- }
3144
- }
3145
- },
3146
- responses: { "201": { description: "Provisioned" } }
3147
- }
3148
- },
3149
- "/instances/{key}": {
3150
- delete: {
3151
- summary: "Revoke a tenant key (admin only)",
3152
- parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
3153
- responses: { "200": { description: "Revoked" } }
3154
- }
3155
- },
3156
- "/health": {
3157
- get: { summary: "Health check", responses: { "200": { description: "OK" } } }
3158
- },
3159
- "/stats": {
3160
- get: { summary: "Get store statistics", responses: { "200": { description: "Store stats" } } }
3161
- },
3162
- "/catalog": {
3163
- get: { summary: "Get data catalog", responses: { "200": { description: "Attribute catalog" } } }
3164
- },
3165
- "/nodes": {
3166
- get: { summary: "List all entities", responses: { "200": { description: "Entity list" } } },
3167
- post: {
3168
- summary: "Create a node",
3169
- requestBody: {
3170
- content: {
3171
- "application/json": {
3172
- schema: {
3173
- type: "object",
3174
- required: ["id", "data"],
3175
- properties: { id: { type: "string" }, data: {}, type: { type: "string" } }
3176
- }
3177
- }
3178
- }
3179
- },
3180
- responses: { "201": { description: "Created" } }
3181
- }
3182
- },
3183
- "/nodes/{id}": {
3184
- get: {
3185
- summary: "Get entity facts",
3186
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3187
- responses: { "200": { description: "Entity facts" } }
3188
- },
3189
- put: {
3190
- summary: "Update a node",
3191
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3192
- requestBody: {
3193
- content: {
3194
- "application/json": {
3195
- schema: {
3196
- type: "object",
3197
- required: ["data"],
3198
- properties: { data: {}, type: { type: "string" } }
3199
- }
3200
- }
3201
- }
3202
- },
3203
- responses: { "200": { description: "Updated" } }
3204
- },
3205
- delete: {
3206
- summary: "Delete a node",
3207
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3208
- responses: { "200": { description: "Deleted" } }
3209
- }
3210
- },
3211
- "/links": {
3212
- get: { summary: "List all links", responses: { "200": { description: "Link list" } } },
3213
- post: {
3214
- summary: "Create a link",
3215
- requestBody: {
3216
- content: {
3217
- "application/json": {
3218
- schema: {
3219
- type: "object",
3220
- required: ["source", "target", "relation"],
3221
- properties: {
3222
- source: { type: "string" },
3223
- target: { type: "string" },
3224
- relation: { type: "string" }
3225
- }
3226
- }
3227
- }
3228
- }
3229
- },
3230
- responses: { "201": { description: "Created" } }
3231
- }
3232
- },
3233
- "/query": {
3234
- get: {
3235
- summary: "Execute query via GET",
3236
- parameters: [
3237
- { name: "q", in: "query", required: true, schema: { type: "string" } },
3238
- { name: "natural", in: "query", schema: { type: "boolean" } }
3239
- ],
3240
- responses: { "200": { description: "Query results" } }
3241
- },
3242
- post: {
3243
- summary: "Execute EQL-S or natural language query",
3244
- requestBody: {
3245
- content: {
3246
- "application/json": {
3247
- schema: {
3248
- type: "object",
3249
- required: ["q"],
3250
- properties: {
3251
- q: { type: "string" },
3252
- natural: { type: "boolean" },
3253
- context: { type: "object" }
3254
- }
3255
- }
3256
- }
3257
- }
3258
- },
3259
- responses: { "200": { description: "Query results" } }
3260
- }
3261
- },
3262
- "/query/datalog": {
3263
- post: {
3264
- summary: "Execute raw Datalog query",
3265
- requestBody: {
3266
- content: {
3267
- "application/json": {
3268
- schema: { type: "object", required: ["query"], properties: { query: {} } }
3269
- }
3270
- }
3271
- },
3272
- responses: { "200": { description: "Query results" } }
3273
- }
3274
- },
3275
- "/workspace": {
3276
- get: {
3277
- summary: "Export workspace config",
3278
- responses: { "200": { description: "Workspace config" } }
3279
- },
3280
- post: {
3281
- summary: "Boot from workspace config or data",
3282
- requestBody: { content: { "application/json": {} } },
3283
- responses: { "200": { description: "Booted" } }
3284
- }
3285
- },
3286
- "/workspace/checkpoint": {
3287
- post: { summary: "Save state snapshot", responses: { "200": { description: "Checkpoint saved" } } }
3288
- },
3289
- "/openapi.json": {
3290
- get: { summary: "OpenAPI spec", responses: { "200": { description: "OpenAPI 3.0 document" } } }
3291
- }
3292
- }
3293
- };
3294
- }
3295
- start() {
3296
- return new Promise((resolve) => {
3297
- const self = this;
3298
- const server = Bun.serve({
3299
- port: this.port,
3300
- async fetch(req) {
3301
- return self.handleRequest(req);
3302
- }
3303
- });
3304
- const actualPort = server.port ?? this.port;
3305
- const mode = this.adminKey ? "multi-tenant" : "single-tenant";
3306
- console.log(`\uD83D\uDE80 TQL HTTP server running at http://localhost:${actualPort} [${mode}]`);
3307
- resolve({ port: actualPort });
3308
- });
3309
- }
3310
- }
3311
- if (__require.main == __require.module) {
3312
- const port = parseInt(process.argv[2] ?? String(DEFAULT_PORT));
3313
- const db = process.argv.find((a) => a.startsWith("--db="))?.split("=")[1];
3314
- const adminKey = process.env.ADMIN_KEY;
3315
- const dataDir = process.env.DATA_DIR;
3316
- const server = new TQLServer({ port, db, adminKey, dataDir });
3317
- server.start();
3318
- }
3319
- export {
3320
- TQLServer
3321
- };