@aprimediet/codewalker 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db.test.ts +148 -2
- package/src/db.ts +196 -29
- package/src/format.test.ts +52 -0
- package/src/format.ts +6 -0
- package/src/index.contract.test.ts +46 -1
- package/src/index.ts +66 -17
- package/src/libs/cards.test.ts +86 -0
- package/src/libs/cards.ts +53 -0
- package/src/libs/dts.test.ts +269 -0
- package/src/libs/dts.ts +213 -0
- package/src/libs/indexer.test.ts +236 -0
- package/src/libs/indexer.ts +291 -0
- package/src/libs/resolve.test.ts +218 -0
- package/src/libs/resolve.ts +120 -0
- package/src/project.ts +3 -1
- package/src/query.test.ts +72 -1
- package/src/query.ts +23 -7
- package/src/types.ts +28 -0
package/package.json
CHANGED
package/src/db.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
|
-
import { openDb, bootstrapDb, upsertSymbol, searchSymbols, getMeta, setMeta, deleteFileSymbols } from './db.ts';
|
|
6
|
+
import { openDb, bootstrapDb, upsertSymbol, searchSymbols, getMeta, setMeta, deleteFileSymbols, upsertLibrary, upsertLibSymbol, deleteLibrary, searchLibSymbols } from './db.ts';
|
|
7
7
|
|
|
8
8
|
describe('db.ts', () => {
|
|
9
9
|
let tmpDir: string;
|
|
@@ -51,7 +51,41 @@ describe('db.ts', () => {
|
|
|
51
51
|
const db = new Database(dbPath);
|
|
52
52
|
bootstrapDb(db);
|
|
53
53
|
const version = db.pragma('user_version', { simple: true }) as number;
|
|
54
|
-
expect(version).toBe(
|
|
54
|
+
expect(version).toBe(2);
|
|
55
|
+
db.close();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('creates libraries, lib_symbols, and lib_symbols_fts tables', () => {
|
|
59
|
+
const db = openDb(dbPath);
|
|
60
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
|
61
|
+
const tableNames = tables.map(t => t.name);
|
|
62
|
+
expect(tableNames).toContain('libraries');
|
|
63
|
+
expect(tableNames).toContain('lib_symbols');
|
|
64
|
+
|
|
65
|
+
const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='lib_symbols_fts'").all() as { name: string }[];
|
|
66
|
+
expect(ftsTables.length).toBe(1);
|
|
67
|
+
db.close();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not destroy existing symbols tables (additive upgrade)', () => {
|
|
71
|
+
// Bootstrap v1 DB first, then re-bootstrap (simulate upgrade)
|
|
72
|
+
const db = new Database(dbPath);
|
|
73
|
+
bootstrapDb(db);
|
|
74
|
+
upsertSymbol(db, {
|
|
75
|
+
name: 'keep', kind: 'function', file_path: 'src/a.ts',
|
|
76
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Call bootstrapDb again (simulates upgrade to v2 adds lib tables)
|
|
80
|
+
bootstrapDb(db);
|
|
81
|
+
|
|
82
|
+
// Symbol still there
|
|
83
|
+
const symbols = searchSymbols(db, 'keep', undefined, 10);
|
|
84
|
+
expect(symbols).toHaveLength(1);
|
|
85
|
+
|
|
86
|
+
// Lib tables exist
|
|
87
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
|
88
|
+
expect(tables.map(t => t.name)).toContain('lib_symbols');
|
|
55
89
|
db.close();
|
|
56
90
|
});
|
|
57
91
|
});
|
|
@@ -173,6 +207,118 @@ describe('db.ts', () => {
|
|
|
173
207
|
});
|
|
174
208
|
});
|
|
175
209
|
|
|
210
|
+
describe('library CRUD', () => {
|
|
211
|
+
it('upsertLibrary inserts or updates a library record', () => {
|
|
212
|
+
const db = openDb(dbPath);
|
|
213
|
+
upsertLibrary(db, { name: 'hono', version: '4.6.3', source: 'node_modules', dts_path: '/a.d.ts', readme: 'Hono web framework' });
|
|
214
|
+
|
|
215
|
+
const row = db.prepare("SELECT * FROM libraries WHERE name = ?").get('hono') as any;
|
|
216
|
+
expect(row).not.toBeUndefined();
|
|
217
|
+
expect(row.version).toBe('4.6.3');
|
|
218
|
+
expect(row.source).toBe('node_modules');
|
|
219
|
+
db.close();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('upsertLibSymbol inserts a lib symbol and it is FTS-searchable', () => {
|
|
223
|
+
const db = openDb(dbPath);
|
|
224
|
+
upsertLibSymbol(db, {
|
|
225
|
+
lib: 'hono', version: '4.6.3', name: 'createMiddleware',
|
|
226
|
+
kind: 'function', signature: 'export declare function createMiddleware(...)',
|
|
227
|
+
doc: 'Define a typed middleware handler.', summary: 'Define a typed middleware handler.',
|
|
228
|
+
card_path: '/cards/hono/createMiddleware.md',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const results = searchLibSymbols(db, 'createMiddleware', undefined, 10);
|
|
232
|
+
expect(results).toHaveLength(1);
|
|
233
|
+
expect(results[0]!.name).toBe('createMiddleware');
|
|
234
|
+
expect(results[0]!.lib).toBe('hono');
|
|
235
|
+
expect(results[0]!.version).toBe('4.6.3');
|
|
236
|
+
expect(results[0]!.source).toBe('lib');
|
|
237
|
+
|
|
238
|
+
db.close();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('searchLibSymbols empty query returns all symbols ordered by name', () => {
|
|
242
|
+
const db = openDb(dbPath);
|
|
243
|
+
upsertLibSymbol(db, {
|
|
244
|
+
lib: 'hono', version: '4.6.3', name: 'zMiddleware',
|
|
245
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
246
|
+
});
|
|
247
|
+
upsertLibSymbol(db, {
|
|
248
|
+
lib: 'hono', version: '4.6.3', name: 'aRouter',
|
|
249
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const results = searchLibSymbols(db, '', undefined, 10);
|
|
253
|
+
expect(results).toHaveLength(2);
|
|
254
|
+
expect(results[0]!.name).toBe('aRouter');
|
|
255
|
+
expect(results[1]!.name).toBe('zMiddleware');
|
|
256
|
+
db.close();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('deleteLibrary removes symbols and FTS rows for all versions of a lib', () => {
|
|
260
|
+
const db = openDb(dbPath);
|
|
261
|
+
|
|
262
|
+
upsertLibSymbol(db, {
|
|
263
|
+
lib: 'hono', version: '4.6.3', name: 'createMiddleware',
|
|
264
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
265
|
+
});
|
|
266
|
+
upsertLibSymbol(db, {
|
|
267
|
+
lib: 'hono', version: '4.5.0', name: 'oldFunc',
|
|
268
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
deleteLibrary(db, 'hono');
|
|
272
|
+
|
|
273
|
+
const results = searchLibSymbols(db, '', undefined, 10);
|
|
274
|
+
expect(results).toHaveLength(0);
|
|
275
|
+
|
|
276
|
+
const libRow = db.prepare("SELECT * FROM libraries WHERE name = ?").get('hono') as any;
|
|
277
|
+
expect(libRow).toBeUndefined();
|
|
278
|
+
|
|
279
|
+
db.close();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('re-inserting same (lib, name) does not create duplicates', () => {
|
|
283
|
+
const db = openDb(dbPath);
|
|
284
|
+
|
|
285
|
+
upsertLibSymbol(db, {
|
|
286
|
+
lib: 'hono', version: '4.6.3', name: 'createMiddleware',
|
|
287
|
+
kind: 'function', signature: 'v1', doc: '', summary: '', card_path: '',
|
|
288
|
+
});
|
|
289
|
+
upsertLibSymbol(db, {
|
|
290
|
+
lib: 'hono', version: '4.6.3', name: 'createMiddleware',
|
|
291
|
+
kind: 'function', signature: 'v2', doc: '', summary: '', card_path: '',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const results = searchLibSymbols(db, 'createMiddleware', undefined, 10);
|
|
295
|
+
expect(results).toHaveLength(1);
|
|
296
|
+
// Latest signature
|
|
297
|
+
expect(results[0]!.signature).toBe('v2');
|
|
298
|
+
|
|
299
|
+
db.close();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('kind filter narrows lib symbol results', () => {
|
|
303
|
+
const db = openDb(dbPath);
|
|
304
|
+
|
|
305
|
+
upsertLibSymbol(db, {
|
|
306
|
+
lib: 'hono', version: '4.6.3', name: 'myFunc',
|
|
307
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
308
|
+
});
|
|
309
|
+
upsertLibSymbol(db, {
|
|
310
|
+
lib: 'hono', version: '4.6.3', name: 'MyType',
|
|
311
|
+
kind: 'type', signature: '', doc: '', summary: '', card_path: '',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const funcs = searchLibSymbols(db, '', 'function', 10);
|
|
315
|
+
expect(funcs).toHaveLength(1);
|
|
316
|
+
expect(funcs[0]!.name).toBe('myFunc');
|
|
317
|
+
|
|
318
|
+
db.close();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
176
322
|
describe('meta', () => {
|
|
177
323
|
it('setMeta and getMeta round-trip values', () => {
|
|
178
324
|
const db = openDb(dbPath);
|
package/src/db.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import Database, { type Database as DatabaseType } from "better-sqlite3";
|
|
15
|
+
import type { LibSymbol } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
export { Database };
|
|
17
18
|
export type { DatabaseType };
|
|
@@ -27,7 +28,7 @@ export function openDb(dbPath: string): DatabaseType {
|
|
|
27
28
|
/** Bootstrap DDL — idempotent (all CREATE use IF NOT EXISTS). */
|
|
28
29
|
export function bootstrapDb(db: DatabaseType): void {
|
|
29
30
|
db.exec(`
|
|
30
|
-
PRAGMA user_version =
|
|
31
|
+
PRAGMA user_version = 2;
|
|
31
32
|
|
|
32
33
|
CREATE TABLE IF NOT EXISTS files (
|
|
33
34
|
path TEXT PRIMARY KEY,
|
|
@@ -55,7 +56,70 @@ export function bootstrapDb(db: DatabaseType): void {
|
|
|
55
56
|
tokenize='unicode61 remove_diacritics 2'
|
|
56
57
|
);
|
|
57
58
|
|
|
59
|
+
CREATE TABLE IF NOT EXISTS libraries (
|
|
60
|
+
name TEXT NOT NULL,
|
|
61
|
+
version TEXT NOT NULL,
|
|
62
|
+
source TEXT,
|
|
63
|
+
dts_path TEXT,
|
|
64
|
+
readme TEXT,
|
|
65
|
+
indexed_at TEXT,
|
|
66
|
+
PRIMARY KEY (name, version)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS lib_symbols (
|
|
70
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
71
|
+
lib TEXT NOT NULL,
|
|
72
|
+
version TEXT NOT NULL,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
kind TEXT,
|
|
75
|
+
signature TEXT,
|
|
76
|
+
doc TEXT,
|
|
77
|
+
summary TEXT,
|
|
78
|
+
card_path TEXT
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lib_symbols_fts USING fts5(
|
|
82
|
+
name, signature, doc, summary,
|
|
83
|
+
content='lib_symbols', content_rowid='id',
|
|
84
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
85
|
+
);
|
|
86
|
+
|
|
58
87
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
88
|
+
|
|
89
|
+
-- Keep the external-content FTS indexes in sync via triggers (the SQLite-documented
|
|
90
|
+
-- pattern). Code must do plain INSERT/UPDATE/DELETE on the base tables and NEVER touch
|
|
91
|
+
-- the *_fts tables directly: the 'delete' command needs the ORIGINAL column values, which
|
|
92
|
+
-- only the triggers (via old.*) have. Hand-rolled FTS maintenance with empty/placeholder
|
|
93
|
+
-- values corrupts the index ("database disk image is malformed").
|
|
94
|
+
CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN
|
|
95
|
+
INSERT INTO symbols_fts(rowid, name, signature, doc, summary)
|
|
96
|
+
VALUES (new.id, new.name, new.signature, new.doc, new.summary);
|
|
97
|
+
END;
|
|
98
|
+
CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN
|
|
99
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, doc, summary)
|
|
100
|
+
VALUES ('delete', old.id, old.name, old.signature, old.doc, old.summary);
|
|
101
|
+
END;
|
|
102
|
+
CREATE TRIGGER IF NOT EXISTS symbols_au AFTER UPDATE ON symbols BEGIN
|
|
103
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, doc, summary)
|
|
104
|
+
VALUES ('delete', old.id, old.name, old.signature, old.doc, old.summary);
|
|
105
|
+
INSERT INTO symbols_fts(rowid, name, signature, doc, summary)
|
|
106
|
+
VALUES (new.id, new.name, new.signature, new.doc, new.summary);
|
|
107
|
+
END;
|
|
108
|
+
|
|
109
|
+
CREATE TRIGGER IF NOT EXISTS lib_symbols_ai AFTER INSERT ON lib_symbols BEGIN
|
|
110
|
+
INSERT INTO lib_symbols_fts(rowid, name, signature, doc, summary)
|
|
111
|
+
VALUES (new.id, new.name, new.signature, new.doc, new.summary);
|
|
112
|
+
END;
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS lib_symbols_ad AFTER DELETE ON lib_symbols BEGIN
|
|
114
|
+
INSERT INTO lib_symbols_fts(lib_symbols_fts, rowid, name, signature, doc, summary)
|
|
115
|
+
VALUES ('delete', old.id, old.name, old.signature, old.doc, old.summary);
|
|
116
|
+
END;
|
|
117
|
+
CREATE TRIGGER IF NOT EXISTS lib_symbols_au AFTER UPDATE ON lib_symbols BEGIN
|
|
118
|
+
INSERT INTO lib_symbols_fts(lib_symbols_fts, rowid, name, signature, doc, summary)
|
|
119
|
+
VALUES ('delete', old.id, old.name, old.signature, old.doc, old.summary);
|
|
120
|
+
INSERT INTO lib_symbols_fts(rowid, name, signature, doc, summary)
|
|
121
|
+
VALUES (new.id, new.name, new.signature, new.doc, new.summary);
|
|
122
|
+
END;
|
|
59
123
|
`);
|
|
60
124
|
}
|
|
61
125
|
|
|
@@ -93,42 +157,19 @@ export function upsertSymbol(
|
|
|
93
157
|
card_path: string;
|
|
94
158
|
},
|
|
95
159
|
): void {
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
"SELECT id FROM symbols WHERE name = ? AND file_path = ?",
|
|
100
|
-
).get(symbol.name, symbol.file_path) as { id: number } | undefined;
|
|
101
|
-
|
|
102
|
-
if (existing) {
|
|
103
|
-
// FTS external content: must delete old content row
|
|
104
|
-
db.prepare("INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, doc, summary) VALUES ('delete', ?, '', '', '', '')").run(existing.id);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const result = db.prepare(
|
|
160
|
+
// Plain INSERT — the symbols_ai trigger keeps symbols_fts in sync. Callers (scan/sync) always
|
|
161
|
+
// delete a file's symbols before re-inserting, so re-indexing is idempotent without an update path.
|
|
162
|
+
db.prepare(
|
|
108
163
|
`INSERT INTO symbols (name, kind, file_path, line_start, line_end, signature, doc, summary, card_path)
|
|
109
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
110
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
111
|
-
kind=excluded.kind, line_start=excluded.line_start, line_end=excluded.line_end,
|
|
112
|
-
signature=excluded.signature, doc=excluded.doc, summary=excluded.summary, card_path=excluded.card_path`,
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
113
165
|
).run(
|
|
114
166
|
symbol.name, symbol.kind, symbol.file_path, symbol.line_start, symbol.line_end,
|
|
115
167
|
symbol.signature, symbol.doc, symbol.summary, symbol.card_path,
|
|
116
168
|
);
|
|
117
|
-
|
|
118
|
-
// Insert into FTS
|
|
119
|
-
const rowId = existing?.id ?? (result.lastInsertRowid as number);
|
|
120
|
-
db.prepare(
|
|
121
|
-
`INSERT INTO symbols_fts(rowid, name, signature, doc, summary)
|
|
122
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
123
|
-
).run(rowId, symbol.name, symbol.signature, symbol.doc, symbol.summary);
|
|
124
169
|
}
|
|
125
170
|
|
|
126
|
-
/** Delete all symbols for a given file. */
|
|
171
|
+
/** Delete all symbols for a given file. The symbols_ad trigger removes their FTS rows. */
|
|
127
172
|
export function deleteFileSymbols(db: DatabaseType, filePath: string): void {
|
|
128
|
-
const rows = db.prepare("SELECT id FROM symbols WHERE file_path = ?").all(filePath) as { id: number }[];
|
|
129
|
-
for (const row of rows) {
|
|
130
|
-
db.prepare("INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, doc, summary) VALUES ('delete', ?, '', '', '', '')").run(row.id);
|
|
131
|
-
}
|
|
132
173
|
db.prepare("DELETE FROM symbols WHERE file_path = ?").run(filePath);
|
|
133
174
|
}
|
|
134
175
|
|
|
@@ -182,6 +223,132 @@ export function searchSymbols(
|
|
|
182
223
|
return db.prepare(sql).all(...params) as typeof results;
|
|
183
224
|
}
|
|
184
225
|
|
|
226
|
+
/** Upsert a library record. */
|
|
227
|
+
export function upsertLibrary(
|
|
228
|
+
db: DatabaseType,
|
|
229
|
+
pkg: { name: string; version: string; source?: string; dts_path?: string | null; readme?: string | null },
|
|
230
|
+
): void {
|
|
231
|
+
db.prepare(
|
|
232
|
+
`INSERT INTO libraries (name, version, source, dts_path, readme, indexed_at)
|
|
233
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
234
|
+
ON CONFLICT(name, version) DO UPDATE SET
|
|
235
|
+
source=excluded.source, dts_path=excluded.dts_path, readme=excluded.readme, indexed_at=excluded.indexed_at`,
|
|
236
|
+
).run(pkg.name, pkg.version, pkg.source ?? null, pkg.dts_path ?? null, pkg.readme ?? null);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Upsert a lib symbol. Replaces on (lib, name) duplicate. */
|
|
240
|
+
export function upsertLibSymbol(
|
|
241
|
+
db: DatabaseType,
|
|
242
|
+
symbol: {
|
|
243
|
+
lib: string;
|
|
244
|
+
version: string;
|
|
245
|
+
name: string;
|
|
246
|
+
kind: string;
|
|
247
|
+
signature: string;
|
|
248
|
+
doc: string;
|
|
249
|
+
summary: string;
|
|
250
|
+
card_path: string;
|
|
251
|
+
},
|
|
252
|
+
): void {
|
|
253
|
+
// The lib_symbols_ai / _au triggers keep lib_symbols_fts in sync — never touch the FTS table here.
|
|
254
|
+
const existing = db.prepare(
|
|
255
|
+
"SELECT id FROM lib_symbols WHERE lib = ? AND name = ?",
|
|
256
|
+
).get(symbol.lib, symbol.name) as { id: number } | undefined;
|
|
257
|
+
|
|
258
|
+
if (existing) {
|
|
259
|
+
db.prepare(
|
|
260
|
+
`UPDATE lib_symbols SET version=?, kind=?, signature=?, doc=?, summary=?, card_path=?
|
|
261
|
+
WHERE id = ?`,
|
|
262
|
+
).run(
|
|
263
|
+
symbol.version, symbol.kind, symbol.signature,
|
|
264
|
+
symbol.doc, symbol.summary, symbol.card_path,
|
|
265
|
+
existing.id,
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
db.prepare(
|
|
269
|
+
`INSERT INTO lib_symbols (lib, version, name, kind, signature, doc, summary, card_path)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
271
|
+
).run(
|
|
272
|
+
symbol.lib, symbol.version, symbol.name, symbol.kind,
|
|
273
|
+
symbol.signature, symbol.doc, symbol.summary, symbol.card_path,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Delete all lib_symbols (all versions) for a library, then the libraries rows.
|
|
279
|
+
* The lib_symbols_ad trigger removes the FTS rows. */
|
|
280
|
+
export function deleteLibrary(db: DatabaseType, libName: string): void {
|
|
281
|
+
db.prepare("DELETE FROM lib_symbols WHERE lib = ?").run(libName);
|
|
282
|
+
db.prepare("DELETE FROM libraries WHERE name = ?").run(libName);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Search lib symbols via FTS5 MATCH, ranked by bm25.
|
|
287
|
+
* Empty query returns all symbols ordered by name.
|
|
288
|
+
*/
|
|
289
|
+
export function searchLibSymbols(
|
|
290
|
+
db: DatabaseType,
|
|
291
|
+
query: string,
|
|
292
|
+
kindFilter?: string,
|
|
293
|
+
limit = 10,
|
|
294
|
+
): Array<{
|
|
295
|
+
id: number;
|
|
296
|
+
name: string;
|
|
297
|
+
kind: string;
|
|
298
|
+
lib: string;
|
|
299
|
+
version: string;
|
|
300
|
+
file_path: string;
|
|
301
|
+
line_start: number;
|
|
302
|
+
line_end: number;
|
|
303
|
+
signature: string;
|
|
304
|
+
summary: string;
|
|
305
|
+
score: number;
|
|
306
|
+
source: "lib";
|
|
307
|
+
}> {
|
|
308
|
+
if (!query.trim()) {
|
|
309
|
+
let sql = "SELECT s.id, s.name, s.kind, s.lib, s.version, s.signature, s.summary, 0.0 as score FROM lib_symbols s";
|
|
310
|
+
const params: unknown[] = [];
|
|
311
|
+
if (kindFilter) {
|
|
312
|
+
sql += " WHERE s.kind = ?";
|
|
313
|
+
params.push(kindFilter);
|
|
314
|
+
}
|
|
315
|
+
sql += " ORDER BY s.name LIMIT ?";
|
|
316
|
+
params.push(limit);
|
|
317
|
+
return db.prepare(sql).all(...params).map((r: any) => ({
|
|
318
|
+
...r,
|
|
319
|
+
file_path: "",
|
|
320
|
+
line_start: 0,
|
|
321
|
+
line_end: 0,
|
|
322
|
+
source: "lib" as const,
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let sql = `
|
|
327
|
+
SELECT s.id, s.name, s.kind, s.lib, s.version, s.signature, s.summary,
|
|
328
|
+
bm25(lib_symbols_fts, 10.0, 5.0, 1.0, 8.0) as score
|
|
329
|
+
FROM lib_symbols_fts
|
|
330
|
+
JOIN lib_symbols s ON s.id = lib_symbols_fts.rowid
|
|
331
|
+
WHERE lib_symbols_fts MATCH ?
|
|
332
|
+
`;
|
|
333
|
+
const params: unknown[] = [query];
|
|
334
|
+
|
|
335
|
+
if (kindFilter) {
|
|
336
|
+
sql += " AND s.kind = ?";
|
|
337
|
+
params.push(kindFilter);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
sql += " ORDER BY score LIMIT ?";
|
|
341
|
+
params.push(limit);
|
|
342
|
+
|
|
343
|
+
return db.prepare(sql).all(...params).map((r: any) => ({
|
|
344
|
+
...r,
|
|
345
|
+
file_path: "",
|
|
346
|
+
line_start: 0,
|
|
347
|
+
line_end: 0,
|
|
348
|
+
source: "lib" as const,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
185
352
|
/** Get a meta value. */
|
|
186
353
|
export function getMeta(db: DatabaseType, key: string): string | null {
|
|
187
354
|
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key) as { value: string } | undefined;
|
package/src/format.test.ts
CHANGED
|
@@ -2,6 +2,24 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { formatCompact, formatCardBody } from './format.ts';
|
|
3
3
|
import type { QueryResultRow, StalenessInfo } from './types.ts';
|
|
4
4
|
|
|
5
|
+
function makeLibRow(overrides: Partial<QueryResultRow> = {}): QueryResultRow {
|
|
6
|
+
return {
|
|
7
|
+
id: 100,
|
|
8
|
+
name: 'createMiddleware',
|
|
9
|
+
kind: 'function',
|
|
10
|
+
file_path: 'hono/dist/helper.d.ts',
|
|
11
|
+
line_start: 0,
|
|
12
|
+
line_end: 0,
|
|
13
|
+
signature: 'export declare function createMiddleware<E>(...): MiddlewareHandler',
|
|
14
|
+
summary: 'Define a typed middleware handler.',
|
|
15
|
+
score: 0.3,
|
|
16
|
+
source: 'lib',
|
|
17
|
+
lib: 'hono',
|
|
18
|
+
version: '4.6.3',
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
function makeRow(overrides: Partial<QueryResultRow> = {}): QueryResultRow {
|
|
6
24
|
return {
|
|
7
25
|
id: 1,
|
|
@@ -63,6 +81,40 @@ describe('formatCompact', () => {
|
|
|
63
81
|
});
|
|
64
82
|
});
|
|
65
83
|
|
|
84
|
+
describe('formatCompact with lib rows', () => {
|
|
85
|
+
it('renders a lib row with [lib@version] origin tag', () => {
|
|
86
|
+
const rows = [makeLibRow()];
|
|
87
|
+
const result = formatCompact(rows, null);
|
|
88
|
+
expect(result).toContain('createMiddleware');
|
|
89
|
+
expect(result).toContain('function');
|
|
90
|
+
expect(result).toContain('[hono@4.6.3]');
|
|
91
|
+
expect(result).toContain('Define a typed middleware handler.');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders mixed code and lib rows', () => {
|
|
95
|
+
const libRow = makeLibRow();
|
|
96
|
+
const codeRow: QueryResultRow = {
|
|
97
|
+
id: 1, name: 'myFunc', kind: 'function',
|
|
98
|
+
file_path: 'src/util/helper.ts', line_start: 10, line_end: 20,
|
|
99
|
+
signature: '(x: number) => string', summary: 'Does something', score: 0.5,
|
|
100
|
+
};
|
|
101
|
+
const result = formatCompact([codeRow, libRow], null);
|
|
102
|
+
expect(result).toContain('helper.ts:10-20');
|
|
103
|
+
expect(result).toContain('[hono@4.6.3]');
|
|
104
|
+
// Two lines
|
|
105
|
+
expect(result.split('\n')).toHaveLength(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('bounded output for lib rows (one line per hit)', () => {
|
|
109
|
+
const rows = Array.from({ length: 5 }, (_, i) =>
|
|
110
|
+
makeLibRow({ name: `fn${i}`, lib: 'test-pkg', version: '1.0.0' })
|
|
111
|
+
);
|
|
112
|
+
const result = formatCompact(rows, null);
|
|
113
|
+
expect(result.split('\n')).toHaveLength(5);
|
|
114
|
+
expect(result).toContain('[test-pkg@1.0.0]');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
66
118
|
describe('formatCardBody', () => {
|
|
67
119
|
it('returns the card body text', () => {
|
|
68
120
|
const body = '# myFunc\n\nDoes something.\n';
|
package/src/format.ts
CHANGED
|
@@ -27,6 +27,12 @@ export function formatCompact(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const lines = rows.map((row) => {
|
|
30
|
+
if (row.source === "lib" && row.lib && row.version) {
|
|
31
|
+
const origin = `[${row.lib}@${row.version}]`;
|
|
32
|
+
const summary = truncate(row.summary || "", SUMMARY_MAX);
|
|
33
|
+
const loc = row.file_path ? `${basename(row.file_path)}:${row.line_start}-${row.line_end}` : `lib`;
|
|
34
|
+
return `${row.name} · ${row.kind} · ${origin} · ${loc} · ${summary}`;
|
|
35
|
+
}
|
|
30
36
|
const loc = `${basename(row.file_path)}:${row.line_start}-${row.line_end}`;
|
|
31
37
|
const summary = truncate(row.summary || "", SUMMARY_MAX);
|
|
32
38
|
return `${row.name} · ${row.kind} · ${loc} · ${summary}`;
|
|
@@ -58,10 +58,15 @@ describe('index.ts contract', () => {
|
|
|
58
58
|
expect(queryTool).toBeDefined();
|
|
59
59
|
expect(queryTool!.description).toContain('code index');
|
|
60
60
|
|
|
61
|
+
// Check tool has a source parameter
|
|
62
|
+
const toolParams = (queryTool!.parameters as any);
|
|
63
|
+
expect(toolParams.properties).toHaveProperty('source');
|
|
64
|
+
|
|
61
65
|
// Check command registered
|
|
62
66
|
const cmd = stub.commands.find(c => c.name === 'codewalker');
|
|
63
67
|
expect(cmd).toBeDefined();
|
|
64
|
-
expect(cmd!.description).
|
|
68
|
+
expect(cmd!.description).toContain('libs');
|
|
69
|
+
expect(cmd!.description).toContain('lib');
|
|
65
70
|
});
|
|
66
71
|
|
|
67
72
|
it('tool.execute returns { content, details } with compact text content', async () => {
|
|
@@ -97,4 +102,44 @@ describe('index.ts contract', () => {
|
|
|
97
102
|
|
|
98
103
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
99
104
|
});
|
|
105
|
+
|
|
106
|
+
it('tool.execute with source="libs" returns valid result even with no lib data', async () => {
|
|
107
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-contract-libs-'));
|
|
108
|
+
const piDir = path.join(tmpDir, '.pi');
|
|
109
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
110
|
+
const markerId = 'test-project-libs-' + Math.random().toString(36).slice(2, 8);
|
|
111
|
+
fs.writeFileSync(path.join(piDir, markerId + '.md'), `---\npi-project: true\nid: ${markerId}\n---\n`);
|
|
112
|
+
|
|
113
|
+
const homePi = path.join(os.homedir(), '.pi', 'projects', markerId, 'codewalker');
|
|
114
|
+
fs.mkdirSync(homePi, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const mod = await import('./index.ts');
|
|
117
|
+
const stub = createPiStub();
|
|
118
|
+
mod.default(stub.api as any);
|
|
119
|
+
|
|
120
|
+
const tool = stub.tools.find(t => t.name === 'codewalker_query')!;
|
|
121
|
+
const result = await tool.execute(
|
|
122
|
+
'test-id',
|
|
123
|
+
{ query: 'test', source: 'libs' },
|
|
124
|
+
new AbortController().signal,
|
|
125
|
+
() => {},
|
|
126
|
+
{ cwd: tmpDir },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result).toHaveProperty('content');
|
|
130
|
+
expect(result.content[0]!.text).toContain('No matches');
|
|
131
|
+
expect(result.details.rows).toEqual([]);
|
|
132
|
+
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('command description mentions libs and lib subcommands', async () => {
|
|
137
|
+
const mod = await import('./index.ts');
|
|
138
|
+
const stub = createPiStub();
|
|
139
|
+
mod.default(stub.api as any);
|
|
140
|
+
|
|
141
|
+
const cmd = stub.commands.find(c => c.name === 'codewalker')!;
|
|
142
|
+
expect(cmd.description).toContain('libs [--dev]');
|
|
143
|
+
expect(cmd.description).toContain('lib <pkg>');
|
|
144
|
+
});
|
|
100
145
|
});
|