opentology 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -10
- package/dist/commands/viz.d.ts +2 -0
- package/dist/commands/viz.js +53 -0
- package/dist/index.js +2 -0
- package/dist/lib/deep-scan-triples.d.ts +15 -0
- package/dist/lib/deep-scan-triples.js +120 -0
- package/dist/lib/deep-scanner-treesitter.d.ts +41 -0
- package/dist/lib/deep-scanner-treesitter.js +180 -0
- package/dist/lib/deep-scanner-ts.d.ts +20 -0
- package/dist/lib/deep-scanner-ts.js +250 -0
- package/dist/lib/deep-scanner.d.ts +69 -0
- package/dist/lib/deep-scanner.js +162 -0
- package/dist/lib/embedded-adapter.d.ts +1 -0
- package/dist/lib/embedded-adapter.js +22 -0
- package/dist/lib/extractors/go.d.ts +19 -0
- package/dist/lib/extractors/go.js +167 -0
- package/dist/lib/extractors/java.d.ts +20 -0
- package/dist/lib/extractors/java.js +145 -0
- package/dist/lib/extractors/python.d.ts +18 -0
- package/dist/lib/extractors/python.js +154 -0
- package/dist/lib/extractors/rust.d.ts +16 -0
- package/dist/lib/extractors/rust.js +176 -0
- package/dist/lib/extractors/swift.d.ts +17 -0
- package/dist/lib/extractors/swift.js +152 -0
- package/dist/lib/http-adapter.d.ts +1 -0
- package/dist/lib/http-adapter.js +22 -0
- package/dist/lib/language-extractor.d.ts +34 -0
- package/dist/lib/language-extractor.js +5 -0
- package/dist/lib/store-adapter.d.ts +12 -0
- package/dist/lib/visualizer.d.ts +24 -0
- package/dist/lib/visualizer.js +149 -0
- package/dist/mcp/server.js +113 -2
- package/dist/templates/otx-ontology.d.ts +1 -1
- package/dist/templates/otx-ontology.js +13 -0
- package/package.json +20 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ graph TB
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
subgraph MCP["MCP Server"]
|
|
25
|
-
Tools[
|
|
25
|
+
Tools[18 Tools]
|
|
26
26
|
Resource["opentology://schema"]
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -97,7 +97,7 @@ graph LR
|
|
|
97
97
|
| Docker required | No (embedded mode) | Yes | Yes |
|
|
98
98
|
| RDFS reasoning | Automatic on push | Manual SPARQL CONSTRUCT | Not native |
|
|
99
99
|
| SHACL validation | Built-in | Manual tooling | N/A |
|
|
100
|
-
| AI integration | MCP server with
|
|
100
|
+
| AI integration | MCP server with 18 tools | None | Plugin ecosystem |
|
|
101
101
|
| Query language | SPARQL (auto-prefixed) | SPARQL (raw) | Cypher |
|
|
102
102
|
| Data format | Turtle files | Turtle/N-Triples | Property graph |
|
|
103
103
|
| Project scoping | Automatic named graphs | Manual | Database-level |
|
|
@@ -126,7 +126,7 @@ graph LR
|
|
|
126
126
|
|
|
127
127
|
**AI Integration**
|
|
128
128
|
|
|
129
|
-
- MCP server with
|
|
129
|
+
- MCP server with 18 tools and 1 resource
|
|
130
130
|
- `opentology://schema` resource auto-loads ontology overview
|
|
131
131
|
- Works with Claude Code, Cursor, and any MCP-compatible client
|
|
132
132
|
|
|
@@ -192,7 +192,7 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
192
192
|
}
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
**
|
|
195
|
+
**18 Tools:**
|
|
196
196
|
|
|
197
197
|
| Tool | Description |
|
|
198
198
|
|------|-------------|
|
|
@@ -207,6 +207,13 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
207
207
|
| `opentology_diff` | Compare local vs graph |
|
|
208
208
|
| `opentology_schema` | Introspect ontology schema |
|
|
209
209
|
| `opentology_infer` | Run RDFS inference |
|
|
210
|
+
| `opentology_graph_list` | List named graphs |
|
|
211
|
+
| `opentology_graph_create` | Create a named graph |
|
|
212
|
+
| `opentology_graph_drop` | Drop a named graph |
|
|
213
|
+
| `opentology_context_init` | Initialize project context graph |
|
|
214
|
+
| `opentology_context_load` | Load project context |
|
|
215
|
+
| `opentology_context_status` | Check context initialization status |
|
|
216
|
+
| `opentology_context_scan` | Scan codebase (module or symbol-level) |
|
|
210
217
|
|
|
211
218
|
**1 Resource:**
|
|
212
219
|
|
|
@@ -270,6 +277,41 @@ opentology push data.ttl
|
|
|
270
277
|
opentology shapes
|
|
271
278
|
```
|
|
272
279
|
|
|
280
|
+
### Deep Scan (Symbol-Level Analysis)
|
|
281
|
+
|
|
282
|
+
The `opentology_context_scan` MCP tool supports symbol-level deep scanning that extracts classes, interfaces, functions, and method call relationships from source code, then pushes them as OTX triples to the context graph.
|
|
283
|
+
|
|
284
|
+
**Supported Languages:**
|
|
285
|
+
|
|
286
|
+
| Language | Engine | Symbols Extracted |
|
|
287
|
+
|----------|--------|-------------------|
|
|
288
|
+
| TypeScript/JavaScript | ts-morph | class, interface, function, method calls |
|
|
289
|
+
| Python | Tree-sitter | class, ABC/Protocol, function, method calls |
|
|
290
|
+
| Go | Tree-sitter | struct, interface, func, method calls |
|
|
291
|
+
| Rust | Tree-sitter | struct, trait, impl, fn, method calls |
|
|
292
|
+
| Java | Tree-sitter | class, interface, method, method calls |
|
|
293
|
+
| Swift | Tree-sitter | class, struct, protocol, func, method calls |
|
|
294
|
+
|
|
295
|
+
**Optional Dependencies:**
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# For TypeScript/JavaScript deep scan
|
|
299
|
+
npm install ts-morph
|
|
300
|
+
|
|
301
|
+
# For Python/Go/Rust/Java/Swift deep scan
|
|
302
|
+
npm install web-tree-sitter tree-sitter-wasms
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Usage (MCP):**
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"depth": "symbol",
|
|
310
|
+
"includeMethodCalls": true,
|
|
311
|
+
"languages": ["typescript", "python"]
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
273
315
|
### Tech Stack
|
|
274
316
|
|
|
275
317
|
- TypeScript, commander.js, n3.js
|
|
@@ -277,11 +319,13 @@ opentology shapes
|
|
|
277
319
|
- @modelcontextprotocol/sdk for MCP server
|
|
278
320
|
- shacl-engine for SHACL validation
|
|
279
321
|
- picocolors for terminal output
|
|
322
|
+
- ts-morph (optional) for TypeScript symbol analysis
|
|
323
|
+
- web-tree-sitter + tree-sitter-wasms (optional) for multi-language symbol analysis
|
|
280
324
|
|
|
281
325
|
### Roadmap
|
|
282
326
|
|
|
283
327
|
- [x] CLI with 14 commands
|
|
284
|
-
- [x] MCP server with
|
|
328
|
+
- [x] MCP server with 18 tools and 1 resource
|
|
285
329
|
- [x] Schema introspection (MCP resource + tool)
|
|
286
330
|
- [x] Complete CRUD (push --replace, drop, delete)
|
|
287
331
|
- [x] SHACL validation (shape constraints on push)
|
|
@@ -290,6 +334,7 @@ opentology shapes
|
|
|
290
334
|
- [x] Prefix management
|
|
291
335
|
- [x] Embedded mode (no Docker required)
|
|
292
336
|
- [x] RDFS reasoning (auto-materialization on push)
|
|
337
|
+
- [x] Multi-language deep scan (TypeScript, Python, Go, Rust, Java, Swift)
|
|
293
338
|
- [ ] OWL reasoning (owl:sameAs, owl:inverseOf)
|
|
294
339
|
- [ ] Remote ontology import
|
|
295
340
|
- [ ] Version control for ontology snapshots
|
|
@@ -324,7 +369,7 @@ graph TB
|
|
|
324
369
|
end
|
|
325
370
|
|
|
326
371
|
subgraph MCP["MCP Server"]
|
|
327
|
-
Tools[
|
|
372
|
+
Tools[18 Tools]
|
|
328
373
|
Resource["opentology://schema"]
|
|
329
374
|
end
|
|
330
375
|
|
|
@@ -399,7 +444,7 @@ graph LR
|
|
|
399
444
|
| Docker 필수 | 아니오 (임베디드 모드) | 예 | 예 |
|
|
400
445
|
| RDFS 추론 | 푸시 시 자동 | SPARQL CONSTRUCT 수동 작성 | 네이티브 미지원 |
|
|
401
446
|
| SHACL 검증 | 내장 | 별도 도구 필요 | 해당 없음 |
|
|
402
|
-
| AI 연동 | MCP 서버
|
|
447
|
+
| AI 연동 | MCP 서버 18개 도구 | 없음 | 플러그인 생태계 |
|
|
403
448
|
| 쿼리 언어 | SPARQL (접두사 자동 삽입) | SPARQL (수동) | Cypher |
|
|
404
449
|
| 데이터 형식 | Turtle 파일 | Turtle/N-Triples | 속성 그래프 |
|
|
405
450
|
| 프로젝트 구분 | Named Graph 자동 | 수동 관리 | 데이터베이스 단위 |
|
|
@@ -428,7 +473,7 @@ graph LR
|
|
|
428
473
|
|
|
429
474
|
**AI 연동**
|
|
430
475
|
|
|
431
|
-
-
|
|
476
|
+
- 18개 도구와 1개 리소스를 제공하는 MCP 서버
|
|
432
477
|
- `opentology://schema` 리소스로 온톨로지 개요 자동 로드
|
|
433
478
|
- Claude Code, Cursor 등 MCP 호환 클라이언트와 연동
|
|
434
479
|
|
|
@@ -494,7 +539,7 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
494
539
|
}
|
|
495
540
|
```
|
|
496
541
|
|
|
497
|
-
**
|
|
542
|
+
**18개 도구:**
|
|
498
543
|
|
|
499
544
|
| 도구 | 설명 |
|
|
500
545
|
|------|------|
|
|
@@ -509,6 +554,13 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
509
554
|
| `opentology_diff` | 로컬 파일과 그래프 비교 |
|
|
510
555
|
| `opentology_schema` | 온톨로지 스키마 조회 |
|
|
511
556
|
| `opentology_infer` | RDFS 추론 실행 |
|
|
557
|
+
| `opentology_graph_list` | Named Graph 목록 조회 |
|
|
558
|
+
| `opentology_graph_create` | Named Graph 생성 |
|
|
559
|
+
| `opentology_graph_drop` | Named Graph 삭제 |
|
|
560
|
+
| `opentology_context_init` | 프로젝트 컨텍스트 그래프 초기화 |
|
|
561
|
+
| `opentology_context_load` | 프로젝트 컨텍스트 로드 |
|
|
562
|
+
| `opentology_context_status` | 컨텍스트 초기화 상태 확인 |
|
|
563
|
+
| `opentology_context_scan` | 코드베이스 스캔 (모듈/심볼 수준) |
|
|
512
564
|
|
|
513
565
|
**1개 리소스:**
|
|
514
566
|
|
|
@@ -572,6 +624,41 @@ opentology push data.ttl
|
|
|
572
624
|
opentology shapes
|
|
573
625
|
```
|
|
574
626
|
|
|
627
|
+
### 딥스캔 (심볼 수준 분석)
|
|
628
|
+
|
|
629
|
+
`opentology_context_scan` MCP 도구는 소스 코드에서 클래스, 인터페이스, 함수, 메서드 호출 관계를 추출하여 OTX 트리플로 컨텍스트 그래프에 푸시하는 심볼 수준 딥스캔을 지원합니다.
|
|
630
|
+
|
|
631
|
+
**지원 언어:**
|
|
632
|
+
|
|
633
|
+
| 언어 | 엔진 | 추출 심볼 |
|
|
634
|
+
|------|------|-----------|
|
|
635
|
+
| TypeScript/JavaScript | ts-morph | class, interface, function, method calls |
|
|
636
|
+
| Python | Tree-sitter | class, ABC/Protocol, function, method calls |
|
|
637
|
+
| Go | Tree-sitter | struct, interface, func, method calls |
|
|
638
|
+
| Rust | Tree-sitter | struct, trait, impl, fn, method calls |
|
|
639
|
+
| Java | Tree-sitter | class, interface, method, method calls |
|
|
640
|
+
| Swift | Tree-sitter | class, struct, protocol, func, method calls |
|
|
641
|
+
|
|
642
|
+
**선택적 의존성:**
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
# TypeScript/JavaScript 딥스캔
|
|
646
|
+
npm install ts-morph
|
|
647
|
+
|
|
648
|
+
# Python/Go/Rust/Java/Swift 딥스캔
|
|
649
|
+
npm install web-tree-sitter tree-sitter-wasms
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**사용법 (MCP):**
|
|
653
|
+
|
|
654
|
+
```json
|
|
655
|
+
{
|
|
656
|
+
"depth": "symbol",
|
|
657
|
+
"includeMethodCalls": true,
|
|
658
|
+
"languages": ["typescript", "python"]
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
575
662
|
### 기술 스택
|
|
576
663
|
|
|
577
664
|
- TypeScript, commander.js, n3.js
|
|
@@ -579,11 +666,13 @@ opentology shapes
|
|
|
579
666
|
- @modelcontextprotocol/sdk (MCP 서버)
|
|
580
667
|
- shacl-engine (SHACL 검증)
|
|
581
668
|
- picocolors (터미널 출력)
|
|
669
|
+
- ts-morph (선택) TypeScript 심볼 분석
|
|
670
|
+
- web-tree-sitter + tree-sitter-wasms (선택) 다중 언어 심볼 분석
|
|
582
671
|
|
|
583
672
|
### 로드맵
|
|
584
673
|
|
|
585
674
|
- [x] 14개 CLI 명령어
|
|
586
|
-
- [x]
|
|
675
|
+
- [x] 18개 도구와 1개 리소스를 갖춘 MCP 서버
|
|
587
676
|
- [x] 스키마 조회 (MCP 리소스 + 도구)
|
|
588
677
|
- [x] 완전한 CRUD (push --replace, drop, delete)
|
|
589
678
|
- [x] SHACL 검증 (푸시 시 형상 제약 자동 검증)
|
|
@@ -592,6 +681,7 @@ opentology shapes
|
|
|
592
681
|
- [x] 접두사 관리
|
|
593
682
|
- [x] 임베디드 모드 (Docker 불필요)
|
|
594
683
|
- [x] RDFS 추론 (푸시 시 자동 물질화)
|
|
684
|
+
- [x] 다중 언어 딥스캔 (TypeScript, Python, Go, Rust, Java, Swift)
|
|
595
685
|
- [ ] OWL 추론 (owl:sameAs, owl:inverseOf)
|
|
596
686
|
- [ ] 원격 온톨로지 임포트
|
|
597
687
|
- [ ] 온톨로지 스냅샷 버전 관리
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { loadConfig, resolveGraphUri } from '../lib/config.js';
|
|
2
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
3
|
+
import { fromSchemaData, toMermaid, toDot } from '../lib/visualizer.js';
|
|
4
|
+
import { writeFileSync } from 'fs';
|
|
5
|
+
export function registerViz(program) {
|
|
6
|
+
const viz = program
|
|
7
|
+
.command('viz')
|
|
8
|
+
.description('Visualize graph data as Mermaid or DOT diagrams');
|
|
9
|
+
viz
|
|
10
|
+
.command('schema')
|
|
11
|
+
.description('Visualize the ontology schema (classes, properties, relationships)')
|
|
12
|
+
.option('--format <type>', 'Output format: mermaid, dot', 'mermaid')
|
|
13
|
+
.option('--output <file>', 'Write output to file instead of stdout')
|
|
14
|
+
.option('--graph <name>', 'Target a specific named graph')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
let config;
|
|
17
|
+
try {
|
|
18
|
+
config = loadConfig();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error(`Error: ${err.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const graphUri = options.graph ? resolveGraphUri(config, options.graph) : config.graphUri;
|
|
25
|
+
const format = options.format || 'mermaid';
|
|
26
|
+
if (format !== 'mermaid' && format !== 'dot') {
|
|
27
|
+
console.error(`Error: unsupported format "${format}". Use "mermaid" or "dot".`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const adapter = await createReadyAdapter(config);
|
|
32
|
+
const overview = await adapter.getSchemaOverview(graphUri);
|
|
33
|
+
const relations = await adapter.getSchemaRelations(graphUri);
|
|
34
|
+
const visGraph = fromSchemaData(overview, relations);
|
|
35
|
+
const output = format === 'dot' ? toDot(visGraph) : toMermaid(visGraph);
|
|
36
|
+
if (options.output) {
|
|
37
|
+
writeFileSync(options.output, output, 'utf-8');
|
|
38
|
+
console.log(`Written to ${options.output}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(output);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const message = err.message;
|
|
46
|
+
console.error(`Error: ${message}`);
|
|
47
|
+
if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
48
|
+
console.error(`Cannot connect to triplestore at ${config.endpoint ?? 'unknown'}. Is it running?`);
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerGraph } from './commands/graph.js';
|
|
|
15
15
|
import { registerInfer } from './commands/infer.js';
|
|
16
16
|
import { registerPrefix } from './commands/prefix.js';
|
|
17
17
|
import { registerContext } from './commands/context.js';
|
|
18
|
+
import { registerViz } from './commands/viz.js';
|
|
18
19
|
const program = new Command();
|
|
19
20
|
program
|
|
20
21
|
.name('opentology')
|
|
@@ -35,4 +36,5 @@ registerGraph(program);
|
|
|
35
36
|
registerInfer(program);
|
|
36
37
|
registerPrefix(program);
|
|
37
38
|
registerContext(program);
|
|
39
|
+
registerViz(program);
|
|
38
40
|
program.parse(process.argv);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts DeepScanResult into OTX-compliant SPARQL INSERT DATA triples
|
|
3
|
+
* with batched insert and scoped delete-then-insert strategy.
|
|
4
|
+
*/
|
|
5
|
+
import type { DeepScanResult } from './deep-scanner.js';
|
|
6
|
+
export declare function generateSymbolTriples(result: DeepScanResult): string[];
|
|
7
|
+
export declare function batchTriples(triples: string[], batchSize?: number): string[][];
|
|
8
|
+
export interface StoreAdapter {
|
|
9
|
+
sparqlUpdate(query: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function deleteExistingSymbols(adapter: StoreAdapter, graphUri: string, modulePaths: string[]): Promise<void>;
|
|
12
|
+
export declare function pushSymbolTriples(adapter: StoreAdapter, graphUri: string, result: DeepScanResult): Promise<{
|
|
13
|
+
triplesInserted: number;
|
|
14
|
+
batchCount: number;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts DeepScanResult into OTX-compliant SPARQL INSERT DATA triples
|
|
3
|
+
* with batched insert and scoped delete-then-insert strategy.
|
|
4
|
+
*/
|
|
5
|
+
const OTX = 'https://opentology.dev/vocab#';
|
|
6
|
+
function encodeSegment(s) {
|
|
7
|
+
return encodeURIComponent(s);
|
|
8
|
+
}
|
|
9
|
+
function symbolUri(filePath, kind, name) {
|
|
10
|
+
return `urn:symbol:${encodeSegment(filePath)}/${encodeSegment(kind)}/${encodeSegment(name)}`;
|
|
11
|
+
}
|
|
12
|
+
function moduleUri(filePath) {
|
|
13
|
+
return `urn:module:${filePath}`;
|
|
14
|
+
}
|
|
15
|
+
function esc(s) {
|
|
16
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
17
|
+
}
|
|
18
|
+
// ── Triple generation ───────────────────────────────────────────
|
|
19
|
+
export function generateSymbolTriples(result) {
|
|
20
|
+
const triples = [];
|
|
21
|
+
for (const cls of result.classes) {
|
|
22
|
+
const uri = symbolUri(cls.filePath, 'class', cls.name);
|
|
23
|
+
triples.push(`<${uri}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <${OTX}Class> .`);
|
|
24
|
+
triples.push(`<${uri}> <${OTX}title> "${esc(cls.name)}" .`);
|
|
25
|
+
triples.push(`<${uri}> <${OTX}definedIn> <${moduleUri(cls.filePath)}> .`);
|
|
26
|
+
if (cls.baseClass) {
|
|
27
|
+
// baseClass is already a qualified path like "src/lib/foo/class/Bar"
|
|
28
|
+
const parts = cls.baseClass.split('/');
|
|
29
|
+
const baseName = parts.pop();
|
|
30
|
+
const baseKind = parts.pop();
|
|
31
|
+
const basePath = parts.join('/');
|
|
32
|
+
const baseUri = symbolUri(basePath, baseKind, baseName);
|
|
33
|
+
triples.push(`<${uri}> <${OTX}extends> <${baseUri}> .`);
|
|
34
|
+
}
|
|
35
|
+
for (const iface of cls.interfaces) {
|
|
36
|
+
// interface is already qualified like "src/lib/foo/interface/Bar"
|
|
37
|
+
const parts = iface.split('/');
|
|
38
|
+
const ifaceName = parts.pop();
|
|
39
|
+
const ifaceKind = parts.pop();
|
|
40
|
+
const ifacePath = parts.join('/');
|
|
41
|
+
const ifaceUri = symbolUri(ifacePath, ifaceKind, ifaceName);
|
|
42
|
+
triples.push(`<${uri}> <${OTX}implements> <${ifaceUri}> .`);
|
|
43
|
+
}
|
|
44
|
+
for (const method of cls.methods) {
|
|
45
|
+
const methodUri = symbolUri(cls.filePath, 'method', `${cls.name}.${method.name}`);
|
|
46
|
+
triples.push(`<${methodUri}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <${OTX}Method> .`);
|
|
47
|
+
triples.push(`<${methodUri}> <${OTX}title> "${esc(method.name)}" .`);
|
|
48
|
+
triples.push(`<${methodUri}> <${OTX}definedIn> <${moduleUri(cls.filePath)}> .`);
|
|
49
|
+
triples.push(`<${uri}> <${OTX}hasMethod> <${methodUri}> .`);
|
|
50
|
+
if (method.returnType && method.returnType !== 'void') {
|
|
51
|
+
triples.push(`<${methodUri}> <${OTX}returns> "${esc(method.returnType)}" .`);
|
|
52
|
+
}
|
|
53
|
+
for (const p of method.parameters) {
|
|
54
|
+
triples.push(`<${methodUri}> <${OTX}paramType> "${esc(p.name)}: ${esc(p.type)}" .`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const iface of result.interfaces) {
|
|
59
|
+
const uri = symbolUri(iface.filePath, 'interface', iface.name);
|
|
60
|
+
triples.push(`<${uri}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <${OTX}Interface> .`);
|
|
61
|
+
triples.push(`<${uri}> <${OTX}title> "${esc(iface.name)}" .`);
|
|
62
|
+
triples.push(`<${uri}> <${OTX}definedIn> <${moduleUri(iface.filePath)}> .`);
|
|
63
|
+
for (const ext of iface.extends) {
|
|
64
|
+
// Interface extends are just names (not fully qualified) for now
|
|
65
|
+
triples.push(`<${uri}> <${OTX}extends> "${esc(ext)}" .`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const fn of result.functions) {
|
|
69
|
+
const uri = symbolUri(fn.filePath, 'function', fn.name);
|
|
70
|
+
triples.push(`<${uri}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <${OTX}Function> .`);
|
|
71
|
+
triples.push(`<${uri}> <${OTX}title> "${esc(fn.name)}" .`);
|
|
72
|
+
triples.push(`<${uri}> <${OTX}definedIn> <${moduleUri(fn.filePath)}> .`);
|
|
73
|
+
if (fn.returnType && fn.returnType !== 'void') {
|
|
74
|
+
triples.push(`<${uri}> <${OTX}returns> "${esc(fn.returnType)}" .`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const call of result.methodCalls) {
|
|
78
|
+
const callerParts = call.caller.split('.');
|
|
79
|
+
const calleeParts = call.callee.split('.');
|
|
80
|
+
// Method calls reference by class.method pattern — generate a simple triple
|
|
81
|
+
triples.push(`<urn:call:${encodeSegment(call.caller)}> <${OTX}calls> <urn:call:${encodeSegment(call.callee)}> .`);
|
|
82
|
+
// Store caller/callee names for queryability
|
|
83
|
+
triples.push(`<urn:call:${encodeSegment(call.caller)}> <${OTX}title> "${esc(call.caller)}" .`);
|
|
84
|
+
triples.push(`<urn:call:${encodeSegment(call.callee)}> <${OTX}title> "${esc(call.callee)}" .`);
|
|
85
|
+
}
|
|
86
|
+
return triples;
|
|
87
|
+
}
|
|
88
|
+
// ── Batching ────────────────────────────────────────────────────
|
|
89
|
+
export function batchTriples(triples, batchSize = 100) {
|
|
90
|
+
const batches = [];
|
|
91
|
+
for (let i = 0; i < triples.length; i += batchSize) {
|
|
92
|
+
batches.push(triples.slice(i, i + batchSize));
|
|
93
|
+
}
|
|
94
|
+
return batches;
|
|
95
|
+
}
|
|
96
|
+
export async function deleteExistingSymbols(adapter, graphUri, modulePaths) {
|
|
97
|
+
for (const modPath of modulePaths) {
|
|
98
|
+
const modUri = moduleUri(modPath);
|
|
99
|
+
await adapter.sparqlUpdate(`DELETE WHERE { GRAPH <${graphUri}> { ?s <${OTX}definedIn> <${modUri}> . ?s ?p ?o } }`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function pushSymbolTriples(adapter, graphUri, result) {
|
|
103
|
+
// Collect all module paths for scoped delete
|
|
104
|
+
const modulePaths = new Set();
|
|
105
|
+
for (const c of result.classes)
|
|
106
|
+
modulePaths.add(c.filePath);
|
|
107
|
+
for (const i of result.interfaces)
|
|
108
|
+
modulePaths.add(i.filePath);
|
|
109
|
+
for (const f of result.functions)
|
|
110
|
+
modulePaths.add(f.filePath);
|
|
111
|
+
// Delete existing symbols for these modules
|
|
112
|
+
await deleteExistingSymbols(adapter, graphUri, [...modulePaths]);
|
|
113
|
+
// Generate and batch-insert
|
|
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} }`);
|
|
118
|
+
}
|
|
119
|
+
return { triplesInserted: triples.length, batchCount: batches.length };
|
|
120
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter base extractor — abstract class that handles WASM loading,
|
|
3
|
+
* parser initialization, and provides query helpers for language-specific extractors.
|
|
4
|
+
*
|
|
5
|
+
* web-tree-sitter and tree-sitter-wasms are optional peerDependencies.
|
|
6
|
+
*/
|
|
7
|
+
import type { LanguageExtractor, ExtractedSymbols } from './language-extractor.js';
|
|
8
|
+
type TSLanguage = import('web-tree-sitter').Language;
|
|
9
|
+
type TSTree = import('web-tree-sitter').Tree;
|
|
10
|
+
type TSNode = import('web-tree-sitter').Node;
|
|
11
|
+
export type { TSNode, TSTree, TSLanguage };
|
|
12
|
+
export declare abstract class TreeSitterExtractor implements LanguageExtractor {
|
|
13
|
+
abstract readonly language: string;
|
|
14
|
+
abstract readonly extensions: string[];
|
|
15
|
+
/** WASM file name, e.g. 'tree-sitter-python.wasm' */
|
|
16
|
+
protected abstract readonly wasmName: string;
|
|
17
|
+
protected lang: TSLanguage | null;
|
|
18
|
+
isAvailable(): Promise<boolean>;
|
|
19
|
+
extract(filePaths: string[], rootDir: string, options: {
|
|
20
|
+
maxSymbols: number;
|
|
21
|
+
timeoutMs: number;
|
|
22
|
+
includeMethodCalls: boolean;
|
|
23
|
+
}): Promise<ExtractedSymbols & {
|
|
24
|
+
warnings: string[];
|
|
25
|
+
capped: boolean;
|
|
26
|
+
fatal?: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Language-specific extraction from a parsed tree.
|
|
30
|
+
* Subclasses implement this to walk the AST and extract symbols.
|
|
31
|
+
*/
|
|
32
|
+
protected abstract extractFromTree(tree: TSTree, relPath: string, source: string, includeMethodCalls: boolean): ExtractedSymbols;
|
|
33
|
+
/** Find all descendant nodes matching a type. */
|
|
34
|
+
protected findNodes(node: TSNode, type: string): TSNode[];
|
|
35
|
+
/** Find first child of a specific type. */
|
|
36
|
+
protected findChild(node: TSNode, type: string): TSNode | null;
|
|
37
|
+
/** Find all direct children of a specific type. */
|
|
38
|
+
protected findChildren(node: TSNode, type: string): TSNode[];
|
|
39
|
+
/** Get the text of a named child field. */
|
|
40
|
+
protected fieldText(node: TSNode, fieldName: string): string | null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter base extractor — abstract class that handles WASM loading,
|
|
3
|
+
* parser initialization, and provides query helpers for language-specific extractors.
|
|
4
|
+
*
|
|
5
|
+
* web-tree-sitter and tree-sitter-wasms are optional peerDependencies.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join, extname } from 'node:path';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
// ── WASM loading cache ──────────────────────────────────────────
|
|
11
|
+
let parserClass = null;
|
|
12
|
+
let languageClass = null;
|
|
13
|
+
let initialized = false;
|
|
14
|
+
const languageCache = new Map();
|
|
15
|
+
async function initTreeSitter() {
|
|
16
|
+
if (initialized)
|
|
17
|
+
return parserClass !== null;
|
|
18
|
+
try {
|
|
19
|
+
const mod = await import('web-tree-sitter');
|
|
20
|
+
const Parser = mod.default ?? mod;
|
|
21
|
+
await Parser.init();
|
|
22
|
+
parserClass = Parser;
|
|
23
|
+
languageClass = Parser.Language ?? mod.Language;
|
|
24
|
+
initialized = true;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
initialized = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function loadLanguage(wasmName) {
|
|
33
|
+
if (languageCache.has(wasmName))
|
|
34
|
+
return languageCache.get(wasmName);
|
|
35
|
+
if (!languageClass)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
const wasmsDir = join(require.resolve('tree-sitter-wasms/package.json'), '..', 'out');
|
|
40
|
+
const wasmPath = join(wasmsDir, wasmName);
|
|
41
|
+
const wasmBuf = await readFile(wasmPath);
|
|
42
|
+
const lang = await languageClass.load(new Uint8Array(wasmBuf.buffer));
|
|
43
|
+
languageCache.set(wasmName, lang);
|
|
44
|
+
return lang;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ── Abstract base class ─────────────────────────────────────────
|
|
51
|
+
export class TreeSitterExtractor {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.lang = null;
|
|
54
|
+
}
|
|
55
|
+
async isAvailable() {
|
|
56
|
+
const ok = await initTreeSitter();
|
|
57
|
+
if (!ok)
|
|
58
|
+
return false;
|
|
59
|
+
this.lang = await loadLanguage(this.wasmName);
|
|
60
|
+
return this.lang !== null;
|
|
61
|
+
}
|
|
62
|
+
async extract(filePaths, rootDir, options) {
|
|
63
|
+
if (!parserClass || !this.lang) {
|
|
64
|
+
return {
|
|
65
|
+
classes: [], interfaces: [], functions: [], methodCalls: [],
|
|
66
|
+
warnings: [], capped: false,
|
|
67
|
+
fatal: `Tree-sitter or ${this.wasmName} not available.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const parser = new parserClass();
|
|
71
|
+
parser.setLanguage(this.lang);
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
const classes = [];
|
|
74
|
+
const interfaces = [];
|
|
75
|
+
const functions = [];
|
|
76
|
+
const methodCalls = [];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
let symbolCount = 0;
|
|
79
|
+
let capped = false;
|
|
80
|
+
const rootPrefix = rootDir.endsWith('/') ? rootDir : rootDir + '/';
|
|
81
|
+
for (const filePath of filePaths) {
|
|
82
|
+
if (Date.now() - start > options.timeoutMs) {
|
|
83
|
+
capped = true;
|
|
84
|
+
warnings.push(`Timeout after ${options.timeoutMs}ms. Returning partial results.`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (symbolCount >= options.maxSymbols) {
|
|
88
|
+
capped = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const source = await readFile(filePath, 'utf-8');
|
|
93
|
+
const tree = parser.parse(source);
|
|
94
|
+
if (!tree) {
|
|
95
|
+
warnings.push(`Failed to parse ${filePath}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const relPath = filePath
|
|
99
|
+
.replace(rootPrefix, '')
|
|
100
|
+
.replace(new RegExp(`\\${extname(filePath)}$`), '');
|
|
101
|
+
const extracted = this.extractFromTree(tree, relPath, source, options.includeMethodCalls);
|
|
102
|
+
for (const c of extracted.classes) {
|
|
103
|
+
if (symbolCount >= options.maxSymbols) {
|
|
104
|
+
capped = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
classes.push(c);
|
|
108
|
+
symbolCount += 1 + c.methods.length;
|
|
109
|
+
}
|
|
110
|
+
if (capped)
|
|
111
|
+
break;
|
|
112
|
+
for (const i of extracted.interfaces) {
|
|
113
|
+
if (symbolCount >= options.maxSymbols) {
|
|
114
|
+
capped = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
interfaces.push(i);
|
|
118
|
+
symbolCount++;
|
|
119
|
+
}
|
|
120
|
+
if (capped)
|
|
121
|
+
break;
|
|
122
|
+
for (const f of extracted.functions) {
|
|
123
|
+
if (symbolCount >= options.maxSymbols) {
|
|
124
|
+
capped = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
functions.push(f);
|
|
128
|
+
symbolCount++;
|
|
129
|
+
}
|
|
130
|
+
if (capped)
|
|
131
|
+
break;
|
|
132
|
+
methodCalls.push(...extracted.methodCalls);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
warnings.push(`Skipped ${filePath}: ${msg}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
parser.delete();
|
|
140
|
+
return { classes, interfaces, functions, methodCalls, warnings, capped };
|
|
141
|
+
}
|
|
142
|
+
// ── Query helpers for subclasses ──────────────────────────────
|
|
143
|
+
/** Find all descendant nodes matching a type. */
|
|
144
|
+
findNodes(node, type) {
|
|
145
|
+
const results = [];
|
|
146
|
+
const walk = (n) => {
|
|
147
|
+
if (n.type === type)
|
|
148
|
+
results.push(n);
|
|
149
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
150
|
+
walk(n.child(i));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
walk(node);
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
/** Find first child of a specific type. */
|
|
157
|
+
findChild(node, type) {
|
|
158
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
159
|
+
const child = node.child(i);
|
|
160
|
+
if (child.type === type)
|
|
161
|
+
return child;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/** Find all direct children of a specific type. */
|
|
166
|
+
findChildren(node, type) {
|
|
167
|
+
const results = [];
|
|
168
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
169
|
+
const child = node.child(i);
|
|
170
|
+
if (child.type === type)
|
|
171
|
+
results.push(child);
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
/** Get the text of a named child field. */
|
|
176
|
+
fieldText(node, fieldName) {
|
|
177
|
+
const child = node.childForFieldName(fieldName);
|
|
178
|
+
return child ? child.text : null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript extractor — uses ts-morph for symbol-level analysis.
|
|
3
|
+
* ts-morph is a peerDependency; when absent isAvailable() returns false.
|
|
4
|
+
*/
|
|
5
|
+
import type { LanguageExtractor, ExtractedSymbols } from './language-extractor.js';
|
|
6
|
+
export declare class TypeScriptExtractor implements LanguageExtractor {
|
|
7
|
+
readonly language = "typescript";
|
|
8
|
+
readonly extensions: string[];
|
|
9
|
+
private tsMorph;
|
|
10
|
+
isAvailable(): Promise<boolean>;
|
|
11
|
+
extract(_filePaths: string[], rootDir: string, options: {
|
|
12
|
+
maxSymbols: number;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
includeMethodCalls: boolean;
|
|
15
|
+
}): Promise<ExtractedSymbols & {
|
|
16
|
+
warnings: string[];
|
|
17
|
+
capped: boolean;
|
|
18
|
+
fatal?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|