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.
@@ -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 declare function pushSymbolTriples(adapter: StoreAdapter, graphUri: string, result: DeepScanResult): Promise<{
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 = 100) {
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, 100);
116
- for (const batch of batches) {
117
- await adapter.sparqlUpdate(`INSERT DATA { GRAPH <${graphUri}> {\n${batch.join('\n')}\n} }`);
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
- return { triplesInserted: triples.length, batchCount: batches.length };
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: {
@@ -156,6 +156,7 @@ export class TypeScriptExtractor {
156
156
  constructor() {
157
157
  this.language = 'typescript';
158
158
  this.extensions = ['.ts', '.tsx', '.js', '.jsx'];
159
+ this.dependencyModel = 'file-based';
159
160
  this.tsMorph = null;
160
161
  }
161
162
  async isAvailable() {
@@ -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;
@@ -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
  /**
@@ -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
- // Non-fatal: push is best-effort
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} in ${pushStats.batchCount} batches`);
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
- return {
416
- ...scanResult,
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 (6 classes, 12 properties)');
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
- This project uses OpenTology as its knowledge graph — treat it as the project's **long-term memory**.
11
- Before reading source files, grep-searching, or making assumptions, **query the graph first**.
12
- It holds architectural decisions, resolved issues, reusable patterns, module dependencies,
13
- symbol-level call graphs, and session history. Prefer graph knowledge over re-deriving facts from code.
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\` | Class definition (symbol scan) |
34
- | \`otx:Interface\` | Interface definition (symbol scan) |
35
- | \`otx:Function\` | Function definition (symbol scan) |
36
- | \`otx:Method\` | Method definition (symbol scan) |
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
- ### Query Examples
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
- OpenTology provides MCP tools to query and manage the project knowledge graph. Use them proactively.
52
+ #### Before Working
87
53
 
88
- #### Pre-Analysis Context Check
89
-
90
- Before exploring code or analyzing architecture, query the knowledge graph for existing context:
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:Knowledge ; otx:title ?title ; otx:body ?body . BIND("knowledge" AS ?type) }
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
- This prevents redundant analysis and ensures past decisions and knowledge inform current work.
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
- - **Decisions**: \`?s a otx:Decision\` why architectural choices were made
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
- # Who calls a specific function?
144
- PREFIX otx: <https://opentology.dev/vocab#>
145
- SELECT ?caller WHERE {
146
- GRAPH <${contextUri}> {
147
- ?s a otx:MethodCall ; otx:callerSymbol ?caller ; otx:calleeSymbol ?callee .
148
- FILTER(CONTAINS(?callee, "persistGraph"))
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
- # Module dependency chain (what depends on a module?)
153
- PREFIX otx: <https://opentology.dev/vocab#>
154
- SELECT ?dependent WHERE {
155
- GRAPH <${contextUri}> {
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
- #### Post-Edit Graph Update
88
+ #### After Working
162
89
 
163
- After significant code changes (new files, renamed functions, changed dependencies), run \`context_scan\` to keep the knowledge graph in sync:
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
- #### Other Useful Tools
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 significant code changes — rescans module/symbol dependencies |
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, or inspect a specific class |
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 Reminder
104
+ ### Session End
180
105
 
181
- At the end of each session, push a summary:
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.6",
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": "./dist/index.js"
7
+ "opentology": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist/"