@nahisaho/yata-local 1.6.6
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/auto-updater.d.ts +101 -0
- package/dist/auto-updater.d.ts.map +1 -0
- package/dist/auto-updater.js +402 -0
- package/dist/auto-updater.js.map +1 -0
- package/dist/database.d.ts +229 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +959 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +436 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +77 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +403 -0
- package/dist/io.js.map +1 -0
- package/dist/kgpr/diff-engine.d.ts +110 -0
- package/dist/kgpr/diff-engine.d.ts.map +1 -0
- package/dist/kgpr/diff-engine.js +335 -0
- package/dist/kgpr/diff-engine.js.map +1 -0
- package/dist/kgpr/index.d.ts +16 -0
- package/dist/kgpr/index.d.ts.map +1 -0
- package/dist/kgpr/index.js +14 -0
- package/dist/kgpr/index.js.map +1 -0
- package/dist/kgpr/kgpr-manager.d.ts +122 -0
- package/dist/kgpr/kgpr-manager.d.ts.map +1 -0
- package/dist/kgpr/kgpr-manager.js +344 -0
- package/dist/kgpr/kgpr-manager.js.map +1 -0
- package/dist/kgpr/privacy-filter.d.ts +109 -0
- package/dist/kgpr/privacy-filter.d.ts.map +1 -0
- package/dist/kgpr/privacy-filter.js +295 -0
- package/dist/kgpr/privacy-filter.js.map +1 -0
- package/dist/kgpr/types.d.ts +234 -0
- package/dist/kgpr/types.d.ts.map +1 -0
- package/dist/kgpr/types.js +54 -0
- package/dist/kgpr/types.js.map +1 -0
- package/dist/query-engine.d.ts +78 -0
- package/dist/query-engine.d.ts.map +1 -0
- package/dist/query-engine.js +368 -0
- package/dist/query-engine.js.map +1 -0
- package/dist/reasoning.d.ts +112 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +455 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/types.d.ts +580 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/wake-sleep/cycle-manager.d.ts +76 -0
- package/dist/wake-sleep/cycle-manager.d.ts.map +1 -0
- package/dist/wake-sleep/cycle-manager.js +291 -0
- package/dist/wake-sleep/cycle-manager.js.map +1 -0
- package/dist/wake-sleep/index.d.ts +15 -0
- package/dist/wake-sleep/index.d.ts.map +1 -0
- package/dist/wake-sleep/index.js +19 -0
- package/dist/wake-sleep/index.js.map +1 -0
- package/dist/wake-sleep/pattern-compressor.d.ts +86 -0
- package/dist/wake-sleep/pattern-compressor.d.ts.map +1 -0
- package/dist/wake-sleep/pattern-compressor.js +333 -0
- package/dist/wake-sleep/pattern-compressor.js.map +1 -0
- package/dist/wake-sleep/sleep-phase.d.ts +79 -0
- package/dist/wake-sleep/sleep-phase.d.ts.map +1 -0
- package/dist/wake-sleep/sleep-phase.js +329 -0
- package/dist/wake-sleep/sleep-phase.js.map +1 -0
- package/dist/wake-sleep/types.d.ts +244 -0
- package/dist/wake-sleep/types.d.ts.map +1 -0
- package/dist/wake-sleep/types.js +35 -0
- package/dist/wake-sleep/types.js.map +1 -0
- package/dist/wake-sleep/wake-phase.d.ts +83 -0
- package/dist/wake-sleep/wake-phase.d.ts.map +1 -0
- package/dist/wake-sleep/wake-phase.js +457 -0
- package/dist/wake-sleep/wake-phase.js.map +1 -0
- package/dist/yata-bridge.d.ts +78 -0
- package/dist/yata-bridge.d.ts.map +1 -0
- package/dist/yata-bridge.js +193 -0
- package/dist/yata-bridge.js.map +1 -0
- package/package.json +60 -0
package/dist/database.js
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YATA Local - SQLite Database Layer
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @module @nahisaho/yata-local/database
|
|
6
|
+
*
|
|
7
|
+
* @see REQ-YL-STORE-001
|
|
8
|
+
* @see REQ-YL-STORE-002
|
|
9
|
+
* @see REQ-WSL-003
|
|
10
|
+
* @see REQ-NFR-001
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_DB_CONFIG } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* SQL schema for knowledge graph
|
|
15
|
+
* @see REQ-YL-STORE-002
|
|
16
|
+
*/
|
|
17
|
+
const SCHEMA = `
|
|
18
|
+
-- Entities table
|
|
19
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
type TEXT NOT NULL,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
24
|
+
file_path TEXT,
|
|
25
|
+
line INTEGER,
|
|
26
|
+
description TEXT,
|
|
27
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
updated_at TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Relationships table
|
|
33
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
source_id TEXT NOT NULL,
|
|
36
|
+
target_id TEXT NOT NULL,
|
|
37
|
+
type TEXT NOT NULL,
|
|
38
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
39
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
40
|
+
created_at TEXT NOT NULL,
|
|
41
|
+
FOREIGN KEY (source_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
42
|
+
FOREIGN KEY (target_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- Indexes for fast queries
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_entities_file_path ON entities(file_path);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_entities_updated ON entities(updated_at);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_type ON relationships(type);
|
|
55
|
+
|
|
56
|
+
-- Full-text search virtual table
|
|
57
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
58
|
+
id,
|
|
59
|
+
name,
|
|
60
|
+
namespace,
|
|
61
|
+
description,
|
|
62
|
+
content='entities',
|
|
63
|
+
content_rowid='rowid'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Triggers to keep FTS in sync
|
|
67
|
+
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
|
68
|
+
INSERT INTO entities_fts(id, name, namespace, description)
|
|
69
|
+
VALUES (new.id, new.name, new.namespace, new.description);
|
|
70
|
+
END;
|
|
71
|
+
|
|
72
|
+
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
|
|
73
|
+
INSERT INTO entities_fts(entities_fts, id, name, namespace, description)
|
|
74
|
+
VALUES ('delete', old.id, old.name, old.namespace, old.description);
|
|
75
|
+
END;
|
|
76
|
+
|
|
77
|
+
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
|
|
78
|
+
INSERT INTO entities_fts(entities_fts, id, name, namespace, description)
|
|
79
|
+
VALUES ('delete', old.id, old.name, old.namespace, old.description);
|
|
80
|
+
INSERT INTO entities_fts(id, name, namespace, description)
|
|
81
|
+
VALUES (new.id, new.name, new.namespace, new.description);
|
|
82
|
+
END;
|
|
83
|
+
|
|
84
|
+
-- Change tracking table
|
|
85
|
+
CREATE TABLE IF NOT EXISTS change_log (
|
|
86
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
87
|
+
entity_id TEXT,
|
|
88
|
+
relationship_id TEXT,
|
|
89
|
+
operation TEXT NOT NULL,
|
|
90
|
+
timestamp TEXT NOT NULL,
|
|
91
|
+
data TEXT
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_change_log_timestamp ON change_log(timestamp);
|
|
95
|
+
|
|
96
|
+
-- Patterns table for Wake-Sleep learning
|
|
97
|
+
-- @see REQ-WSL-003
|
|
98
|
+
-- @see TSK-NFR-003
|
|
99
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
name TEXT NOT NULL,
|
|
102
|
+
category TEXT NOT NULL DEFAULT 'code',
|
|
103
|
+
content TEXT NOT NULL,
|
|
104
|
+
ast TEXT,
|
|
105
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
106
|
+
occurrences INTEGER NOT NULL DEFAULT 1,
|
|
107
|
+
last_used_at TEXT,
|
|
108
|
+
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
109
|
+
source TEXT,
|
|
110
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
111
|
+
created_at TEXT NOT NULL,
|
|
112
|
+
updated_at TEXT NOT NULL
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
-- Pattern indexes for performance
|
|
116
|
+
-- @see REQ-NFR-001
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_category ON patterns(category);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_confidence ON patterns(confidence);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_last_used ON patterns(last_used_at);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_name ON patterns(name);
|
|
121
|
+
|
|
122
|
+
-- Learning cycles table for tracking Wake-Sleep cycles
|
|
123
|
+
-- @see REQ-WSL-005
|
|
124
|
+
-- @see TSK-NFR-004
|
|
125
|
+
CREATE TABLE IF NOT EXISTS learning_cycles (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
wake_extracted INTEGER NOT NULL DEFAULT 0,
|
|
128
|
+
wake_observed_files INTEGER NOT NULL DEFAULT 0,
|
|
129
|
+
sleep_clustered INTEGER NOT NULL DEFAULT 0,
|
|
130
|
+
sleep_decayed INTEGER NOT NULL DEFAULT 0,
|
|
131
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
133
|
+
completed_at TEXT NOT NULL
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_learning_cycles_completed ON learning_cycles(completed_at);
|
|
137
|
+
|
|
138
|
+
-- KGPR tracking table
|
|
139
|
+
-- @see REQ-KGPR-001
|
|
140
|
+
-- @see TSK-KGPR-004
|
|
141
|
+
CREATE TABLE IF NOT EXISTS kgprs (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
title TEXT NOT NULL,
|
|
144
|
+
description TEXT,
|
|
145
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
146
|
+
author TEXT NOT NULL DEFAULT 'local',
|
|
147
|
+
namespace TEXT NOT NULL DEFAULT '*',
|
|
148
|
+
diff_json TEXT NOT NULL DEFAULT '{}',
|
|
149
|
+
privacy_level TEXT NOT NULL DEFAULT 'strict',
|
|
150
|
+
entity_types TEXT NOT NULL DEFAULT '[]',
|
|
151
|
+
created_at TEXT NOT NULL,
|
|
152
|
+
updated_at TEXT NOT NULL,
|
|
153
|
+
submitted_at TEXT,
|
|
154
|
+
reviewed_at TEXT,
|
|
155
|
+
merged_at TEXT,
|
|
156
|
+
closed_at TEXT
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_kgprs_status ON kgprs(status);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_kgprs_author ON kgprs(author);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_kgprs_namespace ON kgprs(namespace);
|
|
162
|
+
|
|
163
|
+
-- KGPR reviews table
|
|
164
|
+
-- @see REQ-KGPR-003
|
|
165
|
+
CREATE TABLE IF NOT EXISTS kgpr_reviews (
|
|
166
|
+
id TEXT PRIMARY KEY,
|
|
167
|
+
kgpr_id TEXT NOT NULL,
|
|
168
|
+
reviewer TEXT NOT NULL,
|
|
169
|
+
action TEXT NOT NULL,
|
|
170
|
+
comment TEXT,
|
|
171
|
+
created_at TEXT NOT NULL,
|
|
172
|
+
FOREIGN KEY (kgpr_id) REFERENCES kgprs(id) ON DELETE CASCADE
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_kgpr_reviews_kgpr ON kgpr_reviews(kgpr_id);
|
|
176
|
+
`;
|
|
177
|
+
/**
|
|
178
|
+
* Database layer for YATA Local
|
|
179
|
+
*/
|
|
180
|
+
export class YataDatabase {
|
|
181
|
+
db = null;
|
|
182
|
+
config;
|
|
183
|
+
constructor(config = {}) {
|
|
184
|
+
this.config = { ...DEFAULT_DB_CONFIG, ...config };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Open database connection
|
|
188
|
+
* @see REQ-YL-STORE-001
|
|
189
|
+
*/
|
|
190
|
+
async open() {
|
|
191
|
+
// Dynamic import for better-sqlite3
|
|
192
|
+
const BetterSqlite3 = (await import('better-sqlite3')).default;
|
|
193
|
+
this.db = new BetterSqlite3(this.config.path);
|
|
194
|
+
// Configure database
|
|
195
|
+
if (this.config.walMode) {
|
|
196
|
+
this.db.pragma('journal_mode = WAL');
|
|
197
|
+
}
|
|
198
|
+
if (this.config.mmapSize > 0) {
|
|
199
|
+
this.db.pragma(`mmap_size = ${this.config.mmapSize}`);
|
|
200
|
+
}
|
|
201
|
+
if (this.config.cacheSize > 0) {
|
|
202
|
+
this.db.pragma(`cache_size = -${this.config.cacheSize}`);
|
|
203
|
+
}
|
|
204
|
+
if (this.config.foreignKeys) {
|
|
205
|
+
this.db.pragma('foreign_keys = ON');
|
|
206
|
+
}
|
|
207
|
+
// Create schema
|
|
208
|
+
this.db.exec(SCHEMA);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Close database connection
|
|
212
|
+
*/
|
|
213
|
+
async close() {
|
|
214
|
+
if (this.db) {
|
|
215
|
+
this.db.close();
|
|
216
|
+
this.db = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Check if database is open
|
|
221
|
+
*/
|
|
222
|
+
isOpen() {
|
|
223
|
+
return this.db !== null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get database instance (throws if not open)
|
|
227
|
+
* Made public for advanced query use cases
|
|
228
|
+
*/
|
|
229
|
+
getDb() {
|
|
230
|
+
if (!this.db) {
|
|
231
|
+
throw new Error('Database is not open. Call open() first.');
|
|
232
|
+
}
|
|
233
|
+
return this.db;
|
|
234
|
+
}
|
|
235
|
+
// Entity operations
|
|
236
|
+
/**
|
|
237
|
+
* Insert entity
|
|
238
|
+
* @see REQ-YL-STORE-003
|
|
239
|
+
*/
|
|
240
|
+
insertEntity(entity) {
|
|
241
|
+
const db = this.getDb();
|
|
242
|
+
const stmt = db.prepare(`
|
|
243
|
+
INSERT INTO entities (id, type, name, namespace, file_path, line, description, metadata, created_at, updated_at)
|
|
244
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
245
|
+
`);
|
|
246
|
+
stmt.run(entity.id, entity.type, entity.name, entity.namespace, entity.filePath ?? null, entity.line ?? null, entity.description ?? null, JSON.stringify(entity.metadata), entity.createdAt.toISOString(), entity.updatedAt.toISOString());
|
|
247
|
+
// Log change
|
|
248
|
+
this.logChange(entity.id, null, 'insert', entity);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Insert multiple entities in transaction
|
|
252
|
+
* @see REQ-YL-STORE-005
|
|
253
|
+
*/
|
|
254
|
+
insertEntities(entities) {
|
|
255
|
+
const db = this.getDb();
|
|
256
|
+
const stmt = db.prepare(`
|
|
257
|
+
INSERT INTO entities (id, type, name, namespace, file_path, line, description, metadata, created_at, updated_at)
|
|
258
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
259
|
+
`);
|
|
260
|
+
const insertMany = db.transaction((items) => {
|
|
261
|
+
for (const entity of items) {
|
|
262
|
+
stmt.run(entity.id, entity.type, entity.name, entity.namespace, entity.filePath ?? null, entity.line ?? null, entity.description ?? null, JSON.stringify(entity.metadata), entity.createdAt.toISOString(), entity.updatedAt.toISOString());
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
insertMany(entities);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get entity by ID
|
|
269
|
+
*/
|
|
270
|
+
getEntity(id) {
|
|
271
|
+
const db = this.getDb();
|
|
272
|
+
const stmt = db.prepare('SELECT * FROM entities WHERE id = ?');
|
|
273
|
+
const row = stmt.get(id);
|
|
274
|
+
return row ? this.rowToEntity(row) : null;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Update entity
|
|
278
|
+
* @see REQ-YL-UPDATE-002
|
|
279
|
+
*/
|
|
280
|
+
updateEntity(id, updates) {
|
|
281
|
+
const db = this.getDb();
|
|
282
|
+
const existing = this.getEntity(id);
|
|
283
|
+
if (!existing) {
|
|
284
|
+
throw new Error(`Entity not found: ${id}`);
|
|
285
|
+
}
|
|
286
|
+
const updated = { ...existing, ...updates, updatedAt: new Date() };
|
|
287
|
+
const stmt = db.prepare(`
|
|
288
|
+
UPDATE entities SET
|
|
289
|
+
type = ?, name = ?, namespace = ?, file_path = ?, line = ?,
|
|
290
|
+
description = ?, metadata = ?, updated_at = ?
|
|
291
|
+
WHERE id = ?
|
|
292
|
+
`);
|
|
293
|
+
stmt.run(updated.type, updated.name, updated.namespace, updated.filePath ?? null, updated.line ?? null, updated.description ?? null, JSON.stringify(updated.metadata), updated.updatedAt.toISOString(), id);
|
|
294
|
+
this.logChange(id, null, 'update', updated);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Delete entity
|
|
298
|
+
* @see REQ-YL-UPDATE-003
|
|
299
|
+
*/
|
|
300
|
+
deleteEntity(id) {
|
|
301
|
+
const db = this.getDb();
|
|
302
|
+
const stmt = db.prepare('DELETE FROM entities WHERE id = ?');
|
|
303
|
+
stmt.run(id);
|
|
304
|
+
this.logChange(id, null, 'delete', null);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Delete entities by file path
|
|
308
|
+
*/
|
|
309
|
+
deleteEntitiesByFile(filePath) {
|
|
310
|
+
const db = this.getDb();
|
|
311
|
+
const stmt = db.prepare('DELETE FROM entities WHERE file_path = ?');
|
|
312
|
+
const result = stmt.run(filePath);
|
|
313
|
+
return result.changes;
|
|
314
|
+
}
|
|
315
|
+
// Relationship operations
|
|
316
|
+
/**
|
|
317
|
+
* Insert relationship
|
|
318
|
+
* @see REQ-YL-STORE-004
|
|
319
|
+
*/
|
|
320
|
+
insertRelationship(rel) {
|
|
321
|
+
const db = this.getDb();
|
|
322
|
+
const stmt = db.prepare(`
|
|
323
|
+
INSERT INTO relationships (id, source_id, target_id, type, weight, metadata, created_at)
|
|
324
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
325
|
+
`);
|
|
326
|
+
stmt.run(rel.id, rel.sourceId, rel.targetId, rel.type, rel.weight, JSON.stringify(rel.metadata), rel.createdAt.toISOString());
|
|
327
|
+
this.logChange(null, rel.id, 'insert', rel);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get relationships for entity
|
|
331
|
+
*/
|
|
332
|
+
getRelationships(entityId, direction = 'both') {
|
|
333
|
+
const db = this.getDb();
|
|
334
|
+
let sql;
|
|
335
|
+
if (direction === 'out') {
|
|
336
|
+
sql = 'SELECT * FROM relationships WHERE source_id = ?';
|
|
337
|
+
}
|
|
338
|
+
else if (direction === 'in') {
|
|
339
|
+
sql = 'SELECT * FROM relationships WHERE target_id = ?';
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
sql = 'SELECT * FROM relationships WHERE source_id = ? OR target_id = ?';
|
|
343
|
+
}
|
|
344
|
+
const stmt = db.prepare(sql);
|
|
345
|
+
const rows = direction === 'both'
|
|
346
|
+
? stmt.all(entityId, entityId)
|
|
347
|
+
: stmt.all(entityId);
|
|
348
|
+
return rows.map(row => this.rowToRelationship(row));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Delete relationship
|
|
352
|
+
*/
|
|
353
|
+
deleteRelationship(id) {
|
|
354
|
+
const db = this.getDb();
|
|
355
|
+
const stmt = db.prepare('DELETE FROM relationships WHERE id = ?');
|
|
356
|
+
stmt.run(id);
|
|
357
|
+
this.logChange(null, id, 'delete', null);
|
|
358
|
+
}
|
|
359
|
+
// Query operations
|
|
360
|
+
/**
|
|
361
|
+
* Query entities with filters
|
|
362
|
+
* @see REQ-YL-QUERY-001
|
|
363
|
+
*/
|
|
364
|
+
queryEntities(filters, limit = 100, offset = 0) {
|
|
365
|
+
const db = this.getDb();
|
|
366
|
+
const conditions = [];
|
|
367
|
+
const params = [];
|
|
368
|
+
if (filters.type) {
|
|
369
|
+
if (Array.isArray(filters.type)) {
|
|
370
|
+
conditions.push(`type IN (${filters.type.map(() => '?').join(', ')})`);
|
|
371
|
+
params.push(...filters.type);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
conditions.push('type = ?');
|
|
375
|
+
params.push(filters.type);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (filters.name) {
|
|
379
|
+
conditions.push('name = ?');
|
|
380
|
+
params.push(filters.name);
|
|
381
|
+
}
|
|
382
|
+
if (filters.namePattern) {
|
|
383
|
+
conditions.push('name LIKE ?');
|
|
384
|
+
params.push(filters.namePattern.replace(/\*/g, '%'));
|
|
385
|
+
}
|
|
386
|
+
if (filters.namespace) {
|
|
387
|
+
conditions.push('namespace = ?');
|
|
388
|
+
params.push(filters.namespace);
|
|
389
|
+
}
|
|
390
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
391
|
+
// Get total count
|
|
392
|
+
const countStmt = db.prepare(`SELECT COUNT(*) as count FROM entities ${whereClause}`);
|
|
393
|
+
const countResult = countStmt.get(...params);
|
|
394
|
+
// Get paginated results
|
|
395
|
+
const stmt = db.prepare(`SELECT * FROM entities ${whereClause} LIMIT ? OFFSET ?`);
|
|
396
|
+
const rows = stmt.all(...params, limit, offset);
|
|
397
|
+
return {
|
|
398
|
+
entities: rows.map(row => this.rowToEntity(row)),
|
|
399
|
+
totalCount: countResult.count,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Full-text search
|
|
404
|
+
* @see REQ-YL-QUERY-005
|
|
405
|
+
*/
|
|
406
|
+
searchEntities(text, limit = 100) {
|
|
407
|
+
const db = this.getDb();
|
|
408
|
+
const stmt = db.prepare(`
|
|
409
|
+
SELECT e.* FROM entities e
|
|
410
|
+
JOIN entities_fts fts ON e.id = fts.id
|
|
411
|
+
WHERE entities_fts MATCH ?
|
|
412
|
+
LIMIT ?
|
|
413
|
+
`);
|
|
414
|
+
const rows = stmt.all(text, limit);
|
|
415
|
+
return rows.map(row => this.rowToEntity(row));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get changes since timestamp
|
|
419
|
+
* @see REQ-YL-UPDATE-005
|
|
420
|
+
*/
|
|
421
|
+
getChangesSince(since) {
|
|
422
|
+
const db = this.getDb();
|
|
423
|
+
const stmt = db.prepare(`
|
|
424
|
+
SELECT * FROM change_log WHERE timestamp > ? ORDER BY id
|
|
425
|
+
`);
|
|
426
|
+
const rows = stmt.all(since.toISOString());
|
|
427
|
+
const result = {
|
|
428
|
+
entities: { added: [], updated: [], deleted: [] },
|
|
429
|
+
relationships: { added: [], deleted: [] },
|
|
430
|
+
};
|
|
431
|
+
for (const row of rows) {
|
|
432
|
+
if (row.entity_id) {
|
|
433
|
+
if (row.operation === 'insert') {
|
|
434
|
+
const entity = this.getEntity(row.entity_id);
|
|
435
|
+
if (entity)
|
|
436
|
+
result.entities.added.push(entity);
|
|
437
|
+
}
|
|
438
|
+
else if (row.operation === 'update') {
|
|
439
|
+
const entity = this.getEntity(row.entity_id);
|
|
440
|
+
if (entity)
|
|
441
|
+
result.entities.updated.push(entity);
|
|
442
|
+
}
|
|
443
|
+
else if (row.operation === 'delete') {
|
|
444
|
+
result.entities.deleted.push(row.entity_id);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else if (row.relationship_id) {
|
|
448
|
+
if (row.operation === 'insert' && row.data) {
|
|
449
|
+
result.relationships.added.push(JSON.parse(row.data));
|
|
450
|
+
}
|
|
451
|
+
else if (row.operation === 'delete') {
|
|
452
|
+
result.relationships.deleted.push(row.relationship_id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get graph statistics
|
|
460
|
+
*/
|
|
461
|
+
getStats() {
|
|
462
|
+
const db = this.getDb();
|
|
463
|
+
// Entity count
|
|
464
|
+
const entityCount = db.prepare('SELECT COUNT(*) as count FROM entities').get().count;
|
|
465
|
+
// Entities by type
|
|
466
|
+
const typeRows = db.prepare('SELECT type, COUNT(*) as count FROM entities GROUP BY type').all();
|
|
467
|
+
const entitiesByType = {};
|
|
468
|
+
for (const row of typeRows) {
|
|
469
|
+
entitiesByType[row.type] = row.count;
|
|
470
|
+
}
|
|
471
|
+
// Relationship count
|
|
472
|
+
const relationshipCount = db.prepare('SELECT COUNT(*) as count FROM relationships').get().count;
|
|
473
|
+
// Relationships by type
|
|
474
|
+
const relTypeRows = db.prepare('SELECT type, COUNT(*) as count FROM relationships GROUP BY type').all();
|
|
475
|
+
const relationshipsByType = {};
|
|
476
|
+
for (const row of relTypeRows) {
|
|
477
|
+
relationshipsByType[row.type] = row.count;
|
|
478
|
+
}
|
|
479
|
+
// Database size (approximate)
|
|
480
|
+
const pageCount = db.prepare('PRAGMA page_count').get().page_count;
|
|
481
|
+
const pageSize = db.prepare('PRAGMA page_size').get().page_size;
|
|
482
|
+
const databaseSize = pageCount * pageSize;
|
|
483
|
+
// Last modified
|
|
484
|
+
const lastModifiedRow = db.prepare('SELECT MAX(updated_at) as last FROM entities').get();
|
|
485
|
+
const lastModified = lastModifiedRow.last ? new Date(lastModifiedRow.last) : new Date();
|
|
486
|
+
return {
|
|
487
|
+
entityCount,
|
|
488
|
+
entitiesByType: entitiesByType,
|
|
489
|
+
relationshipCount,
|
|
490
|
+
relationshipsByType: relationshipsByType,
|
|
491
|
+
databaseSize,
|
|
492
|
+
lastModified,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// ============================================================
|
|
496
|
+
// Pattern Operations (TSK-NFR-003, TSK-WSL-003)
|
|
497
|
+
// @see REQ-WSL-003
|
|
498
|
+
// ============================================================
|
|
499
|
+
/**
|
|
500
|
+
* Insert a learned pattern
|
|
501
|
+
* @see REQ-WSL-001
|
|
502
|
+
*/
|
|
503
|
+
insertPattern(pattern) {
|
|
504
|
+
const db = this.getDb();
|
|
505
|
+
const stmt = db.prepare(`
|
|
506
|
+
INSERT INTO patterns (id, name, category, content, ast, confidence, occurrences, last_used_at, usage_count, source, metadata, created_at, updated_at)
|
|
507
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
508
|
+
`);
|
|
509
|
+
stmt.run(pattern.id, pattern.name, pattern.category, pattern.content, pattern.ast ? JSON.stringify(pattern.ast) : null, pattern.confidence, pattern.occurrences, pattern.lastUsedAt?.toISOString() ?? null, pattern.usageCount, pattern.source ?? null, JSON.stringify(pattern.metadata), pattern.createdAt.toISOString(), pattern.updatedAt.toISOString());
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get a pattern by ID
|
|
513
|
+
*/
|
|
514
|
+
getPattern(id) {
|
|
515
|
+
const db = this.getDb();
|
|
516
|
+
const stmt = db.prepare('SELECT * FROM patterns WHERE id = ?');
|
|
517
|
+
const row = stmt.get(id);
|
|
518
|
+
return row ? this.rowToPattern(row) : null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Update a pattern
|
|
522
|
+
*/
|
|
523
|
+
updatePattern(pattern) {
|
|
524
|
+
const db = this.getDb();
|
|
525
|
+
const stmt = db.prepare(`
|
|
526
|
+
UPDATE patterns SET
|
|
527
|
+
name = ?, category = ?, content = ?, ast = ?, confidence = ?,
|
|
528
|
+
occurrences = ?, last_used_at = ?, usage_count = ?, source = ?,
|
|
529
|
+
metadata = ?, updated_at = ?
|
|
530
|
+
WHERE id = ?
|
|
531
|
+
`);
|
|
532
|
+
stmt.run(pattern.name, pattern.category, pattern.content, pattern.ast ? JSON.stringify(pattern.ast) : null, pattern.confidence, pattern.occurrences, pattern.lastUsedAt?.toISOString() ?? null, pattern.usageCount, pattern.source ?? null, JSON.stringify(pattern.metadata), pattern.updatedAt.toISOString(), pattern.id);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Upsert a pattern (insert or update)
|
|
536
|
+
* @see REQ-WSL-002
|
|
537
|
+
*/
|
|
538
|
+
upsertPattern(pattern) {
|
|
539
|
+
const existing = this.getPattern(pattern.id);
|
|
540
|
+
if (existing) {
|
|
541
|
+
this.updatePattern(pattern);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
this.insertPattern(pattern);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Delete a pattern
|
|
549
|
+
*/
|
|
550
|
+
deletePattern(id) {
|
|
551
|
+
const db = this.getDb();
|
|
552
|
+
const stmt = db.prepare('DELETE FROM patterns WHERE id = ?');
|
|
553
|
+
const result = stmt.run(id);
|
|
554
|
+
return result.changes > 0;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Query patterns with options
|
|
558
|
+
* @see REQ-WSL-003
|
|
559
|
+
*/
|
|
560
|
+
queryPatterns(options = {}) {
|
|
561
|
+
const db = this.getDb();
|
|
562
|
+
const conditions = [];
|
|
563
|
+
const params = [];
|
|
564
|
+
if (options.category) {
|
|
565
|
+
conditions.push('category = ?');
|
|
566
|
+
params.push(options.category);
|
|
567
|
+
}
|
|
568
|
+
if (options.minConfidence !== undefined) {
|
|
569
|
+
conditions.push('confidence >= ?');
|
|
570
|
+
params.push(options.minConfidence);
|
|
571
|
+
}
|
|
572
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
573
|
+
const sortBy = options.sortBy ?? 'created_at';
|
|
574
|
+
const sortOrder = options.sortOrder ?? 'desc';
|
|
575
|
+
const limit = options.limit ?? 100;
|
|
576
|
+
const offset = options.offset ?? 0;
|
|
577
|
+
const sql = `
|
|
578
|
+
SELECT * FROM patterns
|
|
579
|
+
${whereClause}
|
|
580
|
+
ORDER BY ${sortBy} ${sortOrder}
|
|
581
|
+
LIMIT ? OFFSET ?
|
|
582
|
+
`;
|
|
583
|
+
params.push(limit, offset);
|
|
584
|
+
const stmt = db.prepare(sql);
|
|
585
|
+
const rows = stmt.all(...params);
|
|
586
|
+
return rows.map(row => this.rowToPattern(row));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get patterns that haven't been used recently
|
|
590
|
+
* @see REQ-WSL-003
|
|
591
|
+
*/
|
|
592
|
+
getUnusedPatterns(monthsThreshold = 6) {
|
|
593
|
+
const db = this.getDb();
|
|
594
|
+
const thresholdDate = new Date();
|
|
595
|
+
thresholdDate.setMonth(thresholdDate.getMonth() - monthsThreshold);
|
|
596
|
+
const stmt = db.prepare(`
|
|
597
|
+
SELECT * FROM patterns
|
|
598
|
+
WHERE last_used_at IS NULL OR last_used_at < ?
|
|
599
|
+
ORDER BY last_used_at ASC
|
|
600
|
+
`);
|
|
601
|
+
const rows = stmt.all(thresholdDate.toISOString());
|
|
602
|
+
return rows.map(row => this.rowToPattern(row));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get low confidence patterns
|
|
606
|
+
* @see REQ-WSL-003
|
|
607
|
+
*/
|
|
608
|
+
getLowConfidencePatterns(threshold = 0.3) {
|
|
609
|
+
const db = this.getDb();
|
|
610
|
+
const stmt = db.prepare(`
|
|
611
|
+
SELECT * FROM patterns
|
|
612
|
+
WHERE confidence < ?
|
|
613
|
+
ORDER BY confidence ASC
|
|
614
|
+
`);
|
|
615
|
+
const rows = stmt.all(threshold);
|
|
616
|
+
return rows.map(row => this.rowToPattern(row));
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Mark pattern as used (update lastUsedAt)
|
|
620
|
+
* @see REQ-WSL-003
|
|
621
|
+
*/
|
|
622
|
+
markPatternUsed(id) {
|
|
623
|
+
const db = this.getDb();
|
|
624
|
+
const stmt = db.prepare(`
|
|
625
|
+
UPDATE patterns SET
|
|
626
|
+
last_used_at = ?,
|
|
627
|
+
usage_count = usage_count + 1,
|
|
628
|
+
updated_at = ?
|
|
629
|
+
WHERE id = ?
|
|
630
|
+
`);
|
|
631
|
+
const now = new Date().toISOString();
|
|
632
|
+
stmt.run(now, now, id);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Decay pattern confidence
|
|
636
|
+
* @see REQ-WSL-003
|
|
637
|
+
*/
|
|
638
|
+
decayPatternConfidence(id, factor) {
|
|
639
|
+
const db = this.getDb();
|
|
640
|
+
const stmt = db.prepare(`
|
|
641
|
+
UPDATE patterns SET
|
|
642
|
+
confidence = confidence * ?,
|
|
643
|
+
updated_at = ?
|
|
644
|
+
WHERE id = ?
|
|
645
|
+
`);
|
|
646
|
+
stmt.run(factor, new Date().toISOString(), id);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get pattern count
|
|
650
|
+
*/
|
|
651
|
+
getPatternCount() {
|
|
652
|
+
const db = this.getDb();
|
|
653
|
+
const result = db.prepare('SELECT COUNT(*) as count FROM patterns').get();
|
|
654
|
+
return result.count;
|
|
655
|
+
}
|
|
656
|
+
// ============================================================
|
|
657
|
+
// Learning Cycle Operations (TSK-NFR-004)
|
|
658
|
+
// @see REQ-WSL-005
|
|
659
|
+
// ============================================================
|
|
660
|
+
/**
|
|
661
|
+
* Log a learning cycle
|
|
662
|
+
* @see REQ-WSL-005
|
|
663
|
+
*/
|
|
664
|
+
logLearningCycle(cycle) {
|
|
665
|
+
const db = this.getDb();
|
|
666
|
+
const id = `LC-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 6)}`;
|
|
667
|
+
const now = new Date();
|
|
668
|
+
const stmt = db.prepare(`
|
|
669
|
+
INSERT INTO learning_cycles (id, wake_extracted, wake_observed_files, sleep_clustered, sleep_decayed, duration_ms, metadata, completed_at)
|
|
670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
671
|
+
`);
|
|
672
|
+
stmt.run(id, cycle.wakeExtracted, cycle.wakeObservedFiles, cycle.sleepClustered, cycle.sleepDecayed, cycle.durationMs, JSON.stringify(cycle.metadata ?? {}), now.toISOString());
|
|
673
|
+
return {
|
|
674
|
+
id,
|
|
675
|
+
wakeExtracted: cycle.wakeExtracted,
|
|
676
|
+
wakeObservedFiles: cycle.wakeObservedFiles,
|
|
677
|
+
sleepClustered: cycle.sleepClustered,
|
|
678
|
+
sleepDecayed: cycle.sleepDecayed,
|
|
679
|
+
durationMs: cycle.durationMs,
|
|
680
|
+
metadata: cycle.metadata ?? {},
|
|
681
|
+
completedAt: now,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Get recent learning cycles
|
|
686
|
+
* @see REQ-WSL-005
|
|
687
|
+
*/
|
|
688
|
+
getRecentLearningCycles(limit = 10) {
|
|
689
|
+
const db = this.getDb();
|
|
690
|
+
const stmt = db.prepare(`
|
|
691
|
+
SELECT * FROM learning_cycles
|
|
692
|
+
ORDER BY completed_at DESC
|
|
693
|
+
LIMIT ?
|
|
694
|
+
`);
|
|
695
|
+
const rows = stmt.all(limit);
|
|
696
|
+
return rows.map(row => this.rowToLearningCycle(row));
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Get learning statistics
|
|
700
|
+
* @see REQ-WSL-005
|
|
701
|
+
*/
|
|
702
|
+
getLearningStats() {
|
|
703
|
+
const db = this.getDb();
|
|
704
|
+
// Pattern count
|
|
705
|
+
const totalPatterns = this.getPatternCount();
|
|
706
|
+
// Patterns by category
|
|
707
|
+
const categoryRows = db.prepare('SELECT category, COUNT(*) as count FROM patterns GROUP BY category').all();
|
|
708
|
+
const patternsByCategory = {};
|
|
709
|
+
for (const row of categoryRows) {
|
|
710
|
+
patternsByCategory[row.category] = row.count;
|
|
711
|
+
}
|
|
712
|
+
// Average confidence
|
|
713
|
+
const avgResult = db.prepare('SELECT AVG(confidence) as avg FROM patterns').get();
|
|
714
|
+
const averageConfidence = avgResult.avg ?? 0;
|
|
715
|
+
// Total cycles
|
|
716
|
+
const totalCycles = db.prepare('SELECT COUNT(*) as count FROM learning_cycles').get().count;
|
|
717
|
+
// Last cycle
|
|
718
|
+
const lastCycleRow = db.prepare('SELECT MAX(completed_at) as last FROM learning_cycles').get();
|
|
719
|
+
const lastCycleAt = lastCycleRow.last ? new Date(lastCycleRow.last) : undefined;
|
|
720
|
+
return {
|
|
721
|
+
totalPatterns,
|
|
722
|
+
patternsByCategory: patternsByCategory,
|
|
723
|
+
averageConfidence,
|
|
724
|
+
totalCycles,
|
|
725
|
+
lastCycleAt,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
// ============================================================
|
|
729
|
+
// KGPR Local Operations (TSK-KGPR-004)
|
|
730
|
+
// @see REQ-KGPR-001
|
|
731
|
+
// ============================================================
|
|
732
|
+
/**
|
|
733
|
+
* Insert a local KGPR record
|
|
734
|
+
*/
|
|
735
|
+
insertKGPR(kgpr) {
|
|
736
|
+
const db = this.getDb();
|
|
737
|
+
const stmt = db.prepare(`
|
|
738
|
+
INSERT INTO kgprs (id, title, description, status, author, namespace, diff_json, privacy_level, entity_types, created_at, updated_at, submitted_at)
|
|
739
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
740
|
+
`);
|
|
741
|
+
stmt.run(kgpr.id, kgpr.title, kgpr.description ?? null, kgpr.status, kgpr.author, kgpr.namespace ?? '*', kgpr.diffJson, kgpr.privacyLevel, kgpr.entityTypesJson ?? '[]', kgpr.createdAt, kgpr.updatedAt ?? kgpr.createdAt, kgpr.submittedAt ?? null);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Get a KGPR by ID
|
|
745
|
+
*/
|
|
746
|
+
getKGPR(id) {
|
|
747
|
+
const db = this.getDb();
|
|
748
|
+
const stmt = db.prepare('SELECT * FROM kgprs WHERE id = ?');
|
|
749
|
+
const row = stmt.get(id);
|
|
750
|
+
return row ? this.rowToKGPR(row) : null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Update KGPR status
|
|
754
|
+
*/
|
|
755
|
+
updateKGPRStatus(id, status, timestamp) {
|
|
756
|
+
const db = this.getDb();
|
|
757
|
+
const now = timestamp ?? new Date();
|
|
758
|
+
let timestampField = '';
|
|
759
|
+
switch (status) {
|
|
760
|
+
case 'open':
|
|
761
|
+
timestampField = 'submitted_at';
|
|
762
|
+
break;
|
|
763
|
+
case 'approved':
|
|
764
|
+
case 'changes_requested':
|
|
765
|
+
timestampField = 'reviewed_at';
|
|
766
|
+
break;
|
|
767
|
+
case 'merged':
|
|
768
|
+
timestampField = 'merged_at';
|
|
769
|
+
break;
|
|
770
|
+
case 'closed':
|
|
771
|
+
timestampField = 'closed_at';
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
if (timestampField) {
|
|
775
|
+
const stmt = db.prepare(`UPDATE kgprs SET status = ?, ${timestampField} = ? WHERE id = ?`);
|
|
776
|
+
stmt.run(status, now.toISOString(), id);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
const stmt = db.prepare('UPDATE kgprs SET status = ? WHERE id = ?');
|
|
780
|
+
stmt.run(status, id);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* List KGPRs with optional filters
|
|
785
|
+
*/
|
|
786
|
+
listKGPRs(options) {
|
|
787
|
+
const db = this.getDb();
|
|
788
|
+
let sql = 'SELECT * FROM kgprs WHERE 1=1';
|
|
789
|
+
const params = [];
|
|
790
|
+
if (options?.status) {
|
|
791
|
+
sql += ' AND status = ?';
|
|
792
|
+
params.push(options.status);
|
|
793
|
+
}
|
|
794
|
+
if (options?.namespace) {
|
|
795
|
+
sql += ' AND namespace LIKE ?';
|
|
796
|
+
params.push(`${options.namespace}%`);
|
|
797
|
+
}
|
|
798
|
+
sql += ' ORDER BY created_at DESC';
|
|
799
|
+
if (options?.limit) {
|
|
800
|
+
sql += ' LIMIT ?';
|
|
801
|
+
params.push(options.limit);
|
|
802
|
+
if (options?.offset) {
|
|
803
|
+
sql += ' OFFSET ?';
|
|
804
|
+
params.push(options.offset);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const stmt = db.prepare(sql);
|
|
808
|
+
const rows = stmt.all(...params);
|
|
809
|
+
return rows.map(row => this.rowToKGPR(row));
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Insert a KGPR review
|
|
813
|
+
*/
|
|
814
|
+
insertKGPRReview(review) {
|
|
815
|
+
const db = this.getDb();
|
|
816
|
+
const stmt = db.prepare(`
|
|
817
|
+
INSERT INTO kgpr_reviews (id, kgpr_id, reviewer, action, comment, created_at)
|
|
818
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
819
|
+
`);
|
|
820
|
+
stmt.run(review.id, review.kgprId, review.reviewer, review.action, review.comment ?? null, review.createdAt);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Get reviews for a KGPR
|
|
824
|
+
*/
|
|
825
|
+
getKGPRReviews(kgprId) {
|
|
826
|
+
const db = this.getDb();
|
|
827
|
+
const stmt = db.prepare('SELECT * FROM kgpr_reviews WHERE kgpr_id = ? ORDER BY created_at DESC');
|
|
828
|
+
const rows = stmt.all(kgprId);
|
|
829
|
+
return rows.map(row => ({
|
|
830
|
+
id: row.id,
|
|
831
|
+
kgprId: row.kgpr_id,
|
|
832
|
+
reviewer: row.reviewer,
|
|
833
|
+
action: row.action,
|
|
834
|
+
comment: row.comment ?? undefined,
|
|
835
|
+
createdAt: new Date(row.created_at),
|
|
836
|
+
}));
|
|
837
|
+
}
|
|
838
|
+
// ============================================================
|
|
839
|
+
// Transaction Support (TSK-NFR-002)
|
|
840
|
+
// @see REQ-NFR-002
|
|
841
|
+
// ============================================================
|
|
842
|
+
/**
|
|
843
|
+
* Execute operations within a transaction
|
|
844
|
+
* @see REQ-NFR-002
|
|
845
|
+
*/
|
|
846
|
+
withTransaction(fn) {
|
|
847
|
+
const db = this.getDb();
|
|
848
|
+
db.exec('BEGIN TRANSACTION');
|
|
849
|
+
try {
|
|
850
|
+
const result = fn();
|
|
851
|
+
db.exec('COMMIT');
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
db.exec('ROLLBACK');
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Execute async operations within a transaction
|
|
861
|
+
* @see REQ-NFR-002
|
|
862
|
+
*/
|
|
863
|
+
async withTransactionAsync(fn) {
|
|
864
|
+
const db = this.getDb();
|
|
865
|
+
db.exec('BEGIN TRANSACTION');
|
|
866
|
+
try {
|
|
867
|
+
const result = await fn();
|
|
868
|
+
db.exec('COMMIT');
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
db.exec('ROLLBACK');
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Helper methods
|
|
877
|
+
rowToEntity(row) {
|
|
878
|
+
return {
|
|
879
|
+
id: row.id,
|
|
880
|
+
type: row.type,
|
|
881
|
+
name: row.name,
|
|
882
|
+
namespace: row.namespace,
|
|
883
|
+
filePath: row.file_path ?? undefined,
|
|
884
|
+
line: row.line ?? undefined,
|
|
885
|
+
description: row.description ?? undefined,
|
|
886
|
+
metadata: JSON.parse(row.metadata),
|
|
887
|
+
createdAt: new Date(row.created_at),
|
|
888
|
+
updatedAt: new Date(row.updated_at),
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
rowToRelationship(row) {
|
|
892
|
+
return {
|
|
893
|
+
id: row.id,
|
|
894
|
+
sourceId: row.source_id,
|
|
895
|
+
targetId: row.target_id,
|
|
896
|
+
type: row.type,
|
|
897
|
+
weight: row.weight,
|
|
898
|
+
metadata: JSON.parse(row.metadata),
|
|
899
|
+
createdAt: new Date(row.created_at),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
logChange(entityId, relationshipId, operation, data) {
|
|
903
|
+
const db = this.getDb();
|
|
904
|
+
const stmt = db.prepare(`
|
|
905
|
+
INSERT INTO change_log (entity_id, relationship_id, operation, timestamp, data)
|
|
906
|
+
VALUES (?, ?, ?, ?, ?)
|
|
907
|
+
`);
|
|
908
|
+
stmt.run(entityId, relationshipId, operation, new Date().toISOString(), data ? JSON.stringify(data) : null);
|
|
909
|
+
}
|
|
910
|
+
rowToPattern(row) {
|
|
911
|
+
return {
|
|
912
|
+
id: row.id,
|
|
913
|
+
name: row.name,
|
|
914
|
+
category: row.category,
|
|
915
|
+
content: row.content,
|
|
916
|
+
ast: row.ast ? JSON.parse(row.ast) : undefined,
|
|
917
|
+
confidence: row.confidence,
|
|
918
|
+
occurrences: row.occurrences,
|
|
919
|
+
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
|
920
|
+
usageCount: row.usage_count,
|
|
921
|
+
source: row.source ?? undefined,
|
|
922
|
+
metadata: JSON.parse(row.metadata),
|
|
923
|
+
createdAt: new Date(row.created_at),
|
|
924
|
+
updatedAt: new Date(row.updated_at),
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
rowToLearningCycle(row) {
|
|
928
|
+
return {
|
|
929
|
+
id: row.id,
|
|
930
|
+
wakeExtracted: row.wake_extracted,
|
|
931
|
+
wakeObservedFiles: row.wake_observed_files,
|
|
932
|
+
sleepClustered: row.sleep_clustered,
|
|
933
|
+
sleepDecayed: row.sleep_decayed,
|
|
934
|
+
durationMs: row.duration_ms,
|
|
935
|
+
metadata: JSON.parse(row.metadata),
|
|
936
|
+
completedAt: new Date(row.completed_at),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
rowToKGPR(row) {
|
|
940
|
+
return {
|
|
941
|
+
id: row.id,
|
|
942
|
+
title: row.title,
|
|
943
|
+
description: row.description ?? undefined,
|
|
944
|
+
status: row.status,
|
|
945
|
+
author: row.author,
|
|
946
|
+
namespace: row.namespace,
|
|
947
|
+
diffJson: row.diff_json,
|
|
948
|
+
privacyLevel: row.privacy_level,
|
|
949
|
+
entityTypesJson: row.entity_types,
|
|
950
|
+
createdAt: new Date(row.created_at),
|
|
951
|
+
updatedAt: new Date(row.updated_at),
|
|
952
|
+
submittedAt: row.submitted_at ? new Date(row.submitted_at) : undefined,
|
|
953
|
+
reviewedAt: row.reviewed_at ? new Date(row.reviewed_at) : undefined,
|
|
954
|
+
mergedAt: row.merged_at ? new Date(row.merged_at) : undefined,
|
|
955
|
+
closedAt: row.closed_at ? new Date(row.closed_at) : undefined,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
//# sourceMappingURL=database.js.map
|