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,1100 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
QueryEngine,
|
|
4
|
+
literal,
|
|
5
|
+
variable
|
|
6
|
+
} from "./index-hybgxe40.js";
|
|
7
|
+
|
|
8
|
+
// src/core/query/parser.ts
|
|
9
|
+
function tokenize(input) {
|
|
10
|
+
const tokens = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < input.length) {
|
|
13
|
+
if (/\s/.test(input[i])) {
|
|
14
|
+
i++;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (input[i] === "/" && input[i + 1] === "/") {
|
|
18
|
+
while (i < input.length && input[i] !== `
|
|
19
|
+
`)
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const pos = i;
|
|
24
|
+
if ("[](){},:".includes(input[i])) {
|
|
25
|
+
tokens.push({ kind: "symbol", value: input[i], pos });
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (input[i] === "!" && input[i + 1] === "=") {
|
|
30
|
+
tokens.push({ kind: "symbol", value: "!=", pos });
|
|
31
|
+
i += 2;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (input[i] === "<" && input[i + 1] === "=") {
|
|
35
|
+
tokens.push({ kind: "symbol", value: "<=", pos });
|
|
36
|
+
i += 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (input[i] === ">" && input[i + 1] === "=") {
|
|
40
|
+
tokens.push({ kind: "symbol", value: ">=", pos });
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if ("<>=".includes(input[i])) {
|
|
45
|
+
tokens.push({ kind: "symbol", value: input[i], pos });
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (input[i] === '"') {
|
|
50
|
+
i++;
|
|
51
|
+
let s = "";
|
|
52
|
+
while (i < input.length && input[i] !== '"') {
|
|
53
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
54
|
+
s += input[i + 1];
|
|
55
|
+
i += 2;
|
|
56
|
+
} else {
|
|
57
|
+
s += input[i];
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (i < input.length)
|
|
62
|
+
i++;
|
|
63
|
+
tokens.push({ kind: "string", value: s, pos });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (/[0-9]/.test(input[i]) || input[i] === "-" && i + 1 < input.length && /[0-9]/.test(input[i + 1])) {
|
|
67
|
+
let n = input[i];
|
|
68
|
+
i++;
|
|
69
|
+
while (i < input.length && /[0-9.]/.test(input[i])) {
|
|
70
|
+
n += input[i];
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
tokens.push({ kind: "number", value: n, pos });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (/[?a-zA-Z_]/.test(input[i])) {
|
|
77
|
+
let w = "";
|
|
78
|
+
while (i < input.length && /[?a-zA-Z0-9_.:/-]/.test(input[i])) {
|
|
79
|
+
w += input[i];
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
tokens.push({ kind: "word", value: w, pos });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
tokens.push({ kind: "eof", value: "", pos: input.length });
|
|
88
|
+
return tokens;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class Parser {
|
|
92
|
+
tokens;
|
|
93
|
+
pos = 0;
|
|
94
|
+
constructor(tokens) {
|
|
95
|
+
this.tokens = tokens;
|
|
96
|
+
}
|
|
97
|
+
peek() {
|
|
98
|
+
return this.tokens[this.pos];
|
|
99
|
+
}
|
|
100
|
+
advance() {
|
|
101
|
+
return this.tokens[this.pos++];
|
|
102
|
+
}
|
|
103
|
+
expect(kind, value) {
|
|
104
|
+
const t = this.advance();
|
|
105
|
+
if (t.kind !== kind || value !== undefined && t.value !== value) {
|
|
106
|
+
throw new Error(`Expected ${kind}${value ? ` "${value}"` : ""} at pos ${t.pos}, got ${t.kind} "${t.value}"`);
|
|
107
|
+
}
|
|
108
|
+
return t;
|
|
109
|
+
}
|
|
110
|
+
match(kind, value) {
|
|
111
|
+
const t = this.peek();
|
|
112
|
+
if (t.kind === kind && (value === undefined || t.value === value)) {
|
|
113
|
+
this.pos++;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
isAt(kind, value) {
|
|
119
|
+
const t = this.peek();
|
|
120
|
+
return t.kind === kind && (value === undefined || t.value === value);
|
|
121
|
+
}
|
|
122
|
+
parseTerm() {
|
|
123
|
+
const t = this.peek();
|
|
124
|
+
if (t.kind === "word" && t.value.startsWith("?")) {
|
|
125
|
+
this.advance();
|
|
126
|
+
return variable(t.value.slice(1));
|
|
127
|
+
}
|
|
128
|
+
if (t.kind === "string") {
|
|
129
|
+
this.advance();
|
|
130
|
+
return literal(t.value);
|
|
131
|
+
}
|
|
132
|
+
if (t.kind === "number") {
|
|
133
|
+
this.advance();
|
|
134
|
+
const n = Number(t.value);
|
|
135
|
+
return literal(n);
|
|
136
|
+
}
|
|
137
|
+
if (t.kind === "word") {
|
|
138
|
+
const v = t.value;
|
|
139
|
+
this.advance();
|
|
140
|
+
if (v === "true")
|
|
141
|
+
return literal(true);
|
|
142
|
+
if (v === "false")
|
|
143
|
+
return literal(false);
|
|
144
|
+
return literal(v);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unexpected token at pos ${t.pos}: ${t.kind} "${t.value}"`);
|
|
147
|
+
}
|
|
148
|
+
parsePattern() {
|
|
149
|
+
const t = this.peek();
|
|
150
|
+
if (t.kind === "word" && t.value.toUpperCase() === "NOT") {
|
|
151
|
+
this.advance();
|
|
152
|
+
const inner = this.parsePattern();
|
|
153
|
+
return { kind: "not", pattern: inner };
|
|
154
|
+
}
|
|
155
|
+
if (t.kind === "word" && t.value.toUpperCase() === "OR") {
|
|
156
|
+
this.advance();
|
|
157
|
+
const branches = [];
|
|
158
|
+
while (this.isAt("symbol", "{")) {
|
|
159
|
+
this.advance();
|
|
160
|
+
const branch = [];
|
|
161
|
+
while (!this.isAt("symbol", "}") && !this.isAt("eof", undefined)) {
|
|
162
|
+
branch.push(this.parsePattern());
|
|
163
|
+
}
|
|
164
|
+
this.expect("symbol", "}");
|
|
165
|
+
branches.push(branch);
|
|
166
|
+
}
|
|
167
|
+
return { kind: "or", branches };
|
|
168
|
+
}
|
|
169
|
+
if (t.kind === "symbol" && t.value === "[") {
|
|
170
|
+
this.advance();
|
|
171
|
+
const entity = this.parseTerm();
|
|
172
|
+
const attribute = this.parseTerm();
|
|
173
|
+
const value = this.parseTerm();
|
|
174
|
+
this.expect("symbol", "]");
|
|
175
|
+
return { kind: "fact", entity, attribute, value };
|
|
176
|
+
}
|
|
177
|
+
if (t.kind === "symbol" && t.value === "(") {
|
|
178
|
+
this.advance();
|
|
179
|
+
const source = this.parseTerm();
|
|
180
|
+
const attribute = this.parseTerm();
|
|
181
|
+
const target = this.parseTerm();
|
|
182
|
+
this.expect("symbol", ")");
|
|
183
|
+
return { kind: "link", source, attribute, target };
|
|
184
|
+
}
|
|
185
|
+
if (t.kind === "word" && !t.value.startsWith("?")) {
|
|
186
|
+
const name = this.advance().value;
|
|
187
|
+
if (this.isAt("symbol", "(")) {
|
|
188
|
+
this.advance();
|
|
189
|
+
const args = [];
|
|
190
|
+
while (!this.isAt("symbol", ")") && !this.isAt("eof", undefined)) {
|
|
191
|
+
args.push(this.parseTerm());
|
|
192
|
+
this.match("symbol", ",");
|
|
193
|
+
}
|
|
194
|
+
this.expect("symbol", ")");
|
|
195
|
+
return { kind: "rule", name, args };
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Expected '(' after rule name "${name}" at pos ${t.pos}`);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Cannot parse pattern at pos ${t.pos}: ${t.kind} "${t.value}"`);
|
|
200
|
+
}
|
|
201
|
+
parseFilter() {
|
|
202
|
+
const left = this.parseTerm();
|
|
203
|
+
const op = this.advance().value;
|
|
204
|
+
const right = this.parseTerm();
|
|
205
|
+
return { kind: "filter", left, op, right };
|
|
206
|
+
}
|
|
207
|
+
parseQuery() {
|
|
208
|
+
const query = {
|
|
209
|
+
select: [],
|
|
210
|
+
where: [],
|
|
211
|
+
filters: [],
|
|
212
|
+
aggregates: [],
|
|
213
|
+
orderBy: [],
|
|
214
|
+
limit: 0,
|
|
215
|
+
offset: 0
|
|
216
|
+
};
|
|
217
|
+
while (!this.isAt("eof", undefined)) {
|
|
218
|
+
const kw = this.peek();
|
|
219
|
+
if (kw.kind !== "word") {
|
|
220
|
+
throw new Error(`Expected keyword at pos ${kw.pos}, got ${kw.kind} "${kw.value}"`);
|
|
221
|
+
}
|
|
222
|
+
switch (kw.value.toUpperCase()) {
|
|
223
|
+
case "SELECT": {
|
|
224
|
+
this.advance();
|
|
225
|
+
while (this.peek().kind === "word" && this.peek().value.startsWith("?")) {
|
|
226
|
+
query.select.push(this.advance().value.slice(1));
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "WHERE": {
|
|
231
|
+
this.advance();
|
|
232
|
+
this.expect("symbol", "{");
|
|
233
|
+
while (!this.isAt("symbol", "}") && !this.isAt("eof", undefined)) {
|
|
234
|
+
query.where.push(this.parsePattern());
|
|
235
|
+
}
|
|
236
|
+
this.expect("symbol", "}");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "FILTER": {
|
|
240
|
+
this.advance();
|
|
241
|
+
query.filters.push(this.parseFilter());
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "AGGREGATE": {
|
|
245
|
+
this.advance();
|
|
246
|
+
const op = this.advance().value;
|
|
247
|
+
this.expect("symbol", "(");
|
|
248
|
+
const varName = this.advance().value;
|
|
249
|
+
const varClean = varName.startsWith("?") ? varName.slice(1) : varName;
|
|
250
|
+
this.expect("symbol", ")");
|
|
251
|
+
this.expect("word", "AS");
|
|
252
|
+
const asName = this.advance().value;
|
|
253
|
+
const asClean = asName.startsWith("?") ? asName.slice(1) : asName;
|
|
254
|
+
query.aggregates.push({ op, variable: varClean, as: asClean });
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "ORDER": {
|
|
258
|
+
this.advance();
|
|
259
|
+
this.expect("word", "BY");
|
|
260
|
+
while (this.peek().kind === "word" && this.peek().value.startsWith("?")) {
|
|
261
|
+
const v = this.advance().value.slice(1);
|
|
262
|
+
let dir = "asc";
|
|
263
|
+
if (this.peek().kind === "word" && ["ASC", "DESC"].includes(this.peek().value.toUpperCase())) {
|
|
264
|
+
dir = this.advance().value.toLowerCase();
|
|
265
|
+
}
|
|
266
|
+
query.orderBy.push({ variable: v, direction: dir });
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "LIMIT": {
|
|
271
|
+
this.advance();
|
|
272
|
+
query.limit = Number(this.expect("number").value);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "OFFSET": {
|
|
276
|
+
this.advance();
|
|
277
|
+
query.offset = Number(this.expect("number").value);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(`Unknown keyword "${kw.value}" at pos ${kw.pos}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return query;
|
|
285
|
+
}
|
|
286
|
+
parseRule() {
|
|
287
|
+
const name = this.expect("word").value;
|
|
288
|
+
this.expect("symbol", "(");
|
|
289
|
+
const params = [];
|
|
290
|
+
while (!this.isAt("symbol", ")") && !this.isAt("eof", undefined)) {
|
|
291
|
+
const v = this.expect("word").value;
|
|
292
|
+
params.push(v.startsWith("?") ? v.slice(1) : v);
|
|
293
|
+
this.match("symbol", ",");
|
|
294
|
+
}
|
|
295
|
+
this.expect("symbol", ")");
|
|
296
|
+
if (this.isAt("symbol", ":")) {
|
|
297
|
+
this.advance();
|
|
298
|
+
if (this.peek().kind === "number" && this.peek().value.startsWith("-")) {
|
|
299
|
+
this.advance();
|
|
300
|
+
}
|
|
301
|
+
} else if (this.isAt("word", ":-")) {
|
|
302
|
+
this.advance();
|
|
303
|
+
}
|
|
304
|
+
const body = [];
|
|
305
|
+
const filters = [];
|
|
306
|
+
while (!this.isAt("eof", undefined)) {
|
|
307
|
+
if (this.peek().kind === "word" && this.peek().value.toUpperCase() === "FILTER") {
|
|
308
|
+
this.advance();
|
|
309
|
+
filters.push(this.parseFilter());
|
|
310
|
+
} else {
|
|
311
|
+
body.push(this.parsePattern());
|
|
312
|
+
}
|
|
313
|
+
this.match("symbol", ",");
|
|
314
|
+
}
|
|
315
|
+
return { name, params, body, filters };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function parseQuery(input) {
|
|
319
|
+
const tokens = tokenize(input);
|
|
320
|
+
return new Parser(tokens).parseQuery();
|
|
321
|
+
}
|
|
322
|
+
function parseRule(input) {
|
|
323
|
+
const tokens = tokenize(input);
|
|
324
|
+
return new Parser(tokens).parseRule();
|
|
325
|
+
}
|
|
326
|
+
function parseSimple(input) {
|
|
327
|
+
const trimmed = input.trim();
|
|
328
|
+
const upper = trimmed.toUpperCase();
|
|
329
|
+
if (upper.startsWith("SELECT") || upper.startsWith("WHERE")) {
|
|
330
|
+
return parseQuery(trimmed);
|
|
331
|
+
}
|
|
332
|
+
const findMatch = trimmed.match(/^find\s+(.+?)\s+where\s+(.+)$/i);
|
|
333
|
+
if (findMatch) {
|
|
334
|
+
const vars = findMatch[1].trim().split(/\s+/);
|
|
335
|
+
const conditions = findMatch[2].trim();
|
|
336
|
+
const selectVars = vars.map((v) => v.startsWith("?") ? v : `?${v}`);
|
|
337
|
+
const entity = selectVars[0];
|
|
338
|
+
const parts = conditions.split(/\s+and\s+/i);
|
|
339
|
+
const patterns = [];
|
|
340
|
+
const filters = [];
|
|
341
|
+
for (const part of parts) {
|
|
342
|
+
const eqMatch = part.match(/^(\S+)\s*(=|!=|<|<=|>|>=|contains|startsWith|endsWith|matches)\s*(.+)$/);
|
|
343
|
+
if (eqMatch) {
|
|
344
|
+
const [, attr, op, val] = eqMatch;
|
|
345
|
+
const valTrimmed = val.trim();
|
|
346
|
+
if (op === "=") {
|
|
347
|
+
patterns.push(`[${entity} "${attr}" ${valTrimmed}]`);
|
|
348
|
+
} else {
|
|
349
|
+
const tmpVar = `?_${attr.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
350
|
+
patterns.push(`[${entity} "${attr}" ${tmpVar}]`);
|
|
351
|
+
filters.push(`FILTER ${tmpVar} ${op} ${valTrimmed}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const fullQuery = `SELECT ${selectVars.join(" ")}
|
|
356
|
+
WHERE {
|
|
357
|
+
${patterns.join(`
|
|
358
|
+
`)}
|
|
359
|
+
}
|
|
360
|
+
${filters.join(`
|
|
361
|
+
`)}`;
|
|
362
|
+
return parseQuery(fullQuery);
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`Cannot parse query: "${trimmed}". Use full EQL-S syntax or "find ?e where attr = value".`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/core/query/datalog.ts
|
|
368
|
+
function transitiveClosureRules(ruleName, linkAttribute) {
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
name: ruleName,
|
|
372
|
+
params: ["x", "y"],
|
|
373
|
+
body: [
|
|
374
|
+
{
|
|
375
|
+
kind: "link",
|
|
376
|
+
source: variable("x"),
|
|
377
|
+
attribute: literal(linkAttribute),
|
|
378
|
+
target: variable("y")
|
|
379
|
+
}
|
|
380
|
+
],
|
|
381
|
+
filters: []
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: ruleName,
|
|
385
|
+
params: ["x", "y"],
|
|
386
|
+
body: [
|
|
387
|
+
{
|
|
388
|
+
kind: "link",
|
|
389
|
+
source: variable("x"),
|
|
390
|
+
attribute: literal(linkAttribute),
|
|
391
|
+
target: variable("z")
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
kind: "rule",
|
|
395
|
+
name: ruleName,
|
|
396
|
+
args: [variable("z"), variable("y")]
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
filters: []
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function reverseReachabilityRules(ruleName, linkAttribute) {
|
|
404
|
+
return [
|
|
405
|
+
{
|
|
406
|
+
name: ruleName,
|
|
407
|
+
params: ["x", "y"],
|
|
408
|
+
body: [
|
|
409
|
+
{
|
|
410
|
+
kind: "link",
|
|
411
|
+
source: variable("y"),
|
|
412
|
+
attribute: literal(linkAttribute),
|
|
413
|
+
target: variable("x")
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
filters: []
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: ruleName,
|
|
420
|
+
params: ["x", "y"],
|
|
421
|
+
body: [
|
|
422
|
+
{
|
|
423
|
+
kind: "link",
|
|
424
|
+
source: variable("z"),
|
|
425
|
+
attribute: literal(linkAttribute),
|
|
426
|
+
target: variable("x")
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
kind: "rule",
|
|
430
|
+
name: ruleName,
|
|
431
|
+
args: [variable("z"), variable("y")]
|
|
432
|
+
}
|
|
433
|
+
],
|
|
434
|
+
filters: []
|
|
435
|
+
}
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
function siblingRules(ruleName, linkAttribute) {
|
|
439
|
+
return [
|
|
440
|
+
{
|
|
441
|
+
name: ruleName,
|
|
442
|
+
params: ["a", "b"],
|
|
443
|
+
body: [
|
|
444
|
+
{
|
|
445
|
+
kind: "link",
|
|
446
|
+
source: variable("a"),
|
|
447
|
+
attribute: literal(linkAttribute),
|
|
448
|
+
target: variable("parent")
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
kind: "link",
|
|
452
|
+
source: variable("b"),
|
|
453
|
+
attribute: literal(linkAttribute),
|
|
454
|
+
target: variable("parent")
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
filters: [
|
|
458
|
+
{
|
|
459
|
+
kind: "filter",
|
|
460
|
+
left: variable("a"),
|
|
461
|
+
op: "!=",
|
|
462
|
+
right: variable("b")
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
}
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
class DatalogRuntime {
|
|
470
|
+
engine;
|
|
471
|
+
constructor(store) {
|
|
472
|
+
this.engine = new QueryEngine(store);
|
|
473
|
+
}
|
|
474
|
+
addRule(rule) {
|
|
475
|
+
this.engine.addRule(rule);
|
|
476
|
+
}
|
|
477
|
+
addRules(rules) {
|
|
478
|
+
for (const r of rules)
|
|
479
|
+
this.engine.addRule(r);
|
|
480
|
+
}
|
|
481
|
+
removeRule(name) {
|
|
482
|
+
this.engine.removeRule(name);
|
|
483
|
+
}
|
|
484
|
+
registerTransitiveClosure(ruleName, linkAttribute) {
|
|
485
|
+
this.addRules(transitiveClosureRules(ruleName, linkAttribute));
|
|
486
|
+
}
|
|
487
|
+
registerReverseReachability(ruleName, linkAttribute) {
|
|
488
|
+
this.addRules(reverseReachabilityRules(ruleName, linkAttribute));
|
|
489
|
+
}
|
|
490
|
+
registerSiblings(ruleName, linkAttribute) {
|
|
491
|
+
this.addRules(siblingRules(ruleName, linkAttribute));
|
|
492
|
+
}
|
|
493
|
+
getEngine() {
|
|
494
|
+
return this.engine;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// src/core/ontology/registry.ts
|
|
498
|
+
class OntologyRegistry {
|
|
499
|
+
schemas = new Map;
|
|
500
|
+
resolvedEntities = new Map;
|
|
501
|
+
resolvedRelations = new Map;
|
|
502
|
+
register(schema) {
|
|
503
|
+
const existing = this.schemas.get(schema.id);
|
|
504
|
+
if (existing && existing.version === schema.version) {
|
|
505
|
+
throw new Error(`Ontology "${schema.id}" v${schema.version} is already registered.`);
|
|
506
|
+
}
|
|
507
|
+
this.schemas.set(schema.id, schema);
|
|
508
|
+
this._resolve(schema);
|
|
509
|
+
}
|
|
510
|
+
unregister(id) {
|
|
511
|
+
const schema = this.schemas.get(id);
|
|
512
|
+
if (!schema)
|
|
513
|
+
return;
|
|
514
|
+
for (const [name, entry] of this.resolvedEntities) {
|
|
515
|
+
if (entry.ontologyId === id) {
|
|
516
|
+
this.resolvedEntities.delete(name);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const [name, entries] of this.resolvedRelations) {
|
|
520
|
+
const filtered = entries.filter((e) => e.ontologyId !== id);
|
|
521
|
+
if (filtered.length === 0) {
|
|
522
|
+
this.resolvedRelations.delete(name);
|
|
523
|
+
} else {
|
|
524
|
+
this.resolvedRelations.set(name, filtered);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
this.schemas.delete(id);
|
|
528
|
+
}
|
|
529
|
+
get(id) {
|
|
530
|
+
return this.schemas.get(id);
|
|
531
|
+
}
|
|
532
|
+
list() {
|
|
533
|
+
return [...this.schemas.values()];
|
|
534
|
+
}
|
|
535
|
+
getEntityDef(typeName) {
|
|
536
|
+
return this.resolvedEntities.get(typeName)?.def;
|
|
537
|
+
}
|
|
538
|
+
getEntityOntology(typeName) {
|
|
539
|
+
return this.resolvedEntities.get(typeName)?.ontologyId;
|
|
540
|
+
}
|
|
541
|
+
listEntityTypes() {
|
|
542
|
+
return [...this.resolvedEntities.keys()];
|
|
543
|
+
}
|
|
544
|
+
getRelationsForType(typeName) {
|
|
545
|
+
const results = [];
|
|
546
|
+
for (const [, entries] of this.resolvedRelations) {
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (entry.def.sourceTypes.includes(typeName) || entry.def.targetTypes.includes(typeName)) {
|
|
549
|
+
results.push(entry.def);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return results;
|
|
554
|
+
}
|
|
555
|
+
getRelationDef(name) {
|
|
556
|
+
const entries = this.resolvedRelations.get(name);
|
|
557
|
+
return entries?.[0]?.def;
|
|
558
|
+
}
|
|
559
|
+
listRelationNames() {
|
|
560
|
+
return [...this.resolvedRelations.keys()];
|
|
561
|
+
}
|
|
562
|
+
hasEntityType(typeName) {
|
|
563
|
+
return this.resolvedEntities.has(typeName);
|
|
564
|
+
}
|
|
565
|
+
getRequiredAttributes(typeName) {
|
|
566
|
+
const def = this.getEntityDef(typeName);
|
|
567
|
+
if (!def)
|
|
568
|
+
return [];
|
|
569
|
+
return def.attributes.filter((a) => a.required);
|
|
570
|
+
}
|
|
571
|
+
_resolve(schema) {
|
|
572
|
+
for (const entity of schema.entities) {
|
|
573
|
+
const resolved = this._resolveEntity(entity, schema);
|
|
574
|
+
this.resolvedEntities.set(entity.name, {
|
|
575
|
+
def: resolved,
|
|
576
|
+
ontologyId: schema.id
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
for (const relation of schema.relations) {
|
|
580
|
+
const existing = this.resolvedRelations.get(relation.name) ?? [];
|
|
581
|
+
existing.push({ def: relation, ontologyId: schema.id });
|
|
582
|
+
this.resolvedRelations.set(relation.name, existing);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
_resolveEntity(entity, schema) {
|
|
586
|
+
if (!entity.extends)
|
|
587
|
+
return entity;
|
|
588
|
+
let parent = schema.entities.find((e) => e.name === entity.extends);
|
|
589
|
+
if (!parent) {
|
|
590
|
+
const resolved = this.resolvedEntities.get(entity.extends);
|
|
591
|
+
parent = resolved?.def;
|
|
592
|
+
}
|
|
593
|
+
if (!parent) {
|
|
594
|
+
throw new Error(`Entity "${entity.name}" extends "${entity.extends}" which is not defined.`);
|
|
595
|
+
}
|
|
596
|
+
const resolvedParent = this._resolveEntity(parent, schema);
|
|
597
|
+
const childAttrNames = new Set(entity.attributes.map((a) => a.name));
|
|
598
|
+
const mergedAttrs = [
|
|
599
|
+
...resolvedParent.attributes.filter((a) => !childAttrNames.has(a.name)),
|
|
600
|
+
...entity.attributes
|
|
601
|
+
];
|
|
602
|
+
return {
|
|
603
|
+
...entity,
|
|
604
|
+
attributes: mergedAttrs
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/core/ontology/builtins.ts
|
|
610
|
+
var projectOntology = {
|
|
611
|
+
id: "trellis:project",
|
|
612
|
+
name: "Project Ontology",
|
|
613
|
+
version: "1.0.0",
|
|
614
|
+
description: "Entity types for software project management.",
|
|
615
|
+
entities: [
|
|
616
|
+
{
|
|
617
|
+
name: "Project",
|
|
618
|
+
description: "A software project or repository.",
|
|
619
|
+
attributes: [
|
|
620
|
+
{ name: "name", type: "string", required: true, description: "Project name" },
|
|
621
|
+
{ name: "description", type: "string", description: "Project description" },
|
|
622
|
+
{ name: "status", type: "string", enum: ["active", "archived", "draft", "deprecated"], default: "active" },
|
|
623
|
+
{ name: "url", type: "string", description: "Project URL or repository link" },
|
|
624
|
+
{ name: "language", type: "string", description: "Primary programming language" },
|
|
625
|
+
{ name: "createdAt", type: "date", description: "Creation timestamp" },
|
|
626
|
+
{ name: "updatedAt", type: "date", description: "Last update timestamp" }
|
|
627
|
+
]
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "Module",
|
|
631
|
+
description: "A logical module or package within a project.",
|
|
632
|
+
attributes: [
|
|
633
|
+
{ name: "name", type: "string", required: true },
|
|
634
|
+
{ name: "path", type: "string", description: "Filesystem path relative to project root" },
|
|
635
|
+
{ name: "description", type: "string" }
|
|
636
|
+
]
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "Feature",
|
|
640
|
+
description: "A product feature or capability.",
|
|
641
|
+
attributes: [
|
|
642
|
+
{ name: "name", type: "string", required: true },
|
|
643
|
+
{ name: "description", type: "string" },
|
|
644
|
+
{ name: "status", type: "string", enum: ["planned", "in-progress", "shipped", "cut"], default: "planned" },
|
|
645
|
+
{ name: "priority", type: "string", enum: ["critical", "high", "medium", "low"], default: "medium" }
|
|
646
|
+
]
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "Dependency",
|
|
650
|
+
description: "An external dependency or library.",
|
|
651
|
+
attributes: [
|
|
652
|
+
{ name: "name", type: "string", required: true },
|
|
653
|
+
{ name: "version", type: "string" },
|
|
654
|
+
{ name: "registry", type: "string", description: "Package registry (npm, pypi, etc.)" },
|
|
655
|
+
{ name: "scope", type: "string", enum: ["runtime", "dev", "optional"], default: "runtime" }
|
|
656
|
+
]
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "Config",
|
|
660
|
+
description: "A configuration entry or setting.",
|
|
661
|
+
attributes: [
|
|
662
|
+
{ name: "key", type: "string", required: true },
|
|
663
|
+
{ name: "value", type: "any", required: true },
|
|
664
|
+
{ name: "description", type: "string" },
|
|
665
|
+
{ name: "scope", type: "string", enum: ["project", "user", "system"], default: "project" }
|
|
666
|
+
]
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "Artifact",
|
|
670
|
+
description: "A build artifact, release asset, or output file.",
|
|
671
|
+
attributes: [
|
|
672
|
+
{ name: "name", type: "string", required: true },
|
|
673
|
+
{ name: "path", type: "string" },
|
|
674
|
+
{ name: "size", type: "number" },
|
|
675
|
+
{ name: "hash", type: "string" },
|
|
676
|
+
{ name: "format", type: "string" }
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "Release",
|
|
681
|
+
description: "A versioned release of a project.",
|
|
682
|
+
attributes: [
|
|
683
|
+
{ name: "version", type: "string", required: true },
|
|
684
|
+
{ name: "tag", type: "string" },
|
|
685
|
+
{ name: "date", type: "date" },
|
|
686
|
+
{ name: "notes", type: "string" },
|
|
687
|
+
{ name: "status", type: "string", enum: ["draft", "published", "yanked"], default: "draft" }
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
relations: [
|
|
692
|
+
{ name: "contains", sourceTypes: ["Project"], targetTypes: ["Module", "Feature", "Config"], cardinality: "many", description: "Project contains modules/features/configs" },
|
|
693
|
+
{ name: "dependsOn", sourceTypes: ["Project", "Module"], targetTypes: ["Dependency", "Module", "Project"], cardinality: "many", description: "Depends on another entity" },
|
|
694
|
+
{ name: "implementedBy", sourceTypes: ["Feature"], targetTypes: ["Module"], cardinality: "many", description: "Feature is implemented by modules" },
|
|
695
|
+
{ name: "produces", sourceTypes: ["Project", "Release"], targetTypes: ["Artifact"], cardinality: "many", description: "Produces artifacts" },
|
|
696
|
+
{ name: "releases", sourceTypes: ["Project"], targetTypes: ["Release"], cardinality: "many", description: "Project has releases" }
|
|
697
|
+
]
|
|
698
|
+
};
|
|
699
|
+
var teamOntology = {
|
|
700
|
+
id: "trellis:team",
|
|
701
|
+
name: "Team Ontology",
|
|
702
|
+
version: "1.0.0",
|
|
703
|
+
description: "Entity types for team and developer organization.",
|
|
704
|
+
entities: [
|
|
705
|
+
{
|
|
706
|
+
name: "Team",
|
|
707
|
+
description: "A team or organizational group.",
|
|
708
|
+
attributes: [
|
|
709
|
+
{ name: "name", type: "string", required: true },
|
|
710
|
+
{ name: "description", type: "string" },
|
|
711
|
+
{ name: "slug", type: "string", description: "URL-safe identifier" }
|
|
712
|
+
]
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: "Developer",
|
|
716
|
+
description: "A developer or contributor.",
|
|
717
|
+
attributes: [
|
|
718
|
+
{ name: "name", type: "string", required: true },
|
|
719
|
+
{ name: "email", type: "string" },
|
|
720
|
+
{ name: "handle", type: "string", description: "Username or handle" },
|
|
721
|
+
{ name: "role", type: "string", enum: ["admin", "maintainer", "contributor", "reviewer"] }
|
|
722
|
+
]
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "Role",
|
|
726
|
+
description: "A named role with specific permissions.",
|
|
727
|
+
attributes: [
|
|
728
|
+
{ name: "name", type: "string", required: true },
|
|
729
|
+
{ name: "description", type: "string" },
|
|
730
|
+
{ name: "permissions", type: "string", unique: false, description: "Permission strings (multi-valued)" }
|
|
731
|
+
]
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
name: "Capability",
|
|
735
|
+
description: "A skill or capability.",
|
|
736
|
+
attributes: [
|
|
737
|
+
{ name: "name", type: "string", required: true },
|
|
738
|
+
{ name: "category", type: "string" },
|
|
739
|
+
{ name: "level", type: "string", enum: ["beginner", "intermediate", "advanced", "expert"] }
|
|
740
|
+
]
|
|
741
|
+
}
|
|
742
|
+
],
|
|
743
|
+
relations: [
|
|
744
|
+
{ name: "hasMember", sourceTypes: ["Team"], targetTypes: ["Developer"], cardinality: "many", inverse: "memberOf", description: "Team has member" },
|
|
745
|
+
{ name: "memberOf", sourceTypes: ["Developer"], targetTypes: ["Team"], cardinality: "many", inverse: "hasMember", description: "Developer is member of team" },
|
|
746
|
+
{ name: "owns", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer owns/maintains" },
|
|
747
|
+
{ name: "reviewsFor", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer reviews for" },
|
|
748
|
+
{ name: "hasCapability", sourceTypes: ["Developer"], targetTypes: ["Capability"], cardinality: "many", description: "Developer has capability" },
|
|
749
|
+
{ name: "hasRole", sourceTypes: ["Developer"], targetTypes: ["Role"], cardinality: "many", description: "Developer has role" },
|
|
750
|
+
{ name: "assignedTo", sourceTypes: ["Developer"], targetTypes: ["Feature"], cardinality: "many", description: "Developer is assigned to feature" }
|
|
751
|
+
]
|
|
752
|
+
};
|
|
753
|
+
var agentOntology = {
|
|
754
|
+
id: "trellis:agent",
|
|
755
|
+
name: "Agent Ontology",
|
|
756
|
+
version: "1.0.0",
|
|
757
|
+
description: "Entity types for AI agents, runs, plans, and tools.",
|
|
758
|
+
entities: [
|
|
759
|
+
{
|
|
760
|
+
name: "Agent",
|
|
761
|
+
description: "An AI agent definition.",
|
|
762
|
+
attributes: [
|
|
763
|
+
{ name: "name", type: "string", required: true },
|
|
764
|
+
{ name: "description", type: "string" },
|
|
765
|
+
{ name: "model", type: "string", description: "LLM model identifier" },
|
|
766
|
+
{ name: "provider", type: "string", description: "LLM provider (openai, anthropic, local, etc.)" },
|
|
767
|
+
{ name: "systemPrompt", type: "string" },
|
|
768
|
+
{ name: "status", type: "string", enum: ["active", "inactive", "deprecated"], default: "active" }
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "AgentCapability",
|
|
773
|
+
description: "A capability or skill an agent possesses.",
|
|
774
|
+
attributes: [
|
|
775
|
+
{ name: "name", type: "string", required: true },
|
|
776
|
+
{ name: "description", type: "string" },
|
|
777
|
+
{ name: "category", type: "string" }
|
|
778
|
+
]
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "AgentRun",
|
|
782
|
+
description: "A single execution run of an agent.",
|
|
783
|
+
attributes: [
|
|
784
|
+
{ name: "startedAt", type: "date", required: true },
|
|
785
|
+
{ name: "completedAt", type: "date" },
|
|
786
|
+
{ name: "status", type: "string", enum: ["running", "completed", "failed", "cancelled"], default: "running" },
|
|
787
|
+
{ name: "input", type: "string" },
|
|
788
|
+
{ name: "output", type: "string" },
|
|
789
|
+
{ name: "tokenCount", type: "number" }
|
|
790
|
+
]
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "AgentPlan",
|
|
794
|
+
description: "A plan or strategy created by an agent.",
|
|
795
|
+
attributes: [
|
|
796
|
+
{ name: "title", type: "string", required: true },
|
|
797
|
+
{ name: "description", type: "string" },
|
|
798
|
+
{ name: "status", type: "string", enum: ["draft", "active", "completed", "abandoned"], default: "draft" }
|
|
799
|
+
]
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
name: "Tool",
|
|
803
|
+
description: "A tool available to agents.",
|
|
804
|
+
attributes: [
|
|
805
|
+
{ name: "name", type: "string", required: true },
|
|
806
|
+
{ name: "description", type: "string" },
|
|
807
|
+
{ name: "schema", type: "string", description: "JSON schema for tool parameters" },
|
|
808
|
+
{ name: "endpoint", type: "string" }
|
|
809
|
+
]
|
|
810
|
+
}
|
|
811
|
+
],
|
|
812
|
+
relations: [
|
|
813
|
+
{ name: "hasCapability", sourceTypes: ["Agent"], targetTypes: ["AgentCapability"], cardinality: "many" },
|
|
814
|
+
{ name: "hasTool", sourceTypes: ["Agent"], targetTypes: ["Tool"], cardinality: "many" },
|
|
815
|
+
{ name: "executedBy", sourceTypes: ["AgentRun"], targetTypes: ["Agent"], cardinality: "one" },
|
|
816
|
+
{ name: "hasPlan", sourceTypes: ["AgentRun"], targetTypes: ["AgentPlan"], cardinality: "many" },
|
|
817
|
+
{ name: "usedTool", sourceTypes: ["AgentRun"], targetTypes: ["Tool"], cardinality: "many" },
|
|
818
|
+
{ name: "createdBy", sourceTypes: ["AgentPlan"], targetTypes: ["Agent"], cardinality: "one" }
|
|
819
|
+
]
|
|
820
|
+
};
|
|
821
|
+
var builtinOntologies = [
|
|
822
|
+
projectOntology,
|
|
823
|
+
teamOntology,
|
|
824
|
+
agentOntology
|
|
825
|
+
];
|
|
826
|
+
|
|
827
|
+
// src/core/ontology/validator.ts
|
|
828
|
+
function validateEntity(entityId, store, registry) {
|
|
829
|
+
const errors = [];
|
|
830
|
+
const warnings = [];
|
|
831
|
+
const facts = store.getFactsByEntity(entityId);
|
|
832
|
+
if (facts.length === 0) {
|
|
833
|
+
return { valid: true, errors: [], warnings: [] };
|
|
834
|
+
}
|
|
835
|
+
const typeFact = facts.find((f) => f.a === "type");
|
|
836
|
+
if (!typeFact) {
|
|
837
|
+
warnings.push({
|
|
838
|
+
entityId,
|
|
839
|
+
entityType: "(unknown)",
|
|
840
|
+
field: "type",
|
|
841
|
+
message: 'Entity has no "type" attribute.',
|
|
842
|
+
severity: "warning"
|
|
843
|
+
});
|
|
844
|
+
return { valid: true, errors, warnings };
|
|
845
|
+
}
|
|
846
|
+
const entityType = String(typeFact.v);
|
|
847
|
+
const def = registry.getEntityDef(entityType);
|
|
848
|
+
if (!def) {
|
|
849
|
+
warnings.push({
|
|
850
|
+
entityId,
|
|
851
|
+
entityType,
|
|
852
|
+
field: "type",
|
|
853
|
+
message: `Entity type "${entityType}" is not defined in any registered ontology.`,
|
|
854
|
+
severity: "warning"
|
|
855
|
+
});
|
|
856
|
+
return { valid: true, errors, warnings };
|
|
857
|
+
}
|
|
858
|
+
if (def.abstract) {
|
|
859
|
+
errors.push({
|
|
860
|
+
entityId,
|
|
861
|
+
entityType,
|
|
862
|
+
field: "type",
|
|
863
|
+
message: `Cannot instantiate abstract entity type "${entityType}".`,
|
|
864
|
+
severity: "error"
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
for (const attr of def.attributes) {
|
|
868
|
+
if (attr.required && attr.name !== "type") {
|
|
869
|
+
const hasFact = facts.some((f) => f.a === attr.name);
|
|
870
|
+
if (!hasFact) {
|
|
871
|
+
errors.push({
|
|
872
|
+
entityId,
|
|
873
|
+
entityType,
|
|
874
|
+
field: attr.name,
|
|
875
|
+
message: `Required attribute "${attr.name}" is missing.`,
|
|
876
|
+
severity: "error"
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const fact of facts) {
|
|
882
|
+
if (fact.a === "type" || fact.a === "createdAt" || fact.a === "updatedAt")
|
|
883
|
+
continue;
|
|
884
|
+
const attrDef = def.attributes.find((a) => a.name === fact.a);
|
|
885
|
+
if (!attrDef) {
|
|
886
|
+
warnings.push({
|
|
887
|
+
entityId,
|
|
888
|
+
entityType,
|
|
889
|
+
field: fact.a,
|
|
890
|
+
message: `Attribute "${fact.a}" is not defined in the "${entityType}" ontology.`,
|
|
891
|
+
severity: "warning"
|
|
892
|
+
});
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const typeErr = validateAttrType(fact.v, attrDef);
|
|
896
|
+
if (typeErr) {
|
|
897
|
+
errors.push({
|
|
898
|
+
entityId,
|
|
899
|
+
entityType,
|
|
900
|
+
field: fact.a,
|
|
901
|
+
message: typeErr,
|
|
902
|
+
severity: "error"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (attrDef.enum && !attrDef.enum.includes(fact.v)) {
|
|
906
|
+
errors.push({
|
|
907
|
+
entityId,
|
|
908
|
+
entityType,
|
|
909
|
+
field: fact.a,
|
|
910
|
+
message: `Value "${fact.v}" is not in allowed values: [${attrDef.enum.join(", ")}].`,
|
|
911
|
+
severity: "error"
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (attrDef.pattern && typeof fact.v === "string") {
|
|
915
|
+
if (!new RegExp(attrDef.pattern).test(fact.v)) {
|
|
916
|
+
errors.push({
|
|
917
|
+
entityId,
|
|
918
|
+
entityType,
|
|
919
|
+
field: fact.a,
|
|
920
|
+
message: `Value "${fact.v}" does not match pattern /${attrDef.pattern}/.`,
|
|
921
|
+
severity: "error"
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (attrDef.min !== undefined) {
|
|
926
|
+
if (typeof fact.v === "number" && fact.v < attrDef.min) {
|
|
927
|
+
errors.push({
|
|
928
|
+
entityId,
|
|
929
|
+
entityType,
|
|
930
|
+
field: fact.a,
|
|
931
|
+
message: `Value ${fact.v} is below minimum ${attrDef.min}.`,
|
|
932
|
+
severity: "error"
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (typeof fact.v === "string" && fact.v.length < attrDef.min) {
|
|
936
|
+
errors.push({
|
|
937
|
+
entityId,
|
|
938
|
+
entityType,
|
|
939
|
+
field: fact.a,
|
|
940
|
+
message: `String length ${fact.v.length} is below minimum ${attrDef.min}.`,
|
|
941
|
+
severity: "error"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (attrDef.max !== undefined) {
|
|
946
|
+
if (typeof fact.v === "number" && fact.v > attrDef.max) {
|
|
947
|
+
errors.push({
|
|
948
|
+
entityId,
|
|
949
|
+
entityType,
|
|
950
|
+
field: fact.a,
|
|
951
|
+
message: `Value ${fact.v} exceeds maximum ${attrDef.max}.`,
|
|
952
|
+
severity: "error"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (typeof fact.v === "string" && fact.v.length > attrDef.max) {
|
|
956
|
+
errors.push({
|
|
957
|
+
entityId,
|
|
958
|
+
entityType,
|
|
959
|
+
field: fact.a,
|
|
960
|
+
message: `String length ${fact.v.length} exceeds maximum ${attrDef.max}.`,
|
|
961
|
+
severity: "error"
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const links = store.getLinksByEntity(entityId);
|
|
967
|
+
for (const link of links) {
|
|
968
|
+
if (link.e1 !== entityId)
|
|
969
|
+
continue;
|
|
970
|
+
const relDef = registry.getRelationDef(link.a);
|
|
971
|
+
if (!relDef)
|
|
972
|
+
continue;
|
|
973
|
+
if (!relDef.sourceTypes.includes(entityType)) {
|
|
974
|
+
errors.push({
|
|
975
|
+
entityId,
|
|
976
|
+
entityType,
|
|
977
|
+
field: link.a,
|
|
978
|
+
message: `Entity type "${entityType}" is not allowed as source for relation "${link.a}".`,
|
|
979
|
+
severity: "error"
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
const targetFacts = store.getFactsByEntity(link.e2);
|
|
983
|
+
const targetType = targetFacts.find((f) => f.a === "type");
|
|
984
|
+
if (targetType && !relDef.targetTypes.includes(String(targetType.v))) {
|
|
985
|
+
errors.push({
|
|
986
|
+
entityId,
|
|
987
|
+
entityType,
|
|
988
|
+
field: link.a,
|
|
989
|
+
message: `Target type "${targetType.v}" is not allowed for relation "${link.a}" (expected: ${relDef.targetTypes.join(", ")}).`,
|
|
990
|
+
severity: "error"
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
valid: errors.length === 0,
|
|
996
|
+
errors,
|
|
997
|
+
warnings
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function validateStore(store, registry) {
|
|
1001
|
+
const allErrors = [];
|
|
1002
|
+
const allWarnings = [];
|
|
1003
|
+
const typeFacts = store.getFactsByAttribute("type");
|
|
1004
|
+
const entityIds = new Set(typeFacts.map((f) => f.e));
|
|
1005
|
+
for (const entityId of entityIds) {
|
|
1006
|
+
const result = validateEntity(entityId, store, registry);
|
|
1007
|
+
allErrors.push(...result.errors);
|
|
1008
|
+
allWarnings.push(...result.warnings);
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
valid: allErrors.length === 0,
|
|
1012
|
+
errors: allErrors,
|
|
1013
|
+
warnings: allWarnings
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function validateAttrType(value, def) {
|
|
1017
|
+
if (def.type === "any")
|
|
1018
|
+
return null;
|
|
1019
|
+
switch (def.type) {
|
|
1020
|
+
case "string":
|
|
1021
|
+
if (typeof value !== "string")
|
|
1022
|
+
return `Expected string, got ${typeof value}.`;
|
|
1023
|
+
break;
|
|
1024
|
+
case "number":
|
|
1025
|
+
if (typeof value !== "number")
|
|
1026
|
+
return `Expected number, got ${typeof value}.`;
|
|
1027
|
+
break;
|
|
1028
|
+
case "boolean":
|
|
1029
|
+
if (typeof value !== "boolean")
|
|
1030
|
+
return `Expected boolean, got ${typeof value}.`;
|
|
1031
|
+
break;
|
|
1032
|
+
case "date":
|
|
1033
|
+
if (typeof value === "string") {
|
|
1034
|
+
if (isNaN(Date.parse(value)))
|
|
1035
|
+
return `Expected ISO date string, got "${value}".`;
|
|
1036
|
+
} else if (!(value instanceof Date)) {
|
|
1037
|
+
return `Expected date, got ${typeof value}.`;
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
case "ref":
|
|
1041
|
+
if (typeof value !== "string")
|
|
1042
|
+
return `Expected entity reference (string), got ${typeof value}.`;
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
function createValidationMiddleware(registry, options) {
|
|
1048
|
+
const strict = options?.strict ?? false;
|
|
1049
|
+
return {
|
|
1050
|
+
name: "ontology-validator",
|
|
1051
|
+
handleOp: (op, ctx, next) => {
|
|
1052
|
+
if (op.facts && op.facts.length > 0) {
|
|
1053
|
+
for (const fact of op.facts) {
|
|
1054
|
+
if (fact.a === "type")
|
|
1055
|
+
continue;
|
|
1056
|
+
if (fact.a === "createdAt" || fact.a === "updatedAt")
|
|
1057
|
+
continue;
|
|
1058
|
+
const typeFact = op.facts.find((f) => f.e === fact.e && f.a === "type");
|
|
1059
|
+
if (!typeFact)
|
|
1060
|
+
continue;
|
|
1061
|
+
const entityType = String(typeFact.v);
|
|
1062
|
+
const def = registry.getEntityDef(entityType);
|
|
1063
|
+
if (!def) {
|
|
1064
|
+
if (strict) {
|
|
1065
|
+
throw new Error(`[ontology-validator] Unknown entity type "${entityType}" for entity "${fact.e}".`);
|
|
1066
|
+
}
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const attrDef = def.attributes.find((a) => a.name === fact.a);
|
|
1070
|
+
if (!attrDef) {
|
|
1071
|
+
if (strict) {
|
|
1072
|
+
throw new Error(`[ontology-validator] Unknown attribute "${fact.a}" for type "${entityType}".`);
|
|
1073
|
+
}
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const typeErr = validateAttrType(fact.v, attrDef);
|
|
1077
|
+
if (typeErr) {
|
|
1078
|
+
throw new Error(`[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": ${typeErr}`);
|
|
1079
|
+
}
|
|
1080
|
+
if (attrDef.enum && !attrDef.enum.includes(fact.v)) {
|
|
1081
|
+
throw new Error(`[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": value "${fact.v}" not in [${attrDef.enum.join(", ")}].`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (op.links && op.links.length > 0) {
|
|
1086
|
+
for (const link of op.links) {
|
|
1087
|
+
const relDef = registry.getRelationDef(link.a);
|
|
1088
|
+
if (!relDef)
|
|
1089
|
+
continue;
|
|
1090
|
+
const sourceTypeFact = op.facts?.find((f) => f.e === link.e1 && f.a === "type");
|
|
1091
|
+
if (sourceTypeFact && !relDef.sourceTypes.includes(String(sourceTypeFact.v))) {
|
|
1092
|
+
throw new Error(`[ontology-validator] Relation "${link.a}": source type "${sourceTypeFact.v}" not allowed (expected: ${relDef.sourceTypes.join(", ")}).`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return next(op, ctx);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
export { parseQuery, parseRule, parseSimple, DatalogRuntime, OntologyRegistry, projectOntology, teamOntology, agentOntology, builtinOntologies, validateEntity, validateStore, createValidationMiddleware };
|