opentology 0.3.6 → 0.3.8
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/dist/lib/deep-scan-triples.d.ts +7 -2
- package/dist/lib/deep-scan-triples.js +23 -6
- package/dist/lib/deep-scanner-treesitter.d.ts +1 -0
- package/dist/lib/deep-scanner-ts.d.ts +1 -0
- package/dist/lib/deep-scanner-ts.js +1 -0
- package/dist/lib/deep-scanner.d.ts +15 -0
- package/dist/lib/deep-scanner.js +81 -0
- package/dist/lib/extractors/go.d.ts +1 -0
- package/dist/lib/extractors/go.js +1 -0
- package/dist/lib/extractors/java.d.ts +1 -0
- package/dist/lib/extractors/java.js +1 -0
- package/dist/lib/extractors/python.d.ts +1 -0
- package/dist/lib/extractors/python.js +1 -0
- package/dist/lib/extractors/rust.d.ts +1 -0
- package/dist/lib/extractors/rust.js +1 -0
- package/dist/lib/extractors/swift.d.ts +1 -0
- package/dist/lib/extractors/swift.js +1 -0
- package/dist/lib/language-extractor.d.ts +4 -0
- package/dist/mcp/server.js +145 -9
- package/dist/templates/claude-md-context.d.ts +2 -0
- package/dist/templates/claude-md-context.js +97 -116
- package/dist/templates/otx-ontology.d.ts +1 -1
- package/dist/templates/otx-ontology.js +5 -0
- package/dist/templates/stop-session-hook.d.ts +1 -0
- package/dist/templates/stop-session-hook.js +99 -0
- package/package.json +2 -2
|
@@ -8,8 +8,13 @@ export declare function batchTriples(triples: string[], batchSize?: number): str
|
|
|
8
8
|
import type { StoreAdapter as FullStoreAdapter } from './store-adapter.js';
|
|
9
9
|
type StoreAdapter = Pick<FullStoreAdapter, 'sparqlUpdate'>;
|
|
10
10
|
export declare function deleteExistingSymbols(adapter: StoreAdapter, graphUri: string, modulePaths: string[]): Promise<void>;
|
|
11
|
-
export
|
|
11
|
+
export interface PushResult {
|
|
12
12
|
triplesInserted: number;
|
|
13
|
+
triplesFailed: number;
|
|
13
14
|
batchCount: number;
|
|
14
|
-
|
|
15
|
+
batchesFailed: number;
|
|
16
|
+
errors: string[];
|
|
17
|
+
retryHint: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function pushSymbolTriples(adapter: StoreAdapter, graphUri: string, result: DeepScanResult): Promise<PushResult>;
|
|
15
20
|
export {};
|
|
@@ -84,7 +84,7 @@ export function generateSymbolTriples(result) {
|
|
|
84
84
|
return triples;
|
|
85
85
|
}
|
|
86
86
|
// ── Batching ────────────────────────────────────────────────────
|
|
87
|
-
export function batchTriples(triples, batchSize =
|
|
87
|
+
export function batchTriples(triples, batchSize = 25) {
|
|
88
88
|
const batches = [];
|
|
89
89
|
for (let i = 0; i < triples.length; i += batchSize) {
|
|
90
90
|
batches.push(triples.slice(i, i + batchSize));
|
|
@@ -110,11 +110,28 @@ export async function pushSymbolTriples(adapter, graphUri, result) {
|
|
|
110
110
|
modulePaths.add(f.filePath);
|
|
111
111
|
// Delete existing symbols for these modules
|
|
112
112
|
await deleteExistingSymbols(adapter, graphUri, [...modulePaths]);
|
|
113
|
-
// Generate and batch-insert
|
|
113
|
+
// Generate and batch-insert with per-batch error handling
|
|
114
114
|
const triples = generateSymbolTriples(result);
|
|
115
|
-
const batches = batchTriples(triples,
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
const batches = batchTriples(triples, 25);
|
|
116
|
+
let inserted = 0;
|
|
117
|
+
let failed = 0;
|
|
118
|
+
let batchesFailed = 0;
|
|
119
|
+
const errors = [];
|
|
120
|
+
for (let i = 0; i < batches.length; i++) {
|
|
121
|
+
const batch = batches[i];
|
|
122
|
+
try {
|
|
123
|
+
await adapter.sparqlUpdate(`INSERT DATA { GRAPH <${graphUri}> {\n${batch.join('\n')}\n} }`);
|
|
124
|
+
inserted += batch.length;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
batchesFailed++;
|
|
128
|
+
failed += batch.length;
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
errors.push(`Batch ${i + 1}/${batches.length} failed (${batch.length} triples): ${msg}`);
|
|
131
|
+
}
|
|
118
132
|
}
|
|
119
|
-
|
|
133
|
+
const retryHint = batchesFailed > 0
|
|
134
|
+
? `${batchesFailed}/${batches.length} batches failed (${failed} triples lost). Re-run context_scan with depth="symbol" to retry. If failures persist, try reducing maxSymbols or disabling includeMethodCalls.`
|
|
135
|
+
: null;
|
|
136
|
+
return { triplesInserted: inserted, triplesFailed: failed, batchCount: batches.length, batchesFailed, errors, retryHint };
|
|
120
137
|
}
|
|
@@ -12,6 +12,7 @@ export type { TSNode, TSTree, TSLanguage };
|
|
|
12
12
|
export declare abstract class TreeSitterExtractor implements LanguageExtractor {
|
|
13
13
|
abstract readonly language: string;
|
|
14
14
|
abstract readonly extensions: string[];
|
|
15
|
+
abstract readonly dependencyModel: import('./language-extractor.js').DependencyModel;
|
|
15
16
|
/** WASM file name, e.g. 'tree-sitter-python.wasm' */
|
|
16
17
|
protected abstract readonly wasmName: string;
|
|
17
18
|
protected lang: TSLanguage | null;
|
|
@@ -6,6 +6,7 @@ import type { LanguageExtractor, ExtractedSymbols } from './language-extractor.j
|
|
|
6
6
|
export declare class TypeScriptExtractor implements LanguageExtractor {
|
|
7
7
|
readonly language = "typescript";
|
|
8
8
|
readonly extensions: string[];
|
|
9
|
+
readonly dependencyModel: "file-based";
|
|
9
10
|
private tsMorph;
|
|
10
11
|
isAvailable(): Promise<boolean>;
|
|
11
12
|
extract(_filePaths: string[], rootDir: string, options: {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Deep scanner — language-agnostic orchestrator for symbol-level codebase analysis.
|
|
3
3
|
* Delegates to LanguageExtractor implementations (ts-morph, tree-sitter, etc.).
|
|
4
4
|
*/
|
|
5
|
+
import type { DependencyModel } from './language-extractor.js';
|
|
5
6
|
export interface DeepScanOptions {
|
|
6
7
|
maxFiles?: number;
|
|
7
8
|
maxSymbols?: number;
|
|
@@ -48,12 +49,26 @@ export interface MethodCallInfo {
|
|
|
48
49
|
caller: string;
|
|
49
50
|
callee: string;
|
|
50
51
|
}
|
|
52
|
+
export interface LanguageHint {
|
|
53
|
+
language: string;
|
|
54
|
+
dependencyModel: DependencyModel;
|
|
55
|
+
moduleScanApplicable: boolean;
|
|
56
|
+
recommendation: string;
|
|
57
|
+
}
|
|
58
|
+
export interface UnsupportedFileGroup {
|
|
59
|
+
extension: string;
|
|
60
|
+
language: string;
|
|
61
|
+
files: string[];
|
|
62
|
+
count: number;
|
|
63
|
+
}
|
|
51
64
|
export interface DeepScanResult {
|
|
52
65
|
deepScanAvailable: true;
|
|
53
66
|
classes: ClassInfo[];
|
|
54
67
|
interfaces: InterfaceInfo[];
|
|
55
68
|
functions: FunctionInfo[];
|
|
56
69
|
methodCalls: MethodCallInfo[];
|
|
70
|
+
unsupportedFiles: UnsupportedFileGroup[];
|
|
71
|
+
languageHints: LanguageHint[];
|
|
57
72
|
fileCount: number;
|
|
58
73
|
symbolCount: number;
|
|
59
74
|
scanDurationMs: number;
|
package/dist/lib/deep-scanner.js
CHANGED
|
@@ -43,6 +43,80 @@ async function discoverFiles(rootDir, extensions, maxFiles) {
|
|
|
43
43
|
.filter(f => !f.includes('node_modules') && !f.includes('/dist/'));
|
|
44
44
|
return { files: allFiles.slice(0, maxFiles), total: allFiles.length };
|
|
45
45
|
}
|
|
46
|
+
// ── Unsupported language detection ─────────────────────────────
|
|
47
|
+
const EXTENSION_LANGUAGE_MAP = {
|
|
48
|
+
'.rb': 'ruby', '.kt': 'kotlin', '.kts': 'kotlin',
|
|
49
|
+
'.cs': 'csharp', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp',
|
|
50
|
+
'.c': 'c', '.h': 'c-header', '.hpp': 'cpp-header',
|
|
51
|
+
'.php': 'php', '.lua': 'lua', '.dart': 'dart',
|
|
52
|
+
'.scala': 'scala', '.ex': 'elixir', '.exs': 'elixir',
|
|
53
|
+
'.zig': 'zig', '.jl': 'julia', '.r': 'r',
|
|
54
|
+
'.clj': 'clojure', '.erl': 'erlang', '.hs': 'haskell',
|
|
55
|
+
'.ml': 'ocaml', '.nim': 'nim', '.cr': 'crystal',
|
|
56
|
+
'.pl': 'perl', '.pm': 'perl',
|
|
57
|
+
};
|
|
58
|
+
const SKIP_EXTENSIONS = new Set([
|
|
59
|
+
'.json', '.yaml', '.yml', '.toml', '.xml', '.ini', '.cfg', '.conf',
|
|
60
|
+
'.md', '.txt', '.rst', '.csv', '.tsv', '.adoc',
|
|
61
|
+
'.css', '.scss', '.less', '.sass', '.styl',
|
|
62
|
+
'.html', '.htm', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif',
|
|
63
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
64
|
+
'.lock', '.log', '.map', '.d.ts',
|
|
65
|
+
'.wasm', '.bin', '.exe', '.dll', '.so', '.dylib', '.a',
|
|
66
|
+
'.env', '.gitignore', '.gitattributes', '.editorconfig',
|
|
67
|
+
'.prettierrc', '.eslintrc', '.babelrc',
|
|
68
|
+
'.sh', '.bash', '.zsh', '.fish', '.bat', '.cmd', '.ps1',
|
|
69
|
+
]);
|
|
70
|
+
async function discoverUnsupportedFiles(rootDir, supportedExtensions) {
|
|
71
|
+
const { execSync } = await import('node:child_process');
|
|
72
|
+
const { extname } = await import('node:path');
|
|
73
|
+
let stdout;
|
|
74
|
+
try {
|
|
75
|
+
stdout = execSync(`git -C "${rootDir}" ls-files`, {
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const allFiles = stdout.trim().split('\n').filter(Boolean)
|
|
84
|
+
.filter(f => !f.includes('node_modules') && !f.includes('/dist/'));
|
|
85
|
+
const groups = new Map();
|
|
86
|
+
for (const file of allFiles) {
|
|
87
|
+
const ext = extname(file).toLowerCase();
|
|
88
|
+
if (!ext || supportedExtensions.has(ext) || SKIP_EXTENSIONS.has(ext))
|
|
89
|
+
continue;
|
|
90
|
+
if (!EXTENSION_LANGUAGE_MAP[ext])
|
|
91
|
+
continue; // only known source languages
|
|
92
|
+
if (!groups.has(ext))
|
|
93
|
+
groups.set(ext, []);
|
|
94
|
+
groups.get(ext).push(file);
|
|
95
|
+
}
|
|
96
|
+
return Array.from(groups.entries())
|
|
97
|
+
.map(([ext, files]) => ({
|
|
98
|
+
extension: ext,
|
|
99
|
+
language: EXTENSION_LANGUAGE_MAP[ext] ?? ext.slice(1),
|
|
100
|
+
files,
|
|
101
|
+
count: files.length,
|
|
102
|
+
}))
|
|
103
|
+
.filter(g => g.count > 0)
|
|
104
|
+
.sort((a, b) => b.count - a.count);
|
|
105
|
+
}
|
|
106
|
+
// ── Language hint generation ────────────────────────────────────
|
|
107
|
+
const DEPENDENCY_MODEL_RECOMMENDATIONS = {
|
|
108
|
+
'file-based': 'Module-level dependency graph (depth="module") is applicable.',
|
|
109
|
+
'package-based': 'This language uses package-based imports — module-level scan is not applicable. Use depth="symbol" for class/method/call-level analysis.',
|
|
110
|
+
'framework-based': 'This language uses framework-level imports — module-level scan is not applicable. Use depth="symbol" for class/method/call-level analysis.',
|
|
111
|
+
};
|
|
112
|
+
function buildLanguageHints(extractors) {
|
|
113
|
+
return extractors.map(ext => ({
|
|
114
|
+
language: ext.language,
|
|
115
|
+
dependencyModel: ext.dependencyModel,
|
|
116
|
+
moduleScanApplicable: ext.dependencyModel === 'file-based',
|
|
117
|
+
recommendation: DEPENDENCY_MODEL_RECOMMENDATIONS[ext.dependencyModel],
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
46
120
|
// ── Main entry point ────────────────────────────────────────────
|
|
47
121
|
export async function deepScan(rootDir, options) {
|
|
48
122
|
const maxFiles = options?.maxFiles ?? 500;
|
|
@@ -83,6 +157,8 @@ export async function deepScan(rootDir, options) {
|
|
|
83
157
|
interfaces: [],
|
|
84
158
|
functions: [],
|
|
85
159
|
methodCalls: [],
|
|
160
|
+
unsupportedFiles: [],
|
|
161
|
+
languageHints: buildLanguageHints(available),
|
|
86
162
|
fileCount: total,
|
|
87
163
|
symbolCount: 0,
|
|
88
164
|
scanDurationMs: Date.now() - start,
|
|
@@ -147,12 +223,17 @@ export async function deepScan(rootDir, options) {
|
|
|
147
223
|
const symbolCount = classes.reduce((n, c) => n + 1 + c.methods.length, 0)
|
|
148
224
|
+ interfaces.length
|
|
149
225
|
+ functions.length;
|
|
226
|
+
// Discover unsupported language files
|
|
227
|
+
const supportedExtensions = new Set(available.flatMap(e => e.extensions));
|
|
228
|
+
const unsupportedFiles = await discoverUnsupportedFiles(rootDir, supportedExtensions);
|
|
150
229
|
return {
|
|
151
230
|
deepScanAvailable: true,
|
|
152
231
|
classes,
|
|
153
232
|
interfaces,
|
|
154
233
|
functions,
|
|
155
234
|
methodCalls,
|
|
235
|
+
unsupportedFiles,
|
|
236
|
+
languageHints: buildLanguageHints(available),
|
|
156
237
|
fileCount: files.length,
|
|
157
238
|
symbolCount,
|
|
158
239
|
scanDurationMs: Date.now() - start,
|
|
@@ -8,6 +8,7 @@ import type { ExtractedSymbols } from '../language-extractor.js';
|
|
|
8
8
|
export declare class GoExtractor extends TreeSitterExtractor {
|
|
9
9
|
readonly language = "go";
|
|
10
10
|
readonly extensions: string[];
|
|
11
|
+
readonly dependencyModel: "package-based";
|
|
11
12
|
protected readonly wasmName = "tree-sitter-go.wasm";
|
|
12
13
|
protected extractFromTree(tree: TSTree, relPath: string, _source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
13
14
|
private extractStructFields;
|
|
@@ -8,6 +8,7 @@ export class GoExtractor extends TreeSitterExtractor {
|
|
|
8
8
|
super(...arguments);
|
|
9
9
|
this.language = 'go';
|
|
10
10
|
this.extensions = ['.go'];
|
|
11
|
+
this.dependencyModel = 'package-based';
|
|
11
12
|
this.wasmName = 'tree-sitter-go.wasm';
|
|
12
13
|
}
|
|
13
14
|
extractFromTree(tree, relPath, _source, includeMethodCalls) {
|
|
@@ -8,6 +8,7 @@ import type { ExtractedSymbols } from '../language-extractor.js';
|
|
|
8
8
|
export declare class JavaExtractor extends TreeSitterExtractor {
|
|
9
9
|
readonly language = "java";
|
|
10
10
|
readonly extensions: string[];
|
|
11
|
+
readonly dependencyModel: "package-based";
|
|
11
12
|
protected readonly wasmName = "tree-sitter-java.wasm";
|
|
12
13
|
protected extractFromTree(tree: TSTree, relPath: string, _source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
13
14
|
private getSuperclass;
|
|
@@ -8,6 +8,7 @@ export class JavaExtractor extends TreeSitterExtractor {
|
|
|
8
8
|
super(...arguments);
|
|
9
9
|
this.language = 'java';
|
|
10
10
|
this.extensions = ['.java'];
|
|
11
|
+
this.dependencyModel = 'package-based';
|
|
11
12
|
this.wasmName = 'tree-sitter-java.wasm';
|
|
12
13
|
}
|
|
13
14
|
extractFromTree(tree, relPath, _source, includeMethodCalls) {
|
|
@@ -8,6 +8,7 @@ import type { ExtractedSymbols } from '../language-extractor.js';
|
|
|
8
8
|
export declare class PythonExtractor extends TreeSitterExtractor {
|
|
9
9
|
readonly language = "python";
|
|
10
10
|
readonly extensions: string[];
|
|
11
|
+
readonly dependencyModel: "file-based";
|
|
11
12
|
protected readonly wasmName = "tree-sitter-python.wasm";
|
|
12
13
|
protected extractFromTree(tree: TSTree, relPath: string, _source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
13
14
|
private getBaseClasses;
|
|
@@ -9,6 +9,7 @@ export class PythonExtractor extends TreeSitterExtractor {
|
|
|
9
9
|
super(...arguments);
|
|
10
10
|
this.language = 'python';
|
|
11
11
|
this.extensions = ['.py'];
|
|
12
|
+
this.dependencyModel = 'file-based';
|
|
12
13
|
this.wasmName = 'tree-sitter-python.wasm';
|
|
13
14
|
}
|
|
14
15
|
extractFromTree(tree, relPath, _source, includeMethodCalls) {
|
|
@@ -8,6 +8,7 @@ import type { ExtractedSymbols } from '../language-extractor.js';
|
|
|
8
8
|
export declare class RustExtractor extends TreeSitterExtractor {
|
|
9
9
|
readonly language = "rust";
|
|
10
10
|
readonly extensions: string[];
|
|
11
|
+
readonly dependencyModel: "file-based";
|
|
11
12
|
protected readonly wasmName = "tree-sitter-rust.wasm";
|
|
12
13
|
protected extractFromTree(tree: TSTree, relPath: string, _source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
13
14
|
private extractParams;
|
|
@@ -8,6 +8,7 @@ export class RustExtractor extends TreeSitterExtractor {
|
|
|
8
8
|
super(...arguments);
|
|
9
9
|
this.language = 'rust';
|
|
10
10
|
this.extensions = ['.rs'];
|
|
11
|
+
this.dependencyModel = 'file-based';
|
|
11
12
|
this.wasmName = 'tree-sitter-rust.wasm';
|
|
12
13
|
}
|
|
13
14
|
extractFromTree(tree, relPath, _source, includeMethodCalls) {
|
|
@@ -8,6 +8,7 @@ import type { ExtractedSymbols } from '../language-extractor.js';
|
|
|
8
8
|
export declare class SwiftExtractor extends TreeSitterExtractor {
|
|
9
9
|
readonly language = "swift";
|
|
10
10
|
readonly extensions: string[];
|
|
11
|
+
readonly dependencyModel: "framework-based";
|
|
11
12
|
protected readonly wasmName = "tree-sitter-swift.wasm";
|
|
12
13
|
protected extractFromTree(tree: TSTree, relPath: string, _source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
13
14
|
private extractClassLike;
|
|
@@ -8,6 +8,7 @@ export class SwiftExtractor extends TreeSitterExtractor {
|
|
|
8
8
|
super(...arguments);
|
|
9
9
|
this.language = 'swift';
|
|
10
10
|
this.extensions = ['.swift'];
|
|
11
|
+
this.dependencyModel = 'framework-based';
|
|
11
12
|
this.wasmName = 'tree-sitter-swift.wasm';
|
|
12
13
|
}
|
|
13
14
|
extractFromTree(tree, relPath, _source, includeMethodCalls) {
|
|
@@ -9,11 +9,15 @@ export interface ExtractedSymbols {
|
|
|
9
9
|
functions: FunctionInfo[];
|
|
10
10
|
methodCalls: MethodCallInfo[];
|
|
11
11
|
}
|
|
12
|
+
/** How a language resolves inter-file dependencies. */
|
|
13
|
+
export type DependencyModel = 'file-based' | 'package-based' | 'framework-based';
|
|
12
14
|
export interface LanguageExtractor {
|
|
13
15
|
/** Language identifier, e.g. 'typescript', 'python', 'go' */
|
|
14
16
|
readonly language: string;
|
|
15
17
|
/** File extensions handled by this extractor, e.g. ['.ts', '.tsx'] */
|
|
16
18
|
readonly extensions: string[];
|
|
19
|
+
/** How this language resolves dependencies between source files. */
|
|
20
|
+
readonly dependencyModel: DependencyModel;
|
|
17
21
|
/** Check whether the extractor's dependencies are available at runtime. */
|
|
18
22
|
isAvailable(): Promise<boolean>;
|
|
19
23
|
/**
|
package/dist/mcp/server.js
CHANGED
|
@@ -15,11 +15,12 @@ import { fromSchemaData, toMermaid, toDot } from '../lib/visualizer.js';
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
|
|
18
|
-
import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
|
|
18
|
+
import { generateContextSection, updateClaudeMd, updateGlobalClaudeMd } from '../templates/claude-md-context.js';
|
|
19
19
|
import { generateHookScript } from '../templates/session-start-hook.js';
|
|
20
20
|
import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
|
|
21
21
|
import { generateUserPromptHookScript } from '../templates/user-prompt-hook.js';
|
|
22
22
|
import { generatePostErrorHookScript } from '../templates/post-error-hook.js';
|
|
23
|
+
import { generateStopSessionHookScript } from '../templates/stop-session-hook.js';
|
|
23
24
|
import { generateSlashCommands } from '../templates/slash-commands.js';
|
|
24
25
|
import { runDoctor } from '../lib/doctor.js';
|
|
25
26
|
import { syncContext } from '../lib/context-sync.js';
|
|
@@ -380,12 +381,16 @@ async function handleContextScan(args) {
|
|
|
380
381
|
// Auto-push triples server-side
|
|
381
382
|
let pushStats = null;
|
|
382
383
|
let moduleStats = null;
|
|
384
|
+
const pushWarnings = [];
|
|
383
385
|
try {
|
|
384
386
|
const config = loadConfig();
|
|
385
387
|
const contextUri = `${config.graphUri}/context`;
|
|
386
388
|
const adapter = await createReadyAdapter(config);
|
|
387
389
|
// Push symbol triples
|
|
388
390
|
pushStats = await pushSymbolTriples(adapter, contextUri, scanResult);
|
|
391
|
+
if (pushStats.errors.length > 0) {
|
|
392
|
+
pushWarnings.push(...pushStats.errors);
|
|
393
|
+
}
|
|
389
394
|
// Also push module dependency graph (fixes #64)
|
|
390
395
|
const snapshot = await scanCodebase(process.cwd());
|
|
391
396
|
if (snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0) {
|
|
@@ -404,23 +409,89 @@ async function handleContextScan(args) {
|
|
|
404
409
|
}
|
|
405
410
|
await persistGraph(adapter, config, contextUri);
|
|
406
411
|
}
|
|
407
|
-
catch {
|
|
408
|
-
|
|
412
|
+
catch (err) {
|
|
413
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
pushWarnings.push(`Push failed: ${msg}`);
|
|
409
415
|
}
|
|
410
416
|
const hints = [];
|
|
411
|
-
if (pushStats)
|
|
412
|
-
hints.push(`Symbol triples: ${pushStats.triplesInserted}
|
|
417
|
+
if (pushStats) {
|
|
418
|
+
hints.push(`Symbol triples: ${pushStats.triplesInserted} inserted, ${pushStats.triplesFailed} failed, ${pushStats.batchCount} batches`);
|
|
419
|
+
if (pushStats.retryHint)
|
|
420
|
+
hints.push(pushStats.retryHint);
|
|
421
|
+
}
|
|
413
422
|
if (moduleStats)
|
|
414
423
|
hints.push(`Module triples: ${moduleStats.modules} modules, ${moduleStats.edges} edges`);
|
|
415
|
-
|
|
416
|
-
...
|
|
424
|
+
if (pushWarnings.length > 0)
|
|
425
|
+
hints.push(...pushWarnings);
|
|
426
|
+
// Build compact summary — do NOT return full symbol arrays (#66)
|
|
427
|
+
const compact = {
|
|
428
|
+
deepScanAvailable: true,
|
|
429
|
+
counts: {
|
|
430
|
+
classes: scanResult.classes.length,
|
|
431
|
+
interfaces: scanResult.interfaces.length,
|
|
432
|
+
functions: scanResult.functions.length,
|
|
433
|
+
methodCalls: scanResult.methodCalls.length,
|
|
434
|
+
files: scanResult.fileCount,
|
|
435
|
+
symbols: scanResult.symbolCount,
|
|
436
|
+
},
|
|
437
|
+
languageHints: scanResult.languageHints,
|
|
438
|
+
scanDurationMs: scanResult.scanDurationMs,
|
|
439
|
+
capped: scanResult.capped,
|
|
440
|
+
warnings: scanResult.warnings,
|
|
417
441
|
pushStats,
|
|
418
442
|
moduleStats,
|
|
419
443
|
_experimental: true,
|
|
420
444
|
_hint: hints.length
|
|
421
|
-
? `${hints.join('. ')}. Query examples:\n- Classes: SELECT ?c ?name WHERE { ?c a otx:Class ; otx:title ?name }\n- Dependents: SELECT ?dep WHERE { ?dep otx:dependsOn <urn:module:src/lib/store-adapter> }\n- Call graph: SELECT ?caller ?callee WHERE { ?s a otx:MethodCall ; otx:callerSymbol ?caller ; otx:calleeSymbol ?callee }`
|
|
445
|
+
? `${hints.join('. ')}. All symbols auto-pushed to graph. Query examples:\n- Classes: SELECT ?c ?name WHERE { ?c a otx:Class ; otx:title ?name }\n- Dependents: SELECT ?dep WHERE { ?dep otx:dependsOn <urn:module:src/lib/store-adapter> }\n- Call graph: SELECT ?caller ?callee WHERE { ?s a otx:MethodCall ; otx:callerSymbol ?caller ; otx:calleeSymbol ?callee }`
|
|
422
446
|
: 'Deep scan completed but triple push failed. Use push manually with the generated triples.',
|
|
423
447
|
};
|
|
448
|
+
// LLM fallback for unsupported languages (#65)
|
|
449
|
+
if (scanResult.unsupportedFiles.length > 0) {
|
|
450
|
+
const samples = [];
|
|
451
|
+
for (const group of scanResult.unsupportedFiles.slice(0, 5)) {
|
|
452
|
+
// Pick entry-point-like files first, then first files
|
|
453
|
+
const sorted = [...group.files].sort((a, b) => {
|
|
454
|
+
const isEntry = (f) => /\/(main|index|app|mod|lib)\.[^/]+$/.test(f) ? 0 : 1;
|
|
455
|
+
return isEntry(a) - isEntry(b);
|
|
456
|
+
});
|
|
457
|
+
for (const file of sorted.slice(0, 2)) {
|
|
458
|
+
try {
|
|
459
|
+
const fullPath = join(process.cwd(), file);
|
|
460
|
+
const raw = readFileSync(fullPath, 'utf-8');
|
|
461
|
+
const lines = raw.split('\n').slice(0, 30).join('\n');
|
|
462
|
+
samples.push({ path: file, language: group.language, content: lines });
|
|
463
|
+
}
|
|
464
|
+
catch { /* skip unreadable */ }
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
compact.unsupportedFiles = scanResult.unsupportedFiles.map(g => ({
|
|
468
|
+
language: g.language,
|
|
469
|
+
extension: g.extension,
|
|
470
|
+
count: g.count,
|
|
471
|
+
files: g.files.slice(0, 5),
|
|
472
|
+
}));
|
|
473
|
+
if (samples.length > 0) {
|
|
474
|
+
compact.samples = samples;
|
|
475
|
+
compact.turtleTemplate = [
|
|
476
|
+
'# Push symbol triples for unsupported language files.',
|
|
477
|
+
'# Replace {filePath} and {Name} with actual values from the samples above.',
|
|
478
|
+
'@prefix otx: <https://opentology.dev/vocab#> .',
|
|
479
|
+
'',
|
|
480
|
+
'# Class/Struct:',
|
|
481
|
+
'# <urn:symbol:{filePath}/class/{Name}> a otx:Class ;',
|
|
482
|
+
'# otx:title "{Name}" ; otx:definedIn <urn:module:{filePath}> .',
|
|
483
|
+
'',
|
|
484
|
+
'# Function:',
|
|
485
|
+
'# <urn:symbol:{filePath}/function/{name}> a otx:Function ;',
|
|
486
|
+
'# otx:title "{name}" ; otx:definedIn <urn:module:{filePath}> .',
|
|
487
|
+
'',
|
|
488
|
+
'# Interface/Trait/Protocol:',
|
|
489
|
+
'# <urn:symbol:{filePath}/interface/{Name}> a otx:Interface ;',
|
|
490
|
+
'# otx:title "{Name}" ; otx:definedIn <urn:module:{filePath}> .',
|
|
491
|
+
].join('\n');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return compact;
|
|
424
495
|
}
|
|
425
496
|
// Default: module-level scan — now auto-pushes module triples like context_init
|
|
426
497
|
const maxBytes = args.maxSnapshotBytes ?? 15360;
|
|
@@ -452,9 +523,42 @@ async function handleContextScan(args) {
|
|
|
452
523
|
catch {
|
|
453
524
|
// Non-fatal: module triple push is best-effort
|
|
454
525
|
}
|
|
526
|
+
// Generate language hints for module scan — helps LLM understand when module scan isn't applicable
|
|
527
|
+
const moduleLanguageHints = [];
|
|
528
|
+
const fileExtensionMap = {
|
|
529
|
+
'.ts': { lang: 'typescript', model: 'file-based' },
|
|
530
|
+
'.tsx': { lang: 'typescript', model: 'file-based' },
|
|
531
|
+
'.js': { lang: 'javascript', model: 'file-based' },
|
|
532
|
+
'.jsx': { lang: 'javascript', model: 'file-based' },
|
|
533
|
+
'.py': { lang: 'python', model: 'file-based' },
|
|
534
|
+
'.rs': { lang: 'rust', model: 'file-based' },
|
|
535
|
+
'.go': { lang: 'go', model: 'package-based' },
|
|
536
|
+
'.java': { lang: 'java', model: 'package-based' },
|
|
537
|
+
'.swift': { lang: 'swift', model: 'framework-based' },
|
|
538
|
+
};
|
|
539
|
+
if (snapshot.dependencyGraph) {
|
|
540
|
+
const detectedLangs = new Set();
|
|
541
|
+
for (const mod of snapshot.dependencyGraph.modules) {
|
|
542
|
+
const ext = '.' + mod.split('.').pop();
|
|
543
|
+
const info = fileExtensionMap[ext];
|
|
544
|
+
if (info && !detectedLangs.has(info.lang)) {
|
|
545
|
+
detectedLangs.add(info.lang);
|
|
546
|
+
const applicable = info.model === 'file-based';
|
|
547
|
+
moduleLanguageHints.push({
|
|
548
|
+
language: info.lang,
|
|
549
|
+
dependencyModel: info.model,
|
|
550
|
+
moduleScanApplicable: applicable,
|
|
551
|
+
recommendation: applicable
|
|
552
|
+
? 'Module-level dependency graph (depth="module") is applicable.'
|
|
553
|
+
: `This language uses ${info.model} imports — module-level scan is not applicable. Use depth="symbol" for class/method/call-level analysis.`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
455
558
|
return {
|
|
456
559
|
codebaseSnapshot: snapshot,
|
|
457
560
|
moduleStats,
|
|
561
|
+
languageHints: moduleLanguageHints.length > 0 ? moduleLanguageHints : undefined,
|
|
458
562
|
_hint: moduleStats
|
|
459
563
|
? `Module triples auto-pushed: ${moduleStats.modules} modules, ${moduleStats.edges} edges. Query with: SELECT ?m WHERE { ?m a otx:Module }`
|
|
460
564
|
: 'No dependency graph auto-extracted or push failed. Inspect key source files manually.',
|
|
@@ -491,7 +595,7 @@ async function handleContextInit(args) {
|
|
|
491
595
|
if (!config.files[contextUri].includes(relPath)) {
|
|
492
596
|
config.files[contextUri].push(relPath);
|
|
493
597
|
}
|
|
494
|
-
actions.push('Bootstrapped otx ontology (
|
|
598
|
+
actions.push('Bootstrapped otx ontology (13 classes, 24 properties)');
|
|
495
599
|
}
|
|
496
600
|
// Generate hook script
|
|
497
601
|
const hookDir = join(process.cwd(), '.opentology', 'hooks');
|
|
@@ -520,6 +624,12 @@ async function handleContextInit(args) {
|
|
|
520
624
|
writeFileSync(postErrorHookPath, generatePostErrorHookScript(), 'utf-8');
|
|
521
625
|
actions.push('Generated hook: .opentology/hooks/post-error.mjs');
|
|
522
626
|
}
|
|
627
|
+
const stopSessionHookPath = join(hookDir, 'stop-session-reminder.mjs');
|
|
628
|
+
if (!existsSync(stopSessionHookPath) || force) {
|
|
629
|
+
mkdirSync(hookDir, { recursive: true });
|
|
630
|
+
writeFileSync(stopSessionHookPath, generateStopSessionHookScript(), 'utf-8');
|
|
631
|
+
actions.push('Generated hook: .opentology/hooks/stop-session-reminder.mjs');
|
|
632
|
+
}
|
|
523
633
|
// Update CLAUDE.md
|
|
524
634
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
525
635
|
const section = generateContextSection(config.projectId, config.graphUri);
|
|
@@ -531,6 +641,16 @@ async function handleContextInit(args) {
|
|
|
531
641
|
updateClaudeMd(claudeMdPath, section);
|
|
532
642
|
actions.push('Updated CLAUDE.md context section');
|
|
533
643
|
}
|
|
644
|
+
// Update global ~/.claude/CLAUDE.md
|
|
645
|
+
const homedir = (await import('node:os')).homedir();
|
|
646
|
+
const globalClaudeMdPath = join(homedir, '.claude', 'CLAUDE.md');
|
|
647
|
+
try {
|
|
648
|
+
updateGlobalClaudeMd(globalClaudeMdPath);
|
|
649
|
+
actions.push('Updated global ~/.claude/CLAUDE.md OpenTology section');
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// Non-fatal: global CLAUDE.md update is best-effort
|
|
653
|
+
}
|
|
534
654
|
// Generate slash commands
|
|
535
655
|
const commandsDir = join(process.cwd(), '.claude', 'commands');
|
|
536
656
|
const slashCommands = generateSlashCommands();
|
|
@@ -622,6 +742,22 @@ async function handleContextInit(args) {
|
|
|
622
742
|
});
|
|
623
743
|
hooksChanged = true;
|
|
624
744
|
}
|
|
745
|
+
// Stop: session log + knowledge reminder
|
|
746
|
+
const stopSessionCmd = 'node .opentology/hooks/stop-session-reminder.mjs';
|
|
747
|
+
if (!hooks.Stop)
|
|
748
|
+
hooks.Stop = [];
|
|
749
|
+
const hasStopHook = hooks.Stop.some((h) => {
|
|
750
|
+
const entry = h;
|
|
751
|
+
const entryHooks = entry.hooks;
|
|
752
|
+
return entryHooks?.some((hook) => hook.command === stopSessionCmd);
|
|
753
|
+
});
|
|
754
|
+
if (!hasStopHook) {
|
|
755
|
+
hooks.Stop.push({
|
|
756
|
+
matcher: '',
|
|
757
|
+
hooks: [{ type: 'command', command: stopSessionCmd }],
|
|
758
|
+
});
|
|
759
|
+
hooksChanged = true;
|
|
760
|
+
}
|
|
625
761
|
if (hooksChanged) {
|
|
626
762
|
settings.hooks = hooks;
|
|
627
763
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function generateContextSection(projectId: string, graphUri: string): string;
|
|
2
|
+
export declare function generateGlobalSection(): string;
|
|
3
|
+
export declare function updateGlobalClaudeMd(filePath: string): void;
|
|
2
4
|
export declare function updateClaudeMd(filePath: string, section: string): void;
|
|
@@ -7,16 +7,17 @@ export function generateContextSection(projectId, graphUri) {
|
|
|
7
7
|
return `${MARKER_BEGIN}
|
|
8
8
|
## Context Management — OpenTology
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
<principles>
|
|
11
|
+
- **Graph first** — query the knowledge graph before reading source files or making assumptions.
|
|
12
|
+
- **Always record** — push Session logs at session end; record Knowledge, Decisions, and Issues as they arise.
|
|
13
|
+
- **Auto-ingest** — when the user shares a URL or external source, run the ingest protocol automatically.
|
|
14
|
+
</principles>
|
|
14
15
|
|
|
15
16
|
### Graph Structure
|
|
16
17
|
|
|
17
18
|
| Graph | URI | Purpose |
|
|
18
19
|
|-------|-----|---------|
|
|
19
|
-
| context | \`${contextUri}\` | Decisions, issues, knowledge |
|
|
20
|
+
| context | \`${contextUri}\` | Decisions, issues, knowledge, modules, symbols |
|
|
20
21
|
| sessions | \`${sessionsUri}\` | Session work logs |
|
|
21
22
|
|
|
22
23
|
### Ontology (\`otx:\` prefix)
|
|
@@ -29,27 +30,12 @@ symbol-level call graphs, and session history. Prefer graph knowledge over re-de
|
|
|
29
30
|
| \`otx:Knowledge\` | Reusable knowledge |
|
|
30
31
|
| \`otx:Session\` | Session logs |
|
|
31
32
|
| \`otx:Pattern\` | Recurring patterns/conventions |
|
|
33
|
+
| \`otx:Source\` | External knowledge source (article, paper, code, etc.) |
|
|
32
34
|
| \`otx:Module\` | Source file module |
|
|
33
|
-
| \`otx:Class\` |
|
|
34
|
-
| \`otx:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| \`otx:MethodCall\` | Call relationship between symbols (symbol scan) |
|
|
38
|
-
|
|
39
|
-
| Property | Range | Description |
|
|
40
|
-
|----------|-------|-------------|
|
|
41
|
-
| \`otx:title\` | string | Title |
|
|
42
|
-
| \`otx:date\` | date | Date (YYYY-MM-DD) |
|
|
43
|
-
| \`otx:body\` | string | Body content |
|
|
44
|
-
| \`otx:status\` | string | Status (open/resolved/active) |
|
|
45
|
-
| \`otx:reason\` | string | Decision rationale |
|
|
46
|
-
| \`otx:nextTodo\` | string | Next action item |
|
|
47
|
-
| \`otx:relatedTo\` | resource | Related entity |
|
|
48
|
-
| \`otx:dependsOn\` | Module | Module import dependency |
|
|
49
|
-
| \`otx:definedIn\` | Module | Which module a symbol belongs to |
|
|
50
|
-
| \`otx:callerSymbol\` | string | Caller in a MethodCall |
|
|
51
|
-
| \`otx:calleeSymbol\` | string | Callee in a MethodCall |
|
|
52
|
-
| \`otx:calls\` | resource | Call relationship |
|
|
35
|
+
| \`otx:Class\` / \`otx:Interface\` / \`otx:Function\` / \`otx:Method\` | Symbol-level entities (from deep scan) |
|
|
36
|
+
| \`otx:MethodCall\` | Call relationship between symbols |
|
|
37
|
+
|
|
38
|
+
Key properties: \`otx:title\`, \`otx:date\`, \`otx:body\`, \`otx:status\`, \`otx:reason\`, \`otx:nextTodo\`, \`otx:relatedTo\`, \`otx:dependsOn\`, \`otx:definedIn\`, \`otx:callerSymbol\`, \`otx:calleeSymbol\`, \`otx:sourceUrl\`, \`otx:sourceType\`. Full schema: use the \`schema\` tool.
|
|
53
39
|
|
|
54
40
|
### When to Record
|
|
55
41
|
|
|
@@ -58,127 +44,66 @@ symbol-level call graphs, and session history. Prefer graph knowledge over re-de
|
|
|
58
44
|
| Architecture/tech decision | \`otx:Decision\` | context |
|
|
59
45
|
| Bug/issue resolved | \`otx:Issue\` | context |
|
|
60
46
|
| Reusable knowledge | \`otx:Knowledge\` | context |
|
|
47
|
+
| Source ingested | \`otx:Source\` | context |
|
|
61
48
|
| Session end | \`otx:Session\` | sessions |
|
|
62
49
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
\`\`\`sparql
|
|
66
|
-
# Recent sessions
|
|
67
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
68
|
-
SELECT ?title ?date ?nextTodo WHERE {
|
|
69
|
-
GRAPH <${sessionsUri}> {
|
|
70
|
-
?s a otx:Session ; otx:title ?title ; otx:date ?date .
|
|
71
|
-
OPTIONAL { ?s otx:nextTodo ?nextTodo }
|
|
72
|
-
}
|
|
73
|
-
} ORDER BY DESC(?date) LIMIT 5
|
|
74
|
-
|
|
75
|
-
# Open issues
|
|
76
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
77
|
-
SELECT ?title ?date WHERE {
|
|
78
|
-
GRAPH <${contextUri}> {
|
|
79
|
-
?s a otx:Issue ; otx:title ?title ; otx:date ?date ; otx:status "open" .
|
|
80
|
-
}
|
|
81
|
-
} ORDER BY DESC(?date)
|
|
82
|
-
\`\`\`
|
|
83
|
-
|
|
84
|
-
### How to Use OpenTology Tools
|
|
50
|
+
### Tools & Workflows
|
|
85
51
|
|
|
86
|
-
|
|
52
|
+
#### Before Working
|
|
87
53
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- **Decisions**: past architectural choices that may inform the current analysis
|
|
92
|
-
- **Knowledge**: reusable patterns or lessons already recorded
|
|
93
|
-
- **Issues**: known problems related to the area under investigation
|
|
94
|
-
- **Sessions**: recent work in the same area
|
|
54
|
+
1. **Query graph** — check for existing decisions, knowledge, issues, and sessions related to your task.
|
|
55
|
+
2. **Check impact** — before editing a file, run \`context_impact\` to understand the blast radius (dependents, dependencies, related entities).
|
|
56
|
+
3. **Search** — use \`query\` with SPARQL to find anything: \`?s a otx:Decision\`, \`?s a otx:Knowledge\`, \`?s a otx:Module\`, \`?s a otx:MethodCall\`, etc.
|
|
95
57
|
|
|
96
58
|
\`\`\`sparql
|
|
59
|
+
# Context search (replace "keyword" with your search term)
|
|
97
60
|
PREFIX otx: <https://opentology.dev/vocab#>
|
|
98
61
|
SELECT ?type ?title ?body WHERE {
|
|
99
62
|
GRAPH <${contextUri}> {
|
|
100
63
|
{ ?s a otx:Decision ; otx:title ?title ; otx:body ?body . BIND("decision" AS ?type) }
|
|
101
|
-
UNION
|
|
102
|
-
{ ?s a otx:
|
|
103
|
-
UNION
|
|
104
|
-
{ ?s a otx:Issue ; otx:title ?title ; otx:body ?body . BIND("issue" AS ?type) }
|
|
64
|
+
UNION { ?s a otx:Knowledge ; otx:title ?title ; otx:body ?body . BIND("knowledge" AS ?type) }
|
|
65
|
+
UNION { ?s a otx:Issue ; otx:title ?title ; otx:body ?body . BIND("issue" AS ?type) }
|
|
105
66
|
}
|
|
106
67
|
FILTER(CONTAINS(LCASE(?title), "keyword") || CONTAINS(LCASE(?body), "keyword"))
|
|
107
68
|
} LIMIT 10
|
|
108
69
|
\`\`\`
|
|
109
70
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#### Pre-Edit Impact Check
|
|
113
|
-
|
|
114
|
-
Before modifying a file, run \`context_impact\` with the target file path to understand the blast radius:
|
|
115
|
-
- **dependents** — modules that import or depend on this file
|
|
116
|
-
- **dependencies** — modules this file imports
|
|
117
|
-
- **related** — decisions, issues, knowledge linked to this file
|
|
118
|
-
- **impact level** — high / medium / low
|
|
119
|
-
|
|
120
|
-
If impact is **high**, inform the user of affected modules and get confirmation before proceeding.
|
|
121
|
-
|
|
122
|
-
#### Searching the Knowledge Graph
|
|
123
|
-
|
|
124
|
-
Use \`query\` with SPARQL to find anything in the project graph. **Always query the graph before reading source files** when investigating code structure, dependencies, or call relationships:
|
|
71
|
+
#### Ingesting External Sources
|
|
125
72
|
|
|
126
|
-
|
|
127
|
-
- **Issues**: \`?s a otx:Issue ; otx:status "open"\` — known bugs and their status
|
|
128
|
-
- **Knowledge**: \`?s a otx:Knowledge\` — reusable patterns and lessons learned
|
|
129
|
-
- **Sessions**: query the sessions graph for past work logs and next TODOs
|
|
130
|
-
- **Modules**: \`?s a otx:Module\` — all scanned source modules and their dependencies (\`otx:dependsOn\`)
|
|
131
|
-
- **Symbols**: \`?s a otx:Class\`, \`otx:Interface\`, \`otx:Function\`, \`otx:Method\` — code-level entities (available after symbol-depth scan)
|
|
132
|
-
- **Call graph**: \`?s a otx:MethodCall\` — who calls whom (available after symbol scan with \`includeMethodCalls=true\`)
|
|
133
|
-
|
|
134
|
-
\`\`\`sparql
|
|
135
|
-
# Functions in a specific module
|
|
136
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
137
|
-
SELECT ?name WHERE {
|
|
138
|
-
GRAPH <${contextUri}> {
|
|
139
|
-
?f a otx:Function ; otx:title ?name ; otx:definedIn <urn:module:src/mcp/server> .
|
|
140
|
-
}
|
|
141
|
-
}
|
|
73
|
+
When the user shares a URL, file path, or external content, follow this protocol:
|
|
142
74
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
75
|
+
1. **Duplicate check** — Query for existing sources with the same URL or title.
|
|
76
|
+
2. **Register** — Push an \`otx:Source\` with status "pending" using the \`push\` tool.
|
|
77
|
+
3. **Read** — Fetch URL content, read file, or use pasted text directly.
|
|
78
|
+
4. **Extract** — Summarize key concepts. Create \`otx:Knowledge\` triples linked via \`otx:relatedTo\`.
|
|
79
|
+
5. **Cross-reference** — Query existing graph for related decisions/issues/knowledge. Link via \`otx:relatedTo\`.
|
|
80
|
+
6. **Contradictions** — If new knowledge contradicts existing entries, create \`otx:Issue\` with status "open".
|
|
81
|
+
7. **Finalize** — Update source status from "pending" to "ingested". Run audit query.
|
|
151
82
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
?dependent otx:dependsOn+ <urn:module:src/lib/store-adapter> .
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
\`\`\`
|
|
83
|
+
Duplicate check: \`SELECT ?s ?title WHERE { GRAPH <${contextUri}> { ?s a otx:Source ; otx:sourceUrl ?url . FILTER(?url = "URL") } }\`
|
|
84
|
+
Audit: \`SELECT ?s ?title ?status (COUNT(?k) AS ?knowledgeCount) WHERE { GRAPH <${contextUri}> { ?s a otx:Source ; otx:title ?title ; otx:status ?status . OPTIONAL { ?k otx:relatedTo ?s } } } GROUP BY ?s ?title ?status\`
|
|
85
|
+
Registration: \`<urn:source:{slug}> a otx:Source ; otx:title "..." ; otx:sourceUrl "..." ; otx:sourceType "article" ; otx:date "YYYY-MM-DD"^^xsd:date ; otx:status "pending" .\`
|
|
86
|
+
Types: article | paper | code | transcript | documentation | video | podcast | book | other. Status: pending → ingested → stale. Recovery: \`rollback\`.
|
|
160
87
|
|
|
161
|
-
####
|
|
88
|
+
#### After Working
|
|
162
89
|
|
|
163
|
-
|
|
164
|
-
- \`depth="module"\` — fast, updates file-level imports
|
|
165
|
-
- \`depth="symbol"\` with \`includeMethodCalls=true\` — thorough, updates class/function/call graph
|
|
90
|
+
- Run \`context_scan\` after significant code changes (\`depth="module"\` for fast, \`depth="symbol"\` for thorough).
|
|
166
91
|
|
|
167
|
-
####
|
|
92
|
+
#### Tool Reference
|
|
168
93
|
|
|
169
94
|
| Tool | When to Use |
|
|
170
95
|
|------|-------------|
|
|
171
96
|
| \`context_load\` | Session start — loads recent sessions, open issues, recent decisions |
|
|
172
|
-
| \`context_scan\` | After
|
|
97
|
+
| \`context_scan\` | After code changes — rescans module/symbol dependencies |
|
|
173
98
|
| \`context_impact\` | Before editing — checks blast radius of a file change |
|
|
174
|
-
| \`schema\` | Explore ontology classes and properties
|
|
99
|
+
| \`schema\` | Explore ontology classes and properties |
|
|
175
100
|
| \`query\` | Run any SPARQL query against the project graph |
|
|
176
|
-
| \`push\` | Record decisions, issues, knowledge, or session summaries |
|
|
101
|
+
| \`push\` | Record decisions, issues, knowledge, sources, or session summaries |
|
|
177
102
|
| \`doctor\` | Diagnose project health (config, store, hooks, CLAUDE.md) |
|
|
178
103
|
|
|
179
|
-
### Session End
|
|
104
|
+
### Session End
|
|
180
105
|
|
|
181
|
-
|
|
106
|
+
Push a summary at the end of each meaningful session:
|
|
182
107
|
|
|
183
108
|
\`\`\`turtle
|
|
184
109
|
@prefix otx: <https://opentology.dev/vocab#> .
|
|
@@ -192,6 +117,62 @@ At the end of each session, push a summary:
|
|
|
192
117
|
\`\`\`
|
|
193
118
|
${MARKER_END}`;
|
|
194
119
|
}
|
|
120
|
+
const GLOBAL_MARKER_BEGIN = '<!-- OPENTOLOGY:GLOBAL:BEGIN -->';
|
|
121
|
+
const GLOBAL_MARKER_END = '<!-- OPENTOLOGY:GLOBAL:END -->';
|
|
122
|
+
export function generateGlobalSection() {
|
|
123
|
+
return `${GLOBAL_MARKER_BEGIN}
|
|
124
|
+
# OpenTology — RDF 기반 프로젝트 컨텍스트 관리
|
|
125
|
+
|
|
126
|
+
Claude Code는 글로벌 MCP \`opentology\`를 통해 프로젝트 지식을 RDF 그래프로 관리한다.
|
|
127
|
+
온톨로지, 도구 사용법, SPARQL 예시 등 상세 정보는 프로젝트별 CLAUDE.md에 자동 생성된다.
|
|
128
|
+
여기서는 **모든 프로젝트에 공통 적용되는 행동 규칙만** 정의한다.
|
|
129
|
+
|
|
130
|
+
## 핵심 원칙
|
|
131
|
+
|
|
132
|
+
1. **그래프 먼저** — 코드를 읽거나 가정하기 전에 \`query\`로 그래프를 먼저 조회한다.
|
|
133
|
+
2. **항상 기록** — 세션 종료 시 \`otx:Session\`, 의미 있는 작업은 \`Knowledge\`/\`Decision\`/\`Issue\`로 기록.
|
|
134
|
+
3. **자동 수집** — 사용자가 URL이나 외부 소스를 공유하면 ingest 프로토콜을 자동 실행한다.
|
|
135
|
+
4. **영향도 확인** — 파일 수정 전 \`context_impact\`로 blast radius를 확인한다.
|
|
136
|
+
|
|
137
|
+
## URI 규칙
|
|
138
|
+
|
|
139
|
+
| 대상 | 패턴 | 예시 |
|
|
140
|
+
|------|------|------|
|
|
141
|
+
| 프로젝트 | \`urn:project:{name}\` | \`urn:project:opentology\` |
|
|
142
|
+
| 세션 | \`urn:session:{date}\` | \`urn:session:2026-04-05\` |
|
|
143
|
+
| 의사결정 | \`urn:decision:{date}-{slug}\` | \`urn:decision:2026-04-05-ingest-feature\` |
|
|
144
|
+
| 이슈 | \`urn:issue:{id}\` | \`urn:issue:1\` |
|
|
145
|
+
| 지식 | \`urn:knowledge:{slug}\` | \`urn:knowledge:wasm-oxigraph\` |
|
|
146
|
+
| 패턴 | \`urn:pattern:{slug}\` | \`urn:pattern:singleton-adapter\` |
|
|
147
|
+
| 소스 | \`urn:source:{slug}\` | \`urn:source:karpathy-llm-wiki\` |
|
|
148
|
+
|
|
149
|
+
## 기록 기준
|
|
150
|
+
|
|
151
|
+
- **기록함**: 아키텍처 변경, 새 기능 구현, 버그 해결, 재사용 가능한 지식, 외부 소스 수집
|
|
152
|
+
- **기록 안 함**: 오타 수정, 단순 질문 응답, 단순 설정 변경
|
|
153
|
+
- 기록은 **간결하게**. 민감 정보(API 키 등)는 절대 기록하지 않는다.
|
|
154
|
+
- \`opentology\` MCP가 연결되지 않은 프로젝트에서는 기록을 건너뛴다.
|
|
155
|
+
${GLOBAL_MARKER_END}`;
|
|
156
|
+
}
|
|
157
|
+
export function updateGlobalClaudeMd(filePath) {
|
|
158
|
+
const section = generateGlobalSection();
|
|
159
|
+
if (!existsSync(filePath)) {
|
|
160
|
+
writeFileSync(filePath, section + '\n', 'utf-8');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
164
|
+
const beginIdx = content.indexOf(GLOBAL_MARKER_BEGIN);
|
|
165
|
+
const endIdx = content.indexOf(GLOBAL_MARKER_END);
|
|
166
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
167
|
+
// No markers — prepend before existing content
|
|
168
|
+
writeFileSync(filePath, section + '\n\n' + content, 'utf-8');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Replace between markers
|
|
172
|
+
const before = content.substring(0, beginIdx);
|
|
173
|
+
const after = content.substring(endIdx + GLOBAL_MARKER_END.length);
|
|
174
|
+
writeFileSync(filePath, before + section + after, 'utf-8');
|
|
175
|
+
}
|
|
195
176
|
export function updateClaudeMd(filePath, section) {
|
|
196
177
|
if (!existsSync(filePath)) {
|
|
197
178
|
// Case 1: No file — create with just the section
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OTX_BOOTSTRAP_TURTLE = "@prefix otx: <https://opentology.dev/vocab#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\notx:Project a owl:Class .\notx:Decision a owl:Class .\notx:Issue a owl:Class .\notx:Knowledge a owl:Class .\notx:Session a owl:Class .\notx:Pattern a owl:Class .\notx:Module a owl:Class .\n\notx:title a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:date a owl:DatatypeProperty ; rdfs:range xsd:date .\notx:body a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:status a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:reason a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:cause a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:solution a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:nextTodo a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:relatedTo a owl:ObjectProperty .\notx:project a owl:ObjectProperty .\notx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Module .\notx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:Class a owl:Class .\notx:Interface a owl:Class .\notx:Function a owl:Class .\notx:Method a owl:Class .\notx:MethodCall a owl:Class .\n\notx:definedIn a owl:ObjectProperty ; rdfs:range otx:Module .\notx:extends a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Class .\notx:implements a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Interface .\notx:hasMethod a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Method .\notx:calls a owl:ObjectProperty .\notx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .\n";
|
|
1
|
+
export declare const OTX_BOOTSTRAP_TURTLE = "@prefix otx: <https://opentology.dev/vocab#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\notx:Project a owl:Class .\notx:Decision a owl:Class .\notx:Issue a owl:Class .\notx:Knowledge a owl:Class .\notx:Session a owl:Class .\notx:Pattern a owl:Class .\notx:Module a owl:Class .\n\notx:title a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:date a owl:DatatypeProperty ; rdfs:range xsd:date .\notx:body a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:status a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:reason a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:cause a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:solution a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:nextTodo a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:relatedTo a owl:ObjectProperty .\notx:project a owl:ObjectProperty .\notx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Module .\notx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:Source a owl:Class .\n\notx:Class a owl:Class .\notx:Interface a owl:Class .\notx:Function a owl:Class .\notx:Method a owl:Class .\notx:MethodCall a owl:Class .\n\notx:definedIn a owl:ObjectProperty ; rdfs:range otx:Module .\notx:extends a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Class .\notx:implements a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Interface .\notx:hasMethod a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Method .\notx:calls a owl:ObjectProperty .\notx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:sourceUrl a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:sourceType a owl:DatatypeProperty ; rdfs:range xsd:string .\n";
|
|
@@ -26,6 +26,8 @@ otx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Mod
|
|
|
26
26
|
otx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
27
27
|
otx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
28
28
|
|
|
29
|
+
otx:Source a owl:Class .
|
|
30
|
+
|
|
29
31
|
otx:Class a owl:Class .
|
|
30
32
|
otx:Interface a owl:Class .
|
|
31
33
|
otx:Function a owl:Class .
|
|
@@ -41,4 +43,7 @@ otx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .
|
|
|
41
43
|
otx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .
|
|
42
44
|
otx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
43
45
|
otx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
46
|
+
|
|
47
|
+
otx:sourceUrl a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
48
|
+
otx:sourceType a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
44
49
|
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateStopSessionHookScript(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export function generateStopSessionHookScript() {
|
|
2
|
+
return `#!/usr/bin/env node
|
|
3
|
+
// Generated by: opentology context init
|
|
4
|
+
// Stop hook — reminds LLM to push session log and knowledge triples
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
7
|
+
import { join, resolve, dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = parseInt(process.env.OPENTOLOGY_HOOK_TIMEOUT || '5000', 10);
|
|
10
|
+
|
|
11
|
+
function findProjectRoot(startDir) {
|
|
12
|
+
let dir = resolve(startDir);
|
|
13
|
+
while (dir !== dirname(dir)) {
|
|
14
|
+
if (existsSync(join(dir, '.opentology.json'))) return dir;
|
|
15
|
+
dir = dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findBin(projectRoot) {
|
|
21
|
+
const local = join(projectRoot, 'node_modules', '.bin', 'opentology');
|
|
22
|
+
if (existsSync(local)) return { bin: local, args: [] };
|
|
23
|
+
return { bin: 'npx', args: ['opentology'] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(projectRoot, cmdArgs) {
|
|
27
|
+
const { bin, args } = findBin(projectRoot);
|
|
28
|
+
return execFileSync(bin, [...args, ...cmdArgs], {
|
|
29
|
+
cwd: projectRoot,
|
|
30
|
+
timeout: TIMEOUT,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
38
|
+
if (!projectRoot) process.exit(0);
|
|
39
|
+
|
|
40
|
+
// Check recent commits (last 2 hours)
|
|
41
|
+
let log = '';
|
|
42
|
+
try {
|
|
43
|
+
log = execSync('git log --since="2 hours ago" --oneline', {
|
|
44
|
+
cwd: projectRoot, encoding: 'utf-8', timeout: 3000,
|
|
45
|
+
}).trim();
|
|
46
|
+
} catch { process.exit(0); }
|
|
47
|
+
|
|
48
|
+
if (!log) process.exit(0);
|
|
49
|
+
|
|
50
|
+
const commitCount = log.split('\\n').length;
|
|
51
|
+
// Check if source code was changed (not just docs/config)
|
|
52
|
+
let srcChanged = false;
|
|
53
|
+
try {
|
|
54
|
+
const diff = execSync('git diff HEAD~' + Math.min(commitCount, 10) + ' --name-only 2>/dev/null', {
|
|
55
|
+
cwd: projectRoot, encoding: 'utf-8', timeout: 3000,
|
|
56
|
+
}).trim();
|
|
57
|
+
srcChanged = diff.split('\\n').some(f => f.startsWith('src/') || f.startsWith('lib/') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go'));
|
|
58
|
+
} catch { srcChanged = commitCount >= 2; }
|
|
59
|
+
|
|
60
|
+
// Importance: high (3+ commits or src changes), medium (1-2 commits), low (0)
|
|
61
|
+
const importance = (commitCount >= 3 || srcChanged) ? 'high' : commitCount >= 1 ? 'medium' : 'low';
|
|
62
|
+
if (importance === 'low') process.exit(0);
|
|
63
|
+
|
|
64
|
+
// Check if session log already exists for today
|
|
65
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
66
|
+
let sessionExists = false;
|
|
67
|
+
try {
|
|
68
|
+
const raw = run(projectRoot, [
|
|
69
|
+
'query', '--format', 'json',
|
|
70
|
+
'PREFIX otx: <https://opentology.dev/vocab#> ASK WHERE { ?s a otx:Session ; otx:date "' + today + '"^^<http://www.w3.org/2001/XMLSchema#date> }'
|
|
71
|
+
]);
|
|
72
|
+
sessionExists = raw.includes('true');
|
|
73
|
+
} catch { /* query failed, assume not exists */ }
|
|
74
|
+
|
|
75
|
+
const reminders = [];
|
|
76
|
+
|
|
77
|
+
if (!sessionExists) {
|
|
78
|
+
reminders.push('Push an otx:Session summary to the sessions graph (title, date, body, nextTodo).');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Always remind about Knowledge/Decision/Issue recording
|
|
82
|
+
reminders.push('Review if any of these should be recorded:');
|
|
83
|
+
reminders.push('- Architecture/tech decisions → otx:Decision');
|
|
84
|
+
reminders.push('- Reusable patterns or lessons → otx:Knowledge');
|
|
85
|
+
reminders.push('- Resolved bugs/issues → otx:Issue');
|
|
86
|
+
reminders.push('- External sources ingested → otx:Source');
|
|
87
|
+
|
|
88
|
+
if (reminders.length > 0) {
|
|
89
|
+
const header = sessionExists
|
|
90
|
+
? 'Session log exists. Check if additional knowledge should be recorded:'
|
|
91
|
+
: 'Session log missing (' + today + ', ' + commitCount + ' commits, importance: ' + importance + '). Before ending:';
|
|
92
|
+
console.log(header);
|
|
93
|
+
for (const r of reminders) console.log(' ' + r);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentology",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Ontology-powered project memory for AI coding assistants — your codebase as a knowledge graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"opentology": "
|
|
7
|
+
"opentology": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/"
|