opentology 0.3.5 → 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.
- package/dist/lib/deep-scanner.d.ts +7 -0
- package/dist/lib/deep-scanner.js +65 -0
- package/dist/mcp/server.js +122 -6
- package/dist/templates/claude-md-context.d.ts +2 -0
- package/dist/templates/claude-md-context.js +97 -116
- package/dist/templates/otx-ontology.d.ts +1 -1
- package/dist/templates/otx-ontology.js +5 -0
- package/dist/templates/stop-session-hook.d.ts +1 -0
- package/dist/templates/stop-session-hook.js +99 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/lib/deep-scanner.js
CHANGED
|
@@ -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,
|
package/dist/mcp/server.js
CHANGED
|
@@ -15,11 +15,12 @@ import { fromSchemaData, toMermaid, toDot } from '../lib/visualizer.js';
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
|
|
18
|
-
import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
|
|
18
|
+
import { generateContextSection, updateClaudeMd, updateGlobalClaudeMd } from '../templates/claude-md-context.js';
|
|
19
19
|
import { generateHookScript } from '../templates/session-start-hook.js';
|
|
20
20
|
import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
|
|
21
21
|
import { generateUserPromptHookScript } from '../templates/user-prompt-hook.js';
|
|
22
22
|
import { generatePostErrorHookScript } from '../templates/post-error-hook.js';
|
|
23
|
+
import { generateStopSessionHookScript } from '../templates/stop-session-hook.js';
|
|
23
24
|
import { generateSlashCommands } from '../templates/slash-commands.js';
|
|
24
25
|
import { runDoctor } from '../lib/doctor.js';
|
|
25
26
|
import { syncContext } from '../lib/context-sync.js';
|
|
@@ -379,24 +380,107 @@ async function handleContextScan(args) {
|
|
|
379
380
|
}
|
|
380
381
|
// Auto-push triples server-side
|
|
381
382
|
let pushStats = null;
|
|
383
|
+
let moduleStats = null;
|
|
382
384
|
try {
|
|
383
385
|
const config = loadConfig();
|
|
384
386
|
const contextUri = `${config.graphUri}/context`;
|
|
385
387
|
const adapter = await createReadyAdapter(config);
|
|
388
|
+
// Push symbol triples
|
|
386
389
|
pushStats = await pushSymbolTriples(adapter, contextUri, scanResult);
|
|
390
|
+
// Also push module dependency graph (fixes #64)
|
|
391
|
+
const snapshot = await scanCodebase(process.cwd());
|
|
392
|
+
if (snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0) {
|
|
393
|
+
const dg = snapshot.dependencyGraph;
|
|
394
|
+
// Clear stale Module/dependsOn triples
|
|
395
|
+
await adapter.sparqlUpdate(`DELETE { GRAPH <${contextUri}> { ?s ?p ?o } } WHERE { GRAPH <${contextUri}> { ?s ?p ?o . { ?s a <https://opentology.dev/vocab#Module> } UNION { ?s <https://opentology.dev/vocab#dependsOn> ?o } } }`);
|
|
396
|
+
const sparqlTriples = [];
|
|
397
|
+
for (const mod of dg.modules) {
|
|
398
|
+
sparqlTriples.push(`<urn:module:${mod}> a <https://opentology.dev/vocab#Module> ; <https://opentology.dev/vocab#title> "${mod}" .`);
|
|
399
|
+
}
|
|
400
|
+
for (const edge of dg.edges) {
|
|
401
|
+
sparqlTriples.push(`<urn:module:${edge.from}> <https://opentology.dev/vocab#dependsOn> <urn:module:${edge.to}> .`);
|
|
402
|
+
}
|
|
403
|
+
await adapter.sparqlUpdate(`INSERT DATA { GRAPH <${contextUri}> {\n${sparqlTriples.join('\n')}\n} }`);
|
|
404
|
+
moduleStats = { modules: dg.modules.length, edges: dg.edges.length };
|
|
405
|
+
}
|
|
387
406
|
await persistGraph(adapter, config, contextUri);
|
|
388
407
|
}
|
|
389
408
|
catch {
|
|
390
409
|
// Non-fatal: push is best-effort
|
|
391
410
|
}
|
|
392
|
-
|
|
393
|
-
|
|
411
|
+
const hints = [];
|
|
412
|
+
if (pushStats)
|
|
413
|
+
hints.push(`Symbol triples: ${pushStats.triplesInserted} in ${pushStats.batchCount} batches`);
|
|
414
|
+
if (moduleStats)
|
|
415
|
+
hints.push(`Module triples: ${moduleStats.modules} modules, ${moduleStats.edges} edges`);
|
|
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,
|
|
394
430
|
pushStats,
|
|
431
|
+
moduleStats,
|
|
395
432
|
_experimental: true,
|
|
396
|
-
_hint:
|
|
397
|
-
?
|
|
433
|
+
_hint: hints.length
|
|
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 }`
|
|
398
435
|
: 'Deep scan completed but triple push failed. Use push manually with the generated triples.',
|
|
399
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;
|
|
400
484
|
}
|
|
401
485
|
// Default: module-level scan — now auto-pushes module triples like context_init
|
|
402
486
|
const maxBytes = args.maxSnapshotBytes ?? 15360;
|
|
@@ -467,7 +551,7 @@ async function handleContextInit(args) {
|
|
|
467
551
|
if (!config.files[contextUri].includes(relPath)) {
|
|
468
552
|
config.files[contextUri].push(relPath);
|
|
469
553
|
}
|
|
470
|
-
actions.push('Bootstrapped otx ontology (
|
|
554
|
+
actions.push('Bootstrapped otx ontology (13 classes, 24 properties)');
|
|
471
555
|
}
|
|
472
556
|
// Generate hook script
|
|
473
557
|
const hookDir = join(process.cwd(), '.opentology', 'hooks');
|
|
@@ -496,6 +580,12 @@ async function handleContextInit(args) {
|
|
|
496
580
|
writeFileSync(postErrorHookPath, generatePostErrorHookScript(), 'utf-8');
|
|
497
581
|
actions.push('Generated hook: .opentology/hooks/post-error.mjs');
|
|
498
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
|
+
}
|
|
499
589
|
// Update CLAUDE.md
|
|
500
590
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
501
591
|
const section = generateContextSection(config.projectId, config.graphUri);
|
|
@@ -507,6 +597,16 @@ async function handleContextInit(args) {
|
|
|
507
597
|
updateClaudeMd(claudeMdPath, section);
|
|
508
598
|
actions.push('Updated CLAUDE.md context section');
|
|
509
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
|
+
}
|
|
510
610
|
// Generate slash commands
|
|
511
611
|
const commandsDir = join(process.cwd(), '.claude', 'commands');
|
|
512
612
|
const slashCommands = generateSlashCommands();
|
|
@@ -598,6 +698,22 @@ async function handleContextInit(args) {
|
|
|
598
698
|
});
|
|
599
699
|
hooksChanged = true;
|
|
600
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
|
+
}
|
|
601
717
|
if (hooksChanged) {
|
|
602
718
|
settings.hooks = hooks;
|
|
603
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
<principles>
|
|
11
|
+
- **Graph first** — query the knowledge graph before reading source files or making assumptions.
|
|
12
|
+
- **Always record** — push Session logs at session end; record Knowledge, Decisions, and Issues as they arise.
|
|
13
|
+
- **Auto-ingest** — when the user shares a URL or external source, run the ingest protocol automatically.
|
|
14
|
+
</principles>
|
|
14
15
|
|
|
15
16
|
### Graph Structure
|
|
16
17
|
|
|
17
18
|
| Graph | URI | Purpose |
|
|
18
19
|
|-------|-----|---------|
|
|
19
|
-
| context | \`${contextUri}\` | Decisions, issues, knowledge |
|
|
20
|
+
| context | \`${contextUri}\` | Decisions, issues, knowledge, modules, symbols |
|
|
20
21
|
| sessions | \`${sessionsUri}\` | Session work logs |
|
|
21
22
|
|
|
22
23
|
### Ontology (\`otx:\` prefix)
|
|
@@ -29,27 +30,12 @@ symbol-level call graphs, and session history. Prefer graph knowledge over re-de
|
|
|
29
30
|
| \`otx:Knowledge\` | Reusable knowledge |
|
|
30
31
|
| \`otx:Session\` | Session logs |
|
|
31
32
|
| \`otx:Pattern\` | Recurring patterns/conventions |
|
|
33
|
+
| \`otx:Source\` | External knowledge source (article, paper, code, etc.) |
|
|
32
34
|
| \`otx:Module\` | Source file module |
|
|
33
|
-
| \`otx:Class\` |
|
|
34
|
-
| \`otx:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| \`otx:MethodCall\` | Call relationship between symbols (symbol scan) |
|
|
38
|
-
|
|
39
|
-
| Property | Range | Description |
|
|
40
|
-
|----------|-------|-------------|
|
|
41
|
-
| \`otx:title\` | string | Title |
|
|
42
|
-
| \`otx:date\` | date | Date (YYYY-MM-DD) |
|
|
43
|
-
| \`otx:body\` | string | Body content |
|
|
44
|
-
| \`otx:status\` | string | Status (open/resolved/active) |
|
|
45
|
-
| \`otx:reason\` | string | Decision rationale |
|
|
46
|
-
| \`otx:nextTodo\` | string | Next action item |
|
|
47
|
-
| \`otx:relatedTo\` | resource | Related entity |
|
|
48
|
-
| \`otx:dependsOn\` | Module | Module import dependency |
|
|
49
|
-
| \`otx:definedIn\` | Module | Which module a symbol belongs to |
|
|
50
|
-
| \`otx:callerSymbol\` | string | Caller in a MethodCall |
|
|
51
|
-
| \`otx:calleeSymbol\` | string | Callee in a MethodCall |
|
|
52
|
-
| \`otx:calls\` | resource | Call relationship |
|
|
35
|
+
| \`otx:Class\` / \`otx:Interface\` / \`otx:Function\` / \`otx:Method\` | Symbol-level entities (from deep scan) |
|
|
36
|
+
| \`otx:MethodCall\` | Call relationship between symbols |
|
|
37
|
+
|
|
38
|
+
Key properties: \`otx:title\`, \`otx:date\`, \`otx:body\`, \`otx:status\`, \`otx:reason\`, \`otx:nextTodo\`, \`otx:relatedTo\`, \`otx:dependsOn\`, \`otx:definedIn\`, \`otx:callerSymbol\`, \`otx:calleeSymbol\`, \`otx:sourceUrl\`, \`otx:sourceType\`. Full schema: use the \`schema\` tool.
|
|
53
39
|
|
|
54
40
|
### When to Record
|
|
55
41
|
|
|
@@ -58,127 +44,66 @@ symbol-level call graphs, and session history. Prefer graph knowledge over re-de
|
|
|
58
44
|
| Architecture/tech decision | \`otx:Decision\` | context |
|
|
59
45
|
| Bug/issue resolved | \`otx:Issue\` | context |
|
|
60
46
|
| Reusable knowledge | \`otx:Knowledge\` | context |
|
|
47
|
+
| Source ingested | \`otx:Source\` | context |
|
|
61
48
|
| Session end | \`otx:Session\` | sessions |
|
|
62
49
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
\`\`\`sparql
|
|
66
|
-
# Recent sessions
|
|
67
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
68
|
-
SELECT ?title ?date ?nextTodo WHERE {
|
|
69
|
-
GRAPH <${sessionsUri}> {
|
|
70
|
-
?s a otx:Session ; otx:title ?title ; otx:date ?date .
|
|
71
|
-
OPTIONAL { ?s otx:nextTodo ?nextTodo }
|
|
72
|
-
}
|
|
73
|
-
} ORDER BY DESC(?date) LIMIT 5
|
|
74
|
-
|
|
75
|
-
# Open issues
|
|
76
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
77
|
-
SELECT ?title ?date WHERE {
|
|
78
|
-
GRAPH <${contextUri}> {
|
|
79
|
-
?s a otx:Issue ; otx:title ?title ; otx:date ?date ; otx:status "open" .
|
|
80
|
-
}
|
|
81
|
-
} ORDER BY DESC(?date)
|
|
82
|
-
\`\`\`
|
|
83
|
-
|
|
84
|
-
### How to Use OpenTology Tools
|
|
50
|
+
### Tools & Workflows
|
|
85
51
|
|
|
86
|
-
|
|
52
|
+
#### Before Working
|
|
87
53
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- **Decisions**: past architectural choices that may inform the current analysis
|
|
92
|
-
- **Knowledge**: reusable patterns or lessons already recorded
|
|
93
|
-
- **Issues**: known problems related to the area under investigation
|
|
94
|
-
- **Sessions**: recent work in the same area
|
|
54
|
+
1. **Query graph** — check for existing decisions, knowledge, issues, and sessions related to your task.
|
|
55
|
+
2. **Check impact** — before editing a file, run \`context_impact\` to understand the blast radius (dependents, dependencies, related entities).
|
|
56
|
+
3. **Search** — use \`query\` with SPARQL to find anything: \`?s a otx:Decision\`, \`?s a otx:Knowledge\`, \`?s a otx:Module\`, \`?s a otx:MethodCall\`, etc.
|
|
95
57
|
|
|
96
58
|
\`\`\`sparql
|
|
59
|
+
# Context search (replace "keyword" with your search term)
|
|
97
60
|
PREFIX otx: <https://opentology.dev/vocab#>
|
|
98
61
|
SELECT ?type ?title ?body WHERE {
|
|
99
62
|
GRAPH <${contextUri}> {
|
|
100
63
|
{ ?s a otx:Decision ; otx:title ?title ; otx:body ?body . BIND("decision" AS ?type) }
|
|
101
|
-
UNION
|
|
102
|
-
{ ?s a otx:
|
|
103
|
-
UNION
|
|
104
|
-
{ ?s a otx:Issue ; otx:title ?title ; otx:body ?body . BIND("issue" AS ?type) }
|
|
64
|
+
UNION { ?s a otx:Knowledge ; otx:title ?title ; otx:body ?body . BIND("knowledge" AS ?type) }
|
|
65
|
+
UNION { ?s a otx:Issue ; otx:title ?title ; otx:body ?body . BIND("issue" AS ?type) }
|
|
105
66
|
}
|
|
106
67
|
FILTER(CONTAINS(LCASE(?title), "keyword") || CONTAINS(LCASE(?body), "keyword"))
|
|
107
68
|
} LIMIT 10
|
|
108
69
|
\`\`\`
|
|
109
70
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#### Pre-Edit Impact Check
|
|
113
|
-
|
|
114
|
-
Before modifying a file, run \`context_impact\` with the target file path to understand the blast radius:
|
|
115
|
-
- **dependents** — modules that import or depend on this file
|
|
116
|
-
- **dependencies** — modules this file imports
|
|
117
|
-
- **related** — decisions, issues, knowledge linked to this file
|
|
118
|
-
- **impact level** — high / medium / low
|
|
119
|
-
|
|
120
|
-
If impact is **high**, inform the user of affected modules and get confirmation before proceeding.
|
|
121
|
-
|
|
122
|
-
#### Searching the Knowledge Graph
|
|
123
|
-
|
|
124
|
-
Use \`query\` with SPARQL to find anything in the project graph. **Always query the graph before reading source files** when investigating code structure, dependencies, or call relationships:
|
|
71
|
+
#### Ingesting External Sources
|
|
125
72
|
|
|
126
|
-
|
|
127
|
-
- **Issues**: \`?s a otx:Issue ; otx:status "open"\` — known bugs and their status
|
|
128
|
-
- **Knowledge**: \`?s a otx:Knowledge\` — reusable patterns and lessons learned
|
|
129
|
-
- **Sessions**: query the sessions graph for past work logs and next TODOs
|
|
130
|
-
- **Modules**: \`?s a otx:Module\` — all scanned source modules and their dependencies (\`otx:dependsOn\`)
|
|
131
|
-
- **Symbols**: \`?s a otx:Class\`, \`otx:Interface\`, \`otx:Function\`, \`otx:Method\` — code-level entities (available after symbol-depth scan)
|
|
132
|
-
- **Call graph**: \`?s a otx:MethodCall\` — who calls whom (available after symbol scan with \`includeMethodCalls=true\`)
|
|
133
|
-
|
|
134
|
-
\`\`\`sparql
|
|
135
|
-
# Functions in a specific module
|
|
136
|
-
PREFIX otx: <https://opentology.dev/vocab#>
|
|
137
|
-
SELECT ?name WHERE {
|
|
138
|
-
GRAPH <${contextUri}> {
|
|
139
|
-
?f a otx:Function ; otx:title ?name ; otx:definedIn <urn:module:src/mcp/server> .
|
|
140
|
-
}
|
|
141
|
-
}
|
|
73
|
+
When the user shares a URL, file path, or external content, follow this protocol:
|
|
142
74
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
75
|
+
1. **Duplicate check** — Query for existing sources with the same URL or title.
|
|
76
|
+
2. **Register** — Push an \`otx:Source\` with status "pending" using the \`push\` tool.
|
|
77
|
+
3. **Read** — Fetch URL content, read file, or use pasted text directly.
|
|
78
|
+
4. **Extract** — Summarize key concepts. Create \`otx:Knowledge\` triples linked via \`otx:relatedTo\`.
|
|
79
|
+
5. **Cross-reference** — Query existing graph for related decisions/issues/knowledge. Link via \`otx:relatedTo\`.
|
|
80
|
+
6. **Contradictions** — If new knowledge contradicts existing entries, create \`otx:Issue\` with status "open".
|
|
81
|
+
7. **Finalize** — Update source status from "pending" to "ingested". Run audit query.
|
|
151
82
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
?dependent otx:dependsOn+ <urn:module:src/lib/store-adapter> .
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
\`\`\`
|
|
83
|
+
Duplicate check: \`SELECT ?s ?title WHERE { GRAPH <${contextUri}> { ?s a otx:Source ; otx:sourceUrl ?url . FILTER(?url = "URL") } }\`
|
|
84
|
+
Audit: \`SELECT ?s ?title ?status (COUNT(?k) AS ?knowledgeCount) WHERE { GRAPH <${contextUri}> { ?s a otx:Source ; otx:title ?title ; otx:status ?status . OPTIONAL { ?k otx:relatedTo ?s } } } GROUP BY ?s ?title ?status\`
|
|
85
|
+
Registration: \`<urn:source:{slug}> a otx:Source ; otx:title "..." ; otx:sourceUrl "..." ; otx:sourceType "article" ; otx:date "YYYY-MM-DD"^^xsd:date ; otx:status "pending" .\`
|
|
86
|
+
Types: article | paper | code | transcript | documentation | video | podcast | book | other. Status: pending → ingested → stale. Recovery: \`rollback\`.
|
|
160
87
|
|
|
161
|
-
####
|
|
88
|
+
#### After Working
|
|
162
89
|
|
|
163
|
-
|
|
164
|
-
- \`depth="module"\` — fast, updates file-level imports
|
|
165
|
-
- \`depth="symbol"\` with \`includeMethodCalls=true\` — thorough, updates class/function/call graph
|
|
90
|
+
- Run \`context_scan\` after significant code changes (\`depth="module"\` for fast, \`depth="symbol"\` for thorough).
|
|
166
91
|
|
|
167
|
-
####
|
|
92
|
+
#### Tool Reference
|
|
168
93
|
|
|
169
94
|
| Tool | When to Use |
|
|
170
95
|
|------|-------------|
|
|
171
96
|
| \`context_load\` | Session start — loads recent sessions, open issues, recent decisions |
|
|
172
|
-
| \`context_scan\` | After
|
|
97
|
+
| \`context_scan\` | After code changes — rescans module/symbol dependencies |
|
|
173
98
|
| \`context_impact\` | Before editing — checks blast radius of a file change |
|
|
174
|
-
| \`schema\` | Explore ontology classes and properties
|
|
99
|
+
| \`schema\` | Explore ontology classes and properties |
|
|
175
100
|
| \`query\` | Run any SPARQL query against the project graph |
|
|
176
|
-
| \`push\` | Record decisions, issues, knowledge, or session summaries |
|
|
101
|
+
| \`push\` | Record decisions, issues, knowledge, sources, or session summaries |
|
|
177
102
|
| \`doctor\` | Diagnose project health (config, store, hooks, CLAUDE.md) |
|
|
178
103
|
|
|
179
|
-
### Session End
|
|
104
|
+
### Session End
|
|
180
105
|
|
|
181
|
-
|
|
106
|
+
Push a summary at the end of each meaningful session:
|
|
182
107
|
|
|
183
108
|
\`\`\`turtle
|
|
184
109
|
@prefix otx: <https://opentology.dev/vocab#> .
|
|
@@ -192,6 +117,62 @@ At the end of each session, push a summary:
|
|
|
192
117
|
\`\`\`
|
|
193
118
|
${MARKER_END}`;
|
|
194
119
|
}
|
|
120
|
+
const GLOBAL_MARKER_BEGIN = '<!-- OPENTOLOGY:GLOBAL:BEGIN -->';
|
|
121
|
+
const GLOBAL_MARKER_END = '<!-- OPENTOLOGY:GLOBAL:END -->';
|
|
122
|
+
export function generateGlobalSection() {
|
|
123
|
+
return `${GLOBAL_MARKER_BEGIN}
|
|
124
|
+
# OpenTology — RDF 기반 프로젝트 컨텍스트 관리
|
|
125
|
+
|
|
126
|
+
Claude Code는 글로벌 MCP \`opentology\`를 통해 프로젝트 지식을 RDF 그래프로 관리한다.
|
|
127
|
+
온톨로지, 도구 사용법, SPARQL 예시 등 상세 정보는 프로젝트별 CLAUDE.md에 자동 생성된다.
|
|
128
|
+
여기서는 **모든 프로젝트에 공통 적용되는 행동 규칙만** 정의한다.
|
|
129
|
+
|
|
130
|
+
## 핵심 원칙
|
|
131
|
+
|
|
132
|
+
1. **그래프 먼저** — 코드를 읽거나 가정하기 전에 \`query\`로 그래프를 먼저 조회한다.
|
|
133
|
+
2. **항상 기록** — 세션 종료 시 \`otx:Session\`, 의미 있는 작업은 \`Knowledge\`/\`Decision\`/\`Issue\`로 기록.
|
|
134
|
+
3. **자동 수집** — 사용자가 URL이나 외부 소스를 공유하면 ingest 프로토콜을 자동 실행한다.
|
|
135
|
+
4. **영향도 확인** — 파일 수정 전 \`context_impact\`로 blast radius를 확인한다.
|
|
136
|
+
|
|
137
|
+
## URI 규칙
|
|
138
|
+
|
|
139
|
+
| 대상 | 패턴 | 예시 |
|
|
140
|
+
|------|------|------|
|
|
141
|
+
| 프로젝트 | \`urn:project:{name}\` | \`urn:project:opentology\` |
|
|
142
|
+
| 세션 | \`urn:session:{date}\` | \`urn:session:2026-04-05\` |
|
|
143
|
+
| 의사결정 | \`urn:decision:{date}-{slug}\` | \`urn:decision:2026-04-05-ingest-feature\` |
|
|
144
|
+
| 이슈 | \`urn:issue:{id}\` | \`urn:issue:1\` |
|
|
145
|
+
| 지식 | \`urn:knowledge:{slug}\` | \`urn:knowledge:wasm-oxigraph\` |
|
|
146
|
+
| 패턴 | \`urn:pattern:{slug}\` | \`urn:pattern:singleton-adapter\` |
|
|
147
|
+
| 소스 | \`urn:source:{slug}\` | \`urn:source:karpathy-llm-wiki\` |
|
|
148
|
+
|
|
149
|
+
## 기록 기준
|
|
150
|
+
|
|
151
|
+
- **기록함**: 아키텍처 변경, 새 기능 구현, 버그 해결, 재사용 가능한 지식, 외부 소스 수집
|
|
152
|
+
- **기록 안 함**: 오타 수정, 단순 질문 응답, 단순 설정 변경
|
|
153
|
+
- 기록은 **간결하게**. 민감 정보(API 키 등)는 절대 기록하지 않는다.
|
|
154
|
+
- \`opentology\` MCP가 연결되지 않은 프로젝트에서는 기록을 건너뛴다.
|
|
155
|
+
${GLOBAL_MARKER_END}`;
|
|
156
|
+
}
|
|
157
|
+
export function updateGlobalClaudeMd(filePath) {
|
|
158
|
+
const section = generateGlobalSection();
|
|
159
|
+
if (!existsSync(filePath)) {
|
|
160
|
+
writeFileSync(filePath, section + '\n', 'utf-8');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
164
|
+
const beginIdx = content.indexOf(GLOBAL_MARKER_BEGIN);
|
|
165
|
+
const endIdx = content.indexOf(GLOBAL_MARKER_END);
|
|
166
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
167
|
+
// No markers — prepend before existing content
|
|
168
|
+
writeFileSync(filePath, section + '\n\n' + content, 'utf-8');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Replace between markers
|
|
172
|
+
const before = content.substring(0, beginIdx);
|
|
173
|
+
const after = content.substring(endIdx + GLOBAL_MARKER_END.length);
|
|
174
|
+
writeFileSync(filePath, before + section + after, 'utf-8');
|
|
175
|
+
}
|
|
195
176
|
export function updateClaudeMd(filePath, section) {
|
|
196
177
|
if (!existsSync(filePath)) {
|
|
197
178
|
// Case 1: No file — create with just the section
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OTX_BOOTSTRAP_TURTLE = "@prefix otx: <https://opentology.dev/vocab#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\notx:Project a owl:Class .\notx:Decision a owl:Class .\notx:Issue a owl:Class .\notx:Knowledge a owl:Class .\notx:Session a owl:Class .\notx:Pattern a owl:Class .\notx:Module a owl:Class .\n\notx:title a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:date a owl:DatatypeProperty ; rdfs:range xsd:date .\notx:body a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:status a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:reason a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:cause a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:solution a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:nextTodo a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:relatedTo a owl:ObjectProperty .\notx:project a owl:ObjectProperty .\notx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Module .\notx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:Class a owl:Class .\notx:Interface a owl:Class .\notx:Function a owl:Class .\notx:Method a owl:Class .\notx:MethodCall a owl:Class .\n\notx:definedIn a owl:ObjectProperty ; rdfs:range otx:Module .\notx:extends a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Class .\notx:implements a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Interface .\notx:hasMethod a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Method .\notx:calls a owl:ObjectProperty .\notx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .\n";
|
|
1
|
+
export declare const OTX_BOOTSTRAP_TURTLE = "@prefix otx: <https://opentology.dev/vocab#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\notx:Project a owl:Class .\notx:Decision a owl:Class .\notx:Issue a owl:Class .\notx:Knowledge a owl:Class .\notx:Session a owl:Class .\notx:Pattern a owl:Class .\notx:Module a owl:Class .\n\notx:title a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:date a owl:DatatypeProperty ; rdfs:range xsd:date .\notx:body a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:status a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:reason a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:cause a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:solution a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:nextTodo a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:relatedTo a owl:ObjectProperty .\notx:project a owl:ObjectProperty .\notx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Module .\notx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:Source a owl:Class .\n\notx:Class a owl:Class .\notx:Interface a owl:Class .\notx:Function a owl:Class .\notx:Method a owl:Class .\notx:MethodCall a owl:Class .\n\notx:definedIn a owl:ObjectProperty ; rdfs:range otx:Module .\notx:extends a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Class .\notx:implements a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Interface .\notx:hasMethod a owl:ObjectProperty ; rdfs:domain otx:Class ; rdfs:range otx:Method .\notx:calls a owl:ObjectProperty .\notx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .\notx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .\n\notx:sourceUrl a owl:DatatypeProperty ; rdfs:range xsd:string .\notx:sourceType a owl:DatatypeProperty ; rdfs:range xsd:string .\n";
|
|
@@ -26,6 +26,8 @@ otx:dependsOn a owl:ObjectProperty ; rdfs:domain otx:Module ; rdfs:range otx:Mod
|
|
|
26
26
|
otx:stack a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
27
27
|
otx:alternative a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
28
28
|
|
|
29
|
+
otx:Source a owl:Class .
|
|
30
|
+
|
|
29
31
|
otx:Class a owl:Class .
|
|
30
32
|
otx:Interface a owl:Class .
|
|
31
33
|
otx:Function a owl:Class .
|
|
@@ -41,4 +43,7 @@ otx:callerSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .
|
|
|
41
43
|
otx:calleeSymbol a owl:ObjectProperty ; rdfs:domain otx:MethodCall .
|
|
42
44
|
otx:returns a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
43
45
|
otx:paramType a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
46
|
+
|
|
47
|
+
otx:sourceUrl a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
48
|
+
otx:sourceType a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
44
49
|
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateStopSessionHookScript(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export function generateStopSessionHookScript() {
|
|
2
|
+
return `#!/usr/bin/env node
|
|
3
|
+
// Generated by: opentology context init
|
|
4
|
+
// Stop hook — reminds LLM to push session log and knowledge triples
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
7
|
+
import { join, resolve, dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = parseInt(process.env.OPENTOLOGY_HOOK_TIMEOUT || '5000', 10);
|
|
10
|
+
|
|
11
|
+
function findProjectRoot(startDir) {
|
|
12
|
+
let dir = resolve(startDir);
|
|
13
|
+
while (dir !== dirname(dir)) {
|
|
14
|
+
if (existsSync(join(dir, '.opentology.json'))) return dir;
|
|
15
|
+
dir = dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findBin(projectRoot) {
|
|
21
|
+
const local = join(projectRoot, 'node_modules', '.bin', 'opentology');
|
|
22
|
+
if (existsSync(local)) return { bin: local, args: [] };
|
|
23
|
+
return { bin: 'npx', args: ['opentology'] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(projectRoot, cmdArgs) {
|
|
27
|
+
const { bin, args } = findBin(projectRoot);
|
|
28
|
+
return execFileSync(bin, [...args, ...cmdArgs], {
|
|
29
|
+
cwd: projectRoot,
|
|
30
|
+
timeout: TIMEOUT,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
38
|
+
if (!projectRoot) process.exit(0);
|
|
39
|
+
|
|
40
|
+
// Check recent commits (last 2 hours)
|
|
41
|
+
let log = '';
|
|
42
|
+
try {
|
|
43
|
+
log = execSync('git log --since="2 hours ago" --oneline', {
|
|
44
|
+
cwd: projectRoot, encoding: 'utf-8', timeout: 3000,
|
|
45
|
+
}).trim();
|
|
46
|
+
} catch { process.exit(0); }
|
|
47
|
+
|
|
48
|
+
if (!log) process.exit(0);
|
|
49
|
+
|
|
50
|
+
const commitCount = log.split('\\n').length;
|
|
51
|
+
// Check if source code was changed (not just docs/config)
|
|
52
|
+
let srcChanged = false;
|
|
53
|
+
try {
|
|
54
|
+
const diff = execSync('git diff HEAD~' + Math.min(commitCount, 10) + ' --name-only 2>/dev/null', {
|
|
55
|
+
cwd: projectRoot, encoding: 'utf-8', timeout: 3000,
|
|
56
|
+
}).trim();
|
|
57
|
+
srcChanged = diff.split('\\n').some(f => f.startsWith('src/') || f.startsWith('lib/') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go'));
|
|
58
|
+
} catch { srcChanged = commitCount >= 2; }
|
|
59
|
+
|
|
60
|
+
// Importance: high (3+ commits or src changes), medium (1-2 commits), low (0)
|
|
61
|
+
const importance = (commitCount >= 3 || srcChanged) ? 'high' : commitCount >= 1 ? 'medium' : 'low';
|
|
62
|
+
if (importance === 'low') process.exit(0);
|
|
63
|
+
|
|
64
|
+
// Check if session log already exists for today
|
|
65
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
66
|
+
let sessionExists = false;
|
|
67
|
+
try {
|
|
68
|
+
const raw = run(projectRoot, [
|
|
69
|
+
'query', '--format', 'json',
|
|
70
|
+
'PREFIX otx: <https://opentology.dev/vocab#> ASK WHERE { ?s a otx:Session ; otx:date "' + today + '"^^<http://www.w3.org/2001/XMLSchema#date> }'
|
|
71
|
+
]);
|
|
72
|
+
sessionExists = raw.includes('true');
|
|
73
|
+
} catch { /* query failed, assume not exists */ }
|
|
74
|
+
|
|
75
|
+
const reminders = [];
|
|
76
|
+
|
|
77
|
+
if (!sessionExists) {
|
|
78
|
+
reminders.push('Push an otx:Session summary to the sessions graph (title, date, body, nextTodo).');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Always remind about Knowledge/Decision/Issue recording
|
|
82
|
+
reminders.push('Review if any of these should be recorded:');
|
|
83
|
+
reminders.push('- Architecture/tech decisions → otx:Decision');
|
|
84
|
+
reminders.push('- Reusable patterns or lessons → otx:Knowledge');
|
|
85
|
+
reminders.push('- Resolved bugs/issues → otx:Issue');
|
|
86
|
+
reminders.push('- External sources ingested → otx:Source');
|
|
87
|
+
|
|
88
|
+
if (reminders.length > 0) {
|
|
89
|
+
const header = sessionExists
|
|
90
|
+
? 'Session log exists. Check if additional knowledge should be recorded:'
|
|
91
|
+
: 'Session log missing (' + today + ', ' + commitCount + ' commits, importance: ' + importance + '). Before ending:';
|
|
92
|
+
console.log(header);
|
|
93
|
+
for (const r of reminders) console.log(' ' + r);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
}
|