opentology 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +609 -0
- package/dist/commands/context.d.ts +29 -0
- package/dist/commands/context.js +369 -0
- package/dist/commands/delete.d.ts +2 -0
- package/dist/commands/delete.js +46 -0
- package/dist/commands/diff.d.ts +2 -0
- package/dist/commands/diff.js +43 -0
- package/dist/commands/drop.d.ts +2 -0
- package/dist/commands/drop.js +41 -0
- package/dist/commands/graph.d.ts +2 -0
- package/dist/commands/graph.js +130 -0
- package/dist/commands/infer.d.ts +2 -0
- package/dist/commands/infer.js +47 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +53 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +9 -0
- package/dist/commands/prefix.d.ts +2 -0
- package/dist/commands/prefix.js +73 -0
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +43 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +79 -0
- package/dist/commands/query.d.ts +2 -0
- package/dist/commands/query.js +119 -0
- package/dist/commands/shapes.d.ts +2 -0
- package/dist/commands/shapes.js +67 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +47 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/lib/codebase-scanner.d.ts +41 -0
- package/dist/lib/codebase-scanner.js +360 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +70 -0
- package/dist/lib/embedded-adapter.d.ts +45 -0
- package/dist/lib/embedded-adapter.js +202 -0
- package/dist/lib/http-adapter.d.ts +41 -0
- package/dist/lib/http-adapter.js +169 -0
- package/dist/lib/oxigraph.d.ts +62 -0
- package/dist/lib/oxigraph.js +323 -0
- package/dist/lib/reasoner.d.ts +19 -0
- package/dist/lib/reasoner.js +310 -0
- package/dist/lib/shacl.d.ts +22 -0
- package/dist/lib/shacl.js +105 -0
- package/dist/lib/sparql-utils.d.ts +28 -0
- package/dist/lib/sparql-utils.js +217 -0
- package/dist/lib/store-adapter.d.ts +50 -0
- package/dist/lib/store-adapter.js +1 -0
- package/dist/lib/store-factory.d.ts +9 -0
- package/dist/lib/store-factory.js +71 -0
- package/dist/lib/validator.d.ts +10 -0
- package/dist/lib/validator.js +40 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +1020 -0
- package/dist/templates/claude-md-context.d.ts +4 -0
- package/dist/templates/claude-md-context.js +104 -0
- package/dist/templates/otx-ontology.d.ts +2 -0
- package/dist/templates/otx-ontology.js +31 -0
- package/dist/templates/session-start-hook.d.ts +1 -0
- package/dist/templates/session-start-hook.js +94 -0
- package/dist/templates/slash-commands.d.ts +5 -0
- package/dist/templates/slash-commands.js +108 -0
- package/package.json +58 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import rdf from 'rdf-ext';
|
|
2
|
+
import { Parser } from 'n3';
|
|
3
|
+
import { Validator } from 'shacl-engine';
|
|
4
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Discover .ttl shape files in the given directory (defaults to shapes/ in cwd).
|
|
8
|
+
*/
|
|
9
|
+
export function discoverShapes(dir) {
|
|
10
|
+
const shapesDir = dir ?? join(process.cwd(), 'shapes');
|
|
11
|
+
if (!existsSync(shapesDir)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return readdirSync(shapesDir)
|
|
15
|
+
.filter((f) => f.endsWith('.ttl'))
|
|
16
|
+
.map((f) => join(shapesDir, f));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check whether the shapes directory exists and contains at least one .ttl file.
|
|
20
|
+
*/
|
|
21
|
+
export function hasShapes(shapesDir) {
|
|
22
|
+
return discoverShapes(shapesDir).length > 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a Turtle string into an rdf-ext Dataset using n3.
|
|
26
|
+
*/
|
|
27
|
+
function parseTurtleToDataset(turtle) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const parser = new Parser();
|
|
30
|
+
const quads = [];
|
|
31
|
+
parser.parse(turtle, (err, quad) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
reject(err);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (quad) {
|
|
37
|
+
quads.push(quad);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
41
|
+
resolve(rdf.dataset(quads));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load and combine multiple shape .ttl files into a single rdf-ext Dataset.
|
|
48
|
+
*/
|
|
49
|
+
async function loadShapes(shapePaths) {
|
|
50
|
+
const allQuads = [];
|
|
51
|
+
for (const shapePath of shapePaths) {
|
|
52
|
+
const turtle = readFileSync(shapePath, 'utf-8');
|
|
53
|
+
const dataset = await parseTurtleToDataset(turtle);
|
|
54
|
+
for (const quad of dataset) {
|
|
55
|
+
allQuads.push(quad);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
59
|
+
return rdf.dataset(allQuads);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract a string value from a SHACL result message, which may be a Literal object or a plain string.
|
|
63
|
+
*/
|
|
64
|
+
function extractMessageValue(msg) {
|
|
65
|
+
if (!msg)
|
|
66
|
+
return '';
|
|
67
|
+
if (Array.isArray(msg)) {
|
|
68
|
+
return msg.map(extractMessageValue).join('; ');
|
|
69
|
+
}
|
|
70
|
+
if (typeof msg === 'object' && msg !== null && 'value' in msg) {
|
|
71
|
+
return String(msg.value);
|
|
72
|
+
}
|
|
73
|
+
return String(msg);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract the local name from a URI (e.g. "http://...#Violation" → "Violation").
|
|
77
|
+
*/
|
|
78
|
+
function extractLocalName(uri) {
|
|
79
|
+
const hashIdx = uri.lastIndexOf('#');
|
|
80
|
+
if (hashIdx !== -1)
|
|
81
|
+
return uri.slice(hashIdx + 1);
|
|
82
|
+
const slashIdx = uri.lastIndexOf('/');
|
|
83
|
+
if (slashIdx !== -1)
|
|
84
|
+
return uri.slice(slashIdx + 1);
|
|
85
|
+
return uri;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate a Turtle data string against the given SHACL shape files.
|
|
89
|
+
*/
|
|
90
|
+
export async function validateWithShacl(dataTurtle, shapePaths) {
|
|
91
|
+
const dataDataset = await parseTurtleToDataset(dataTurtle);
|
|
92
|
+
const shapesDataset = await loadShapes(shapePaths);
|
|
93
|
+
const validator = new Validator(shapesDataset, { factory: rdf });
|
|
94
|
+
const report = await validator.validate({ dataset: dataDataset });
|
|
95
|
+
const violations = (report.results ?? []).map((result) => ({
|
|
96
|
+
focusNode: result.focusNode?.value ?? '',
|
|
97
|
+
path: result.path?.value ?? null,
|
|
98
|
+
message: extractMessageValue(result.message),
|
|
99
|
+
severity: extractLocalName(result.severity?.value ?? ''),
|
|
100
|
+
}));
|
|
101
|
+
return {
|
|
102
|
+
conforms: Boolean(report.conforms),
|
|
103
|
+
violations,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Quad } from 'n3';
|
|
2
|
+
export declare const WELL_KNOWN_PREFIXES: Record<string, string>;
|
|
3
|
+
export declare function termToSparql(term: Quad['subject'] | Quad['predicate'] | Quad['object']): string;
|
|
4
|
+
export declare function parseTurtle(turtle: string): Promise<Quad[]>;
|
|
5
|
+
export declare function serializeQuadsToTurtle(quads: Quad[]): Promise<string>;
|
|
6
|
+
export declare function extractPrefixes(uris: string[]): Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Returns true if the query already manages its own graph scoping.
|
|
9
|
+
* Matches GRAPH, FROM NAMED, or FROM < (case-insensitive).
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasGraphScope(sparql: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Wraps the WHERE body of a SPARQL SELECT/CONSTRUCT/ASK/DESCRIBE query so
|
|
14
|
+
* that all triple patterns are scoped to `graphUri`.
|
|
15
|
+
*
|
|
16
|
+
* Handles:
|
|
17
|
+
* SELECT ... WHERE { ... } -> standard form
|
|
18
|
+
* SELECT ... { ... } -> shorthand (no WHERE keyword)
|
|
19
|
+
*
|
|
20
|
+
* Returns null if the outermost `{ ... }` block cannot be located safely.
|
|
21
|
+
*/
|
|
22
|
+
export declare function autoScopeQuery(sparql: string, graphUri: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Auto-scope a SPARQL query with UNION over asserted + inference graphs.
|
|
25
|
+
* Returns null if brace matching fails.
|
|
26
|
+
*/
|
|
27
|
+
export declare function autoScopeQueryWithInference(sparql: string, graphUri: string, inferenceGraphUri: string): string | null;
|
|
28
|
+
export declare function getInferenceGraphUri(graphUri: string): string;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Parser, Writer } from 'n3';
|
|
2
|
+
// ── Well-known prefixes ───────────────────────────────────────────────
|
|
3
|
+
export const WELL_KNOWN_PREFIXES = {
|
|
4
|
+
'http://www.w3.org/1999/02/22-rdf-syntax-ns#': 'rdf',
|
|
5
|
+
'http://www.w3.org/2000/01/rdf-schema#': 'rdfs',
|
|
6
|
+
'http://www.w3.org/2002/07/owl#': 'owl',
|
|
7
|
+
'http://schema.org/': 'schema',
|
|
8
|
+
'http://xmlns.com/foaf/0.1/': 'foaf',
|
|
9
|
+
'http://www.w3.org/2001/XMLSchema#': 'xsd',
|
|
10
|
+
'http://purl.org/dc/elements/1.1/': 'dc',
|
|
11
|
+
'http://purl.org/dc/terms/': 'dcterms',
|
|
12
|
+
'http://www.w3.org/2004/02/skos/core#': 'skos',
|
|
13
|
+
'http://www.w3.org/ns/prov#': 'prov',
|
|
14
|
+
};
|
|
15
|
+
// ── Term / Turtle helpers ─────────────────────────────────────────────
|
|
16
|
+
export function termToSparql(term) {
|
|
17
|
+
switch (term.termType) {
|
|
18
|
+
case 'NamedNode':
|
|
19
|
+
return `<${term.value}>`;
|
|
20
|
+
case 'BlankNode':
|
|
21
|
+
return `_:${term.value}`;
|
|
22
|
+
case 'Literal': {
|
|
23
|
+
const escaped = term.value
|
|
24
|
+
.replace(/\\/g, '\\\\')
|
|
25
|
+
.replace(/"/g, '\\"')
|
|
26
|
+
.replace(/\n/g, '\\n')
|
|
27
|
+
.replace(/\r/g, '\\r')
|
|
28
|
+
.replace(/\t/g, '\\t');
|
|
29
|
+
if (term.language) {
|
|
30
|
+
return `"${escaped}"@${term.language}`;
|
|
31
|
+
}
|
|
32
|
+
if (term.datatype && term.datatype.value !== 'http://www.w3.org/2001/XMLSchema#string') {
|
|
33
|
+
return `"${escaped}"^^<${term.datatype.value}>`;
|
|
34
|
+
}
|
|
35
|
+
return `"${escaped}"`;
|
|
36
|
+
}
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unsupported term type: ${term.termType}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function parseTurtle(turtle) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const parser = new Parser();
|
|
44
|
+
const quads = [];
|
|
45
|
+
parser.parse(turtle, (err, quad) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
reject(new Error(`Failed to parse Turtle: ${err.message}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (quad) {
|
|
51
|
+
quads.push(quad);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve(quads);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function serializeQuadsToTurtle(quads) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const writer = new Writer({ format: 'Turtle' });
|
|
62
|
+
writer.addQuads(quads);
|
|
63
|
+
writer.end((err, result) => {
|
|
64
|
+
if (err) {
|
|
65
|
+
reject(new Error(`Failed to serialize Turtle: ${err.message}`));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
resolve(result);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// ── Prefix extraction ─────────────────────────────────────────────────
|
|
74
|
+
export function extractPrefixes(uris) {
|
|
75
|
+
const prefixes = {};
|
|
76
|
+
for (const uri of uris) {
|
|
77
|
+
// Check well-known prefixes first
|
|
78
|
+
for (const [ns, prefix] of Object.entries(WELL_KNOWN_PREFIXES)) {
|
|
79
|
+
if (uri.startsWith(ns) && !(prefix in Object.values(prefixes))) {
|
|
80
|
+
prefixes[prefix] = ns;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// If not matched by well-known, derive from URI structure
|
|
85
|
+
if (!Object.values(prefixes).some((ns) => uri.startsWith(ns))) {
|
|
86
|
+
// Try hash-based namespace (e.g. http://example.org/ontology#)
|
|
87
|
+
const hashIdx = uri.lastIndexOf('#');
|
|
88
|
+
if (hashIdx !== -1) {
|
|
89
|
+
const ns = uri.slice(0, hashIdx + 1);
|
|
90
|
+
if (!Object.values(prefixes).includes(ns)) {
|
|
91
|
+
// Derive a short prefix from the last path segment before the hash
|
|
92
|
+
const pathSegments = ns.replace(/#$/, '').split('/').filter(Boolean);
|
|
93
|
+
const candidate = pathSegments[pathSegments.length - 1]
|
|
94
|
+
?.toLowerCase()
|
|
95
|
+
.replace(/[^a-z0-9]/g, '')
|
|
96
|
+
.slice(0, 8);
|
|
97
|
+
if (candidate && !(candidate in prefixes)) {
|
|
98
|
+
prefixes[candidate] = ns;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Slash-based namespace (e.g. http://example.org/ontology/)
|
|
104
|
+
const slashIdx = uri.lastIndexOf('/');
|
|
105
|
+
if (slashIdx !== -1) {
|
|
106
|
+
const ns = uri.slice(0, slashIdx + 1);
|
|
107
|
+
if (!Object.values(prefixes).includes(ns)) {
|
|
108
|
+
const pathSegments = ns.replace(/\/$/, '').split('/').filter(Boolean);
|
|
109
|
+
const candidate = pathSegments[pathSegments.length - 1]
|
|
110
|
+
?.toLowerCase()
|
|
111
|
+
.replace(/[^a-z0-9]/g, '')
|
|
112
|
+
.slice(0, 8);
|
|
113
|
+
if (candidate && !(candidate in prefixes)) {
|
|
114
|
+
prefixes[candidate] = ns;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return prefixes;
|
|
122
|
+
}
|
|
123
|
+
// ── Query scoping ─────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Returns true if the query already manages its own graph scoping.
|
|
126
|
+
* Matches GRAPH, FROM NAMED, or FROM < (case-insensitive).
|
|
127
|
+
*/
|
|
128
|
+
export function hasGraphScope(sparql) {
|
|
129
|
+
return /\bGRAPH\b|\bFROM\s+NAMED\b|\bFROM\s*</i.test(sparql);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wraps the WHERE body of a SPARQL SELECT/CONSTRUCT/ASK/DESCRIBE query so
|
|
133
|
+
* that all triple patterns are scoped to `graphUri`.
|
|
134
|
+
*
|
|
135
|
+
* Handles:
|
|
136
|
+
* SELECT ... WHERE { ... } -> standard form
|
|
137
|
+
* SELECT ... { ... } -> shorthand (no WHERE keyword)
|
|
138
|
+
*
|
|
139
|
+
* Returns null if the outermost `{ ... }` block cannot be located safely.
|
|
140
|
+
*/
|
|
141
|
+
export function autoScopeQuery(sparql, graphUri) {
|
|
142
|
+
// Find the first `{` that opens the body.
|
|
143
|
+
// We look for WHERE { or, as a fallback, any { after the projection clause.
|
|
144
|
+
const whereMatch = sparql.match(/\bWHERE\s*\{/i);
|
|
145
|
+
let braceStart;
|
|
146
|
+
if (whereMatch && whereMatch.index !== undefined) {
|
|
147
|
+
// Position of `{` inside `WHERE {`
|
|
148
|
+
braceStart = whereMatch.index + whereMatch[0].length - 1;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Shorthand: find the first `{`
|
|
152
|
+
const firstBrace = sparql.indexOf('{');
|
|
153
|
+
if (firstBrace === -1)
|
|
154
|
+
return null;
|
|
155
|
+
braceStart = firstBrace;
|
|
156
|
+
}
|
|
157
|
+
// Walk forward to find the matching closing `}` (respecting nesting).
|
|
158
|
+
let depth = 0;
|
|
159
|
+
let braceEnd = -1;
|
|
160
|
+
for (let i = braceStart; i < sparql.length; i++) {
|
|
161
|
+
if (sparql[i] === '{')
|
|
162
|
+
depth++;
|
|
163
|
+
else if (sparql[i] === '}') {
|
|
164
|
+
depth--;
|
|
165
|
+
if (depth === 0) {
|
|
166
|
+
braceEnd = i;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (braceEnd === -1)
|
|
172
|
+
return null;
|
|
173
|
+
const before = sparql.slice(0, braceStart + 1); // up to and including `{`
|
|
174
|
+
const inner = sparql.slice(braceStart + 1, braceEnd); // content between `{ ... }`
|
|
175
|
+
const after = sparql.slice(braceEnd); // from `}` onwards
|
|
176
|
+
return `${before} GRAPH <${graphUri}> {${inner}} ${after}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Auto-scope a SPARQL query with UNION over asserted + inference graphs.
|
|
180
|
+
* Returns null if brace matching fails.
|
|
181
|
+
*/
|
|
182
|
+
export function autoScopeQueryWithInference(sparql, graphUri, inferenceGraphUri) {
|
|
183
|
+
const whereMatch = sparql.match(/\bWHERE\s*\{/i);
|
|
184
|
+
let braceStart;
|
|
185
|
+
if (whereMatch && whereMatch.index !== undefined) {
|
|
186
|
+
braceStart = whereMatch.index + whereMatch[0].length - 1;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const firstBrace = sparql.indexOf('{');
|
|
190
|
+
if (firstBrace === -1)
|
|
191
|
+
return null;
|
|
192
|
+
braceStart = firstBrace;
|
|
193
|
+
}
|
|
194
|
+
let depth = 0;
|
|
195
|
+
let braceEnd = -1;
|
|
196
|
+
for (let i = braceStart; i < sparql.length; i++) {
|
|
197
|
+
if (sparql[i] === '{')
|
|
198
|
+
depth++;
|
|
199
|
+
else if (sparql[i] === '}') {
|
|
200
|
+
depth--;
|
|
201
|
+
if (depth === 0) {
|
|
202
|
+
braceEnd = i;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (braceEnd === -1)
|
|
208
|
+
return null;
|
|
209
|
+
const before = sparql.slice(0, braceStart + 1);
|
|
210
|
+
const inner = sparql.slice(braceStart + 1, braceEnd);
|
|
211
|
+
const after = sparql.slice(braceEnd);
|
|
212
|
+
return `${before} GRAPH ?__g {${inner}} FILTER(?__g = <${graphUri}> || ?__g = <${inferenceGraphUri}>) ${after}`;
|
|
213
|
+
}
|
|
214
|
+
// ── Inference graph URI ───────────────────────────────────────────────
|
|
215
|
+
export function getInferenceGraphUri(graphUri) {
|
|
216
|
+
return `${graphUri}/inferred`;
|
|
217
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface SparqlResults {
|
|
2
|
+
head: {
|
|
3
|
+
vars: string[];
|
|
4
|
+
};
|
|
5
|
+
results: {
|
|
6
|
+
bindings: Array<Record<string, {
|
|
7
|
+
type: string;
|
|
8
|
+
value: string;
|
|
9
|
+
datatype?: string;
|
|
10
|
+
}>>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface StoreAdapter {
|
|
14
|
+
sparqlQuery(query: string): Promise<SparqlResults>;
|
|
15
|
+
askQuery(query: string): Promise<boolean>;
|
|
16
|
+
sparqlUpdate(update: string): Promise<void>;
|
|
17
|
+
constructQuery(query: string): Promise<string>;
|
|
18
|
+
insertTurtle(graphUri: string, turtle: string): Promise<void>;
|
|
19
|
+
dropGraph(graphUri: string): Promise<void>;
|
|
20
|
+
deleteTriples(graphUri: string, options: {
|
|
21
|
+
turtle?: string;
|
|
22
|
+
where?: string;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
getGraphTripleCount(graphUri: string): Promise<number>;
|
|
25
|
+
exportGraph(graphUri: string): Promise<string>;
|
|
26
|
+
diffGraph(graphUri: string, localTurtle: string): Promise<{
|
|
27
|
+
added: string[];
|
|
28
|
+
removed: string[];
|
|
29
|
+
unchanged: number;
|
|
30
|
+
}>;
|
|
31
|
+
getSchemaOverview(graphUri: string): Promise<{
|
|
32
|
+
prefixes: Record<string, string>;
|
|
33
|
+
classes: string[];
|
|
34
|
+
properties: string[];
|
|
35
|
+
tripleCount: number;
|
|
36
|
+
}>;
|
|
37
|
+
getClassDetails(graphUri: string, classUri: string): Promise<{
|
|
38
|
+
classUri: string;
|
|
39
|
+
instanceCount: number;
|
|
40
|
+
properties: Array<{
|
|
41
|
+
property: string;
|
|
42
|
+
count: number;
|
|
43
|
+
}>;
|
|
44
|
+
sampleTriples: Array<{
|
|
45
|
+
s: string;
|
|
46
|
+
p: string;
|
|
47
|
+
o: string;
|
|
48
|
+
}>;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StoreAdapter } from './store-adapter.js';
|
|
2
|
+
import type { OpenTologyConfig } from './config.js';
|
|
3
|
+
export declare function createAdapter(config: OpenTologyConfig): StoreAdapter;
|
|
4
|
+
/**
|
|
5
|
+
* Reset the cached embedded adapter. Useful for tests or after config-level
|
|
6
|
+
* changes that invalidate the entire store (e.g. project re-init).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resetAdapterCache(): void;
|
|
9
|
+
export declare function createReadyAdapter(config: OpenTologyConfig): Promise<StoreAdapter>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { getTrackedFiles } from './config.js';
|
|
4
|
+
import { HttpAdapter } from './http-adapter.js';
|
|
5
|
+
import { EmbeddedAdapter } from './embedded-adapter.js';
|
|
6
|
+
import { materializeInferences } from './reasoner.js';
|
|
7
|
+
// Singleton cache for embedded mode — keeps data alive across MCP tool calls.
|
|
8
|
+
let cachedAdapter = null;
|
|
9
|
+
let loadedFileKeys = new Set();
|
|
10
|
+
export function createAdapter(config) {
|
|
11
|
+
if (config.mode === 'embedded') {
|
|
12
|
+
return new EmbeddedAdapter();
|
|
13
|
+
}
|
|
14
|
+
return new HttpAdapter(config.endpoint ?? 'http://localhost:7878');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reset the cached embedded adapter. Useful for tests or after config-level
|
|
18
|
+
* changes that invalidate the entire store (e.g. project re-init).
|
|
19
|
+
*/
|
|
20
|
+
export function resetAdapterCache() {
|
|
21
|
+
cachedAdapter = null;
|
|
22
|
+
loadedFileKeys = new Set();
|
|
23
|
+
}
|
|
24
|
+
export async function createReadyAdapter(config) {
|
|
25
|
+
if (config.mode !== 'embedded') {
|
|
26
|
+
return new HttpAdapter(config.endpoint ?? 'http://localhost:7878');
|
|
27
|
+
}
|
|
28
|
+
// Reuse cached adapter so data persists across MCP tool calls.
|
|
29
|
+
if (!cachedAdapter) {
|
|
30
|
+
cachedAdapter = new EmbeddedAdapter();
|
|
31
|
+
loadedFileKeys = new Set();
|
|
32
|
+
}
|
|
33
|
+
const adapter = cachedAdapter;
|
|
34
|
+
// Incrementally load only newly-tracked files into the existing store.
|
|
35
|
+
const allGraphs = [config.graphUri, ...Object.keys(config.graphs ?? {}).map(k => config.graphs[k])];
|
|
36
|
+
let newFilesLoaded = false;
|
|
37
|
+
for (const graphUri of allGraphs) {
|
|
38
|
+
const files = getTrackedFiles(config, graphUri);
|
|
39
|
+
for (const f of files) {
|
|
40
|
+
const key = `${graphUri}::${resolve(f)}`;
|
|
41
|
+
if (!loadedFileKeys.has(key)) {
|
|
42
|
+
try {
|
|
43
|
+
const content = readFileSync(resolve(f), 'utf-8');
|
|
44
|
+
adapter.loadTurtleIntoGraph(content, graphUri);
|
|
45
|
+
loadedFileKeys.add(key);
|
|
46
|
+
newFilesLoaded = true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File may have been deleted — skip silently
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Re-materialize inferences only when new file data was loaded.
|
|
55
|
+
if (newFilesLoaded) {
|
|
56
|
+
const allGraphUris = new Set();
|
|
57
|
+
allGraphUris.add(config.graphUri);
|
|
58
|
+
if (config.graphs) {
|
|
59
|
+
for (const uri of Object.values(config.graphs)) {
|
|
60
|
+
allGraphUris.add(uri);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const graphUri of allGraphUris) {
|
|
64
|
+
const count = await adapter.getGraphTripleCount(graphUri);
|
|
65
|
+
if (count > 0) {
|
|
66
|
+
await materializeInferences(adapter, graphUri);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return adapter;
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ValidationResult = {
|
|
2
|
+
valid: true;
|
|
3
|
+
tripleCount: number;
|
|
4
|
+
prefixes: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
valid: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function validateTurtle(content: string): Promise<ValidationResult>;
|
|
10
|
+
export declare function validateTurtleFile(filePath: string): Promise<ValidationResult>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Parser } from 'n3';
|
|
3
|
+
export async function validateTurtle(content) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const parser = new Parser();
|
|
6
|
+
const quads = [];
|
|
7
|
+
const prefixes = {};
|
|
8
|
+
parser.parse(content, (err, quad, prefixMap) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
resolve({ valid: false, error: err.message });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (quad) {
|
|
14
|
+
quads.push(quad);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Done — prefixMap is provided as the third argument on completion
|
|
18
|
+
if (prefixMap) {
|
|
19
|
+
for (const [prefix, iri] of Object.entries(prefixMap)) {
|
|
20
|
+
prefixes[prefix] = iri.value ?? String(iri);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
resolve({ valid: true, tripleCount: quads.length, prefixes });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function validateTurtleFile(filePath) {
|
|
29
|
+
let content;
|
|
30
|
+
try {
|
|
31
|
+
content = readFileSync(filePath, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: `Could not read file '${filePath}': ${err.message}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return validateTurtle(content);
|
|
40
|
+
}
|