opentology 0.2.1 → 0.2.2
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 +24 -16
- package/dist/commands/context.js +199 -12
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +132 -0
- package/dist/index.js +3 -1
- package/dist/lib/context-sync.d.ts +15 -0
- package/dist/lib/context-sync.js +229 -0
- package/dist/lib/graph-server.js +27 -2
- package/dist/mcp/server.js +183 -13
- package/dist/templates/pre-edit-hook.d.ts +1 -0
- package/dist/templates/pre-edit-hook.js +101 -0
- package/dist/templates/session-start-hook.js +28 -1
- package/package.json +1 -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[23 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 23 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 |
|
|
@@ -106,7 +106,7 @@ graph LR
|
|
|
106
106
|
|
|
107
107
|
**Core**
|
|
108
108
|
|
|
109
|
-
-
|
|
109
|
+
- 18 CLI commands covering the full RDF lifecycle
|
|
110
110
|
- Project-level configuration with `.opentology.json`
|
|
111
111
|
- Named graph scoping -- queries are automatically scoped to your project
|
|
112
112
|
- Two modes: HTTP (Oxigraph server) and embedded (WASM, zero Docker)
|
|
@@ -126,7 +126,7 @@ graph LR
|
|
|
126
126
|
|
|
127
127
|
**AI Integration**
|
|
128
128
|
|
|
129
|
-
- MCP server with
|
|
129
|
+
- MCP server with 23 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
|
|
|
@@ -181,8 +181,9 @@ For embedded mode, no additional setup is needed.
|
|
|
181
181
|
| `opentology infer` | Run RDFS materialization (`--clear` to remove inferred triples) |
|
|
182
182
|
| `opentology graph` | List, create, or drop named graphs |
|
|
183
183
|
| `opentology prefix` | List, add, or remove project prefixes |
|
|
184
|
-
| `opentology context` | Project context management (`init`, `load`, `status`, `scan`, `graph`) |
|
|
184
|
+
| `opentology context` | Project context management (`init`, `load`, `status`, `scan`, `impact`, `sync`, `graph`) |
|
|
185
185
|
| `opentology viz` | Visualize ontology schema (`schema`) |
|
|
186
|
+
| `opentology doctor` | Check project health (config, store, context, hooks, dependencies) |
|
|
186
187
|
| `opentology mcp` | Start the MCP server |
|
|
187
188
|
|
|
188
189
|
### MCP Integration
|
|
@@ -200,7 +201,7 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
200
201
|
}
|
|
201
202
|
```
|
|
202
203
|
|
|
203
|
-
**
|
|
204
|
+
**23 Tools:**
|
|
204
205
|
|
|
205
206
|
| Tool | Description |
|
|
206
207
|
|------|-------------|
|
|
@@ -223,7 +224,10 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
223
224
|
| `opentology_context_load` | Load project context |
|
|
224
225
|
| `opentology_context_status` | Check context initialization status |
|
|
225
226
|
| `opentology_context_scan` | Scan codebase (module or symbol-level) |
|
|
227
|
+
| `opentology_context_impact` | Analyze file modification impact (dependents, dependencies, related context) |
|
|
228
|
+
| `opentology_context_sync` | Auto-sync: recover missed sessions from git, rescan module graph |
|
|
226
229
|
| `opentology_context_graph` | Start interactive graph visualization web UI |
|
|
230
|
+
| `opentology_doctor` | Check project health (config, store, hooks, dependencies) |
|
|
227
231
|
|
|
228
232
|
**1 Resource:**
|
|
229
233
|
|
|
@@ -334,8 +338,8 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
334
338
|
|
|
335
339
|
### Roadmap
|
|
336
340
|
|
|
337
|
-
- [x] CLI with
|
|
338
|
-
- [x] MCP server with
|
|
341
|
+
- [x] CLI with 18 commands
|
|
342
|
+
- [x] MCP server with 23 tools and 1 resource
|
|
339
343
|
- [x] Schema introspection (MCP resource + tool)
|
|
340
344
|
- [x] Complete CRUD (push --replace, drop, delete)
|
|
341
345
|
- [x] SHACL validation (shape constraints on push)
|
|
@@ -379,7 +383,7 @@ graph TB
|
|
|
379
383
|
end
|
|
380
384
|
|
|
381
385
|
subgraph MCP["MCP Server"]
|
|
382
|
-
Tools[
|
|
386
|
+
Tools[23 Tools]
|
|
383
387
|
Resource["opentology://schema"]
|
|
384
388
|
end
|
|
385
389
|
|
|
@@ -454,7 +458,7 @@ graph LR
|
|
|
454
458
|
| Docker 필수 | 아니오 (임베디드 모드) | 예 | 예 |
|
|
455
459
|
| RDFS 추론 | 푸시 시 자동 | SPARQL CONSTRUCT 수동 작성 | 네이티브 미지원 |
|
|
456
460
|
| SHACL 검증 | 내장 | 별도 도구 필요 | 해당 없음 |
|
|
457
|
-
| AI 연동 | MCP 서버
|
|
461
|
+
| AI 연동 | MCP 서버 23개 도구 | 없음 | 플러그인 생태계 |
|
|
458
462
|
| 쿼리 언어 | SPARQL (접두사 자동 삽입) | SPARQL (수동) | Cypher |
|
|
459
463
|
| 데이터 형식 | Turtle 파일 | Turtle/N-Triples | 속성 그래프 |
|
|
460
464
|
| 프로젝트 구분 | Named Graph 자동 | 수동 관리 | 데이터베이스 단위 |
|
|
@@ -463,7 +467,7 @@ graph LR
|
|
|
463
467
|
|
|
464
468
|
**핵심**
|
|
465
469
|
|
|
466
|
-
- RDF 전체 생애주기를 다루는
|
|
470
|
+
- RDF 전체 생애주기를 다루는 18개 CLI 명령어
|
|
467
471
|
- `.opentology.json` 기반 프로젝트 설정
|
|
468
472
|
- Named Graph 자동 스코핑 -- 쿼리가 프로젝트 그래프에 자동 한정
|
|
469
473
|
- HTTP 모드(Oxigraph 서버)와 임베디드 모드(WASM, Docker 불필요) 지원
|
|
@@ -483,7 +487,7 @@ graph LR
|
|
|
483
487
|
|
|
484
488
|
**AI 연동**
|
|
485
489
|
|
|
486
|
-
-
|
|
490
|
+
- 23개 도구와 1개 리소스를 제공하는 MCP 서버
|
|
487
491
|
- `opentology://schema` 리소스로 온톨로지 개요 자동 로드
|
|
488
492
|
- Claude Code, Cursor 등 MCP 호환 클라이언트와 연동
|
|
489
493
|
|
|
@@ -538,8 +542,9 @@ docker run -p 7878:7878 ghcr.io/oxigraph/oxigraph \
|
|
|
538
542
|
| `opentology infer` | RDFS 물질화 실행 (`--clear`로 추론 트리플 제거) |
|
|
539
543
|
| `opentology graph` | Named Graph 목록/생성/삭제 |
|
|
540
544
|
| `opentology prefix` | 프로젝트 접두사 목록/추가/제거 |
|
|
541
|
-
| `opentology context` | 프로젝트 컨텍스트 관리 (`init`, `load`, `status`, `scan`, `graph`) |
|
|
545
|
+
| `opentology context` | 프로젝트 컨텍스트 관리 (`init`, `load`, `status`, `scan`, `impact`, `sync`, `graph`) |
|
|
542
546
|
| `opentology viz` | 온톨로지 스키마 시각화 (`schema`) |
|
|
547
|
+
| `opentology doctor` | 프로젝트 건강 진단 (설정, 스토어, 컨텍스트, 훅, 의존성) |
|
|
543
548
|
| `opentology mcp` | MCP 서버 시작 |
|
|
544
549
|
|
|
545
550
|
### MCP 연동
|
|
@@ -557,7 +562,7 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
557
562
|
}
|
|
558
563
|
```
|
|
559
564
|
|
|
560
|
-
**
|
|
565
|
+
**23개 도구:**
|
|
561
566
|
|
|
562
567
|
| 도구 | 설명 |
|
|
563
568
|
|------|------|
|
|
@@ -580,7 +585,10 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
580
585
|
| `opentology_context_load` | 프로젝트 컨텍스트 로드 |
|
|
581
586
|
| `opentology_context_status` | 컨텍스트 초기화 상태 확인 |
|
|
582
587
|
| `opentology_context_scan` | 코드베이스 스캔 (모듈/심볼 수준) |
|
|
588
|
+
| `opentology_context_impact` | 파일 수정 영향 분석 (의존 모듈, 관련 컨텍스트) |
|
|
589
|
+
| `opentology_context_sync` | 자동 동기화: git에서 누락 세션 복구, 모듈 그래프 재스캔 |
|
|
583
590
|
| `opentology_context_graph` | 인터랙티브 그래프 시각화 웹 UI 시작 |
|
|
591
|
+
| `opentology_doctor` | 프로젝트 건강 진단 (설정, 스토어, 훅, 의존성) |
|
|
584
592
|
|
|
585
593
|
**1개 리소스:**
|
|
586
594
|
|
|
@@ -691,8 +699,8 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
691
699
|
|
|
692
700
|
### 로드맵
|
|
693
701
|
|
|
694
|
-
- [x]
|
|
695
|
-
- [x]
|
|
702
|
+
- [x] 18개 CLI 명령어
|
|
703
|
+
- [x] 23개 도구와 1개 리소스를 갖춘 MCP 서버
|
|
696
704
|
- [x] 스키마 조회 (MCP 리소스 + 도구)
|
|
697
705
|
- [x] 완전한 CRUD (push --replace, drop, delete)
|
|
698
706
|
- [x] SHACL 검증 (푸시 시 형상 제약 자동 검증)
|
package/dist/commands/context.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
5
6
|
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
6
7
|
import { startGraphServer } from '../lib/graph-server.js';
|
|
8
|
+
import { syncContext } from '../lib/context-sync.js';
|
|
7
9
|
import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
|
|
8
10
|
import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
|
|
9
11
|
import { generateHookScript } from '../templates/session-start-hook.js';
|
|
12
|
+
import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
|
|
10
13
|
import { generateSlashCommands } from '../templates/slash-commands.js';
|
|
14
|
+
function ask(question) {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(question, (answer) => {
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(answer.trim().toLowerCase());
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
11
23
|
export function registerContext(program) {
|
|
12
24
|
const context = program
|
|
13
25
|
.command('context')
|
|
@@ -87,6 +99,16 @@ export function registerContext(program) {
|
|
|
87
99
|
else {
|
|
88
100
|
console.log(pc.dim(' Hook script already exists — skipped (use --force to regenerate)'));
|
|
89
101
|
}
|
|
102
|
+
// Step 3b: Write pre-edit hook script
|
|
103
|
+
const preEditHookPath = join(hookDir, 'pre-edit.mjs');
|
|
104
|
+
if (!existsSync(preEditHookPath) || opts.force) {
|
|
105
|
+
mkdirSync(hookDir, { recursive: true });
|
|
106
|
+
writeFileSync(preEditHookPath, generatePreEditHookScript(), 'utf-8');
|
|
107
|
+
console.log(pc.green(` Generated hook script: .opentology/hooks/pre-edit.mjs`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(pc.dim(' Pre-edit hook already exists — skipped'));
|
|
111
|
+
}
|
|
90
112
|
// Step 4: Update CLAUDE.md
|
|
91
113
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
92
114
|
const section = generateContextSection(config.projectId, config.graphUri);
|
|
@@ -139,18 +161,51 @@ export function registerContext(program) {
|
|
|
139
161
|
}
|
|
140
162
|
// Step 6: Save config LAST (atomic commit point)
|
|
141
163
|
saveConfig(config);
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
// Step 7: Register hooks in .claude/settings.json
|
|
165
|
+
const settingsDir = join(process.cwd(), '.claude');
|
|
166
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
167
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
168
|
+
let settings = {};
|
|
169
|
+
if (existsSync(settingsPath)) {
|
|
170
|
+
try {
|
|
171
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
172
|
+
}
|
|
173
|
+
catch { /* start fresh */ }
|
|
174
|
+
}
|
|
175
|
+
const hooks = (settings.hooks ?? {});
|
|
176
|
+
// Always register SessionStart hook
|
|
177
|
+
const sessionStartCmd = 'node .opentology/hooks/session-start.mjs';
|
|
178
|
+
if (!hooks.SessionStart)
|
|
179
|
+
hooks.SessionStart = [];
|
|
180
|
+
const hasSessionHook = hooks.SessionStart.some((h) => h.command === sessionStartCmd);
|
|
181
|
+
if (!hasSessionHook) {
|
|
182
|
+
hooks.SessionStart.push({ type: 'command', command: sessionStartCmd });
|
|
183
|
+
console.log(pc.green(' Registered SessionStart hook in .claude/settings.json'));
|
|
184
|
+
}
|
|
185
|
+
// Ask about PreToolUse impact hook
|
|
145
186
|
console.log('');
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
const enableImpact = await ask(pc.bold('Enable PreToolUse impact analysis hook? ') +
|
|
188
|
+
pc.dim('(auto-shows dependents before Edit/Write)') +
|
|
189
|
+
' [Y/n] ');
|
|
190
|
+
if (enableImpact !== 'n' && enableImpact !== 'no') {
|
|
191
|
+
const preEditCmd = 'node .opentology/hooks/pre-edit.mjs';
|
|
192
|
+
if (!hooks.PreToolUse)
|
|
193
|
+
hooks.PreToolUse = [];
|
|
194
|
+
const hasPreEditHook = hooks.PreToolUse.some((h) => h.command === preEditCmd);
|
|
195
|
+
if (!hasPreEditHook) {
|
|
196
|
+
hooks.PreToolUse.push({
|
|
197
|
+
type: 'command',
|
|
198
|
+
command: preEditCmd,
|
|
199
|
+
matcher: { tool_name: 'Edit|Write' },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
console.log(pc.green(' Registered PreToolUse impact hook in .claude/settings.json'));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(pc.dim(' Skipped PreToolUse hook registration'));
|
|
206
|
+
}
|
|
207
|
+
settings.hooks = hooks;
|
|
208
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
154
209
|
console.log('');
|
|
155
210
|
console.log(pc.dim('Consider adding .opentology/hooks/ to version control so team members share the hook.'));
|
|
156
211
|
}
|
|
@@ -367,6 +422,138 @@ export function registerContext(program) {
|
|
|
367
422
|
console.log('CLAUDE.md: ' + (hasMarkers ? pc.green('markers present') : pc.yellow('markers missing')));
|
|
368
423
|
}
|
|
369
424
|
});
|
|
425
|
+
// --- context impact ---
|
|
426
|
+
context
|
|
427
|
+
.command('impact')
|
|
428
|
+
.description('Analyze impact of modifying a file (dependents, dependencies, related context)')
|
|
429
|
+
.requiredOption('--file <path>', 'Relative file path to analyze')
|
|
430
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
431
|
+
.action(async (opts) => {
|
|
432
|
+
let config;
|
|
433
|
+
try {
|
|
434
|
+
config = loadConfig();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const graphs = config.graphs ?? {};
|
|
441
|
+
if (!graphs['context']) {
|
|
442
|
+
console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const contextUri = graphs['context'];
|
|
446
|
+
const OTX = 'https://opentology.dev/vocab#';
|
|
447
|
+
const moduleUriStr = `urn:module:${opts.file}`;
|
|
448
|
+
try {
|
|
449
|
+
const adapter = await createReadyAdapter(config);
|
|
450
|
+
// Dependents (reverse deps)
|
|
451
|
+
const dependentsResult = await adapter.sparqlQuery(`
|
|
452
|
+
SELECT ?dependent WHERE {
|
|
453
|
+
GRAPH <${contextUri}> {
|
|
454
|
+
?dependent <${OTX}dependsOn> <${moduleUriStr}> .
|
|
455
|
+
}
|
|
456
|
+
}`);
|
|
457
|
+
const dependents = (dependentsResult.results?.bindings ?? []).map((b) => b.dependent?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
458
|
+
// Dependencies
|
|
459
|
+
const depsResult = await adapter.sparqlQuery(`
|
|
460
|
+
SELECT ?dep WHERE {
|
|
461
|
+
GRAPH <${contextUri}> {
|
|
462
|
+
<${moduleUriStr}> <${OTX}dependsOn> ?dep .
|
|
463
|
+
}
|
|
464
|
+
}`);
|
|
465
|
+
const dependencies = (depsResult.results?.bindings ?? []).map((b) => b.dep?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
466
|
+
// Related context entries
|
|
467
|
+
let related = [];
|
|
468
|
+
try {
|
|
469
|
+
const relatedResult = await adapter.sparqlQuery(`
|
|
470
|
+
SELECT ?type ?title ?status ?date WHERE {
|
|
471
|
+
GRAPH <${contextUri}> {
|
|
472
|
+
?s <${OTX}body> ?body .
|
|
473
|
+
?s a ?type .
|
|
474
|
+
?s <${OTX}title> ?title .
|
|
475
|
+
OPTIONAL { ?s <${OTX}status> ?status }
|
|
476
|
+
OPTIONAL { ?s <${OTX}date> ?date }
|
|
477
|
+
FILTER(CONTAINS(?body, "${opts.file}"))
|
|
478
|
+
}
|
|
479
|
+
} LIMIT 10`);
|
|
480
|
+
related = (relatedResult.results?.bindings ?? []).map((b) => ({
|
|
481
|
+
type: b.type?.value?.replace(OTX, '') ?? '',
|
|
482
|
+
title: b.title?.value ?? '',
|
|
483
|
+
status: b.status?.value,
|
|
484
|
+
date: b.date?.value,
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// FILTER/CONTAINS may not be supported
|
|
489
|
+
}
|
|
490
|
+
const impact = dependents.length === 0 ? 'low' : dependents.length <= 3 ? 'medium' : 'high';
|
|
491
|
+
if (opts.format === 'json') {
|
|
492
|
+
console.log(JSON.stringify({ filePath: opts.file, dependents, dependencies, related, impact }, null, 2));
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.log(pc.bold(`Impact Analysis: ${opts.file}`));
|
|
496
|
+
console.log(`Impact level: ${impact === 'high' ? pc.red(impact) : impact === 'medium' ? pc.yellow(impact) : pc.green(impact)}`);
|
|
497
|
+
console.log('');
|
|
498
|
+
if (dependents.length > 0) {
|
|
499
|
+
console.log(pc.bold(`Dependents (${dependents.length}):`));
|
|
500
|
+
for (const d of dependents)
|
|
501
|
+
console.log(` ${d}`);
|
|
502
|
+
console.log('');
|
|
503
|
+
}
|
|
504
|
+
if (dependencies.length > 0) {
|
|
505
|
+
console.log(pc.bold(`Dependencies (${dependencies.length}):`));
|
|
506
|
+
for (const d of dependencies)
|
|
507
|
+
console.log(` ${d}`);
|
|
508
|
+
console.log('');
|
|
509
|
+
}
|
|
510
|
+
if (related.length > 0) {
|
|
511
|
+
console.log(pc.bold('Related context:'));
|
|
512
|
+
for (const r of related) {
|
|
513
|
+
console.log(` [${r.type}] ${r.title}${r.status ? ` (${r.status})` : ''}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (dependents.length === 0 && dependencies.length === 0) {
|
|
517
|
+
console.log(pc.dim('No module dependencies found. Run `opentology context scan` to populate.'));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// --- context sync ---
|
|
527
|
+
context
|
|
528
|
+
.command('sync')
|
|
529
|
+
.description('Auto-sync context graph: recover missed sessions from git log, rescan module dependencies')
|
|
530
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
531
|
+
.action(async (opts) => {
|
|
532
|
+
let config;
|
|
533
|
+
try {
|
|
534
|
+
config = loadConfig();
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const result = await syncContext(config, process.cwd());
|
|
542
|
+
if (opts.format === 'json') {
|
|
543
|
+
console.log(JSON.stringify(result, null, 2));
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
console.log(pc.bold('Context Sync'));
|
|
547
|
+
for (const action of result.actions) {
|
|
548
|
+
console.log(` ${pc.green('•')} ${action}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
370
557
|
// --- context graph ---
|
|
371
558
|
context
|
|
372
559
|
.command('graph')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
interface CheckResult {
|
|
3
|
+
name: string;
|
|
4
|
+
status: 'ok' | 'warn' | 'fail';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runDoctor(): Promise<CheckResult[]>;
|
|
8
|
+
export declare function registerDoctor(program: Command): void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { configExists, loadConfig } from '../lib/config.js';
|
|
5
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
6
|
+
export async function runDoctor() {
|
|
7
|
+
const results = [];
|
|
8
|
+
// 1. Config check
|
|
9
|
+
if (!configExists()) {
|
|
10
|
+
results.push({ name: 'Config', status: 'fail', message: 'No .opentology.json found. Run `opentology init` first.' });
|
|
11
|
+
return results; // Can't proceed without config
|
|
12
|
+
}
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
results.push({ name: 'Config', status: 'ok', message: `Project: ${config.projectId} (${config.mode} mode)` });
|
|
15
|
+
// 2. Store connectivity
|
|
16
|
+
try {
|
|
17
|
+
const adapter = await createReadyAdapter(config);
|
|
18
|
+
const r = await adapter.sparqlQuery(`SELECT (COUNT(*) AS ?c) WHERE { GRAPH <${config.graphUri}> { ?s ?p ?o } }`);
|
|
19
|
+
const count = r.results?.bindings?.[0]?.c?.value ?? '0';
|
|
20
|
+
results.push({ name: 'Store', status: 'ok', message: `Connected — ${count} triples in default graph` });
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
results.push({ name: 'Store', status: 'fail', message: `Cannot connect: ${err.message}` });
|
|
24
|
+
}
|
|
25
|
+
// 3. Context initialization
|
|
26
|
+
const graphs = config.graphs ?? {};
|
|
27
|
+
if (graphs['context'] && graphs['sessions']) {
|
|
28
|
+
results.push({ name: 'Context', status: 'ok', message: `context: ${graphs['context']}` });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
results.push({ name: 'Context', status: 'warn', message: 'Not initialized. Run `opentology context init`.' });
|
|
32
|
+
}
|
|
33
|
+
// 4. Hook scripts
|
|
34
|
+
const hookDir = join(process.cwd(), '.opentology', 'hooks');
|
|
35
|
+
const sessionHook = join(hookDir, 'session-start.mjs');
|
|
36
|
+
const preEditHook = join(hookDir, 'pre-edit.mjs');
|
|
37
|
+
const hooksExist = existsSync(sessionHook) && existsSync(preEditHook);
|
|
38
|
+
if (hooksExist) {
|
|
39
|
+
results.push({ name: 'Hooks', status: 'ok', message: 'session-start.mjs + pre-edit.mjs present' });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
results.push({ name: 'Hooks', status: 'warn', message: 'Hook scripts missing. Run `opentology context init`.' });
|
|
43
|
+
}
|
|
44
|
+
// 5. Hook registration in .claude/settings.json
|
|
45
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
46
|
+
if (existsSync(settingsPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
49
|
+
const hooks = settings.hooks ?? {};
|
|
50
|
+
const hasSession = (hooks.SessionStart ?? []).some((h) => h.command?.includes('session-start.mjs'));
|
|
51
|
+
const hasPreEdit = (hooks.PreToolUse ?? []).some((h) => h.command?.includes('pre-edit.mjs'));
|
|
52
|
+
if (hasSession && hasPreEdit) {
|
|
53
|
+
results.push({ name: 'Settings', status: 'ok', message: 'Both hooks registered in .claude/settings.json' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const missing = [];
|
|
57
|
+
if (!hasSession)
|
|
58
|
+
missing.push('SessionStart');
|
|
59
|
+
if (!hasPreEdit)
|
|
60
|
+
missing.push('PreToolUse');
|
|
61
|
+
results.push({ name: 'Settings', status: 'warn', message: `Missing hooks: ${missing.join(', ')}. Run \`opentology context init\`.` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
results.push({ name: 'Settings', status: 'warn', message: 'Cannot parse .claude/settings.json' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
results.push({ name: 'Settings', status: 'warn', message: 'No .claude/settings.json found. Run `opentology context init`.' });
|
|
70
|
+
}
|
|
71
|
+
// 6. CLAUDE.md
|
|
72
|
+
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
73
|
+
if (existsSync(claudeMdPath)) {
|
|
74
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
75
|
+
if (content.includes('OPENTOLOGY:CONTEXT:BEGIN') || content.includes('OpenTology')) {
|
|
76
|
+
results.push({ name: 'CLAUDE.md', status: 'ok', message: 'Context section present' });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
results.push({ name: 'CLAUDE.md', status: 'warn', message: 'Exists but no OpenTology context section' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
results.push({ name: 'CLAUDE.md', status: 'warn', message: 'Not found. Run `opentology context init`.' });
|
|
84
|
+
}
|
|
85
|
+
// 7. Optional dependencies
|
|
86
|
+
const optDeps = [
|
|
87
|
+
{ name: 'ts-morph', desc: 'TypeScript deep scan' },
|
|
88
|
+
{ name: 'web-tree-sitter', desc: 'Multi-language deep scan' },
|
|
89
|
+
];
|
|
90
|
+
for (const dep of optDeps) {
|
|
91
|
+
try {
|
|
92
|
+
await import(dep.name);
|
|
93
|
+
results.push({ name: dep.name, status: 'ok', message: dep.desc });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
results.push({ name: dep.name, status: 'warn', message: `Not installed (optional — ${dep.desc})` });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
export function registerDoctor(program) {
|
|
102
|
+
program
|
|
103
|
+
.command('doctor')
|
|
104
|
+
.description('Check project health: config, store, context, hooks, dependencies')
|
|
105
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const results = await runDoctor();
|
|
108
|
+
if (opts.format === 'json') {
|
|
109
|
+
console.log(JSON.stringify(results, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(pc.bold('\nOpenTology Doctor\n'));
|
|
113
|
+
for (const r of results) {
|
|
114
|
+
const icon = r.status === 'ok' ? pc.green('✓') : r.status === 'warn' ? pc.yellow('!') : pc.red('✗');
|
|
115
|
+
const msg = r.status === 'fail' ? pc.red(r.message) : r.status === 'warn' ? pc.yellow(r.message) : r.message;
|
|
116
|
+
console.log(` ${icon} ${pc.bold(r.name)}: ${msg}`);
|
|
117
|
+
}
|
|
118
|
+
const fails = results.filter((r) => r.status === 'fail').length;
|
|
119
|
+
const warns = results.filter((r) => r.status === 'warn').length;
|
|
120
|
+
console.log('');
|
|
121
|
+
if (fails > 0) {
|
|
122
|
+
console.log(pc.red(` ${fails} error(s), ${warns} warning(s)`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
else if (warns > 0) {
|
|
126
|
+
console.log(pc.yellow(` ${warns} warning(s), everything else looks good`));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(pc.green(' All checks passed!'));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,10 +16,11 @@ import { registerInfer } from './commands/infer.js';
|
|
|
16
16
|
import { registerPrefix } from './commands/prefix.js';
|
|
17
17
|
import { registerContext } from './commands/context.js';
|
|
18
18
|
import { registerViz } from './commands/viz.js';
|
|
19
|
+
import { registerDoctor } from './commands/doctor.js';
|
|
19
20
|
const program = new Command();
|
|
20
21
|
program
|
|
21
22
|
.name('opentology')
|
|
22
|
-
.version('0.
|
|
23
|
+
.version('0.2.2')
|
|
23
24
|
.description('CLI-managed RDF/SPARQL infrastructure — Supabase for RDF');
|
|
24
25
|
registerInit(program);
|
|
25
26
|
registerValidate(program);
|
|
@@ -37,4 +38,5 @@ registerInfer(program);
|
|
|
37
38
|
registerPrefix(program);
|
|
38
39
|
registerContext(program);
|
|
39
40
|
registerViz(program);
|
|
41
|
+
registerDoctor(program);
|
|
40
42
|
program.parse(process.argv);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OpenTologyConfig } from './config.js';
|
|
2
|
+
export interface SyncResult {
|
|
3
|
+
sessionsRecovered: number;
|
|
4
|
+
modulesUpdated: boolean;
|
|
5
|
+
moduleStats: {
|
|
6
|
+
modules: number;
|
|
7
|
+
edges: number;
|
|
8
|
+
} | null;
|
|
9
|
+
actions: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Core sync logic: recover missed sessions and rescan modules.
|
|
13
|
+
* Persists via .ttl files for embedded mode compatibility.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncContext(config: OpenTologyConfig, projectRoot: string): Promise<SyncResult>;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { addTrackedFile, saveConfig } from './config.js';
|
|
5
|
+
import { createReadyAdapter } from './store-factory.js';
|
|
6
|
+
import { extractDependencyGraph } from './codebase-scanner.js';
|
|
7
|
+
const OTX = 'https://opentology.dev/vocab#';
|
|
8
|
+
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
9
|
+
/**
|
|
10
|
+
* Get the most recent session date from the sessions graph.
|
|
11
|
+
* Returns ISO date string (YYYY-MM-DD) or null if no sessions exist.
|
|
12
|
+
*/
|
|
13
|
+
async function getLastSessionDate(adapter, sessionsUri) {
|
|
14
|
+
const r = await adapter.sparqlQuery(`
|
|
15
|
+
SELECT ?date WHERE {
|
|
16
|
+
GRAPH <${sessionsUri}> {
|
|
17
|
+
?s a <${OTX}Session> ; <${OTX}date> ?date .
|
|
18
|
+
}
|
|
19
|
+
} ORDER BY DESC(?date) LIMIT 1
|
|
20
|
+
`);
|
|
21
|
+
const binding = r.results?.bindings?.[0];
|
|
22
|
+
return binding?.date?.value ?? null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get git commits since a given date, grouped by date.
|
|
26
|
+
* Returns map of date -> commit messages.
|
|
27
|
+
*/
|
|
28
|
+
function getCommitsSince(projectRoot, sinceDate) {
|
|
29
|
+
const dateMap = new Map();
|
|
30
|
+
try {
|
|
31
|
+
const args = ['log', '--format=%ad|%s', '--date=short'];
|
|
32
|
+
if (sinceDate) {
|
|
33
|
+
args.push(`--after=${sinceDate}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
args.push('--since=7 days ago');
|
|
37
|
+
}
|
|
38
|
+
const output = execFileSync('git', args, {
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
timeout: 5000,
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (!output)
|
|
45
|
+
return dateMap;
|
|
46
|
+
for (const line of output.split('\n')) {
|
|
47
|
+
const sepIdx = line.indexOf('|');
|
|
48
|
+
if (sepIdx === -1)
|
|
49
|
+
continue;
|
|
50
|
+
const date = line.slice(0, sepIdx).trim();
|
|
51
|
+
const msg = line.slice(sepIdx + 1).trim();
|
|
52
|
+
if (!date || !msg)
|
|
53
|
+
continue;
|
|
54
|
+
if (!dateMap.has(date))
|
|
55
|
+
dateMap.set(date, []);
|
|
56
|
+
dateMap.get(date).push(msg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// git not available or not a repo
|
|
61
|
+
}
|
|
62
|
+
return dateMap;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if any source files changed since a given date.
|
|
66
|
+
*/
|
|
67
|
+
function hasSourceChanges(projectRoot, sinceDate) {
|
|
68
|
+
if (!sinceDate)
|
|
69
|
+
return true;
|
|
70
|
+
try {
|
|
71
|
+
const output = execFileSync('git', [
|
|
72
|
+
'log', '--oneline', `--after=${sinceDate}`,
|
|
73
|
+
'--', '*.ts', '*.js', '*.tsx', '*.jsx', '*.py', '*.go', '*.rs', '*.java', '*.swift',
|
|
74
|
+
], {
|
|
75
|
+
cwd: projectRoot,
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
timeout: 5000,
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
}).trim();
|
|
80
|
+
return output.length > 0;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate Turtle content for auto-recovered sessions.
|
|
88
|
+
*/
|
|
89
|
+
function generateSessionTurtle(commitsByDate) {
|
|
90
|
+
const lines = [
|
|
91
|
+
`@prefix otx: <${OTX}> .`,
|
|
92
|
+
`@prefix xsd: <${XSD}> .`,
|
|
93
|
+
'',
|
|
94
|
+
];
|
|
95
|
+
for (const [date, messages] of commitsByDate) {
|
|
96
|
+
const title = messages.length === 1
|
|
97
|
+
? messages[0]
|
|
98
|
+
: `${messages.length} commits`;
|
|
99
|
+
const body = messages.join('\n');
|
|
100
|
+
const uri = `urn:session:${date}-auto`;
|
|
101
|
+
lines.push(`<${uri}> a otx:Session ;`);
|
|
102
|
+
lines.push(` otx:title "${escapeTurtle(title)}" ;`);
|
|
103
|
+
lines.push(` otx:date "${date}"^^xsd:date ;`);
|
|
104
|
+
lines.push(` otx:body "${escapeTurtle(body)}" .`);
|
|
105
|
+
lines.push('');
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generate Turtle content for module dependency graph.
|
|
111
|
+
*/
|
|
112
|
+
function generateModuleTurtle(modules, edges) {
|
|
113
|
+
const lines = [
|
|
114
|
+
`@prefix otx: <${OTX}> .`,
|
|
115
|
+
'',
|
|
116
|
+
];
|
|
117
|
+
for (const mod of modules) {
|
|
118
|
+
lines.push(`<urn:module:${mod}> a otx:Module .`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
for (const edge of edges) {
|
|
122
|
+
lines.push(`<urn:module:${edge.from}> otx:dependsOn <urn:module:${edge.to}> .`);
|
|
123
|
+
}
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Core sync logic: recover missed sessions and rescan modules.
|
|
128
|
+
* Persists via .ttl files for embedded mode compatibility.
|
|
129
|
+
*/
|
|
130
|
+
export async function syncContext(config, projectRoot) {
|
|
131
|
+
const graphs = config.graphs ?? {};
|
|
132
|
+
const contextUri = graphs['context'];
|
|
133
|
+
const sessionsUri = graphs['sessions'];
|
|
134
|
+
if (!contextUri || !sessionsUri) {
|
|
135
|
+
throw new Error('Context not initialized. Run `opentology context init` first.');
|
|
136
|
+
}
|
|
137
|
+
const adapter = await createReadyAdapter(config);
|
|
138
|
+
const actions = [];
|
|
139
|
+
let sessionsRecovered = 0;
|
|
140
|
+
let modulesUpdated = false;
|
|
141
|
+
let moduleStats = null;
|
|
142
|
+
const syncDir = join(projectRoot, '.opentology', 'sync');
|
|
143
|
+
mkdirSync(syncDir, { recursive: true });
|
|
144
|
+
// === Step 1: Recover missed sessions from git log ===
|
|
145
|
+
const lastDate = await getLastSessionDate(adapter, sessionsUri);
|
|
146
|
+
const commitsByDate = getCommitsSince(projectRoot, lastDate);
|
|
147
|
+
if (commitsByDate.size > 0) {
|
|
148
|
+
const turtle = generateSessionTurtle(commitsByDate);
|
|
149
|
+
const sessionsFile = join(syncDir, 'auto-sessions.ttl');
|
|
150
|
+
// Append to existing auto-sessions file or create new
|
|
151
|
+
if (existsSync(sessionsFile)) {
|
|
152
|
+
const existing = readFileSync(sessionsFile, 'utf-8');
|
|
153
|
+
// Only add new sessions (check URIs)
|
|
154
|
+
const newEntries = [];
|
|
155
|
+
for (const [date] of commitsByDate) {
|
|
156
|
+
const uri = `urn:session:${date}-auto`;
|
|
157
|
+
if (!existing.includes(uri)) {
|
|
158
|
+
newEntries.push(date);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (newEntries.length > 0) {
|
|
162
|
+
const newMap = new Map();
|
|
163
|
+
for (const date of newEntries) {
|
|
164
|
+
newMap.set(date, commitsByDate.get(date));
|
|
165
|
+
}
|
|
166
|
+
if (newMap.size > 0) {
|
|
167
|
+
const newTurtle = generateSessionTurtle(newMap);
|
|
168
|
+
// Remove prefix lines from appended content
|
|
169
|
+
const body = newTurtle.split('\n').filter(l => !l.startsWith('@prefix') && l.trim() !== '').join('\n');
|
|
170
|
+
writeFileSync(sessionsFile, existing.trimEnd() + '\n\n' + body + '\n', 'utf-8');
|
|
171
|
+
await adapter.insertTurtle(sessionsUri, newTurtle);
|
|
172
|
+
sessionsRecovered = newMap.size;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
writeFileSync(sessionsFile, turtle, 'utf-8');
|
|
178
|
+
await adapter.insertTurtle(sessionsUri, turtle);
|
|
179
|
+
sessionsRecovered = commitsByDate.size;
|
|
180
|
+
}
|
|
181
|
+
if (sessionsRecovered > 0) {
|
|
182
|
+
// Track the file for embedded persistence
|
|
183
|
+
const relPath = '.opentology/sync/auto-sessions.ttl';
|
|
184
|
+
addTrackedFile(config, sessionsUri, relPath);
|
|
185
|
+
const totalCommits = [...commitsByDate.values()].reduce((s, m) => s + m.length, 0);
|
|
186
|
+
actions.push(`Recovered ${sessionsRecovered} session(s) from ${totalCommits} git commit(s)`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
actions.push('No missed sessions to recover');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
actions.push('No missed sessions to recover');
|
|
194
|
+
}
|
|
195
|
+
// === Step 2: Rescan module dependency graph if source files changed ===
|
|
196
|
+
const shouldRescan = hasSourceChanges(projectRoot, lastDate);
|
|
197
|
+
if (shouldRescan) {
|
|
198
|
+
try {
|
|
199
|
+
const dg = await extractDependencyGraph(projectRoot, null);
|
|
200
|
+
if (dg.modules.length > 0) {
|
|
201
|
+
const turtle = generateModuleTurtle(dg.modules, dg.edges);
|
|
202
|
+
const modulesFile = join(syncDir, 'modules.ttl');
|
|
203
|
+
// Replace modules file entirely (fresh scan)
|
|
204
|
+
writeFileSync(modulesFile, turtle, 'utf-8');
|
|
205
|
+
// Drop old module triples, then insert fresh
|
|
206
|
+
await adapter.sparqlUpdate(`DELETE { GRAPH <${contextUri}> { ?s ?p ?o } } WHERE { GRAPH <${contextUri}> { ?s a <${OTX}Module> . ?s ?p ?o } }`);
|
|
207
|
+
await adapter.sparqlUpdate(`DELETE { GRAPH <${contextUri}> { ?s <${OTX}dependsOn> ?o } } WHERE { GRAPH <${contextUri}> { ?s <${OTX}dependsOn> ?o } }`);
|
|
208
|
+
await adapter.insertTurtle(contextUri, turtle);
|
|
209
|
+
const relPath = '.opentology/sync/modules.ttl';
|
|
210
|
+
addTrackedFile(config, contextUri, relPath);
|
|
211
|
+
moduleStats = { modules: dg.modules.length, edges: dg.edges.length };
|
|
212
|
+
modulesUpdated = true;
|
|
213
|
+
actions.push(`Rescanned modules: ${dg.modules.length} modules, ${dg.edges.length} edges`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
actions.push('Module rescan failed (non-fatal)');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
actions.push('No source changes detected — module rescan skipped');
|
|
222
|
+
}
|
|
223
|
+
// Save config with tracked files
|
|
224
|
+
saveConfig(config);
|
|
225
|
+
return { sessionsRecovered, modulesUpdated, moduleStats, actions };
|
|
226
|
+
}
|
|
227
|
+
function escapeTurtle(s) {
|
|
228
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
229
|
+
}
|
package/dist/lib/graph-server.js
CHANGED
|
@@ -70,8 +70,9 @@ function html(config) {
|
|
|
70
70
|
<div id="app">
|
|
71
71
|
<div id="sidebar">
|
|
72
72
|
<h1>OpenTology — ${projectId}</h1>
|
|
73
|
-
<div id="graph-list">
|
|
74
|
-
<select id="graphSelect"><option value="">Loading graphs...</option></select>
|
|
73
|
+
<div id="graph-list" style="display:flex;gap:6px;align-items:center;">
|
|
74
|
+
<select id="graphSelect" style="flex:1;"><option value="">Loading graphs...</option></select>
|
|
75
|
+
<button id="refreshBtn" onclick="refreshAll()" title="Refresh graph data" style="padding:5px 10px;background:#0550ae;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;white-space:nowrap;">↻ Refresh</button>
|
|
75
76
|
</div>
|
|
76
77
|
<div class="stats" id="stats"></div>
|
|
77
78
|
<div id="query-box">
|
|
@@ -189,6 +190,30 @@ function html(config) {
|
|
|
189
190
|
loadGraph(sel.value);
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
async function refreshAll() {
|
|
194
|
+
const btn = document.getElementById('refreshBtn');
|
|
195
|
+
btn.textContent = '\\u21bb ...';
|
|
196
|
+
btn.disabled = true;
|
|
197
|
+
try {
|
|
198
|
+
const gs = await fetch('/api/graphs').then(r => r.json());
|
|
199
|
+
const sel = document.getElementById('graphSelect');
|
|
200
|
+
const prev = sel.value;
|
|
201
|
+
sel.innerHTML = '';
|
|
202
|
+
for (const g of gs) {
|
|
203
|
+
sel.innerHTML += '<option value="' + g.uri + '"' + (g.uri === prev ? ' selected' : '') + '>' + g.name + ' (' + g.triples + ')</option>';
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const schema = await fetch('/api/schema').then(r => r.json());
|
|
207
|
+
classSet = new Set();
|
|
208
|
+
(schema.classes || []).forEach(c => classSet.add(c));
|
|
209
|
+
} catch {}
|
|
210
|
+
await runQuery();
|
|
211
|
+
} finally {
|
|
212
|
+
btn.innerHTML = '↻ Refresh';
|
|
213
|
+
btn.disabled = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
async function loadGraph(graphUri) {
|
|
193
218
|
const q = graphUri
|
|
194
219
|
? 'SELECT ?s ?p ?o WHERE { GRAPH <' + graphUri + '> { ?s ?p ?o } } LIMIT 500'
|
package/dist/mcp/server.js
CHANGED
|
@@ -10,12 +10,15 @@ import { validateTurtle } from '../lib/validator.js';
|
|
|
10
10
|
import { discoverShapes, validateWithShacl, hasShapes } from '../lib/shacl.js';
|
|
11
11
|
import { materializeInferences, clearInferences } from '../lib/reasoner.js';
|
|
12
12
|
import { fromSchemaData, toMermaid, toDot } from '../lib/visualizer.js';
|
|
13
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
|
|
16
16
|
import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
|
|
17
17
|
import { generateHookScript } from '../templates/session-start-hook.js';
|
|
18
|
+
import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
|
|
18
19
|
import { generateSlashCommands } from '../templates/slash-commands.js';
|
|
20
|
+
import { runDoctor } from '../commands/doctor.js';
|
|
21
|
+
import { syncContext } from '../lib/context-sync.js';
|
|
19
22
|
import { scanCodebase } from '../lib/codebase-scanner.js';
|
|
20
23
|
import { startGraphServer } from '../lib/graph-server.js';
|
|
21
24
|
export const MAX_TRIPLES_PER_PUSH = 100;
|
|
@@ -337,14 +340,39 @@ async function handleContextScan(args) {
|
|
|
337
340
|
: 'Deep scan completed but triple push failed. Use push manually with the generated triples.',
|
|
338
341
|
};
|
|
339
342
|
}
|
|
340
|
-
// Default: module-level scan
|
|
343
|
+
// Default: module-level scan — now auto-pushes module triples like context_init
|
|
341
344
|
const maxBytes = args.maxSnapshotBytes ?? 15360;
|
|
342
345
|
const snapshot = await scanCodebase(process.cwd(), maxBytes);
|
|
346
|
+
let moduleStats = null;
|
|
347
|
+
try {
|
|
348
|
+
const config = loadConfig();
|
|
349
|
+
const contextUri = `${config.graphUri}/context`;
|
|
350
|
+
const adapter = await createReadyAdapter(config);
|
|
351
|
+
if (snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0) {
|
|
352
|
+
const dg = snapshot.dependencyGraph;
|
|
353
|
+
// Scoped delete: clear stale module triples before re-insert
|
|
354
|
+
await adapter.sparqlUpdate(`DELETE WHERE { GRAPH <${contextUri}> { ?m a <https://opentology.dev/vocab#Module> . ?m ?p ?o } }`);
|
|
355
|
+
// Insert fresh triples
|
|
356
|
+
const sparqlTriples = [];
|
|
357
|
+
for (const mod of dg.modules) {
|
|
358
|
+
sparqlTriples.push(`<urn:module:${mod}> a <https://opentology.dev/vocab#Module> .`);
|
|
359
|
+
}
|
|
360
|
+
for (const edge of dg.edges) {
|
|
361
|
+
sparqlTriples.push(`<urn:module:${edge.from}> <https://opentology.dev/vocab#dependsOn> <urn:module:${edge.to}> .`);
|
|
362
|
+
}
|
|
363
|
+
await adapter.sparqlUpdate(`INSERT DATA { GRAPH <${contextUri}> {\n${sparqlTriples.join('\n')}\n} }`);
|
|
364
|
+
moduleStats = { modules: dg.modules.length, edges: dg.edges.length };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Non-fatal: module triple push is best-effort
|
|
369
|
+
}
|
|
343
370
|
return {
|
|
344
371
|
codebaseSnapshot: snapshot,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
372
|
+
moduleStats,
|
|
373
|
+
_hint: moduleStats
|
|
374
|
+
? `Module triples auto-pushed: ${moduleStats.modules} modules, ${moduleStats.edges} edges. Query with: SELECT ?m WHERE { ?m a otx:Module }`
|
|
375
|
+
: 'No dependency graph auto-extracted or push failed. Inspect key source files manually.',
|
|
348
376
|
};
|
|
349
377
|
}
|
|
350
378
|
async function handleContextInit(args) {
|
|
@@ -388,6 +416,13 @@ async function handleContextInit(args) {
|
|
|
388
416
|
writeFileSync(hookPath, generateHookScript(), 'utf-8');
|
|
389
417
|
actions.push('Generated hook: .opentology/hooks/session-start.mjs');
|
|
390
418
|
}
|
|
419
|
+
// Generate pre-edit hook script
|
|
420
|
+
const preEditHookPath = join(hookDir, 'pre-edit.mjs');
|
|
421
|
+
if (!existsSync(preEditHookPath) || force) {
|
|
422
|
+
mkdirSync(hookDir, { recursive: true });
|
|
423
|
+
writeFileSync(preEditHookPath, generatePreEditHookScript(), 'utf-8');
|
|
424
|
+
actions.push('Generated hook: .opentology/hooks/pre-edit.mjs');
|
|
425
|
+
}
|
|
391
426
|
// Update CLAUDE.md
|
|
392
427
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
393
428
|
const section = generateContextSection(config.projectId, config.graphUri);
|
|
@@ -415,6 +450,44 @@ async function handleContextInit(args) {
|
|
|
415
450
|
actions.push(`Generated ${slashCreated} slash commands in .claude/commands/`);
|
|
416
451
|
}
|
|
417
452
|
saveConfig(config);
|
|
453
|
+
// Auto-register hooks in .claude/settings.json (non-interactive, both hooks)
|
|
454
|
+
const settingsDir = join(process.cwd(), '.claude');
|
|
455
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
456
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
457
|
+
let settings = {};
|
|
458
|
+
if (existsSync(settingsPath)) {
|
|
459
|
+
try {
|
|
460
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
461
|
+
}
|
|
462
|
+
catch { /* start fresh */ }
|
|
463
|
+
}
|
|
464
|
+
const hooks = (settings.hooks ?? {});
|
|
465
|
+
let hooksChanged = false;
|
|
466
|
+
const sessionStartCmd = 'node .opentology/hooks/session-start.mjs';
|
|
467
|
+
if (!hooks.SessionStart)
|
|
468
|
+
hooks.SessionStart = [];
|
|
469
|
+
const hasSessionHook = hooks.SessionStart.some((h) => h.command === sessionStartCmd);
|
|
470
|
+
if (!hasSessionHook) {
|
|
471
|
+
hooks.SessionStart.push({ type: 'command', command: sessionStartCmd });
|
|
472
|
+
hooksChanged = true;
|
|
473
|
+
}
|
|
474
|
+
const preEditCmd = 'node .opentology/hooks/pre-edit.mjs';
|
|
475
|
+
if (!hooks.PreToolUse)
|
|
476
|
+
hooks.PreToolUse = [];
|
|
477
|
+
const hasPreEditHook = hooks.PreToolUse.some((h) => h.command === preEditCmd);
|
|
478
|
+
if (!hasPreEditHook) {
|
|
479
|
+
hooks.PreToolUse.push({
|
|
480
|
+
type: 'command',
|
|
481
|
+
command: preEditCmd,
|
|
482
|
+
matcher: { tool_name: 'Edit|Write' },
|
|
483
|
+
});
|
|
484
|
+
hooksChanged = true;
|
|
485
|
+
}
|
|
486
|
+
if (hooksChanged) {
|
|
487
|
+
settings.hooks = hooks;
|
|
488
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
489
|
+
actions.push('Auto-registered hooks in .claude/settings.json');
|
|
490
|
+
}
|
|
418
491
|
// Auto-push Module triples from dependency graph
|
|
419
492
|
let moduleStats = null;
|
|
420
493
|
try {
|
|
@@ -448,14 +521,7 @@ async function handleContextInit(args) {
|
|
|
448
521
|
actions,
|
|
449
522
|
moduleStats,
|
|
450
523
|
dependencyHint,
|
|
451
|
-
|
|
452
|
-
hooks: {
|
|
453
|
-
SessionStart: [{
|
|
454
|
-
type: 'command',
|
|
455
|
-
command: 'node .opentology/hooks/session-start.mjs',
|
|
456
|
-
}],
|
|
457
|
-
},
|
|
458
|
-
},
|
|
524
|
+
hooksAutoInstalled: hooksChanged,
|
|
459
525
|
};
|
|
460
526
|
}
|
|
461
527
|
async function handleContextLoad() {
|
|
@@ -553,6 +619,71 @@ async function handleContextLoad() {
|
|
|
553
619
|
delete output.warnings;
|
|
554
620
|
return output;
|
|
555
621
|
}
|
|
622
|
+
async function handleContextImpact(args) {
|
|
623
|
+
const filePath = args.filePath;
|
|
624
|
+
if (!filePath)
|
|
625
|
+
throw new Error('filePath is required');
|
|
626
|
+
const config = loadConfig();
|
|
627
|
+
const contextUri = `${config.graphUri}/context`;
|
|
628
|
+
const adapter = await createReadyAdapter(config);
|
|
629
|
+
const OTX = 'https://opentology.dev/vocab#';
|
|
630
|
+
const moduleUriStr = `urn:module:${filePath}`;
|
|
631
|
+
// 1. Modules that depend on this file (dependents / reverse deps)
|
|
632
|
+
const dependentsQuery = `
|
|
633
|
+
SELECT ?dependent WHERE {
|
|
634
|
+
GRAPH <${contextUri}> {
|
|
635
|
+
?dependent <${OTX}dependsOn> <${moduleUriStr}> .
|
|
636
|
+
}
|
|
637
|
+
}`;
|
|
638
|
+
const dependentsResult = await adapter.sparqlQuery(dependentsQuery);
|
|
639
|
+
const dependents = (dependentsResult.results?.bindings ?? []).map((b) => b.dependent?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
640
|
+
// 2. Modules this file depends on (dependencies)
|
|
641
|
+
const depsQuery = `
|
|
642
|
+
SELECT ?dep WHERE {
|
|
643
|
+
GRAPH <${contextUri}> {
|
|
644
|
+
<${moduleUriStr}> <${OTX}dependsOn> ?dep .
|
|
645
|
+
}
|
|
646
|
+
}`;
|
|
647
|
+
const depsResult = await adapter.sparqlQuery(depsQuery);
|
|
648
|
+
const dependencies = (depsResult.results?.bindings ?? []).map((b) => b.dep?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
649
|
+
// 3. Related decisions, issues, knowledge mentioning this file
|
|
650
|
+
const relatedQuery = `
|
|
651
|
+
SELECT ?type ?title ?status ?date WHERE {
|
|
652
|
+
GRAPH <${contextUri}> {
|
|
653
|
+
?s <${OTX}body> ?body .
|
|
654
|
+
?s a ?type .
|
|
655
|
+
?s <${OTX}title> ?title .
|
|
656
|
+
OPTIONAL { ?s <${OTX}status> ?status }
|
|
657
|
+
OPTIONAL { ?s <${OTX}date> ?date }
|
|
658
|
+
FILTER(CONTAINS(?body, "${filePath}"))
|
|
659
|
+
}
|
|
660
|
+
} LIMIT 10`;
|
|
661
|
+
let related = [];
|
|
662
|
+
try {
|
|
663
|
+
const relatedResult = await adapter.sparqlQuery(relatedQuery);
|
|
664
|
+
related = (relatedResult.results?.bindings ?? []).map((b) => ({
|
|
665
|
+
type: b.type?.value?.replace(OTX, '') ?? '',
|
|
666
|
+
title: b.title?.value ?? '',
|
|
667
|
+
status: b.status?.value,
|
|
668
|
+
date: b.date?.value,
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// FILTER/CONTAINS may not be supported — skip gracefully
|
|
673
|
+
}
|
|
674
|
+
const hasDeps = dependents.length > 0 || dependencies.length > 0;
|
|
675
|
+
return {
|
|
676
|
+
filePath,
|
|
677
|
+
moduleUri: moduleUriStr,
|
|
678
|
+
dependents,
|
|
679
|
+
dependencies,
|
|
680
|
+
related,
|
|
681
|
+
impact: dependents.length === 0 ? 'low' : dependents.length <= 3 ? 'medium' : 'high',
|
|
682
|
+
_hint: hasDeps
|
|
683
|
+
? `This file has ${dependents.length} dependent(s) and ${dependencies.length} dependency(ies). Review dependents before making breaking changes.`
|
|
684
|
+
: 'No module dependencies found in the graph. Run context_scan first to populate module triples.',
|
|
685
|
+
};
|
|
686
|
+
}
|
|
556
687
|
async function handleContextStatus() {
|
|
557
688
|
const config = loadConfig();
|
|
558
689
|
const graphs = config.graphs ?? {};
|
|
@@ -1063,6 +1194,36 @@ export async function startMcpServer() {
|
|
|
1063
1194
|
},
|
|
1064
1195
|
},
|
|
1065
1196
|
},
|
|
1197
|
+
{
|
|
1198
|
+
name: 'context_impact',
|
|
1199
|
+
description: 'Analyze the impact of modifying a file. Returns modules that depend on the target, modules it depends on, and related decisions/issues/knowledge from the context graph. Use this BEFORE editing files to understand the blast radius of changes.',
|
|
1200
|
+
inputSchema: {
|
|
1201
|
+
type: 'object',
|
|
1202
|
+
properties: {
|
|
1203
|
+
filePath: {
|
|
1204
|
+
type: 'string',
|
|
1205
|
+
description: 'Relative file path to analyze (e.g., "src/lib/store-factory.ts")',
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
required: ['filePath'],
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
name: 'context_sync',
|
|
1213
|
+
description: 'Auto-sync context graph: recover missed sessions from git log and rescan module dependency graph. Call this at session start to ensure the graph is up to date. Idempotent — safe to call multiple times.',
|
|
1214
|
+
inputSchema: {
|
|
1215
|
+
type: 'object',
|
|
1216
|
+
properties: {},
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
name: 'doctor',
|
|
1221
|
+
description: 'Check project health: config, store connectivity, context initialization, hook scripts, Claude Code settings, and optional dependencies. Returns a list of checks with ok/warn/fail status.',
|
|
1222
|
+
inputSchema: {
|
|
1223
|
+
type: 'object',
|
|
1224
|
+
properties: {},
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1066
1227
|
],
|
|
1067
1228
|
}));
|
|
1068
1229
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1133,6 +1294,15 @@ export async function startMcpServer() {
|
|
|
1133
1294
|
result = { url: `http://localhost:${srv.port}`, port: srv.port, message: `Graph server running at http://localhost:${srv.port}` };
|
|
1134
1295
|
break;
|
|
1135
1296
|
}
|
|
1297
|
+
case 'context_impact':
|
|
1298
|
+
result = await handleContextImpact(args);
|
|
1299
|
+
break;
|
|
1300
|
+
case 'context_sync':
|
|
1301
|
+
result = await syncContext(loadConfig(), process.cwd());
|
|
1302
|
+
break;
|
|
1303
|
+
case 'doctor':
|
|
1304
|
+
result = await runDoctor();
|
|
1305
|
+
break;
|
|
1136
1306
|
default:
|
|
1137
1307
|
throw new Error(`Unknown tool: ${name}`);
|
|
1138
1308
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generatePreEditHookScript(): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export function generatePreEditHookScript() {
|
|
2
|
+
return `#!/usr/bin/env node
|
|
3
|
+
// Generated by: opentology context init
|
|
4
|
+
// PreToolUse hook — queries impact analysis before Edit/Write
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { join, resolve, dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = parseInt(process.env.OPENTOLOGY_HOOK_TIMEOUT || '3000', 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
|
+
// Parse tool input from stdin (Claude Code passes JSON via stdin for hooks)
|
|
41
|
+
let input = '';
|
|
42
|
+
try {
|
|
43
|
+
input = require('fs').readFileSync('/dev/stdin', 'utf-8');
|
|
44
|
+
} catch {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let toolInput;
|
|
49
|
+
try {
|
|
50
|
+
toolInput = JSON.parse(input);
|
|
51
|
+
} catch {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract file path from Edit or Write tool arguments
|
|
56
|
+
const filePath = toolInput?.tool_input?.file_path;
|
|
57
|
+
if (!filePath) process.exit(0);
|
|
58
|
+
|
|
59
|
+
// Convert absolute path to project-relative path
|
|
60
|
+
const relative = filePath.startsWith(projectRoot)
|
|
61
|
+
? filePath.slice(projectRoot.length + 1)
|
|
62
|
+
: filePath;
|
|
63
|
+
|
|
64
|
+
// Skip non-source files
|
|
65
|
+
if (relative.startsWith('node_modules/') || relative.startsWith('.')) process.exit(0);
|
|
66
|
+
|
|
67
|
+
const raw = run(projectRoot, ['context', 'impact', '--file', relative, '--format', 'json']);
|
|
68
|
+
const data = JSON.parse(raw);
|
|
69
|
+
|
|
70
|
+
// Only output if there are meaningful dependencies
|
|
71
|
+
if (data.dependents?.length > 0 || data.related?.length > 0) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(\`[Impact Analysis] \${relative}\`);
|
|
74
|
+
|
|
75
|
+
if (data.dependents.length > 0) {
|
|
76
|
+
lines.push(\`Dependents (\${data.dependents.length}): \${data.dependents.slice(0, 5).join(', ')}\${data.dependents.length > 5 ? '...' : ''}\`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (data.dependencies?.length > 0) {
|
|
80
|
+
lines.push(\`Dependencies (\${data.dependencies.length}): \${data.dependencies.slice(0, 5).join(', ')}\${data.dependencies.length > 5 ? '...' : ''}\`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (data.related?.length > 0) {
|
|
84
|
+
lines.push('Related:');
|
|
85
|
+
for (const r of data.related.slice(0, 3)) {
|
|
86
|
+
lines.push(\` - [\${r.type}] \${r.title}\${r.status ? ' (' + r.status + ')' : ''}\`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (data.impact === 'high') {
|
|
91
|
+
lines.push('\\u26a0\\ufe0f HIGH IMPACT — multiple modules depend on this file. Review dependents before making breaking changes.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(lines.join('\\n'));
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Silent exit — do not block editing
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
@@ -74,6 +74,26 @@ try {
|
|
|
74
74
|
const projectRoot = findProjectRoot(process.cwd());
|
|
75
75
|
if (!projectRoot) process.exit(0);
|
|
76
76
|
|
|
77
|
+
// Step 1: Auto-sync — recover missed sessions and rescan modules
|
|
78
|
+
let syncSummary = '';
|
|
79
|
+
try {
|
|
80
|
+
const syncRaw = run(projectRoot, ['context', 'sync', '--format', 'json']);
|
|
81
|
+
const syncData = JSON.parse(syncRaw);
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (syncData.sessionsRecovered > 0) {
|
|
84
|
+
parts.push(\`\${syncData.sessionsRecovered} session(s) recovered from git\`);
|
|
85
|
+
}
|
|
86
|
+
if (syncData.modulesUpdated) {
|
|
87
|
+
parts.push(\`modules rescanned (\${syncData.moduleStats?.modules ?? 0} modules)\`);
|
|
88
|
+
}
|
|
89
|
+
if (parts.length > 0) {
|
|
90
|
+
syncSummary = 'Auto-sync: ' + parts.join(', ') + '.';
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Sync failed — continue with load
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step 2: Load context
|
|
77
97
|
const raw = run(projectRoot, ['context', 'load', '--format', 'json']);
|
|
78
98
|
const data = JSON.parse(raw);
|
|
79
99
|
|
|
@@ -82,8 +102,15 @@ try {
|
|
|
82
102
|
(data.recentDecisions?.length > 0);
|
|
83
103
|
|
|
84
104
|
if (hasData) {
|
|
85
|
-
|
|
105
|
+
const output = formatOutput(data);
|
|
106
|
+
if (syncSummary) {
|
|
107
|
+
console.log(syncSummary + '\\n');
|
|
108
|
+
}
|
|
109
|
+
console.log(output);
|
|
86
110
|
} else {
|
|
111
|
+
if (syncSummary) {
|
|
112
|
+
console.log(syncSummary);
|
|
113
|
+
}
|
|
87
114
|
console.log(\`OpenTology context active for \${data.projectId}. No session data yet.\`);
|
|
88
115
|
}
|
|
89
116
|
} catch {
|