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,47 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { loadConfig, resolveGraphUri } from '../lib/config.js';
|
|
3
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
4
|
+
import { materializeInferences, clearInferences } from '../lib/reasoner.js';
|
|
5
|
+
export function registerInfer(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('infer')
|
|
8
|
+
.description('Run RDFS inference on the project graph')
|
|
9
|
+
.option('--clear', 'Clear the inference graph')
|
|
10
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
let config;
|
|
13
|
+
try {
|
|
14
|
+
config = loadConfig();
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.error(`Error: ${err.message}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
|
|
21
|
+
try {
|
|
22
|
+
const adapter = await createReadyAdapter(config);
|
|
23
|
+
if (opts.clear) {
|
|
24
|
+
await clearInferences(adapter, graphUri);
|
|
25
|
+
console.log(pc.green('Cleared inference graph'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const result = await materializeInferences(adapter, graphUri);
|
|
29
|
+
console.log(pc.green(`Inferred ${pc.cyan(String(result.inferredCount))} triples`));
|
|
30
|
+
const activeRules = Object.entries(result.rules).filter(([, n]) => n > 0);
|
|
31
|
+
if (activeRules.length > 0) {
|
|
32
|
+
const breakdown = activeRules.map(([rule, n]) => `${rule}: ${pc.cyan(String(n))}`).join(', ');
|
|
33
|
+
console.log(breakdown);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const message = err.message;
|
|
38
|
+
if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
39
|
+
console.error(`Cannot connect to Oxigraph at ${config.endpoint ?? 'unknown'}. Is it running? Start with: docker compose up -d`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.error(`Error: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { configExists, saveConfig } from '../lib/config.js';
|
|
4
|
+
const DEFAULT_PREFIXES = {
|
|
5
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
6
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
7
|
+
owl: 'http://www.w3.org/2002/07/owl#',
|
|
8
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
9
|
+
schema: 'http://schema.org/',
|
|
10
|
+
};
|
|
11
|
+
function slugify(name) {
|
|
12
|
+
return name
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
15
|
+
.replace(/^-|-$/g, '');
|
|
16
|
+
}
|
|
17
|
+
export function registerInit(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('init [projectId]')
|
|
20
|
+
.description('Initialize a new OpenTology project')
|
|
21
|
+
.option('--embedded', 'Use embedded mode (no server needed)')
|
|
22
|
+
.action((projectId, opts) => {
|
|
23
|
+
if (configExists()) {
|
|
24
|
+
console.error('Error: .opentology.json already exists in this directory.');
|
|
25
|
+
console.error('Remove it first if you want to re-initialize.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const id = projectId || slugify(basename(process.cwd()));
|
|
29
|
+
if (!id) {
|
|
30
|
+
console.error('Error: Could not determine project ID. Please provide one explicitly.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const graphUri = `https://opentology.dev/${id}`;
|
|
34
|
+
if (opts.embedded) {
|
|
35
|
+
saveConfig({ projectId: id, mode: 'embedded', graphUri, prefixes: DEFAULT_PREFIXES });
|
|
36
|
+
console.log(pc.green(`Initialized OpenTology project.`));
|
|
37
|
+
console.log(` Project ID: ${id}`);
|
|
38
|
+
console.log(` Mode: embedded (no server needed)`);
|
|
39
|
+
console.log(` Graph URI: ${graphUri}`);
|
|
40
|
+
console.log(`\nConfig saved to .opentology.json`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const endpoint = 'http://localhost:7878';
|
|
44
|
+
saveConfig({ projectId: id, mode: 'http', endpoint, graphUri, prefixes: DEFAULT_PREFIXES });
|
|
45
|
+
console.log(pc.green(`Initialized OpenTology project.`));
|
|
46
|
+
console.log(` Project ID: ${id}`);
|
|
47
|
+
console.log(` Mode: http`);
|
|
48
|
+
console.log(` Endpoint: ${endpoint}`);
|
|
49
|
+
console.log(` Graph URI: ${graphUri}`);
|
|
50
|
+
console.log(`\nConfig saved to .opentology.json`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
3
|
+
export function registerPrefix(program) {
|
|
4
|
+
const prefix = program
|
|
5
|
+
.command('prefix')
|
|
6
|
+
.description('Manage project-level SPARQL prefix declarations');
|
|
7
|
+
prefix
|
|
8
|
+
.command('list')
|
|
9
|
+
.description('Show all registered prefixes')
|
|
10
|
+
.action(() => {
|
|
11
|
+
let config;
|
|
12
|
+
try {
|
|
13
|
+
config = loadConfig();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.error(`Error: ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const prefixes = config.prefixes ?? {};
|
|
20
|
+
const entries = Object.entries(prefixes);
|
|
21
|
+
if (entries.length === 0) {
|
|
22
|
+
console.log('No prefixes registered. Use `opentology prefix add <name> <uri>` to add one.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const maxName = Math.max(...entries.map(([n]) => n.length), 6);
|
|
26
|
+
console.log(`${'PREFIX'.padEnd(maxName)} URI`);
|
|
27
|
+
console.log(`${'-'.repeat(maxName)} ${'-'.repeat(40)}`);
|
|
28
|
+
for (const [name, uri] of entries) {
|
|
29
|
+
console.log(`${pc.cyan(name.padEnd(maxName))} ${uri}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
prefix
|
|
33
|
+
.command('add <name> <uri>')
|
|
34
|
+
.description('Add a prefix mapping')
|
|
35
|
+
.action((name, uri) => {
|
|
36
|
+
let config;
|
|
37
|
+
try {
|
|
38
|
+
config = loadConfig();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`Error: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!config.prefixes)
|
|
45
|
+
config.prefixes = {};
|
|
46
|
+
config.prefixes[name] = uri;
|
|
47
|
+
saveConfig(config);
|
|
48
|
+
console.log(pc.green(`Added prefix ${pc.cyan(name)}: → ${uri}`));
|
|
49
|
+
});
|
|
50
|
+
prefix
|
|
51
|
+
.command('remove <name>')
|
|
52
|
+
.description('Remove a prefix mapping')
|
|
53
|
+
.action((name) => {
|
|
54
|
+
let config;
|
|
55
|
+
try {
|
|
56
|
+
config = loadConfig();
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`Error: ${err.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (!config.prefixes?.[name]) {
|
|
63
|
+
console.error(`Error: prefix '${name}' is not registered.`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
delete config.prefixes[name];
|
|
67
|
+
if (Object.keys(config.prefixes).length === 0) {
|
|
68
|
+
config.prefixes = undefined;
|
|
69
|
+
}
|
|
70
|
+
saveConfig(config);
|
|
71
|
+
console.log(pc.green(`Removed prefix ${pc.cyan(name)}`));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { loadConfig, resolveGraphUri } from '../lib/config.js';
|
|
3
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
4
|
+
export function registerPull(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('pull [output]')
|
|
7
|
+
.description('Export graph from triplestore as Turtle')
|
|
8
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
9
|
+
.action(async (output, opts) => {
|
|
10
|
+
let config;
|
|
11
|
+
try {
|
|
12
|
+
config = loadConfig();
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
console.error(`Error: ${err.message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
|
|
19
|
+
try {
|
|
20
|
+
const adapter = await createReadyAdapter(config);
|
|
21
|
+
const turtle = await adapter.exportGraph(graphUri);
|
|
22
|
+
const count = await adapter.getGraphTripleCount(graphUri);
|
|
23
|
+
if (output) {
|
|
24
|
+
writeFileSync(output, turtle, 'utf-8');
|
|
25
|
+
console.log(`Exported ${count} triples to ${output}`);
|
|
26
|
+
}
|
|
27
|
+
else if (process.stdout.isTTY) {
|
|
28
|
+
// Interactive terminal — write to default file
|
|
29
|
+
const filename = `${config.projectId}.ttl`;
|
|
30
|
+
writeFileSync(filename, turtle, 'utf-8');
|
|
31
|
+
console.log(`Exported ${count} triples to ${filename}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Piped — write turtle to stdout
|
|
35
|
+
process.stdout.write(turtle);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`Error: ${err.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { loadConfig, resolveGraphUri, saveConfig, addTrackedFile } from '../lib/config.js';
|
|
4
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
5
|
+
import { validateTurtleFile } from '../lib/validator.js';
|
|
6
|
+
import { discoverShapes, validateWithShacl, hasShapes } from '../lib/shacl.js';
|
|
7
|
+
import { materializeInferences } from '../lib/reasoner.js';
|
|
8
|
+
export function registerPush(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('push <file>')
|
|
11
|
+
.description('Push a Turtle file to the triplestore')
|
|
12
|
+
.option('--replace', 'Replace entire graph contents (drop + push)')
|
|
13
|
+
.option('--no-shacl', 'Skip SHACL validation')
|
|
14
|
+
.option('--no-infer', 'Skip RDFS inference after push')
|
|
15
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
16
|
+
.action(async (file, opts) => {
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = loadConfig();
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error(`Error: ${err.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
|
|
26
|
+
try {
|
|
27
|
+
const result = await validateTurtleFile(file);
|
|
28
|
+
if (!result.valid) {
|
|
29
|
+
console.error(`Validation failed: ${result.error}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const turtle = readFileSync(file, 'utf-8');
|
|
33
|
+
// Auto-validate against SHACL when shapes exist (unless --no-shacl)
|
|
34
|
+
if (opts.shacl !== false && hasShapes()) {
|
|
35
|
+
const shapePaths = discoverShapes();
|
|
36
|
+
const report = await validateWithShacl(turtle, shapePaths);
|
|
37
|
+
if (!report.conforms) {
|
|
38
|
+
for (const v of report.violations) {
|
|
39
|
+
console.error(pc.red(`SHACL Violation: ${v.focusNode} — ${v.message} (path: ${v.path})`));
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
console.log('SHACL: conforms');
|
|
44
|
+
}
|
|
45
|
+
const adapter = await createReadyAdapter(config);
|
|
46
|
+
if (opts.replace) {
|
|
47
|
+
await adapter.dropGraph(graphUri);
|
|
48
|
+
}
|
|
49
|
+
await adapter.insertTurtle(graphUri, turtle);
|
|
50
|
+
// In embedded mode, track the file
|
|
51
|
+
if (config.mode === 'embedded') {
|
|
52
|
+
addTrackedFile(config, graphUri, file);
|
|
53
|
+
saveConfig(config);
|
|
54
|
+
}
|
|
55
|
+
if (opts.replace) {
|
|
56
|
+
console.log(pc.green(`Replaced graph with ${result.tripleCount} triples`));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(pc.green(`Pushed ${result.tripleCount} triples to ${graphUri}`));
|
|
60
|
+
}
|
|
61
|
+
if (opts.infer !== false) {
|
|
62
|
+
const inference = await materializeInferences(adapter, graphUri);
|
|
63
|
+
if (inference.inferredCount > 0) {
|
|
64
|
+
console.log(pc.green(`Inferred ${pc.cyan(String(inference.inferredCount))} additional triples`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const message = err.message;
|
|
70
|
+
if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
71
|
+
console.error(`Cannot connect to Oxigraph at ${config.endpoint ?? 'unknown'}. Is it running? Start with: docker compose up -d`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.error(`Error: ${message}`);
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { loadConfig, resolveGraphUri } from '../lib/config.js';
|
|
2
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
3
|
+
import { hasGraphScope, autoScopeQuery } from '../lib/sparql-utils.js';
|
|
4
|
+
function injectPrefixes(sparql, prefixes) {
|
|
5
|
+
const lines = Object.entries(prefixes)
|
|
6
|
+
.filter(([prefix]) => {
|
|
7
|
+
// Skip if the query already declares this prefix
|
|
8
|
+
const re = new RegExp(`PREFIX\\s+${prefix}\\s*:`, 'i');
|
|
9
|
+
return !re.test(sparql);
|
|
10
|
+
})
|
|
11
|
+
.map(([prefix, uri]) => `PREFIX ${prefix}: <${uri}>`);
|
|
12
|
+
if (lines.length === 0)
|
|
13
|
+
return sparql;
|
|
14
|
+
return lines.join('\n') + '\n' + sparql;
|
|
15
|
+
}
|
|
16
|
+
function formatTable(vars, bindings) {
|
|
17
|
+
if (bindings.length === 0) {
|
|
18
|
+
return '(no results)';
|
|
19
|
+
}
|
|
20
|
+
// Calculate column widths
|
|
21
|
+
const widths = {};
|
|
22
|
+
for (const v of vars) {
|
|
23
|
+
widths[v] = v.length;
|
|
24
|
+
}
|
|
25
|
+
for (const row of bindings) {
|
|
26
|
+
for (const v of vars) {
|
|
27
|
+
const val = row[v]?.value ?? '';
|
|
28
|
+
widths[v] = Math.max(widths[v], val.length);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Build header
|
|
32
|
+
const header = vars.map(v => v.padEnd(widths[v])).join(' ');
|
|
33
|
+
const separator = vars.map(v => '-'.repeat(widths[v])).join(' ');
|
|
34
|
+
// Build rows
|
|
35
|
+
const rows = bindings.map(row => vars.map(v => (row[v]?.value ?? '').padEnd(widths[v])).join(' '));
|
|
36
|
+
return [header, separator, ...rows].join('\n');
|
|
37
|
+
}
|
|
38
|
+
function formatCsv(vars, bindings) {
|
|
39
|
+
const header = vars.join(',');
|
|
40
|
+
const rows = bindings.map(row => vars.map(v => {
|
|
41
|
+
const val = row[v]?.value ?? '';
|
|
42
|
+
// Escape values containing commas, quotes, or newlines
|
|
43
|
+
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
|
|
44
|
+
return `"${val.replace(/"/g, '""')}"`;
|
|
45
|
+
}
|
|
46
|
+
return val;
|
|
47
|
+
}).join(','));
|
|
48
|
+
return [header, ...rows].join('\n');
|
|
49
|
+
}
|
|
50
|
+
export function registerQuery(program) {
|
|
51
|
+
program
|
|
52
|
+
.command('query <sparql>')
|
|
53
|
+
.description('Run a SPARQL query against the triplestore')
|
|
54
|
+
.option('--format <type>', 'Output format: table, json, csv', 'table')
|
|
55
|
+
.option('--json', 'Output raw JSON (alias for --format json)')
|
|
56
|
+
.option('--raw', 'Skip automatic Named Graph scoping')
|
|
57
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
58
|
+
.action(async (sparql, options) => {
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
config = loadConfig();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`Error: ${err.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const graphUri = options.graph ? resolveGraphUri(config, options.graph) : config.graphUri;
|
|
68
|
+
// Resolve format: --json flag overrides --format
|
|
69
|
+
const format = options.json ? 'json' : (options.format || 'table');
|
|
70
|
+
// Inject project-level PREFIX declarations from config
|
|
71
|
+
let effectiveSparql = sparql;
|
|
72
|
+
if (config.prefixes) {
|
|
73
|
+
effectiveSparql = injectPrefixes(effectiveSparql, config.prefixes);
|
|
74
|
+
}
|
|
75
|
+
// Auto-scope the query to the project's Named Graph unless the user
|
|
76
|
+
// has already specified graph scoping or passed --raw.
|
|
77
|
+
if (!options.raw && !hasGraphScope(effectiveSparql)) {
|
|
78
|
+
const scoped = autoScopeQuery(effectiveSparql, graphUri);
|
|
79
|
+
if (scoped !== null) {
|
|
80
|
+
effectiveSparql = scoped;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Transformation failed — run as-is and warn.
|
|
84
|
+
console.warn(`Warning: could not auto-scope query. Add GRAPH <${graphUri}> manually or use --raw.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const adapter = await createReadyAdapter(config);
|
|
89
|
+
const results = await adapter.sparqlQuery(effectiveSparql);
|
|
90
|
+
switch (format) {
|
|
91
|
+
case 'json':
|
|
92
|
+
console.log(JSON.stringify(results, null, 2));
|
|
93
|
+
break;
|
|
94
|
+
case 'csv':
|
|
95
|
+
console.log(formatCsv(results.head.vars, results.results.bindings));
|
|
96
|
+
break;
|
|
97
|
+
default: {
|
|
98
|
+
const output = formatTable(results.head.vars, results.results.bindings);
|
|
99
|
+
console.log(output);
|
|
100
|
+
if (results.results.bindings.length === 0) {
|
|
101
|
+
console.log(`\nHint: use GRAPH <${graphUri}> in your WHERE clause`);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err.message;
|
|
109
|
+
console.error(`Error: ${message}`);
|
|
110
|
+
if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
111
|
+
console.error(`Cannot connect to triplestore at ${config.endpoint ?? 'unknown'}. Is it running? Start with: docker compose up -d`);
|
|
112
|
+
}
|
|
113
|
+
if (message.toLowerCase().includes('parse') && sparql.includes('\\!')) {
|
|
114
|
+
console.error(`Hint: if your query contains !=, your shell may have escaped the '!' character. Try wrapping the query in $'...' or use FILTER(?x NOT IN (<uri>)) instead.`);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { Parser } from 'n3';
|
|
4
|
+
import { discoverShapes } from '../lib/shacl.js';
|
|
5
|
+
/**
|
|
6
|
+
* Parse a Turtle file and extract sh:targetClass values.
|
|
7
|
+
*/
|
|
8
|
+
function extractTargetClasses(turtle) {
|
|
9
|
+
const classes = [];
|
|
10
|
+
const parser = new Parser();
|
|
11
|
+
const quads = parser.parse(turtle);
|
|
12
|
+
for (const quad of quads) {
|
|
13
|
+
if (quad.predicate.value === 'http://www.w3.org/ns/shacl#targetClass') {
|
|
14
|
+
classes.push(quad.object.value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return classes;
|
|
18
|
+
}
|
|
19
|
+
export function registerShapes(program) {
|
|
20
|
+
const shapes = program
|
|
21
|
+
.command('shapes')
|
|
22
|
+
.description('List and inspect SHACL shapes');
|
|
23
|
+
shapes
|
|
24
|
+
.command('list', { isDefault: true })
|
|
25
|
+
.description('List all shapes in shapes/ directory')
|
|
26
|
+
.action(() => {
|
|
27
|
+
const shapePaths = discoverShapes();
|
|
28
|
+
if (shapePaths.length === 0) {
|
|
29
|
+
console.log('No shapes found in shapes/ directory');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log('Shapes:');
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(padRight('File', 30) + padRight('Target Classes', 50));
|
|
35
|
+
console.log(padRight('—', 30, '—') + padRight('—', 50, '—'));
|
|
36
|
+
for (const shapePath of shapePaths) {
|
|
37
|
+
const name = basename(shapePath);
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(shapePath, 'utf-8');
|
|
40
|
+
const targets = extractTargetClasses(content);
|
|
41
|
+
const targetStr = targets.length > 0 ? targets.join(', ') : '(none)';
|
|
42
|
+
console.log(padRight(name, 30) + padRight(targetStr, 50));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
console.log(padRight(name, 30) + padRight('(parse error)', 50));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
shapes
|
|
50
|
+
.command('show <file>')
|
|
51
|
+
.description('Display a shape file\'s contents')
|
|
52
|
+
.action((file) => {
|
|
53
|
+
const shapePaths = discoverShapes();
|
|
54
|
+
const match = shapePaths.find((p) => basename(p) === file || p === file);
|
|
55
|
+
if (!match) {
|
|
56
|
+
console.error(`Shape file not found: ${file}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const content = readFileSync(match, 'utf-8');
|
|
60
|
+
console.log(content);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function padRight(str, len, fill = ' ') {
|
|
64
|
+
if (str.length >= len)
|
|
65
|
+
return str;
|
|
66
|
+
return str + fill.repeat(len - str.length);
|
|
67
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { loadConfig, resolveGraphUri, getTrackedFiles } from '../lib/config.js';
|
|
3
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
4
|
+
import { getInferenceGraphUri } from '../lib/sparql-utils.js';
|
|
5
|
+
export function registerStatus(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('status')
|
|
8
|
+
.description('Show project status and triplestore info')
|
|
9
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
let config;
|
|
12
|
+
try {
|
|
13
|
+
config = loadConfig();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.error(`Error: ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
|
|
20
|
+
console.log(`${pc.cyan('Project:')} ${config.projectId}`);
|
|
21
|
+
console.log(`${pc.cyan('Graph URI:')} ${graphUri}`);
|
|
22
|
+
console.log(`${pc.cyan('Mode:')} ${config.mode}`);
|
|
23
|
+
if (config.endpoint) {
|
|
24
|
+
console.log(`${pc.cyan('Endpoint:')} ${config.endpoint}`);
|
|
25
|
+
}
|
|
26
|
+
if (opts.graph) {
|
|
27
|
+
console.log(`${pc.cyan('Graph:')} ${opts.graph}`);
|
|
28
|
+
}
|
|
29
|
+
if (config.mode === 'embedded') {
|
|
30
|
+
const trackedCount = getTrackedFiles(config, graphUri).length;
|
|
31
|
+
console.log(`${pc.cyan('Tracked files:')} ${trackedCount}`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const adapter = await createReadyAdapter(config);
|
|
35
|
+
const inferenceGraphUri = getInferenceGraphUri(graphUri);
|
|
36
|
+
const assertedCount = await adapter.getGraphTripleCount(graphUri);
|
|
37
|
+
const inferredCount = await adapter.getGraphTripleCount(inferenceGraphUri);
|
|
38
|
+
const totalCount = assertedCount + inferredCount;
|
|
39
|
+
console.log(`${pc.cyan('Triples (asserted):')} ${assertedCount}`);
|
|
40
|
+
console.log(`${pc.cyan('Triples (inferred):')} ${inferredCount}`);
|
|
41
|
+
console.log(`${pc.cyan('Triples (total):')} ${totalCount}`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.log(`${pc.cyan('Triples:')} Cannot connect to triplestore. Is it running?`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { validateTurtleFile } from '../lib/validator.js';
|
|
4
|
+
import { discoverShapes, validateWithShacl } from '../lib/shacl.js';
|
|
5
|
+
export function registerValidate(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('validate <file>')
|
|
8
|
+
.description('Validate a local Turtle/RDF file')
|
|
9
|
+
.option('--shacl', 'Also validate against SHACL shapes in shapes/ directory')
|
|
10
|
+
.action(async (file, opts) => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await validateTurtleFile(file);
|
|
13
|
+
if (result.valid) {
|
|
14
|
+
const prefixList = Object.keys(result.prefixes).join(', ') || '(none)';
|
|
15
|
+
console.log(pc.green(`Valid — ${result.tripleCount} triples, prefixes: ${prefixList}`));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.error(pc.red(`Validation failed: ${result.error}`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
if (opts.shacl) {
|
|
22
|
+
const shapePaths = discoverShapes();
|
|
23
|
+
if (shapePaths.length === 0) {
|
|
24
|
+
console.log('SHACL: no shapes found in shapes/ directory');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const content = readFileSync(file, 'utf-8');
|
|
28
|
+
const report = await validateWithShacl(content, shapePaths);
|
|
29
|
+
if (report.conforms) {
|
|
30
|
+
console.log(pc.green('SHACL: conforms'));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const v of report.violations) {
|
|
34
|
+
console.error(pc.yellow(`SHACL Violation: ${v.focusNode} — ${v.message} (path: ${v.path})`));
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error(`Error: ${err.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED