opentology 0.3.9 → 0.4.1
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 +12 -10
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +29 -0
- package/dist/index.js +0 -0
- package/dist/lib/ask-engine.d.ts +31 -0
- package/dist/lib/ask-engine.js +111 -0
- package/dist/lib/context-sync.js +1 -3
- package/dist/lib/deep-scan-triples.js +2 -3
- package/dist/lib/git-utils.d.ts +6 -0
- package/dist/lib/git-utils.js +34 -0
- package/dist/lib/persist.d.ts +10 -0
- package/dist/lib/persist.js +29 -0
- package/dist/lib/scan-state.d.ts +8 -0
- package/dist/lib/scan-state.js +32 -0
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/sparql-utils.d.ts +5 -0
- package/dist/lib/sparql-utils.js +16 -6
- package/dist/mcp/handlers/context.d.ts +33 -0
- package/dist/mcp/handlers/context.js +713 -0
- package/dist/mcp/handlers/rdf.d.ts +18 -0
- package/dist/mcp/handlers/rdf.js +182 -0
- package/dist/mcp/handlers/schema.d.ts +25 -0
- package/dist/mcp/handlers/schema.js +55 -0
- package/dist/mcp/handlers/system.d.ts +22 -0
- package/dist/mcp/handlers/system.js +135 -0
- package/dist/mcp/server.d.ts +1 -37
- package/dist/mcp/server.js +78 -1085
- package/dist/templates/builtin-predicates.d.ts +1 -0
- package/dist/templates/builtin-predicates.js +33 -0
- package/dist/templates/claude-md-context.js +44 -68
- package/dist/templates/otx-ontology.d.ts +1 -1
- package/dist/templates/otx-ontology.js +41 -0
- package/dist/templates/post-edit-hook.d.ts +1 -0
- package/dist/templates/post-edit-hook.js +78 -0
- package/dist/templates/slash-commands.js +237 -11
- package/dist/templates/stop-session-hook.js +15 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,11 +117,11 @@ Everything lives in RDF named graphs with the `otx:` ontology:
|
|
|
117
117
|
```
|
|
118
118
|
context graph sessions graph
|
|
119
119
|
├── otx:Module (source files) └── otx:Session (work logs)
|
|
120
|
-
│ └── otx:dependsOn ├── otx:
|
|
121
|
-
├── otx:Class / otx:Interface
|
|
122
|
-
│ └── otx:Method / otx:Function
|
|
123
|
-
├── otx:Decision (architecture choices)
|
|
124
|
-
├── otx:Issue (bugs, status tracking)
|
|
120
|
+
│ └── otx:dependsOn ├── otx:hasActivity → otx:Activity
|
|
121
|
+
├── otx:Class / otx:Interface ├── otx:Todo (open → done)
|
|
122
|
+
│ └── otx:Method / otx:Function ├── otx:Insight (patterns learned)
|
|
123
|
+
├── otx:Decision (architecture choices) ├── otx:Domain (work area tags)
|
|
124
|
+
├── otx:Issue (bugs, status tracking) └── otx:followsUp (session chaining)
|
|
125
125
|
└── otx:Knowledge (reusable patterns)
|
|
126
126
|
```
|
|
127
127
|
|
|
@@ -319,6 +319,7 @@ opentology doctor
|
|
|
319
319
|
- [x] Auto-sync from git history
|
|
320
320
|
- [x] AI behavioral instructions via CLAUDE.md injection
|
|
321
321
|
- [x] Interactive graph visualization web UI
|
|
322
|
+
- [x] Structured session schema (Activity, Todo, Insight, Domain) with proactive save
|
|
322
323
|
- [ ] OWL reasoning (owl:sameAs, owl:inverseOf)
|
|
323
324
|
- [ ] Remote ontology import
|
|
324
325
|
- [ ] Ontology snapshot versioning
|
|
@@ -445,11 +446,11 @@ SELECT ?from ?to WHERE {
|
|
|
445
446
|
```
|
|
446
447
|
context graph sessions graph
|
|
447
448
|
├── otx:Module (소스 파일) └── otx:Session (작업 로그)
|
|
448
|
-
│ └── otx:dependsOn ├── otx:
|
|
449
|
-
├── otx:Class / otx:Interface
|
|
450
|
-
│ └── otx:Method / otx:Function
|
|
451
|
-
├── otx:Decision (아키텍처 의사결정)
|
|
452
|
-
├── otx:Issue (버그, 상태 추적)
|
|
449
|
+
│ └── otx:dependsOn ├── otx:hasActivity → otx:Activity
|
|
450
|
+
├── otx:Class / otx:Interface ├── otx:Todo (open → done)
|
|
451
|
+
│ └── otx:Method / otx:Function ├── otx:Insight (학습된 패턴)
|
|
452
|
+
├── otx:Decision (아키텍처 의사결정) ├── otx:Domain (작업 영역 태그)
|
|
453
|
+
├── otx:Issue (버그, 상태 추적) └── otx:followsUp (세션 체이닝)
|
|
453
454
|
└── otx:Knowledge (재사용 가능한 패턴)
|
|
454
455
|
```
|
|
455
456
|
|
|
@@ -647,6 +648,7 @@ opentology doctor
|
|
|
647
648
|
- [x] git 이력에서 자동 동기화
|
|
648
649
|
- [x] CLAUDE.md 주입을 통한 AI 행동 지침
|
|
649
650
|
- [x] 인터랙티브 그래프 시각화 웹 UI
|
|
651
|
+
- [x] 구조화된 세션 스키마 (Activity, Todo, Insight, Domain) + 프로액티브 저장
|
|
650
652
|
- [ ] OWL 추론 (owl:sameAs, owl:inverseOf)
|
|
651
653
|
- [ ] 원격 온톨로지 임포트
|
|
652
654
|
- [ ] 온톨로지 스냅샷 버전 관리
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { handleContextScan } from '../mcp/server.js';
|
|
2
|
+
export function registerContext(program) {
|
|
3
|
+
const ctx = program.command('context').description('Context management commands');
|
|
4
|
+
ctx
|
|
5
|
+
.command('scan')
|
|
6
|
+
.description('Scan the codebase and update the knowledge graph')
|
|
7
|
+
.option('--depth <depth>', 'Scan depth: "module" or "symbol"', 'symbol')
|
|
8
|
+
.option('--incremental', 'Only scan files changed since the last scan', false)
|
|
9
|
+
.option('--max-files <n>', 'Maximum files to scan (symbol depth)', parseInt)
|
|
10
|
+
.option('--max-symbols <n>', 'Maximum symbols to extract (symbol depth)', parseInt)
|
|
11
|
+
.option('--timeout <ms>', 'Timeout in milliseconds (symbol depth)', parseInt)
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await handleContextScan({
|
|
15
|
+
depth: opts.depth,
|
|
16
|
+
incremental: opts.incremental,
|
|
17
|
+
maxFiles: opts.maxFiles,
|
|
18
|
+
maxSymbols: opts.maxSymbols,
|
|
19
|
+
timeoutMs: opts.timeout,
|
|
20
|
+
});
|
|
21
|
+
console.log(JSON.stringify(result, null, 2));
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25
|
+
console.error(`context scan failed: ${msg}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StoreAdapter } from './store-adapter.js';
|
|
2
|
+
export interface AskInput {
|
|
3
|
+
predicate: string;
|
|
4
|
+
context: Record<string, string>;
|
|
5
|
+
record?: boolean;
|
|
6
|
+
graph?: string;
|
|
7
|
+
graphUri?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AskOutput {
|
|
10
|
+
answer: boolean | null;
|
|
11
|
+
reason?: string;
|
|
12
|
+
missing?: string[];
|
|
13
|
+
evaluationUri?: string;
|
|
14
|
+
bindings?: Array<Record<string, string>>;
|
|
15
|
+
}
|
|
16
|
+
interface ResolvedPredicate {
|
|
17
|
+
uri: string;
|
|
18
|
+
title: string;
|
|
19
|
+
sparqlTemplate: string;
|
|
20
|
+
requiredParams: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function resolvePredicate(adapter: StoreAdapter, predicateName: string, contextGraphUri: string): Promise<ResolvedPredicate | null>;
|
|
23
|
+
export declare function validateContext(predicate: ResolvedPredicate, context: Record<string, string>): string[];
|
|
24
|
+
export declare function bindTemplate(template: string, context: Record<string, string>, graphUri: string): string;
|
|
25
|
+
export declare function evaluate(adapter: StoreAdapter, sparql: string): Promise<{
|
|
26
|
+
answer: boolean;
|
|
27
|
+
bindings?: Array<Record<string, string>>;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function recordEvaluation(adapter: StoreAdapter, graphUri: string, predicateUri: string, context: Record<string, string>, answer: boolean | null, sessionUri?: string): Promise<string>;
|
|
30
|
+
export declare function ask(adapter: StoreAdapter, contextGraphUri: string, input: AskInput): Promise<AskOutput>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ── Resolve ──
|
|
2
|
+
export async function resolvePredicate(adapter, predicateName, contextGraphUri) {
|
|
3
|
+
const sparql = `
|
|
4
|
+
PREFIX otx: <https://opentology.dev/vocab#>
|
|
5
|
+
SELECT ?uri ?title ?template ?param WHERE {
|
|
6
|
+
GRAPH <${contextGraphUri}> {
|
|
7
|
+
?uri a otx:Predicate ;
|
|
8
|
+
otx:title ?title ;
|
|
9
|
+
otx:sparqlTemplate ?template .
|
|
10
|
+
OPTIONAL { ?uri otx:requiredParam ?param }
|
|
11
|
+
FILTER(?title = "${predicateName}" || ?uri = <urn:predicate:${predicateName}>)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
const results = await adapter.sparqlQuery(sparql);
|
|
16
|
+
const bindings = results.results.bindings;
|
|
17
|
+
if (bindings.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const first = bindings[0];
|
|
20
|
+
const requiredParams = bindings
|
|
21
|
+
.map((b) => b.param?.value)
|
|
22
|
+
.filter((v) => !!v);
|
|
23
|
+
return {
|
|
24
|
+
uri: first.uri.value,
|
|
25
|
+
title: first.title.value,
|
|
26
|
+
sparqlTemplate: first.template.value,
|
|
27
|
+
requiredParams: [...new Set(requiredParams)],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ── Validate ──
|
|
31
|
+
export function validateContext(predicate, context) {
|
|
32
|
+
return predicate.requiredParams.filter((p) => !(p in context));
|
|
33
|
+
}
|
|
34
|
+
// ── Evaluate ──
|
|
35
|
+
export function bindTemplate(template, context, graphUri) {
|
|
36
|
+
let bound = template;
|
|
37
|
+
bound = bound.replace(/\{\{graphUri\}\}/g, graphUri);
|
|
38
|
+
for (const [key, value] of Object.entries(context)) {
|
|
39
|
+
bound = bound.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
40
|
+
}
|
|
41
|
+
return bound;
|
|
42
|
+
}
|
|
43
|
+
export async function evaluate(adapter, sparql) {
|
|
44
|
+
const trimmed = sparql.trim();
|
|
45
|
+
// ASK query → boolean
|
|
46
|
+
if (/^(PREFIX\s+\S+:\s+<[^>]+>\s+)*ASK\b/i.test(trimmed)) {
|
|
47
|
+
const answer = await adapter.askQuery(trimmed);
|
|
48
|
+
return { answer };
|
|
49
|
+
}
|
|
50
|
+
// SELECT query → has results?
|
|
51
|
+
const results = await adapter.sparqlQuery(trimmed);
|
|
52
|
+
const bindings = results.results.bindings.map((b) => {
|
|
53
|
+
const row = {};
|
|
54
|
+
for (const [k, v] of Object.entries(b)) {
|
|
55
|
+
row[k] = v.value;
|
|
56
|
+
}
|
|
57
|
+
return row;
|
|
58
|
+
});
|
|
59
|
+
return { answer: bindings.length > 0, bindings: bindings.length > 0 ? bindings : undefined };
|
|
60
|
+
}
|
|
61
|
+
// ── Record ──
|
|
62
|
+
export async function recordEvaluation(adapter, graphUri, predicateUri, context, answer, sessionUri) {
|
|
63
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
64
|
+
const seq = Date.now() % 100000;
|
|
65
|
+
const evalUri = `urn:evaluation:${now}-${seq}`;
|
|
66
|
+
const inputJson = JSON.stringify(context).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
67
|
+
let turtle = `
|
|
68
|
+
@prefix otx: <https://opentology.dev/vocab#> .
|
|
69
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
70
|
+
|
|
71
|
+
<${evalUri}> a otx:Evaluation ;
|
|
72
|
+
otx:predicate <${predicateUri}> ;
|
|
73
|
+
otx:input "${inputJson}" ;
|
|
74
|
+
otx:result "${String(answer)}" ;
|
|
75
|
+
otx:date "${now}"^^xsd:date .
|
|
76
|
+
`;
|
|
77
|
+
if (sessionUri) {
|
|
78
|
+
turtle += `<${evalUri}> otx:createdIn <${sessionUri}> .\n`;
|
|
79
|
+
}
|
|
80
|
+
await adapter.insertTurtle(graphUri, turtle);
|
|
81
|
+
return evalUri;
|
|
82
|
+
}
|
|
83
|
+
// ── Main ask() ──
|
|
84
|
+
export async function ask(adapter, contextGraphUri, input) {
|
|
85
|
+
// 1. Resolve predicate
|
|
86
|
+
const predicate = await resolvePredicate(adapter, input.predicate, contextGraphUri);
|
|
87
|
+
if (!predicate) {
|
|
88
|
+
return { answer: null, reason: `Unknown predicate: "${input.predicate}"` };
|
|
89
|
+
}
|
|
90
|
+
// 2. Validate required params
|
|
91
|
+
const missing = validateContext(predicate, input.context);
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
return { answer: null, missing, reason: `Missing required parameters: ${missing.join(', ')}` };
|
|
94
|
+
}
|
|
95
|
+
// 3. Bind template and evaluate
|
|
96
|
+
const boundSparql = bindTemplate(predicate.sparqlTemplate, input.context, contextGraphUri);
|
|
97
|
+
const { answer, bindings } = await evaluate(adapter, boundSparql);
|
|
98
|
+
// 4. Optionally record
|
|
99
|
+
let evaluationUri;
|
|
100
|
+
if (input.record !== false) {
|
|
101
|
+
evaluationUri = await recordEvaluation(adapter, contextGraphUri, predicate.uri, input.context, answer);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
answer,
|
|
105
|
+
reason: answer
|
|
106
|
+
? `Predicate "${predicate.title}" evaluated to true`
|
|
107
|
+
: `Predicate "${predicate.title}" evaluated to false`,
|
|
108
|
+
evaluationUri,
|
|
109
|
+
bindings,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/lib/context-sync.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { addTrackedFile, saveConfig } from './config.js';
|
|
5
5
|
import { createReadyAdapter } from './store-factory.js';
|
|
6
6
|
import { extractDependencyGraph } from './codebase-scanner.js';
|
|
7
|
+
import { escapeTurtleLiteral as escapeTurtle } from './sparql-utils.js';
|
|
7
8
|
const OTX = 'https://opentology.dev/vocab#';
|
|
8
9
|
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
9
10
|
/**
|
|
@@ -224,6 +225,3 @@ export async function syncContext(config, projectRoot) {
|
|
|
224
225
|
saveConfig(config);
|
|
225
226
|
return { sessionsRecovered, modulesUpdated, moduleStats, actions };
|
|
226
227
|
}
|
|
227
|
-
function escapeTurtle(s) {
|
|
228
|
-
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
229
|
-
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Converts DeepScanResult into OTX-compliant SPARQL INSERT DATA triples
|
|
3
3
|
* with batched insert and scoped delete-then-insert strategy.
|
|
4
4
|
*/
|
|
5
|
+
import { escapeTurtleLiteral } from './sparql-utils.js';
|
|
5
6
|
const OTX = 'https://opentology.dev/vocab#';
|
|
6
7
|
function encodeSegment(s) {
|
|
7
8
|
return encodeURIComponent(s);
|
|
@@ -12,9 +13,7 @@ function symbolUri(filePath, kind, name) {
|
|
|
12
13
|
function moduleUri(filePath) {
|
|
13
14
|
return `urn:module:${filePath}`;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
17
|
-
}
|
|
16
|
+
const esc = escapeTurtleLiteral;
|
|
18
17
|
// ── Triple generation ───────────────────────────────────────────
|
|
19
18
|
export function generateSymbolTriples(result) {
|
|
20
19
|
const triples = [];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns absolute paths of source files changed since the given git ref.
|
|
3
|
+
* Includes both committed changes (sinceRef..HEAD) and staged changes.
|
|
4
|
+
* Returns [] on any git error (graceful fallback).
|
|
5
|
+
*/
|
|
6
|
+
export declare function getChangedSourceFiles(rootDir: string, sinceRef: string): string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
5
|
+
'.ts', '.js', '.tsx', '.jsx',
|
|
6
|
+
'.py', '.go', '.rs', '.java', '.swift',
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Returns absolute paths of source files changed since the given git ref.
|
|
10
|
+
* Includes both committed changes (sinceRef..HEAD) and staged changes.
|
|
11
|
+
* Returns [] on any git error (graceful fallback).
|
|
12
|
+
*/
|
|
13
|
+
export function getChangedSourceFiles(rootDir, sinceRef) {
|
|
14
|
+
try {
|
|
15
|
+
// Verify sinceRef is reachable (guards against shallow clones or rebased history)
|
|
16
|
+
execFileSync('git', ['cat-file', '-e', sinceRef], {
|
|
17
|
+
cwd: rootDir, timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
});
|
|
19
|
+
const committed = execFileSync('git', ['diff', '--name-only', sinceRef, 'HEAD'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
20
|
+
const staged = execFileSync('git', ['diff', '--name-only', '--cached'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
21
|
+
const relative = [
|
|
22
|
+
...committed.split('\n').filter(Boolean),
|
|
23
|
+
...staged.split('\n').filter(Boolean),
|
|
24
|
+
];
|
|
25
|
+
const unique = [...new Set(relative)];
|
|
26
|
+
return unique
|
|
27
|
+
.filter(f => SOURCE_EXTENSIONS.has(extname(f).toLowerCase()))
|
|
28
|
+
.map(f => join(rootDir, f))
|
|
29
|
+
.filter(f => existsSync(f));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StoreAdapter } from './store-adapter.js';
|
|
2
|
+
import type { OpenTologyConfig } from './config.js';
|
|
3
|
+
export declare const MAX_TRIPLES_PER_PUSH = 100;
|
|
4
|
+
export declare function assertTripleLimit(tripleCount: number): void;
|
|
5
|
+
/**
|
|
6
|
+
* Persist a named graph to a .ttl file in embedded mode.
|
|
7
|
+
* Exports the full graph, writes to .opentology/data/{slug}.ttl, and tracks
|
|
8
|
+
* the file in config — mirroring CLI push behavior.
|
|
9
|
+
*/
|
|
10
|
+
export declare function persistGraph(adapter: StoreAdapter, config: OpenTologyConfig, graphUri: string): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { addTrackedFile, saveConfig } from './config.js';
|
|
4
|
+
export const MAX_TRIPLES_PER_PUSH = 100;
|
|
5
|
+
export function assertTripleLimit(tripleCount) {
|
|
6
|
+
if (tripleCount > MAX_TRIPLES_PER_PUSH) {
|
|
7
|
+
throw new Error(`Too many triples (${tripleCount}). Maximum is ${MAX_TRIPLES_PER_PUSH} per push. Split your data into smaller batches.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Persist a named graph to a .ttl file in embedded mode.
|
|
12
|
+
* Exports the full graph, writes to .opentology/data/{slug}.ttl, and tracks
|
|
13
|
+
* the file in config — mirroring CLI push behavior.
|
|
14
|
+
*/
|
|
15
|
+
export async function persistGraph(adapter, config, graphUri) {
|
|
16
|
+
if (config.mode !== 'embedded')
|
|
17
|
+
return;
|
|
18
|
+
const exported = await adapter.exportGraph(graphUri);
|
|
19
|
+
if (!exported.trim())
|
|
20
|
+
return;
|
|
21
|
+
// Derive a filename slug from the graph URI
|
|
22
|
+
const slug = graphUri.replace(/[^a-zA-Z0-9-]/g, '_').replace(/_+/g, '_');
|
|
23
|
+
const dataDir = join(process.cwd(), '.opentology', 'data');
|
|
24
|
+
const filePath = join(dataDir, `${slug}.ttl`);
|
|
25
|
+
mkdirSync(dataDir, { recursive: true });
|
|
26
|
+
writeFileSync(filePath, exported, 'utf-8');
|
|
27
|
+
addTrackedFile(config, graphUri, filePath);
|
|
28
|
+
saveConfig(config);
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ScanState {
|
|
2
|
+
lastScanRef: string;
|
|
3
|
+
lastScanAt: string;
|
|
4
|
+
scannedFiles: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function readScanState(rootDir: string): ScanState | null;
|
|
7
|
+
export declare function writeScanState(rootDir: string, state: ScanState): void;
|
|
8
|
+
export declare function getCurrentGitRef(rootDir: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const STATE_FILE = '.opentology/last-scan.json';
|
|
5
|
+
export function readScanState(rootDir) {
|
|
6
|
+
const stateFile = join(rootDir, STATE_FILE);
|
|
7
|
+
if (!existsSync(stateFile))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function writeScanState(rootDir, state) {
|
|
17
|
+
const stateFile = join(rootDir, STATE_FILE);
|
|
18
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
export function getCurrentGitRef(rootDir) {
|
|
21
|
+
try {
|
|
22
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
23
|
+
cwd: rootDir,
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 'HEAD';
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/lib/snapshot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { persistGraph } from '
|
|
3
|
+
import { persistGraph } from './persist.js';
|
|
4
4
|
const DEFAULT_RETENTION = 5;
|
|
5
5
|
/**
|
|
6
6
|
* Derive a filesystem-safe slug from a graph URI.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Quad } from 'n3';
|
|
2
2
|
export declare const WELL_KNOWN_PREFIXES: Record<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Escape a string for use inside a Turtle double-quoted literal.
|
|
5
|
+
* Handles backslash, quotes, control characters, and non-BMP Unicode.
|
|
6
|
+
*/
|
|
7
|
+
export declare function escapeTurtleLiteral(s: string): string;
|
|
3
8
|
export declare function termToSparql(term: Quad['subject'] | Quad['predicate'] | Quad['object']): string;
|
|
4
9
|
export declare function parseTurtle(turtle: string): Promise<Quad[]>;
|
|
5
10
|
export declare function serializeQuadsToTurtle(quads: Quad[]): Promise<string>;
|
package/dist/lib/sparql-utils.js
CHANGED
|
@@ -13,6 +13,21 @@ export const WELL_KNOWN_PREFIXES = {
|
|
|
13
13
|
'http://www.w3.org/ns/prov#': 'prov',
|
|
14
14
|
};
|
|
15
15
|
// ── Term / Turtle helpers ─────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Escape a string for use inside a Turtle double-quoted literal.
|
|
18
|
+
* Handles backslash, quotes, control characters, and non-BMP Unicode.
|
|
19
|
+
*/
|
|
20
|
+
export function escapeTurtleLiteral(s) {
|
|
21
|
+
return s
|
|
22
|
+
.replace(/\\/g, '\\\\')
|
|
23
|
+
.replace(/"/g, '\\"')
|
|
24
|
+
.replace(/\n/g, '\\n')
|
|
25
|
+
.replace(/\r/g, '\\r')
|
|
26
|
+
.replace(/\t/g, '\\t')
|
|
27
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, (ch) => {
|
|
28
|
+
return '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
16
31
|
export function termToSparql(term) {
|
|
17
32
|
switch (term.termType) {
|
|
18
33
|
case 'NamedNode':
|
|
@@ -20,12 +35,7 @@ export function termToSparql(term) {
|
|
|
20
35
|
case 'BlankNode':
|
|
21
36
|
return `_:${term.value}`;
|
|
22
37
|
case 'Literal': {
|
|
23
|
-
const escaped = term.value
|
|
24
|
-
.replace(/\\/g, '\\\\')
|
|
25
|
-
.replace(/"/g, '\\"')
|
|
26
|
-
.replace(/\n/g, '\\n')
|
|
27
|
-
.replace(/\r/g, '\\r')
|
|
28
|
-
.replace(/\t/g, '\\t');
|
|
38
|
+
const escaped = escapeTurtleLiteral(term.value);
|
|
29
39
|
if (term.language) {
|
|
30
40
|
return `"${escaped}"@${term.language}`;
|
|
31
41
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ContextLoadOutput {
|
|
2
|
+
projectId: string;
|
|
3
|
+
graphUri: string;
|
|
4
|
+
sessions: Array<{
|
|
5
|
+
uri: string;
|
|
6
|
+
title: string;
|
|
7
|
+
date: string;
|
|
8
|
+
nextTodo?: string;
|
|
9
|
+
}>;
|
|
10
|
+
openIssues: Array<{
|
|
11
|
+
uri: string;
|
|
12
|
+
title: string;
|
|
13
|
+
date: string;
|
|
14
|
+
}>;
|
|
15
|
+
recentDecisions: Array<{
|
|
16
|
+
uri: string;
|
|
17
|
+
title: string;
|
|
18
|
+
date: string;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}>;
|
|
21
|
+
meta: {
|
|
22
|
+
contextTripleCount: number;
|
|
23
|
+
sessionsTripleCount: number;
|
|
24
|
+
loadedAt: string;
|
|
25
|
+
};
|
|
26
|
+
warnings?: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function handleContextScan(args: Record<string, unknown>): Promise<unknown>;
|
|
29
|
+
export declare function handleContextInit(args: Record<string, unknown>): Promise<unknown>;
|
|
30
|
+
export declare function handleContextLoad(): Promise<ContextLoadOutput>;
|
|
31
|
+
export declare function handleContextSearch(args: Record<string, unknown>): Promise<unknown>;
|
|
32
|
+
export declare function handleContextImpact(args: Record<string, unknown>): Promise<unknown>;
|
|
33
|
+
export declare function handleContextStatus(): Promise<unknown>;
|