opentology 0.3.6 → 0.3.7

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.
@@ -48,12 +48,19 @@ export interface MethodCallInfo {
48
48
  caller: string;
49
49
  callee: string;
50
50
  }
51
+ export interface UnsupportedFileGroup {
52
+ extension: string;
53
+ language: string;
54
+ files: string[];
55
+ count: number;
56
+ }
51
57
  export interface DeepScanResult {
52
58
  deepScanAvailable: true;
53
59
  classes: ClassInfo[];
54
60
  interfaces: InterfaceInfo[];
55
61
  functions: FunctionInfo[];
56
62
  methodCalls: MethodCallInfo[];
63
+ unsupportedFiles: UnsupportedFileGroup[];
57
64
  fileCount: number;
58
65
  symbolCount: number;
59
66
  scanDurationMs: number;
@@ -43,6 +43,66 @@ 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
+ }
46
106
  // ── Main entry point ────────────────────────────────────────────
47
107
  export async function deepScan(rootDir, options) {
48
108
  const maxFiles = options?.maxFiles ?? 500;
@@ -83,6 +143,7 @@ export async function deepScan(rootDir, options) {
83
143
  interfaces: [],
84
144
  functions: [],
85
145
  methodCalls: [],
146
+ unsupportedFiles: [],
86
147
  fileCount: total,
87
148
  symbolCount: 0,
88
149
  scanDurationMs: Date.now() - start,
@@ -147,12 +208,16 @@ export async function deepScan(rootDir, options) {
147
208
  const symbolCount = classes.reduce((n, c) => n + 1 + c.methods.length, 0)
148
209
  + interfaces.length
149
210
  + functions.length;
211
+ // Discover unsupported language files
212
+ const supportedExtensions = new Set(available.flatMap(e => e.extensions));
213
+ const unsupportedFiles = await discoverUnsupportedFiles(rootDir, supportedExtensions);
150
214
  return {
151
215
  deepScanAvailable: true,
152
216
  classes,
153
217
  interfaces,
154
218
  functions,
155
219
  methodCalls,
220
+ unsupportedFiles,
156
221
  fileCount: files.length,
157
222
  symbolCount,
158
223
  scanDurationMs: Date.now() - start,
@@ -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';
@@ -412,15 +413,74 @@ async function handleContextScan(args) {
412
413
  hints.push(`Symbol triples: ${pushStats.triplesInserted} in ${pushStats.batchCount} batches`);
413
414
  if (moduleStats)
414
415
  hints.push(`Module triples: ${moduleStats.modules} modules, ${moduleStats.edges} edges`);
415
- return {
416
- ...scanResult,
416
+ // Build compact summary — do NOT return full symbol arrays (#66)
417
+ const compact = {
418
+ deepScanAvailable: true,
419
+ counts: {
420
+ classes: scanResult.classes.length,
421
+ interfaces: scanResult.interfaces.length,
422
+ functions: scanResult.functions.length,
423
+ methodCalls: scanResult.methodCalls.length,
424
+ files: scanResult.fileCount,
425
+ symbols: scanResult.symbolCount,
426
+ },
427
+ scanDurationMs: scanResult.scanDurationMs,
428
+ capped: scanResult.capped,
429
+ warnings: scanResult.warnings,
417
430
  pushStats,
418
431
  moduleStats,
419
432
  _experimental: true,
420
433
  _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 }`
434
+ ? `${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
435
  : 'Deep scan completed but triple push failed. Use push manually with the generated triples.',
423
436
  };
437
+ // LLM fallback for unsupported languages (#65)
438
+ if (scanResult.unsupportedFiles.length > 0) {
439
+ const samples = [];
440
+ for (const group of scanResult.unsupportedFiles.slice(0, 5)) {
441
+ // Pick entry-point-like files first, then first files
442
+ const sorted = [...group.files].sort((a, b) => {
443
+ const isEntry = (f) => /\/(main|index|app|mod|lib)\.[^/]+$/.test(f) ? 0 : 1;
444
+ return isEntry(a) - isEntry(b);
445
+ });
446
+ for (const file of sorted.slice(0, 2)) {
447
+ try {
448
+ const fullPath = join(process.cwd(), file);
449
+ const raw = readFileSync(fullPath, 'utf-8');
450
+ const lines = raw.split('\n').slice(0, 30).join('\n');
451
+ samples.push({ path: file, language: group.language, content: lines });
452
+ }
453
+ catch { /* skip unreadable */ }
454
+ }
455
+ }
456
+ compact.unsupportedFiles = scanResult.unsupportedFiles.map(g => ({
457
+ language: g.language,
458
+ extension: g.extension,
459
+ count: g.count,
460
+ files: g.files.slice(0, 5),
461
+ }));
462
+ if (samples.length > 0) {
463
+ compact.samples = samples;
464
+ compact.turtleTemplate = [
465
+ '# Push symbol triples for unsupported language files.',
466
+ '# Replace {filePath} and {Name} with actual values from the samples above.',
467
+ '@prefix otx: <https://opentology.dev/vocab#> .',
468
+ '',
469
+ '# Class/Struct:',
470
+ '# <urn:symbol:{filePath}/class/{Name}> a otx:Class ;',
471
+ '# otx:title "{Name}" ; otx:definedIn <urn:module:{filePath}> .',
472
+ '',
473
+ '# Function:',
474
+ '# <urn:symbol:{filePath}/function/{name}> a otx:Function ;',
475
+ '# otx:title "{name}" ; otx:definedIn <urn:module:{filePath}> .',
476
+ '',
477
+ '# Interface/Trait/Protocol:',
478
+ '# <urn:symbol:{filePath}/interface/{Name}> a otx:Interface ;',
479
+ '# otx:title "{Name}" ; otx:definedIn <urn:module:{filePath}> .',
480
+ ].join('\n');
481
+ }
482
+ }
483
+ return compact;
424
484
  }
425
485
  // Default: module-level scan — now auto-pushes module triples like context_init
426
486
  const maxBytes = args.maxSnapshotBytes ?? 15360;
@@ -491,7 +551,7 @@ async function handleContextInit(args) {
491
551
  if (!config.files[contextUri].includes(relPath)) {
492
552
  config.files[contextUri].push(relPath);
493
553
  }
494
- actions.push('Bootstrapped otx ontology (6 classes, 12 properties)');
554
+ actions.push('Bootstrapped otx ontology (13 classes, 24 properties)');
495
555
  }
496
556
  // Generate hook script
497
557
  const hookDir = join(process.cwd(), '.opentology', 'hooks');
@@ -520,6 +580,12 @@ async function handleContextInit(args) {
520
580
  writeFileSync(postErrorHookPath, generatePostErrorHookScript(), 'utf-8');
521
581
  actions.push('Generated hook: .opentology/hooks/post-error.mjs');
522
582
  }
583
+ const stopSessionHookPath = join(hookDir, 'stop-session-reminder.mjs');
584
+ if (!existsSync(stopSessionHookPath) || force) {
585
+ mkdirSync(hookDir, { recursive: true });
586
+ writeFileSync(stopSessionHookPath, generateStopSessionHookScript(), 'utf-8');
587
+ actions.push('Generated hook: .opentology/hooks/stop-session-reminder.mjs');
588
+ }
523
589
  // Update CLAUDE.md
524
590
  const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
525
591
  const section = generateContextSection(config.projectId, config.graphUri);
@@ -531,6 +597,16 @@ async function handleContextInit(args) {
531
597
  updateClaudeMd(claudeMdPath, section);
532
598
  actions.push('Updated CLAUDE.md context section');
533
599
  }
600
+ // Update global ~/.claude/CLAUDE.md
601
+ const homedir = (await import('node:os')).homedir();
602
+ const globalClaudeMdPath = join(homedir, '.claude', 'CLAUDE.md');
603
+ try {
604
+ updateGlobalClaudeMd(globalClaudeMdPath);
605
+ actions.push('Updated global ~/.claude/CLAUDE.md OpenTology section');
606
+ }
607
+ catch {
608
+ // Non-fatal: global CLAUDE.md update is best-effort
609
+ }
534
610
  // Generate slash commands
535
611
  const commandsDir = join(process.cwd(), '.claude', 'commands');
536
612
  const slashCommands = generateSlashCommands();
@@ -622,6 +698,22 @@ async function handleContextInit(args) {
622
698
  });
623
699
  hooksChanged = true;
624
700
  }
701
+ // Stop: session log + knowledge reminder
702
+ const stopSessionCmd = 'node .opentology/hooks/stop-session-reminder.mjs';
703
+ if (!hooks.Stop)
704
+ hooks.Stop = [];
705
+ const hasStopHook = hooks.Stop.some((h) => {
706
+ const entry = h;
707
+ const entryHooks = entry.hooks;
708
+ return entryHooks?.some((hook) => hook.command === stopSessionCmd);
709
+ });
710
+ if (!hasStopHook) {
711
+ hooks.Stop.push({
712
+ matcher: '',
713
+ hooks: [{ type: 'command', command: stopSessionCmd }],
714
+ });
715
+ hooksChanged = true;
716
+ }
625
717
  if (hooksChanged) {
626
718
  settings.hooks = hooks;
627
719
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "opentology",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Ontology-powered project memory for AI coding assistants — your codebase as a knowledge graph",
5
5
  "type": "module",
6
6
  "bin": {