trellis 2.0.8 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -116
- package/dist/cli/index.js +655 -4
- package/dist/core/index.js +471 -2
- package/dist/embeddings/index.js +5 -1
- package/dist/{index-s603ev6w.js → index-5b01h414.js} +1 -1
- package/dist/index-5m0g9r0y.js +1100 -0
- package/dist/{index-zf6htvnm.js → index-7gvjxt27.js} +166 -2
- package/dist/index-hybgxe40.js +1174 -0
- package/dist/index.js +7 -2
- package/dist/transformers.node-bx3q9d7k.js +33130 -0
- package/package.json +9 -4
- package/src/cli/index.ts +939 -0
- package/src/core/agents/harness.ts +380 -0
- package/src/core/agents/index.ts +18 -0
- package/src/core/agents/types.ts +90 -0
- package/src/core/index.ts +85 -2
- package/src/core/kernel/trellis-kernel.ts +593 -0
- package/src/core/ontology/builtins.ts +248 -0
- package/src/core/ontology/index.ts +34 -0
- package/src/core/ontology/registry.ts +209 -0
- package/src/core/ontology/types.ts +124 -0
- package/src/core/ontology/validator.ts +382 -0
- package/src/core/persist/backend.ts +10 -0
- package/src/core/persist/sqlite-backend.ts +298 -0
- package/src/core/plugins/index.ts +17 -0
- package/src/core/plugins/registry.ts +322 -0
- package/src/core/plugins/types.ts +126 -0
- package/src/core/query/datalog.ts +188 -0
- package/src/core/query/engine.ts +370 -0
- package/src/core/query/index.ts +34 -0
- package/src/core/query/parser.ts +481 -0
- package/src/core/query/types.ts +200 -0
- package/src/embeddings/auto-embed.ts +248 -0
- package/src/embeddings/index.ts +7 -0
- package/src/embeddings/model.ts +21 -4
- package/src/embeddings/types.ts +8 -1
- package/src/index.ts +9 -0
- package/src/sync/http-transport.ts +144 -0
- package/src/sync/index.ts +11 -0
- package/src/sync/multi-repo.ts +200 -0
- package/src/sync/ws-transport.ts +145 -0
- package/dist/index-5bhe57y9.js +0 -326
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Registry — Load, register, and manage plugins.
|
|
3
|
+
*
|
|
4
|
+
* Handles plugin lifecycle, dependency resolution, event dispatching,
|
|
5
|
+
* and workspace configuration.
|
|
6
|
+
*
|
|
7
|
+
* @module trellis/core/plugins
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TrellisKernel } from '../kernel/trellis-kernel.js';
|
|
11
|
+
import type { OntologyRegistry } from '../ontology/registry.js';
|
|
12
|
+
import type { QueryEngine } from '../query/engine.js';
|
|
13
|
+
import type {
|
|
14
|
+
PluginDef,
|
|
15
|
+
PluginContext,
|
|
16
|
+
PluginManifest,
|
|
17
|
+
EventCallback,
|
|
18
|
+
EventHandler,
|
|
19
|
+
WorkspaceConfig,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Event Bus
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export class EventBus {
|
|
27
|
+
private handlers: Map<string, Set<EventCallback>> = new Map();
|
|
28
|
+
|
|
29
|
+
on(event: string, handler: EventCallback): void {
|
|
30
|
+
const set = this.handlers.get(event) ?? new Set();
|
|
31
|
+
set.add(handler);
|
|
32
|
+
this.handlers.set(event, set);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
off(event: string, handler: EventCallback): void {
|
|
36
|
+
const set = this.handlers.get(event);
|
|
37
|
+
if (set) {
|
|
38
|
+
set.delete(handler);
|
|
39
|
+
if (set.size === 0) this.handlers.delete(event);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async emit(event: string, data?: unknown): Promise<void> {
|
|
44
|
+
// Exact match
|
|
45
|
+
const exact = this.handlers.get(event);
|
|
46
|
+
if (exact) {
|
|
47
|
+
for (const h of exact) await h(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wildcard match (e.g. "op:*" matches "op:applied")
|
|
51
|
+
for (const [pattern, handlers] of this.handlers) {
|
|
52
|
+
if (pattern === event) continue; // Already handled
|
|
53
|
+
if (pattern.endsWith('*') && event.startsWith(pattern.slice(0, -1))) {
|
|
54
|
+
for (const h of handlers) await h(data);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
listEvents(): string[] {
|
|
60
|
+
return [...this.handlers.keys()];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear(): void {
|
|
64
|
+
this.handlers.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Plugin Registry
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export class PluginRegistry {
|
|
73
|
+
private plugins: Map<string, { def: PluginDef; loaded: boolean }> = new Map();
|
|
74
|
+
private eventBus: EventBus = new EventBus();
|
|
75
|
+
private workspaceConfig: WorkspaceConfig = {};
|
|
76
|
+
private logs: Array<{ pluginId: string; message: string; timestamp: string }> = [];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a plugin definition. Does not load it yet.
|
|
80
|
+
*/
|
|
81
|
+
register(def: PluginDef): void {
|
|
82
|
+
if (this.plugins.has(def.id)) {
|
|
83
|
+
throw new Error(`Plugin "${def.id}" is already registered.`);
|
|
84
|
+
}
|
|
85
|
+
this.plugins.set(def.id, { def, loaded: false });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Unregister a plugin. Unloads it first if loaded.
|
|
90
|
+
*/
|
|
91
|
+
async unregister(id: string): Promise<void> {
|
|
92
|
+
const entry = this.plugins.get(id);
|
|
93
|
+
if (!entry) return;
|
|
94
|
+
if (entry.loaded) await this.unload(id);
|
|
95
|
+
this.plugins.delete(id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load a plugin (call onLoad, register middleware/ontologies/rules/events).
|
|
100
|
+
* Resolves dependencies first.
|
|
101
|
+
*/
|
|
102
|
+
async load(
|
|
103
|
+
id: string,
|
|
104
|
+
kernel?: TrellisKernel,
|
|
105
|
+
ontologyRegistry?: OntologyRegistry,
|
|
106
|
+
queryEngine?: QueryEngine,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const entry = this.plugins.get(id);
|
|
109
|
+
if (!entry) throw new Error(`Plugin "${id}" is not registered.`);
|
|
110
|
+
if (entry.loaded) return;
|
|
111
|
+
|
|
112
|
+
// Check dependencies
|
|
113
|
+
if (entry.def.dependencies) {
|
|
114
|
+
for (const dep of entry.def.dependencies) {
|
|
115
|
+
const depEntry = this.plugins.get(dep);
|
|
116
|
+
if (!depEntry) {
|
|
117
|
+
throw new Error(`Plugin "${id}" depends on "${dep}" which is not registered.`);
|
|
118
|
+
}
|
|
119
|
+
if (!depEntry.loaded) {
|
|
120
|
+
await this.load(dep, kernel, ontologyRegistry, queryEngine);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Register middleware
|
|
126
|
+
if (entry.def.middleware && kernel) {
|
|
127
|
+
for (const mw of entry.def.middleware) {
|
|
128
|
+
kernel.addMiddleware(mw);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Register ontologies
|
|
133
|
+
if (entry.def.ontologies && ontologyRegistry) {
|
|
134
|
+
for (const schema of entry.def.ontologies) {
|
|
135
|
+
try { ontologyRegistry.register(schema); } catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Register rules
|
|
140
|
+
if (entry.def.rules && queryEngine) {
|
|
141
|
+
for (const rule of entry.def.rules) {
|
|
142
|
+
queryEngine.addRule(rule);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Register event handlers
|
|
147
|
+
if (entry.def.eventHandlers) {
|
|
148
|
+
for (const eh of entry.def.eventHandlers) {
|
|
149
|
+
this.eventBus.on(eh.event, eh.handler);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build plugin context
|
|
154
|
+
const ctx = this._buildContext(id);
|
|
155
|
+
|
|
156
|
+
// Call onLoad
|
|
157
|
+
if (entry.def.onLoad) {
|
|
158
|
+
await entry.def.onLoad(ctx);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
entry.loaded = true;
|
|
162
|
+
await this.eventBus.emit('plugin:loaded', { pluginId: id });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Unload a plugin (call onUnload, remove middleware/events).
|
|
167
|
+
*/
|
|
168
|
+
async unload(id: string): Promise<void> {
|
|
169
|
+
const entry = this.plugins.get(id);
|
|
170
|
+
if (!entry || !entry.loaded) return;
|
|
171
|
+
|
|
172
|
+
const ctx = this._buildContext(id);
|
|
173
|
+
if (entry.def.onUnload) {
|
|
174
|
+
await entry.def.onUnload(ctx);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Remove event handlers
|
|
178
|
+
if (entry.def.eventHandlers) {
|
|
179
|
+
for (const eh of entry.def.eventHandlers) {
|
|
180
|
+
this.eventBus.off(eh.event, eh.handler);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
entry.loaded = false;
|
|
185
|
+
await this.eventBus.emit('plugin:unloaded', { pluginId: id });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Load all registered plugins in dependency order.
|
|
190
|
+
*/
|
|
191
|
+
async loadAll(
|
|
192
|
+
kernel?: TrellisKernel,
|
|
193
|
+
ontologyRegistry?: OntologyRegistry,
|
|
194
|
+
queryEngine?: QueryEngine,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const order = this._resolveDependencyOrder();
|
|
197
|
+
for (const id of order) {
|
|
198
|
+
await this.load(id, kernel, ontologyRegistry, queryEngine);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Unload all plugins in reverse order.
|
|
204
|
+
*/
|
|
205
|
+
async unloadAll(): Promise<void> {
|
|
206
|
+
const order = this._resolveDependencyOrder().reverse();
|
|
207
|
+
for (const id of order) {
|
|
208
|
+
await this.unload(id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
// Queries
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
get(id: string): PluginDef | undefined {
|
|
217
|
+
return this.plugins.get(id)?.def;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
isLoaded(id: string): boolean {
|
|
221
|
+
return this.plugins.get(id)?.loaded ?? false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
list(): Array<{ def: PluginDef; loaded: boolean }> {
|
|
225
|
+
return [...this.plugins.values()];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
listLoaded(): PluginDef[] {
|
|
229
|
+
return [...this.plugins.values()]
|
|
230
|
+
.filter((e) => e.loaded)
|
|
231
|
+
.map((e) => e.def);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
// Event bus access
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
getEventBus(): EventBus {
|
|
239
|
+
return this.eventBus;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async emit(event: string, data?: unknown): Promise<void> {
|
|
243
|
+
await this.eventBus.emit(event, data);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
on(event: string, handler: EventCallback): void {
|
|
247
|
+
this.eventBus.on(event, handler);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// -------------------------------------------------------------------------
|
|
251
|
+
// Workspace config
|
|
252
|
+
// -------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
getWorkspaceConfig(): WorkspaceConfig {
|
|
255
|
+
return this.workspaceConfig;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setWorkspaceConfig(config: WorkspaceConfig): void {
|
|
259
|
+
this.workspaceConfig = config;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
getConfigValue(key: string): unknown {
|
|
263
|
+
return this.workspaceConfig.settings?.[key];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setConfigValue(key: string, value: unknown): void {
|
|
267
|
+
if (!this.workspaceConfig.settings) this.workspaceConfig.settings = {};
|
|
268
|
+
this.workspaceConfig.settings[key] = value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
// Logs
|
|
273
|
+
// -------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
getLogs(pluginId?: string): typeof this.logs {
|
|
276
|
+
if (pluginId) return this.logs.filter((l) => l.pluginId === pluginId);
|
|
277
|
+
return [...this.logs];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -------------------------------------------------------------------------
|
|
281
|
+
// Internal
|
|
282
|
+
// -------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
private _buildContext(pluginId: string): PluginContext {
|
|
285
|
+
return {
|
|
286
|
+
pluginId,
|
|
287
|
+
on: (event, handler) => this.eventBus.on(event, handler),
|
|
288
|
+
emit: (event, data) => { this.eventBus.emit(event, data); },
|
|
289
|
+
getConfig: (key) => this.getConfigValue(key),
|
|
290
|
+
log: (message) => {
|
|
291
|
+
this.logs.push({ pluginId, message, timestamp: new Date().toISOString() });
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private _resolveDependencyOrder(): string[] {
|
|
297
|
+
const visited = new Set<string>();
|
|
298
|
+
const order: string[] = [];
|
|
299
|
+
|
|
300
|
+
const visit = (id: string, stack: Set<string>) => {
|
|
301
|
+
if (visited.has(id)) return;
|
|
302
|
+
if (stack.has(id)) throw new Error(`Circular dependency detected: ${[...stack, id].join(' → ')}`);
|
|
303
|
+
|
|
304
|
+
stack.add(id);
|
|
305
|
+
const entry = this.plugins.get(id);
|
|
306
|
+
if (entry?.def.dependencies) {
|
|
307
|
+
for (const dep of entry.def.dependencies) {
|
|
308
|
+
visit(dep, stack);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
stack.delete(id);
|
|
312
|
+
visited.add(id);
|
|
313
|
+
order.push(id);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
for (const id of this.plugins.keys()) {
|
|
317
|
+
visit(id, new Set());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return order;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin System Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the plugin interface, manifest format, lifecycle hooks,
|
|
5
|
+
* and event system for extensibility.
|
|
6
|
+
*
|
|
7
|
+
* @module trellis/core/plugins
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { KernelMiddleware } from '../kernel/middleware.js';
|
|
11
|
+
import type { OntologySchema } from '../ontology/types.js';
|
|
12
|
+
import type { DatalogRule } from '../query/types.js';
|
|
13
|
+
import type { KernelOp } from '../persist/backend.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Plugin definition
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface PluginDef {
|
|
20
|
+
/** Unique plugin identifier (e.g. "trellis:security", "my-org:custom"). */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Human-readable name. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Semantic version. */
|
|
25
|
+
version: string;
|
|
26
|
+
/** Description. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Plugin dependencies (other plugin IDs). */
|
|
29
|
+
dependencies?: string[];
|
|
30
|
+
|
|
31
|
+
/** Kernel middleware provided by this plugin. */
|
|
32
|
+
middleware?: KernelMiddleware[];
|
|
33
|
+
/** Ontology schemas provided by this plugin. */
|
|
34
|
+
ontologies?: OntologySchema[];
|
|
35
|
+
/** Datalog rules provided by this plugin. */
|
|
36
|
+
rules?: DatalogRule[];
|
|
37
|
+
/** Event listeners provided by this plugin. */
|
|
38
|
+
eventHandlers?: EventHandler[];
|
|
39
|
+
|
|
40
|
+
/** Called when the plugin is loaded. */
|
|
41
|
+
onLoad?: (ctx: PluginContext) => void | Promise<void>;
|
|
42
|
+
/** Called when the plugin is unloaded. */
|
|
43
|
+
onUnload?: (ctx: PluginContext) => void | Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Plugin context — what the plugin receives during lifecycle
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export interface PluginContext {
|
|
51
|
+
/** The plugin's own ID. */
|
|
52
|
+
pluginId: string;
|
|
53
|
+
/** Subscribe to events. */
|
|
54
|
+
on: (event: string, handler: EventCallback) => void;
|
|
55
|
+
/** Emit an event. */
|
|
56
|
+
emit: (event: string, data?: unknown) => void;
|
|
57
|
+
/** Get workspace config value. */
|
|
58
|
+
getConfig: (key: string) => unknown;
|
|
59
|
+
/** Log a message. */
|
|
60
|
+
log: (message: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Event system
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export type EventCallback = (data: unknown) => void | Promise<void>;
|
|
68
|
+
|
|
69
|
+
export interface EventHandler {
|
|
70
|
+
/** Event name pattern (e.g. "op:*", "entity:created", "milestone:created"). */
|
|
71
|
+
event: string;
|
|
72
|
+
/** Handler function. */
|
|
73
|
+
handler: EventCallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Well-known event names. */
|
|
77
|
+
export type WellKnownEvent =
|
|
78
|
+
| 'op:applied'
|
|
79
|
+
| 'entity:created'
|
|
80
|
+
| 'entity:updated'
|
|
81
|
+
| 'entity:deleted'
|
|
82
|
+
| 'link:added'
|
|
83
|
+
| 'link:removed'
|
|
84
|
+
| 'milestone:created'
|
|
85
|
+
| 'issue:created'
|
|
86
|
+
| 'issue:closed'
|
|
87
|
+
| 'plugin:loaded'
|
|
88
|
+
| 'plugin:unloaded';
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Workspace configuration
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export interface WorkspaceConfig {
|
|
95
|
+
/** Active ontology IDs. */
|
|
96
|
+
ontologies?: string[];
|
|
97
|
+
/** Active plugin IDs. */
|
|
98
|
+
plugins?: string[];
|
|
99
|
+
/** Tracked file patterns (globs). */
|
|
100
|
+
trackedPaths?: string[];
|
|
101
|
+
/** Ignore patterns. */
|
|
102
|
+
ignorePaths?: string[];
|
|
103
|
+
/** Branch policies. */
|
|
104
|
+
branchPolicies?: Record<string, { linear?: boolean }>;
|
|
105
|
+
/** Embedding model override. */
|
|
106
|
+
embeddingModel?: string;
|
|
107
|
+
/** Snapshot threshold (ops between auto-snapshots). */
|
|
108
|
+
snapshotThreshold?: number;
|
|
109
|
+
/** Custom key-value settings. */
|
|
110
|
+
settings?: Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Plugin manifest (for on-disk .trellis/plugins.json)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export interface PluginManifest {
|
|
118
|
+
/** Plugin ID. */
|
|
119
|
+
id: string;
|
|
120
|
+
/** Installed version. */
|
|
121
|
+
version: string;
|
|
122
|
+
/** Whether the plugin is enabled. */
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
/** Plugin-specific configuration. */
|
|
125
|
+
config?: Record<string, unknown>;
|
|
126
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datalog Evaluator — Rule-based recursive queries.
|
|
3
|
+
*
|
|
4
|
+
* Provides transitive closure, reachability, and other recursive
|
|
5
|
+
* graph queries via Datalog-style rules evaluated over the EAV store.
|
|
6
|
+
*
|
|
7
|
+
* Built-in rules:
|
|
8
|
+
* - `reachable(?src, ?tgt)` via a named link attribute
|
|
9
|
+
* - `ancestor(?x, ?y)` (generic transitive closure over any link)
|
|
10
|
+
*
|
|
11
|
+
* Custom rules can be registered via `addRule()`.
|
|
12
|
+
*
|
|
13
|
+
* @module trellis/core/query
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { EAVStore } from '../store/eav-store.js';
|
|
17
|
+
import type { DatalogRule } from './types.js';
|
|
18
|
+
import { variable, literal } from './types.js';
|
|
19
|
+
import { QueryEngine } from './engine.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Built-in rule constructors
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a transitive closure rule over a specific link attribute.
|
|
27
|
+
*
|
|
28
|
+
* `reachable(?x, ?y) :- (x attr y)`
|
|
29
|
+
* `reachable(?x, ?y) :- (x attr ?z), reachable(?z, ?y)`
|
|
30
|
+
*/
|
|
31
|
+
export function transitiveClosureRules(ruleName: string, linkAttribute: string): DatalogRule[] {
|
|
32
|
+
return [
|
|
33
|
+
// Base case: direct link
|
|
34
|
+
{
|
|
35
|
+
name: ruleName,
|
|
36
|
+
params: ['x', 'y'],
|
|
37
|
+
body: [
|
|
38
|
+
{
|
|
39
|
+
kind: 'link',
|
|
40
|
+
source: variable('x'),
|
|
41
|
+
attribute: literal(linkAttribute),
|
|
42
|
+
target: variable('y'),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
filters: [],
|
|
46
|
+
},
|
|
47
|
+
// Recursive case: indirect via intermediate
|
|
48
|
+
{
|
|
49
|
+
name: ruleName,
|
|
50
|
+
params: ['x', 'y'],
|
|
51
|
+
body: [
|
|
52
|
+
{
|
|
53
|
+
kind: 'link',
|
|
54
|
+
source: variable('x'),
|
|
55
|
+
attribute: literal(linkAttribute),
|
|
56
|
+
target: variable('z'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
kind: 'rule',
|
|
60
|
+
name: ruleName,
|
|
61
|
+
args: [variable('z'), variable('y')],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
filters: [],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a reverse reachability rule (follows links backwards).
|
|
71
|
+
*/
|
|
72
|
+
export function reverseReachabilityRules(ruleName: string, linkAttribute: string): DatalogRule[] {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name: ruleName,
|
|
76
|
+
params: ['x', 'y'],
|
|
77
|
+
body: [
|
|
78
|
+
{
|
|
79
|
+
kind: 'link',
|
|
80
|
+
source: variable('y'),
|
|
81
|
+
attribute: literal(linkAttribute),
|
|
82
|
+
target: variable('x'),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
filters: [],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: ruleName,
|
|
89
|
+
params: ['x', 'y'],
|
|
90
|
+
body: [
|
|
91
|
+
{
|
|
92
|
+
kind: 'link',
|
|
93
|
+
source: variable('z'),
|
|
94
|
+
attribute: literal(linkAttribute),
|
|
95
|
+
target: variable('x'),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
kind: 'rule',
|
|
99
|
+
name: ruleName,
|
|
100
|
+
args: [variable('z'), variable('y')],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
filters: [],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a "sibling" rule — entities that share a common parent via a link attribute.
|
|
110
|
+
*
|
|
111
|
+
* `sibling(?a, ?b) :- (?a attr ?parent), (?b attr ?parent)`
|
|
112
|
+
* FILTER ?a != ?b
|
|
113
|
+
*/
|
|
114
|
+
export function siblingRules(ruleName: string, linkAttribute: string): DatalogRule[] {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
name: ruleName,
|
|
118
|
+
params: ['a', 'b'],
|
|
119
|
+
body: [
|
|
120
|
+
{
|
|
121
|
+
kind: 'link',
|
|
122
|
+
source: variable('a'),
|
|
123
|
+
attribute: literal(linkAttribute),
|
|
124
|
+
target: variable('parent'),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
kind: 'link',
|
|
128
|
+
source: variable('b'),
|
|
129
|
+
attribute: literal(linkAttribute),
|
|
130
|
+
target: variable('parent'),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
filters: [
|
|
134
|
+
{
|
|
135
|
+
kind: 'filter',
|
|
136
|
+
left: variable('a'),
|
|
137
|
+
op: '!=',
|
|
138
|
+
right: variable('b'),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Datalog Runtime — convenience wrapper around QueryEngine + rules
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export class DatalogRuntime {
|
|
150
|
+
private engine: QueryEngine;
|
|
151
|
+
|
|
152
|
+
constructor(store: EAVStore) {
|
|
153
|
+
this.engine = new QueryEngine(store);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Register a Datalog rule (or multiple). */
|
|
157
|
+
addRule(rule: DatalogRule): void {
|
|
158
|
+
this.engine.addRule(rule);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
addRules(rules: DatalogRule[]): void {
|
|
162
|
+
for (const r of rules) this.engine.addRule(r);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
removeRule(name: string): void {
|
|
166
|
+
this.engine.removeRule(name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Register built-in transitive closure for a link attribute. */
|
|
170
|
+
registerTransitiveClosure(ruleName: string, linkAttribute: string): void {
|
|
171
|
+
this.addRules(transitiveClosureRules(ruleName, linkAttribute));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Register built-in reverse reachability for a link attribute. */
|
|
175
|
+
registerReverseReachability(ruleName: string, linkAttribute: string): void {
|
|
176
|
+
this.addRules(reverseReachabilityRules(ruleName, linkAttribute));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Register built-in sibling rule for a link attribute. */
|
|
180
|
+
registerSiblings(ruleName: string, linkAttribute: string): void {
|
|
181
|
+
this.addRules(siblingRules(ruleName, linkAttribute));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Get the underlying QueryEngine for direct query execution. */
|
|
185
|
+
getEngine(): QueryEngine {
|
|
186
|
+
return this.engine;
|
|
187
|
+
}
|
|
188
|
+
}
|