@aprimediet/codewalker 1.1.0 → 1.3.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/README.md +28 -3
- package/package.json +1 -1
- package/prompts/codewalker.md +3 -1
- package/skills/codewalker/SKILL.md +118 -28
- package/src/cards.test.ts +123 -1
- package/src/cards.ts +53 -0
- package/src/db.test.ts +405 -3
- package/src/db.ts +402 -29
- package/src/enrich.test.ts +102 -0
- package/src/enrich.ts +107 -0
- package/src/format.test.ts +103 -0
- package/src/format.ts +11 -0
- package/src/index.contract.test.ts +77 -1
- package/src/index.ts +273 -19
- package/src/indexer.heal.test.ts +90 -0
- package/src/indexer.ts +9 -1
- 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/notes-cards.test.ts +99 -0
- package/src/notes-cards.ts +92 -0
- package/src/notes.test.ts +172 -0
- package/src/notes.ts +145 -0
- package/src/project.test.ts +12 -1
- package/src/project.ts +7 -1
- package/src/query.test.ts +148 -1
- package/src/query.ts +28 -8
- package/src/types.ts +44 -0
package/src/notes.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note orchestration for codewalker v1.3.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `addNote()`: render card → atomic write → upsertNote
|
|
6
|
+
* - `rebuildNotesDbFromCards()`: disposable-index rebuild from card files
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the style of src/libs/indexer.ts (atomic write, tmp + rename, mode 0o600).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { openDb, upsertNote } from "./db.ts";
|
|
14
|
+
import { renderNoteCard, parseNoteCard } from "./notes-cards.ts";
|
|
15
|
+
import type { Note, NoteKind } from "./types.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write a note: render card → atomic write → upsert DB row.
|
|
19
|
+
*
|
|
20
|
+
* The note's note_kind determines which directory to use.
|
|
21
|
+
* When `notesDir` is provided, it is used directly (backward compat).
|
|
22
|
+
* When both `glossaryDir` and `decisionsDir` are available, pass them via an options bag.
|
|
23
|
+
*/
|
|
24
|
+
export function addNote(
|
|
25
|
+
dbPath: string,
|
|
26
|
+
note: Note,
|
|
27
|
+
notesDir?: string,
|
|
28
|
+
): void {
|
|
29
|
+
const dir = notesDir;
|
|
30
|
+
if (!dir) throw new Error("notesDir is required");
|
|
31
|
+
|
|
32
|
+
// Ensure directory exists
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// Generate slug from title
|
|
36
|
+
const slug = slugifyNoteTitle(note.title);
|
|
37
|
+
const cardPath = path.join(dir, `${slug}.md`);
|
|
38
|
+
|
|
39
|
+
// Render card
|
|
40
|
+
const card = renderNoteCard(note);
|
|
41
|
+
|
|
42
|
+
// Atomic write
|
|
43
|
+
const tmpPath = cardPath + ".tmp";
|
|
44
|
+
fs.writeFileSync(tmpPath, card, { encoding: "utf-8", mode: 0o600 });
|
|
45
|
+
fs.renameSync(tmpPath, cardPath);
|
|
46
|
+
|
|
47
|
+
// Upsert DB row with the actual card_path
|
|
48
|
+
const db = openDb(dbPath);
|
|
49
|
+
try {
|
|
50
|
+
upsertNote(db, {
|
|
51
|
+
...note,
|
|
52
|
+
card_path: cardPath,
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
db.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rebuild the notes DB tables from glossary + decisions cards alone.
|
|
61
|
+
* Demonstrates the disposable-index property: cards are the source of truth.
|
|
62
|
+
*/
|
|
63
|
+
export function rebuildNotesDbFromCards(
|
|
64
|
+
dbPath: string,
|
|
65
|
+
glossaryDir: string,
|
|
66
|
+
decisionsDir: string,
|
|
67
|
+
): void {
|
|
68
|
+
const db = openDb(dbPath);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
db.exec("BEGIN TRANSACTION");
|
|
72
|
+
|
|
73
|
+
// Clear existing notes
|
|
74
|
+
db.prepare("DELETE FROM notes").run();
|
|
75
|
+
|
|
76
|
+
// Process glossary cards
|
|
77
|
+
if (fs.existsSync(glossaryDir)) {
|
|
78
|
+
processCardsInDir(db, glossaryDir, "glossary");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Process decisions cards
|
|
82
|
+
if (fs.existsSync(decisionsDir)) {
|
|
83
|
+
processCardsInDir(db, decisionsDir, "decision");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
db.exec("COMMIT");
|
|
87
|
+
} catch (e) {
|
|
88
|
+
db.exec("ROLLBACK");
|
|
89
|
+
throw e;
|
|
90
|
+
} finally {
|
|
91
|
+
db.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Internal helpers ───────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function processCardsInDir(
|
|
98
|
+
db: ReturnType<typeof openDb>,
|
|
99
|
+
dir: string,
|
|
100
|
+
expectedKind: NoteKind,
|
|
101
|
+
): void {
|
|
102
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
104
|
+
|
|
105
|
+
const cardPath = path.join(dir, entry.name);
|
|
106
|
+
const cardText = fs.readFileSync(cardPath, "utf-8");
|
|
107
|
+
const parsed = parseNoteCard(cardText);
|
|
108
|
+
if (!parsed) continue;
|
|
109
|
+
if (parsed.note_kind !== expectedKind) continue;
|
|
110
|
+
|
|
111
|
+
// Extract body from card
|
|
112
|
+
const body = extractNoteBody(cardText);
|
|
113
|
+
|
|
114
|
+
upsertNote(db, {
|
|
115
|
+
note_kind: parsed.note_kind,
|
|
116
|
+
title: parsed.title,
|
|
117
|
+
body,
|
|
118
|
+
tags: parsed.tags,
|
|
119
|
+
related: parsed.related,
|
|
120
|
+
card_path: cardPath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Extract the body (after frontmatter) from a note card, stripping the # title header. */
|
|
126
|
+
function extractNoteBody(cardText: string): string {
|
|
127
|
+
const trimmed = cardText.trim();
|
|
128
|
+
if (!trimmed.startsWith("---")) return "";
|
|
129
|
+
|
|
130
|
+
const endOfFm = trimmed.indexOf("\n---", 3);
|
|
131
|
+
if (endOfFm === -1) return "";
|
|
132
|
+
|
|
133
|
+
return trimmed.slice(endOfFm + 4).trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function slugifyNoteTitle(title: string): string {
|
|
137
|
+
return title
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
140
|
+
.replace(/^-+|-+$/g, "")
|
|
141
|
+
.slice(0, 60) || "untitled";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Re-export for convenience
|
|
145
|
+
export { slugifyNoteTitle };
|
package/src/project.test.ts
CHANGED
|
@@ -80,6 +80,14 @@ describe('project.ts', () => {
|
|
|
80
80
|
expect(p.metaFile).toBe(path.join(p.codewalkerDir, 'meta.json'));
|
|
81
81
|
expect(p.entriesDir).toBe(path.join(p.codewalkerDir, 'entries'));
|
|
82
82
|
expect(p.symbolsDir).toBe(path.join(p.codewalkerDir, 'entries', 'symbols'));
|
|
83
|
+
expect(p.libsDir).toBe(path.join(p.codewalkerDir, 'entries', 'libs'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('exposes glossaryDir and decisionsDir paths', async () => {
|
|
87
|
+
const mod = await import('./project.ts');
|
|
88
|
+
const p = mod.resolveProject(tmpDir);
|
|
89
|
+
expect(p.glossaryDir).toBe(path.join(p.codewalkerDir, 'entries', 'glossary'));
|
|
90
|
+
expect(p.decisionsDir).toBe(path.join(p.codewalkerDir, 'entries', 'decisions'));
|
|
83
91
|
});
|
|
84
92
|
});
|
|
85
93
|
|
|
@@ -89,10 +97,13 @@ describe('project.ts', () => {
|
|
|
89
97
|
const p = await mod.ensureProject(tmpDir);
|
|
90
98
|
// marker file exists
|
|
91
99
|
expect(fs.existsSync(p.markerPath)).toBe(true);
|
|
92
|
-
// codewalker dir
|
|
100
|
+
// codewalker dir + subdirs are created
|
|
93
101
|
expect(fs.existsSync(p.codewalkerDir)).toBe(true);
|
|
94
102
|
expect(fs.existsSync(p.entriesDir)).toBe(true);
|
|
95
103
|
expect(fs.existsSync(p.symbolsDir)).toBe(true);
|
|
104
|
+
expect(fs.existsSync(p.libsDir)).toBe(true);
|
|
105
|
+
expect(fs.existsSync(p.glossaryDir)).toBe(true);
|
|
106
|
+
expect(fs.existsSync(p.decisionsDir)).toBe(true);
|
|
96
107
|
// meta.json written
|
|
97
108
|
expect(fs.existsSync(p.metaFile)).toBe(true);
|
|
98
109
|
// meta.json has correct shape
|
package/src/project.ts
CHANGED
|
@@ -30,6 +30,9 @@ export interface ProjectPaths {
|
|
|
30
30
|
metaFile: string;
|
|
31
31
|
entriesDir: string;
|
|
32
32
|
symbolsDir: string;
|
|
33
|
+
libsDir: string;
|
|
34
|
+
glossaryDir: string;
|
|
35
|
+
decisionsDir: string;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function piHome(): string {
|
|
@@ -123,6 +126,9 @@ function pathsForId(id: string, root: string, configDir: string, markerPath: str
|
|
|
123
126
|
metaFile: path.join(globalDir, "codewalker", "meta.json"),
|
|
124
127
|
entriesDir: path.join(globalDir, "codewalker", "entries"),
|
|
125
128
|
symbolsDir: path.join(globalDir, "codewalker", "entries", "symbols"),
|
|
129
|
+
libsDir: path.join(globalDir, "codewalker", "entries", "libs"),
|
|
130
|
+
glossaryDir: path.join(globalDir, "codewalker", "entries", "glossary"),
|
|
131
|
+
decisionsDir: path.join(globalDir, "codewalker", "entries", "decisions"),
|
|
126
132
|
};
|
|
127
133
|
}
|
|
128
134
|
|
|
@@ -164,7 +170,7 @@ export async function ensureProject(cwd: string): Promise<ProjectPaths> {
|
|
|
164
170
|
const p = resolveProject(cwd);
|
|
165
171
|
const nowISO = new Date().toISOString();
|
|
166
172
|
|
|
167
|
-
for (const dir of [p.codewalkerDir, p.entriesDir, p.symbolsDir]) {
|
|
173
|
+
for (const dir of [p.codewalkerDir, p.entriesDir, p.symbolsDir, p.libsDir, p.glossaryDir, p.decisionsDir]) {
|
|
168
174
|
fs.mkdirSync(dir, { recursive: true });
|
|
169
175
|
}
|
|
170
176
|
|
package/src/query.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
|
-
import { openDb, upsertSymbol, setMeta, getMeta,
|
|
5
|
+
import { openDb, upsertSymbol, setMeta, getMeta, upsertLibSymbol, upsertNote, searchLibSymbols } from './db.ts';
|
|
6
6
|
import { runQuery } from './query.ts';
|
|
7
7
|
|
|
8
8
|
describe('query.ts', () => {
|
|
@@ -95,4 +95,151 @@ describe('query.ts', () => {
|
|
|
95
95
|
// In a non-git dir, staleness should be null
|
|
96
96
|
expect(result.staleness).toBeNull();
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
// ── source filter ───────────────────────────────────────────
|
|
100
|
+
it('source="code" returns only code symbols (default)', () => {
|
|
101
|
+
const db = openDb(dbPath);
|
|
102
|
+
upsertSymbol(db, {
|
|
103
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
104
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
105
|
+
});
|
|
106
|
+
upsertLibSymbol(db, {
|
|
107
|
+
lib: 'hono', version: '4.6.3', name: 'honoFunc',
|
|
108
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
109
|
+
});
|
|
110
|
+
db.close();
|
|
111
|
+
|
|
112
|
+
const result = runQuery(dbPath, { query: '', source: 'code' });
|
|
113
|
+
expect(result.rows).toHaveLength(1);
|
|
114
|
+
expect(result.rows[0]!.name).toBe('myFunc');
|
|
115
|
+
expect(result.rows[0]!.source).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('source="libs" returns only lib symbols', () => {
|
|
119
|
+
const db = openDb(dbPath);
|
|
120
|
+
upsertSymbol(db, {
|
|
121
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
122
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
123
|
+
});
|
|
124
|
+
upsertLibSymbol(db, {
|
|
125
|
+
lib: 'hono', version: '4.6.3', name: 'honoFunc',
|
|
126
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
127
|
+
});
|
|
128
|
+
db.close();
|
|
129
|
+
|
|
130
|
+
const result = runQuery(dbPath, { query: '', source: 'libs' });
|
|
131
|
+
expect(result.rows).toHaveLength(1);
|
|
132
|
+
expect(result.rows[0]!.name).toBe('honoFunc');
|
|
133
|
+
expect(result.rows[0]!.source).toBe('lib');
|
|
134
|
+
expect(result.rows[0]!.lib).toBe('hono');
|
|
135
|
+
expect(result.rows[0]!.version).toBe('4.6.3');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('source="all" returns merged code and lib symbols', () => {
|
|
139
|
+
const db = openDb(dbPath);
|
|
140
|
+
upsertSymbol(db, {
|
|
141
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
142
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
143
|
+
});
|
|
144
|
+
upsertLibSymbol(db, {
|
|
145
|
+
lib: 'hono', version: '4.6.3', name: 'honoFunc',
|
|
146
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
147
|
+
});
|
|
148
|
+
db.close();
|
|
149
|
+
|
|
150
|
+
const result = runQuery(dbPath, { query: '', source: 'all' });
|
|
151
|
+
expect(result.rows).toHaveLength(2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('source=all respects limit across merged set', () => {
|
|
155
|
+
const db = openDb(dbPath);
|
|
156
|
+
upsertSymbol(db, {
|
|
157
|
+
name: 'aCode', kind: 'function', file_path: 'src/a.ts',
|
|
158
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
159
|
+
});
|
|
160
|
+
upsertLibSymbol(db, {
|
|
161
|
+
lib: 'pkg', version: '1.0.0', name: 'bLib',
|
|
162
|
+
kind: 'function', signature: '', doc: '', summary: '', card_path: '',
|
|
163
|
+
});
|
|
164
|
+
db.close();
|
|
165
|
+
|
|
166
|
+
const result = runQuery(dbPath, { query: '', source: 'all', limit: 1 });
|
|
167
|
+
expect(result.rows).toHaveLength(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── notes source ───────────────────────────────────────────
|
|
171
|
+
it('source="notes" returns only notes', () => {
|
|
172
|
+
const db = openDb(dbPath);
|
|
173
|
+
upsertSymbol(db, {
|
|
174
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
175
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
176
|
+
});
|
|
177
|
+
upsertNote(db, {
|
|
178
|
+
note_kind: 'glossary', title: 'Glossary Term', body: 'A term', tags: '', related: '', card_path: '',
|
|
179
|
+
});
|
|
180
|
+
db.close();
|
|
181
|
+
|
|
182
|
+
const result = runQuery(dbPath, { query: '', source: 'notes' });
|
|
183
|
+
expect(result.rows).toHaveLength(1);
|
|
184
|
+
expect(result.rows[0]!.name).toBe('Glossary Term');
|
|
185
|
+
expect(result.rows[0]!.source).toBe('note');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('source="all" includes notes interleaved with code and libs', () => {
|
|
189
|
+
const db = openDb(dbPath);
|
|
190
|
+
upsertSymbol(db, {
|
|
191
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
192
|
+
line_start: 1, line_end: 1, signature: '', doc: 'refresh token', summary: '', card_path: '',
|
|
193
|
+
});
|
|
194
|
+
upsertLibSymbol(db, {
|
|
195
|
+
lib: 'hono', version: '4.6.3', name: 'honoFunc',
|
|
196
|
+
kind: 'function', signature: '', doc: 'refresh token', summary: '', card_path: '',
|
|
197
|
+
});
|
|
198
|
+
upsertNote(db, {
|
|
199
|
+
note_kind: 'glossary', title: 'Refresh Token', body: 'A token used to refresh auth without re-login.', tags: 'auth', related: 'myFunc', card_path: '',
|
|
200
|
+
});
|
|
201
|
+
db.close();
|
|
202
|
+
|
|
203
|
+
const result = runQuery(dbPath, { query: 'refresh', source: 'all' });
|
|
204
|
+
expect(result.rows.length).toBeGreaterThanOrEqual(2);
|
|
205
|
+
// Should have note + lib + code (code rows have source undefined)
|
|
206
|
+
const sources = result.rows.map(r => r.source);
|
|
207
|
+
expect(sources).toContain('note');
|
|
208
|
+
expect(sources).toContain('lib');
|
|
209
|
+
expect(sources).toContain(undefined); // code rows have no source
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('source kind filter works for notes source', () => {
|
|
213
|
+
const db = openDb(dbPath);
|
|
214
|
+
upsertNote(db, {
|
|
215
|
+
note_kind: 'glossary', title: 'Term', body: 'A glossary term', tags: '', related: '', card_path: '',
|
|
216
|
+
});
|
|
217
|
+
upsertNote(db, {
|
|
218
|
+
note_kind: 'decision', title: 'Decision', body: 'A decision note', tags: '', related: '', card_path: '',
|
|
219
|
+
});
|
|
220
|
+
db.close();
|
|
221
|
+
|
|
222
|
+
const result = runQuery(dbPath, { query: '', source: 'notes', kind: 'glossary' });
|
|
223
|
+
expect(result.rows).toHaveLength(1);
|
|
224
|
+
expect(result.rows[0]!.name).toBe('Term');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('source=all with note in query works (note matches alongside code)', () => {
|
|
228
|
+
const db = openDb(dbPath);
|
|
229
|
+
upsertSymbol(db, {
|
|
230
|
+
name: 'myFunc', kind: 'function', file_path: 'src/payments.ts',
|
|
231
|
+
line_start: 1, line_end: 10, signature: '', doc: 'processes charges', summary: '', card_path: '',
|
|
232
|
+
});
|
|
233
|
+
upsertNote(db, {
|
|
234
|
+
note_kind: 'glossary', title: 'charge', body: 'A payment processing concept.',
|
|
235
|
+
tags: 'payments', related: 'myFunc', card_path: '',
|
|
236
|
+
});
|
|
237
|
+
db.close();
|
|
238
|
+
|
|
239
|
+
const result = runQuery(dbPath, { query: 'charge', source: 'all' });
|
|
240
|
+
expect(result.rows.length).toBeGreaterThanOrEqual(1);
|
|
241
|
+
const sources = result.rows.map(r => r.source);
|
|
242
|
+
expect(sources).toContain('note');
|
|
243
|
+
});
|
|
244
|
+
|
|
98
245
|
});
|
package/src/query.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Query orchestration: wraps DB search with staleness detection.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { QueryResult, QueryResultRow, StalenessInfo } from "./types.ts";
|
|
6
|
-
import { openDb, searchSymbols, getMeta } from "./db.ts";
|
|
5
|
+
import type { QueryResult, QueryResultRow, StalenessInfo, NoteKind } from "./types.ts";
|
|
6
|
+
import { openDb, searchSymbols, searchLibSymbols, searchNotes, getMeta } from "./db.ts";
|
|
7
7
|
import { getHeadSha, changedFilesSince } from "./git.ts";
|
|
8
8
|
|
|
9
9
|
export interface QueryParams {
|
|
10
10
|
query: string;
|
|
11
11
|
kind?: string;
|
|
12
12
|
limit?: number;
|
|
13
|
+
/** Source scope: "code" (default, only code symbols), "libs" (only lib symbols), "notes" (only notes), or "all" (all three). */
|
|
14
|
+
source?: "code" | "libs" | "notes" | "all";
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -27,12 +29,30 @@ export function runQuery(
|
|
|
27
29
|
const db = openDb(dbPath);
|
|
28
30
|
|
|
29
31
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
32
|
+
const source = params.source ?? "code";
|
|
33
|
+
const limit = params.limit ?? 10;
|
|
34
|
+
|
|
35
|
+
let rows: QueryResultRow[];
|
|
36
|
+
|
|
37
|
+
if (source === "libs") {
|
|
38
|
+
rows = searchLibSymbols(db, params.query, params.kind, limit) as unknown as QueryResultRow[];
|
|
39
|
+
} else if (source === "notes") {
|
|
40
|
+
const noteRows = searchNotes(db, params.query, params.kind as NoteKind | undefined, limit);
|
|
41
|
+
rows = noteRows as unknown as QueryResultRow[];
|
|
42
|
+
} else if (source === "all") {
|
|
43
|
+
// Run code + lib + note searches, merge, sort by score, apply limit
|
|
44
|
+
const codeRows = searchSymbols(db, params.query, params.kind, limit);
|
|
45
|
+
const libRows = searchLibSymbols(db, params.query, params.kind, limit) as unknown as QueryResultRow[];
|
|
46
|
+
const noteRows = searchNotes(db, params.query, params.kind as NoteKind | undefined, limit * 2) as unknown as QueryResultRow[];
|
|
47
|
+
|
|
48
|
+
// Merge and sort by score ascending (lower bm25 = better match)
|
|
49
|
+
const merged: QueryResultRow[] = [...codeRows, ...libRows, ...noteRows];
|
|
50
|
+
merged.sort((a, b) => a.score - b.score);
|
|
51
|
+
rows = merged.slice(0, limit);
|
|
52
|
+
} else {
|
|
53
|
+
// "code" — default, existing behavior
|
|
54
|
+
rows = searchSymbols(db, params.query, params.kind, limit);
|
|
55
|
+
}
|
|
36
56
|
|
|
37
57
|
const staleness = detectStaleness(db, repoDir);
|
|
38
58
|
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* These types are shared across all layers (extraction, cards, DB, query).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/** Library symbol kind — extends SymbolKind with reexport + namespace. */
|
|
8
|
+
export type LibSymbolKind =
|
|
9
|
+
| "function"
|
|
10
|
+
| "const"
|
|
11
|
+
| "class"
|
|
12
|
+
| "interface"
|
|
13
|
+
| "type"
|
|
14
|
+
| "enum"
|
|
15
|
+
| "namespace"
|
|
16
|
+
| "reexport"
|
|
17
|
+
| "module";
|
|
18
|
+
|
|
7
19
|
/** The kind of a code symbol. */
|
|
8
20
|
export type SymbolKind =
|
|
9
21
|
| "function"
|
|
@@ -17,6 +29,18 @@ export type SymbolKind =
|
|
|
17
29
|
| "namespace"
|
|
18
30
|
| "module";
|
|
19
31
|
|
|
32
|
+
/** A single symbol extracted from a library's .d.ts file. */
|
|
33
|
+
export interface LibSymbol {
|
|
34
|
+
lib: string;
|
|
35
|
+
version: string;
|
|
36
|
+
name: string;
|
|
37
|
+
kind: LibSymbolKind;
|
|
38
|
+
signature: string;
|
|
39
|
+
doc: string;
|
|
40
|
+
summary: string;
|
|
41
|
+
card_path: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
/** A single symbol extracted from source code. */
|
|
21
45
|
export interface Symbol {
|
|
22
46
|
name: string;
|
|
@@ -40,6 +64,19 @@ export interface CardHead {
|
|
|
40
64
|
summary: string;
|
|
41
65
|
}
|
|
42
66
|
|
|
67
|
+
/** Note kind discriminator for bridge cards. */
|
|
68
|
+
export type NoteKind = "glossary" | "decision";
|
|
69
|
+
|
|
70
|
+
/** A glossary/decision note (bridge card) for conceptual knowledge. */
|
|
71
|
+
export interface Note {
|
|
72
|
+
note_kind: NoteKind;
|
|
73
|
+
title: string;
|
|
74
|
+
body: string;
|
|
75
|
+
tags: string;
|
|
76
|
+
related: string;
|
|
77
|
+
card_path: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
43
80
|
/** A single row returned from a query. */
|
|
44
81
|
export interface QueryResultRow {
|
|
45
82
|
name: string;
|
|
@@ -51,6 +88,13 @@ export interface QueryResultRow {
|
|
|
51
88
|
summary: string;
|
|
52
89
|
score: number;
|
|
53
90
|
id: number;
|
|
91
|
+
/** Origin fields — code rows omit these; lib / note rows set them. */
|
|
92
|
+
source?: "code" | "lib" | "note";
|
|
93
|
+
lib?: string;
|
|
94
|
+
version?: string;
|
|
95
|
+
/** Note-specific fields — only for source === "note" rows. */
|
|
96
|
+
note_kind?: NoteKind;
|
|
97
|
+
tags?: string;
|
|
54
98
|
}
|
|
55
99
|
|
|
56
100
|
/** The full result of a query, including staleness info. */
|