pmem-ai 0.7.6 → 0.8.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/CHANGELOG.md +14 -0
- package/README.md +6 -4
- package/dist/commands/ask.d.ts +5 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +48 -240
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/rebuild.d.ts.map +1 -1
- package/dist/commands/rebuild.js +74 -10
- package/dist/commands/rebuild.js.map +1 -1
- package/dist/commands/recall.d.ts +1 -0
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +1 -1
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +30 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/db.d.ts +13 -0
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/db.js +36 -0
- package/dist/core/db.js.map +1 -1
- package/dist/core/format.d.ts.map +1 -1
- package/dist/core/format.js +37 -0
- package/dist/core/format.js.map +1 -1
- package/dist/core/query/ask.d.ts +14 -3
- package/dist/core/query/ask.d.ts.map +1 -1
- package/dist/core/query/ask.js +60 -200
- package/dist/core/query/ask.js.map +1 -1
- package/dist/core/query/context.d.ts.map +1 -1
- package/dist/core/query/context.js +8 -2
- package/dist/core/query/context.js.map +1 -1
- package/dist/core/query/engine/candidates.d.ts +11 -0
- package/dist/core/query/engine/candidates.d.ts.map +1 -0
- package/dist/core/query/engine/candidates.js +207 -0
- package/dist/core/query/engine/candidates.js.map +1 -0
- package/dist/core/query/engine/intent.d.ts +14 -0
- package/dist/core/query/engine/intent.d.ts.map +1 -0
- package/dist/core/query/engine/intent.js +53 -0
- package/dist/core/query/engine/intent.js.map +1 -0
- package/dist/core/query/engine/pack.d.ts +33 -0
- package/dist/core/query/engine/pack.d.ts.map +1 -0
- package/dist/core/query/engine/pack.js +92 -0
- package/dist/core/query/engine/pack.js.map +1 -0
- package/dist/core/query/engine/scoring.d.ts +50 -0
- package/dist/core/query/engine/scoring.d.ts.map +1 -0
- package/dist/core/query/engine/scoring.js +132 -0
- package/dist/core/query/engine/scoring.js.map +1 -0
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/v0.8 pre-design.md +203 -0
- package/package.json +2 -2
- package/skills/pmem/SKILL.md +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pmem are documented here.
|
|
4
4
|
|
|
5
|
+
## v0.7.6-a — Stale Card Cleanup & missing_card_file Fix (2026-06-29)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`missing_card_file` rebuild dead loop (#12)**: `pmem verify` previously reported `missing_card_file` and suggested `pmem rebuild`, but incremental rebuild only processed existing `.md` files on disk and never cleaned up DB rows for deleted cards — creating an unresolvable loop. Incremental rebuild now detects stale DB cards whose source files no longer exist, marks them `is_deleted = 1`, and cleans up their edges/aliases/tags/paths.
|
|
10
|
+
- **`deleteOrphanEdges` now runs unconditionally**: Previously only called during `--full` rebuild; now runs in all rebuild modes since stale card cleanup can create new orphan edges in incremental mode.
|
|
11
|
+
- **`--fix` / `--fix-stale` directly handle `missing_card_file`**: Before calling rebuild, these options now directly remove stale card rows from the database, providing an immediate repair path without waiting for index rebuild.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Verify fix hint updated**: `missing_card_file` now suggests `"pmem rebuild (incremental rebuild will clean up stale card references)"` instead of just `"pmem rebuild"`.
|
|
16
|
+
- **`card_id` added to `missing_card_file` verify issues**: Enables direct DB cleanup by `--fix` / `--fix-stale`.
|
|
17
|
+
- **Rebuild summary reports cleaned stale cards**: `"Cleaned N stale card(s) (source files deleted)"` when applicable.
|
|
18
|
+
|
|
5
19
|
## v0.7.6 — Agent Contract & Write-Path Integrity (2026-06-28)
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -215,16 +215,18 @@ Do not edit SQLite directly. Edit Markdown cards or use pmem workflow commands,
|
|
|
215
215
|
|
|
216
216
|
### Recall And Ask
|
|
217
217
|
|
|
218
|
-
Use `recall` for the current project state:
|
|
218
|
+
Use `recall` for the current project state. v0.8 (upcoming on `main` branch, not yet released to npm) adds budget modes: `brief` returns L0 + read-if-needed paths, `normal` is the default agent context, and `deep` preserves more detail when budget allows.
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
221
|
pmem recall --budget 2000
|
|
222
|
+
pmem recall --mode brief --budget 500
|
|
222
223
|
```
|
|
223
224
|
|
|
224
|
-
Use `ask` for targeted retrieval:
|
|
225
|
+
Use `ask` for targeted retrieval. v0.8 (upcoming on `main` branch, not yet released to npm) uses the Hybrid Recall Engine: exact IDs, aliases, tags, source file paths, always-on FTS5/BM25, graph expansion, recency, and stale/dirty penalties are fused into a deterministic score. Add `--explain` to see why each card was recalled.
|
|
225
226
|
|
|
226
227
|
```bash
|
|
227
228
|
pmem ask "sqlite runtime" --format compact
|
|
229
|
+
pmem ask "src/core/query/recall.ts" --explain --limit 5
|
|
228
230
|
pmem ask "release checklist" --format json
|
|
229
231
|
```
|
|
230
232
|
|
|
@@ -382,8 +384,8 @@ pmem init [project-name] [--guided] [--description <text>] [--stage <text>] [--n
|
|
|
382
384
|
pmem context <task> [--budget N] [--format compact|json]
|
|
383
385
|
pmem capture [--auto] [-s <summary>] [-n <next>] [--full] [--force]
|
|
384
386
|
|
|
385
|
-
pmem recall [--budget N] [--format compact|json|paths|pack] [--since <duration>]
|
|
386
|
-
pmem ask <query> [--format compact|json|paths|pack]
|
|
387
|
+
pmem recall [--budget N] [--mode brief|normal|deep] [--format compact|json|paths|pack] [--since <duration>]
|
|
388
|
+
pmem ask <query> [--format compact|json|paths|pack] [--explain] [--limit N]
|
|
387
389
|
pmem discover [--dry-run] [--format compact|json] [--min-confidence <n>]
|
|
388
390
|
[--lang auto|nodejs,python,rust,go,cpp,java]
|
|
389
391
|
[--pattern-file <path>]
|
package/dist/commands/ask.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { CliFormat } from '../types';
|
|
2
|
-
export
|
|
2
|
+
export interface AskCommandOptions {
|
|
3
|
+
explain?: boolean;
|
|
4
|
+
limit?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function askCommand(query: string, format?: CliFormat, options?: AskCommandOptions): void;
|
|
3
7
|
//# sourceMappingURL=ask.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/commands/ask.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/commands/ask.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAI1C,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,SAAqB,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAkF9G"}
|
package/dist/commands/ask.js
CHANGED
|
@@ -36,11 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.askCommand = askCommand;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const fs_1 = require("../core/fs");
|
|
39
|
-
const db_1 = require("../core/db");
|
|
40
39
|
const format_1 = require("../core/format");
|
|
41
|
-
const
|
|
40
|
+
const ask_1 = require("../core/query/ask");
|
|
42
41
|
const PMEM_DIR = '.pmem';
|
|
43
|
-
function askCommand(query, format = 'compact') {
|
|
42
|
+
function askCommand(query, format = 'compact', options = {}) {
|
|
44
43
|
const cwd = process.cwd();
|
|
45
44
|
const pmemPath = path.join(cwd, PMEM_DIR);
|
|
46
45
|
const dbPath = path.join(pmemPath, 'pmem.db');
|
|
@@ -52,222 +51,30 @@ function askCommand(query, format = 'compact') {
|
|
|
52
51
|
console.log('No SQLite database found. Run `pmem rebuild` first.');
|
|
53
52
|
return;
|
|
54
53
|
}
|
|
55
|
-
let
|
|
54
|
+
let result;
|
|
56
55
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
result = (0, ask_1.askQuery)(pmemPath, query, {
|
|
57
|
+
explain: options.explain,
|
|
58
|
+
limit: options.limit,
|
|
59
|
+
});
|
|
59
60
|
}
|
|
60
61
|
catch (err) {
|
|
61
62
|
if (err?.message?.includes('not a valid SQLite database')) {
|
|
62
63
|
console.log(err.message);
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
|
-
console.log(`
|
|
66
|
+
console.log(`Ask query failed: ${err?.message || err}`);
|
|
66
67
|
console.log('Run `pmem rebuild --full` to rebuild the database.');
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const matches = [];
|
|
72
|
-
const seenIds = new Set();
|
|
73
|
-
// Step 1: Exact match — card ID
|
|
74
|
-
const idMatches = db.prepare("SELECT * FROM cards WHERE (id = ? OR id LIKE ?) AND is_deleted = 0").all(normalizedQuery, `%${normalizedQuery}%`);
|
|
75
|
-
for (const card of idMatches) {
|
|
76
|
-
if (seenIds.has(card.id))
|
|
77
|
-
continue;
|
|
78
|
-
seenIds.add(card.id);
|
|
79
|
-
matches.push({
|
|
80
|
-
id: card.id,
|
|
81
|
-
title: card.title,
|
|
82
|
-
match_type: card.id === normalizedQuery ? 'exact_id' : 'exact_title',
|
|
83
|
-
confidence: card.id === normalizedQuery ? 0.95 : 0.85,
|
|
84
|
-
graph_distance: 0,
|
|
85
|
-
file: card.file_path,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
// Step 2: Exact match — aliases
|
|
89
|
-
const aliasMatches = db.prepare(`SELECT c.* FROM cards c
|
|
90
|
-
JOIN aliases a ON c.id = a.card_id
|
|
91
|
-
WHERE (a.normalized_alias = ? OR a.normalized_alias LIKE ?)
|
|
92
|
-
AND c.is_deleted = 0`).all(normalizedQuery, `%${normalizedQuery}%`);
|
|
93
|
-
for (const card of aliasMatches) {
|
|
94
|
-
if (seenIds.has(card.id))
|
|
95
|
-
continue;
|
|
96
|
-
seenIds.add(card.id);
|
|
97
|
-
matches.push({
|
|
98
|
-
id: card.id,
|
|
99
|
-
title: card.title,
|
|
100
|
-
match_type: 'alias',
|
|
101
|
-
confidence: 0.9,
|
|
102
|
-
graph_distance: 0,
|
|
103
|
-
file: card.file_path,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
// Step 3: Exact match — tags
|
|
107
|
-
const tagMatches = db.prepare(`SELECT c.* FROM cards c
|
|
108
|
-
JOIN tags t ON c.id = t.card_id
|
|
109
|
-
WHERE t.normalized_tag = ?
|
|
110
|
-
AND c.is_deleted = 0`).all(normalizedQuery);
|
|
111
|
-
for (const card of tagMatches) {
|
|
112
|
-
if (seenIds.has(card.id))
|
|
113
|
-
continue;
|
|
114
|
-
seenIds.add(card.id);
|
|
115
|
-
matches.push({
|
|
116
|
-
id: card.id,
|
|
117
|
-
title: card.title,
|
|
118
|
-
match_type: 'tag',
|
|
119
|
-
confidence: 0.7,
|
|
120
|
-
graph_distance: 0,
|
|
121
|
-
file: card.file_path,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
// Also try tag matching with individual query tokens
|
|
125
|
-
for (const token of queryTokens) {
|
|
126
|
-
const tokenTagMatches = db.prepare(`SELECT c.* FROM cards c
|
|
127
|
-
JOIN tags t ON c.id = t.card_id
|
|
128
|
-
WHERE t.normalized_tag LIKE ?
|
|
129
|
-
AND c.is_deleted = 0`).all(`%${token}%`);
|
|
130
|
-
for (const card of tokenTagMatches) {
|
|
131
|
-
if (seenIds.has(card.id))
|
|
132
|
-
continue;
|
|
133
|
-
seenIds.add(card.id);
|
|
134
|
-
matches.push({
|
|
135
|
-
id: card.id,
|
|
136
|
-
title: card.title,
|
|
137
|
-
match_type: 'tag',
|
|
138
|
-
confidence: 0.6,
|
|
139
|
-
graph_distance: 0,
|
|
140
|
-
file: card.file_path,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Step 4: Graph expansion — 1-hop neighbors from matched cards
|
|
145
|
-
const matchedIdsAtThisPoint = new Set(matches.map(m => m.id));
|
|
146
|
-
for (const match of matches) {
|
|
147
|
-
const edges = db.prepare("SELECT * FROM edges WHERE from_id = ? OR to_id = ?").all(match.id, match.id);
|
|
148
|
-
for (const edge of edges) {
|
|
149
|
-
const neighborId = edge.from_id === match.id ? edge.to_id : edge.from_id;
|
|
150
|
-
if (matchedIdsAtThisPoint.has(neighborId) || seenIds.has(neighborId))
|
|
151
|
-
continue;
|
|
152
|
-
seenIds.add(neighborId);
|
|
153
|
-
const neighborCard = db.prepare("SELECT * FROM cards WHERE id = ? AND is_deleted = 0").get(neighborId);
|
|
154
|
-
if (neighborCard) {
|
|
155
|
-
matches.push({
|
|
156
|
-
id: neighborCard.id,
|
|
157
|
-
title: neighborCard.title,
|
|
158
|
-
match_type: 'graph_expansion',
|
|
159
|
-
confidence: 0.6,
|
|
160
|
-
graph_distance: 1,
|
|
161
|
-
file: neighborCard.file_path,
|
|
162
|
-
edge_type: edge.type,
|
|
163
|
-
from_card: match.id,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// Step 5: Keyword fallback — FTS5 if available, else LIKE
|
|
169
|
-
if (matches.filter(m => m.match_type !== 'graph_expansion').length === 0) {
|
|
170
|
-
// Filter out expansions for the "no direct matches" check
|
|
171
|
-
const directMatchesBeforeFallback = matches.filter(m => m.match_type !== 'graph_expansion');
|
|
172
|
-
if (directMatchesBeforeFallback.length === 0) {
|
|
173
|
-
if ((0, db_1.hasFTS5)(db)) {
|
|
174
|
-
try {
|
|
175
|
-
const ftsResults = db.prepare("SELECT c.*, rank FROM card_fts JOIN cards c ON card_fts.card_id = c.id WHERE card_fts MATCH ? AND c.is_deleted = 0 ORDER BY rank").all(normalizedQuery);
|
|
176
|
-
for (const row of ftsResults) {
|
|
177
|
-
if (seenIds.has(row.id))
|
|
178
|
-
continue;
|
|
179
|
-
seenIds.add(row.id);
|
|
180
|
-
matches.push({
|
|
181
|
-
id: row.id,
|
|
182
|
-
title: row.title,
|
|
183
|
-
match_type: 'keyword_fallback',
|
|
184
|
-
confidence: Math.min(0.5, 1 / (1 + (row.rank || 1))),
|
|
185
|
-
graph_distance: 0,
|
|
186
|
-
file: row.file_path,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
// FTS5 query failed, fall through to LIKE fallback
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// LIKE fallback (used if FTS5 unavailable or FTS5 returned no results)
|
|
195
|
-
if (matches.filter(m => m.match_type === 'keyword_fallback').length === 0) {
|
|
196
|
-
const likePattern = `%${normalizedQuery}%`;
|
|
197
|
-
const likeResults = db.prepare("SELECT * FROM cards WHERE (title LIKE ? OR summary LIKE ?) AND is_deleted = 0").all(likePattern, likePattern);
|
|
198
|
-
for (const card of likeResults) {
|
|
199
|
-
if (seenIds.has(card.id))
|
|
200
|
-
continue;
|
|
201
|
-
seenIds.add(card.id);
|
|
202
|
-
// Simple relevance: count token overlap in title
|
|
203
|
-
const titleLower = card.title.toLowerCase();
|
|
204
|
-
const tokenOverlap = queryTokens.filter(t => titleLower.includes(t)).length;
|
|
205
|
-
matches.push({
|
|
206
|
-
id: card.id,
|
|
207
|
-
title: card.title,
|
|
208
|
-
match_type: 'keyword_fallback',
|
|
209
|
-
confidence: Math.min(0.5, tokenOverlap / Math.max(1, queryTokens.length)),
|
|
210
|
-
graph_distance: 0,
|
|
211
|
-
file: card.file_path,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Step 6: Rerank
|
|
218
|
-
const typeOrder = {
|
|
219
|
-
exact_id: 5,
|
|
220
|
-
exact_title: 4,
|
|
221
|
-
alias: 3,
|
|
222
|
-
tag: 2,
|
|
223
|
-
graph_expansion: 1,
|
|
224
|
-
keyword_fallback: 0,
|
|
225
|
-
};
|
|
226
|
-
matches.sort((a, b) => {
|
|
227
|
-
return ((typeOrder[b.match_type] - typeOrder[a.match_type]) ||
|
|
228
|
-
(b.confidence - a.confidence) ||
|
|
229
|
-
(a.graph_distance - b.graph_distance));
|
|
230
|
-
});
|
|
231
|
-
// Step 7: Deduplicate (keep first/highest-ranked match per id)
|
|
232
|
-
const dedupedIds = new Set();
|
|
233
|
-
const deduped = [];
|
|
234
|
-
for (const m of matches) {
|
|
235
|
-
if (dedupedIds.has(m.id))
|
|
236
|
-
continue;
|
|
237
|
-
dedupedIds.add(m.id);
|
|
238
|
-
deduped.push(m);
|
|
239
|
-
}
|
|
240
|
-
// Build recommended_files and evidence_paths
|
|
241
|
-
const recommendedFiles = [];
|
|
242
|
-
for (const m of deduped.slice(0, 8)) {
|
|
243
|
-
recommendedFiles.push(m.file);
|
|
244
|
-
}
|
|
245
|
-
const manifest = (0, manifest_1.loadManifest)(pmemPath);
|
|
246
|
-
const config = manifest ? (0, manifest_1.resolveConfig)(manifest) : { evidence_types: ['decision', 'trace'] };
|
|
247
|
-
const evidenceTypes = config.evidence_types;
|
|
248
|
-
const evidencePaths = [];
|
|
249
|
-
for (const m of deduped) {
|
|
250
|
-
const card = db.prepare("SELECT type, file_path FROM cards WHERE id = ? AND is_deleted = 0").get(m.id);
|
|
251
|
-
if (card && evidenceTypes.includes(card.type)) {
|
|
252
|
-
evidencePaths.push(card.file_path);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const result = {
|
|
256
|
-
query,
|
|
257
|
-
matched: deduped,
|
|
258
|
-
recommended_files: recommendedFiles,
|
|
259
|
-
evidence_paths: evidencePaths,
|
|
260
|
-
};
|
|
261
|
-
// Build guidance for empty results
|
|
262
|
-
const askMessage = deduped.length > 0
|
|
263
|
-
? `Found ${deduped.length} match(es).`
|
|
70
|
+
const askMessage = result.matched.length > 0
|
|
71
|
+
? `Found ${result.matched.length} match(es).`
|
|
264
72
|
: 'No matching memory cards found.';
|
|
265
|
-
const askNextSteps =
|
|
73
|
+
const askNextSteps = result.matched.length > 0 ? [] : [
|
|
266
74
|
'Try a different query keyword',
|
|
267
75
|
'Run `pmem recall` for full project context',
|
|
268
76
|
'Check that cards have relevant aliases and tags',
|
|
269
77
|
];
|
|
270
|
-
// Output
|
|
271
78
|
if (format === 'json') {
|
|
272
79
|
console.log(JSON.stringify({
|
|
273
80
|
...result,
|
|
@@ -275,46 +82,47 @@ function askCommand(query, format = 'compact') {
|
|
|
275
82
|
next_steps: askNextSteps,
|
|
276
83
|
}, null, 2));
|
|
277
84
|
}
|
|
278
|
-
else {
|
|
279
|
-
|
|
280
|
-
const formatCompat = {
|
|
281
|
-
query: result.query,
|
|
282
|
-
matched: result.matched.map(m => ({
|
|
283
|
-
id: m.id,
|
|
284
|
-
title: m.title,
|
|
285
|
-
matchType: m.match_type,
|
|
286
|
-
match_type: m.match_type,
|
|
287
|
-
confidence: m.confidence,
|
|
288
|
-
graphDistance: m.graph_distance,
|
|
289
|
-
graph_distance: m.graph_distance,
|
|
290
|
-
file: m.file,
|
|
291
|
-
edgeType: m.edge_type,
|
|
292
|
-
edge_type: m.edge_type,
|
|
293
|
-
fromCard: m.from_card,
|
|
294
|
-
})),
|
|
295
|
-
recommended_files: result.recommended_files,
|
|
296
|
-
evidencePaths: result.evidence_paths,
|
|
297
|
-
};
|
|
298
|
-
console.log((0, format_1.formatOutput)(formatCompat, format, 2000));
|
|
85
|
+
else if (format === 'paths' || format === 'pack') {
|
|
86
|
+
console.log((0, format_1.formatOutput)(result, format, 2000));
|
|
299
87
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (/[一-鿿]/.test(word)) {
|
|
308
|
-
const cjkChars = word.match(/[一-鿿]/g) || [];
|
|
309
|
-
tokens.push(...cjkChars);
|
|
310
|
-
const nonCjk = word.replace(/[一-鿿]/g, '').trim();
|
|
311
|
-
if (nonCjk)
|
|
312
|
-
tokens.push(nonCjk.toLowerCase());
|
|
88
|
+
else {
|
|
89
|
+
const lines = [`Query: ${result.query}`, ''];
|
|
90
|
+
if (result.matched.length === 0) {
|
|
91
|
+
lines.push('No matching memory cards found.');
|
|
92
|
+
lines.push('');
|
|
93
|
+
for (const step of askNextSteps)
|
|
94
|
+
lines.push(` - ${step}`);
|
|
313
95
|
}
|
|
314
96
|
else {
|
|
315
|
-
|
|
97
|
+
lines.push('Matched:');
|
|
98
|
+
for (const m of result.matched) {
|
|
99
|
+
const score = m.score !== undefined ? ` (${m.score})` : '';
|
|
100
|
+
const stale = m.stale ? ' ⚠stale' : '';
|
|
101
|
+
let annotation = '';
|
|
102
|
+
if (options.explain && m.reasons && m.reasons.length > 0) {
|
|
103
|
+
const parts = m.reasons.map(r => `${r.channel}:${r.detail}`);
|
|
104
|
+
annotation = ` [${parts.join('; ')}]`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const via = m.match_type === 'graph_expansion' && m.from_card
|
|
108
|
+
? `via ${m.from_card}`
|
|
109
|
+
: m.match_type;
|
|
110
|
+
annotation = ` [${via}]`;
|
|
111
|
+
}
|
|
112
|
+
lines.push(` - ${m.id}${score}${annotation}${stale}: "${m.title}"`);
|
|
113
|
+
}
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('Recommended:');
|
|
116
|
+
for (const f of result.recommended_files)
|
|
117
|
+
lines.push(` ${f}`);
|
|
118
|
+
if (result.evidence_paths.length > 0) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('Evidence:');
|
|
121
|
+
for (const f of result.evidence_paths.slice(0, 8))
|
|
122
|
+
lines.push(` ${f}`);
|
|
123
|
+
}
|
|
316
124
|
}
|
|
125
|
+
console.log(lines.join('\n'));
|
|
317
126
|
}
|
|
318
|
-
return [...new Set(tokens)];
|
|
319
127
|
}
|
|
320
128
|
//# sourceMappingURL=ask.js.map
|
package/dist/commands/ask.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/commands/ask.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/commands/ask.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuild.d.ts","sourceRoot":"","sources":["../../src/commands/rebuild.ts"],"names":[],"mappings":"AAWA,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AASD,wBAAgB,cAAc,CAAC,OAAO,GAAE,cAAmB,GAAG,IAAI,CAYjE;
|
|
1
|
+
{"version":3,"file":"rebuild.d.ts","sourceRoot":"","sources":["../../src/commands/rebuild.ts"],"names":[],"mappings":"AAWA,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AASD,wBAAgB,cAAc,CAAC,OAAO,GAAE,cAAmB,GAAG,IAAI,CAYjE;AAqjBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ3D"}
|
package/dist/commands/rebuild.js
CHANGED
|
@@ -136,6 +136,11 @@ function rebuildLocked(pmemPath, options) {
|
|
|
136
136
|
let skipped = 0;
|
|
137
137
|
let updated = 0;
|
|
138
138
|
let snappedEdges = [];
|
|
139
|
+
// v0.8: FTS rows collected during the card loop, written after createFTS5.
|
|
140
|
+
// For skipped (hash-match) cards we still collect so a missing FTS row can
|
|
141
|
+
// be backfilled — the card_fts table did not exist before v0.8.
|
|
142
|
+
const ftsRows = [];
|
|
143
|
+
const ftsSkippedIds = [];
|
|
139
144
|
// Pre-scan all card IDs for wikilink validation (so [[card-id]] refs
|
|
140
145
|
// resolve even when the target card hasn't been processed yet)
|
|
141
146
|
const validCardIds = new Set();
|
|
@@ -201,6 +206,17 @@ function rebuildLocked(pmemPath, options) {
|
|
|
201
206
|
if (card && !card.updated_at) {
|
|
202
207
|
db.prepare('UPDATE cards SET updated_at = ? WHERE file_path = ?').run(resolveUpdatedAt(fm.updated, file), relPath);
|
|
203
208
|
}
|
|
209
|
+
// v0.8 FTS backfill: card content unchanged, but its FTS row may
|
|
210
|
+
// not exist yet (table introduced in v0.8). Collect for backfill.
|
|
211
|
+
ftsRows.push({
|
|
212
|
+
id: fm.id,
|
|
213
|
+
title,
|
|
214
|
+
summary: extractCardSummary(fm, parsed.bodyText),
|
|
215
|
+
body: parsed.bodyText,
|
|
216
|
+
aliases: fm.aliases ?? [],
|
|
217
|
+
tags: fm.tags ?? [],
|
|
218
|
+
});
|
|
219
|
+
ftsSkippedIds.push(fm.id);
|
|
204
220
|
skipped++;
|
|
205
221
|
continue;
|
|
206
222
|
}
|
|
@@ -232,6 +248,14 @@ function rebuildLocked(pmemPath, options) {
|
|
|
232
248
|
is_candidate: isCandidate,
|
|
233
249
|
};
|
|
234
250
|
(0, db_1.upsertCard)(db, cardRow);
|
|
251
|
+
ftsRows.push({
|
|
252
|
+
id: fm.id,
|
|
253
|
+
title,
|
|
254
|
+
summary: cardRow.summary,
|
|
255
|
+
body: parsed.bodyText,
|
|
256
|
+
aliases: fm.aliases ?? [],
|
|
257
|
+
tags: fm.tags ?? [],
|
|
258
|
+
});
|
|
235
259
|
// Clear existing explicit, mention, and inferred relations before
|
|
236
260
|
// re-inserting from the current frontmatter. v0.7.3 (issue #6):
|
|
237
261
|
// inferred edges (e.g. task→module next_step_of) must also be
|
|
@@ -331,17 +355,39 @@ function rebuildLocked(pmemPath, options) {
|
|
|
331
355
|
});
|
|
332
356
|
// Execute all SQLite writes in a single transaction
|
|
333
357
|
doRebuild();
|
|
334
|
-
// v0.7.
|
|
335
|
-
// The rebuild loop
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
358
|
+
// v0.7.6-a (issue #12): clean up stale DB cards whose source .md files
|
|
359
|
+
// have been deleted from disk. The rebuild loop only processes files
|
|
360
|
+
// that exist — it never removes DB rows for deleted files — so an
|
|
361
|
+
// incremental rebuild would otherwise dead-loop on `missing_card_file`
|
|
362
|
+
// verify warnings with no way to resolve them short of `--full`.
|
|
363
|
+
//
|
|
364
|
+
// Wrapped in a transaction so a crash mid-cleanup leaves the DB in a
|
|
365
|
+
// consistent state (all-or-nothing per stale card batch).
|
|
366
|
+
let cleanedStaleCards = 0;
|
|
367
|
+
{
|
|
368
|
+
const cleanupTx = db.transaction(() => {
|
|
369
|
+
const staleCards = db.prepare("SELECT id, file_path FROM cards WHERE is_deleted = 0").all();
|
|
370
|
+
for (const card of staleCards) {
|
|
371
|
+
const absPath = path.join(cwd, card.file_path);
|
|
372
|
+
if (!(0, fs_1.fileExists)(absPath)) {
|
|
373
|
+
db.prepare("UPDATE cards SET is_deleted = 1 WHERE id = ?").run(card.id);
|
|
374
|
+
// Bidirectional edge cleanup — covers both outgoing and incoming edges
|
|
375
|
+
// for the deleted card, matching the verify.ts cleanupMissingCards pattern.
|
|
376
|
+
db.prepare("DELETE FROM edges WHERE from_id = ? OR to_id = ?").run(card.id, card.id);
|
|
377
|
+
(0, db_1.deleteCardAliases)(db, card.id);
|
|
378
|
+
(0, db_1.deleteCardTags)(db, card.id);
|
|
379
|
+
(0, db_1.deleteCardPaths)(db, card.id);
|
|
380
|
+
(0, db_1.deleteCardFts)(db, card.id);
|
|
381
|
+
cleanedStaleCards++;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
cleanupTx();
|
|
344
386
|
}
|
|
387
|
+
// v0.7.3 (issue #6): prune orphan edges after rebuild.
|
|
388
|
+
// v0.7.7 (issue #12): run unconditionally — stale card cleanup above
|
|
389
|
+
// can create new orphan edges even in incremental mode.
|
|
390
|
+
(0, db_1.deleteOrphanEdges)(db);
|
|
345
391
|
// v0.7.0-a (revised in v0.7.3, issue #6): restore edges that the
|
|
346
392
|
// rebuild loop did not and could not have re-derived. We now
|
|
347
393
|
// restrict restoration to:
|
|
@@ -397,6 +443,21 @@ function rebuildLocked(pmemPath, options) {
|
|
|
397
443
|
}
|
|
398
444
|
// Create FTS5 virtual table for full-text search (outside transaction)
|
|
399
445
|
(0, db_1.createFTS5)(db);
|
|
446
|
+
// v0.8: populate FTS index. Updated cards always refresh; skipped cards
|
|
447
|
+
// only backfill when their FTS row is missing. Full rebuild starts clean.
|
|
448
|
+
{
|
|
449
|
+
const ftsSkipped = new Set(ftsSkippedIds);
|
|
450
|
+
const ftsTx = db.transaction(() => {
|
|
451
|
+
if (isFull)
|
|
452
|
+
(0, db_1.clearCardFts)(db);
|
|
453
|
+
for (const row of ftsRows) {
|
|
454
|
+
if (!isFull && ftsSkipped.has(row.id) && (0, db_1.cardFtsRowExists)(db, row.id))
|
|
455
|
+
continue;
|
|
456
|
+
(0, db_1.refreshCardFts)(db, row);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
ftsTx();
|
|
460
|
+
}
|
|
400
461
|
// Write legacy graph.json for backward compatibility
|
|
401
462
|
const allContent = cardFiles.map(f => (0, fs_1.readFile)(f) || '').join('');
|
|
402
463
|
const sourceHash = (0, hash_1.computeHash)(allContent);
|
|
@@ -433,6 +494,9 @@ function rebuildLocked(pmemPath, options) {
|
|
|
433
494
|
const dbEdgeCount = db.prepare("SELECT COUNT(*) as cnt FROM edges").get().cnt;
|
|
434
495
|
console.log(`${modeLabel}: ${processed} cards processed, ${skipped} skipped (hash match), ${updated} updated`);
|
|
435
496
|
console.log(`Graph: ${nodes.length} nodes, ${dbEdgeCount} edges`);
|
|
497
|
+
if (cleanedStaleCards > 0) {
|
|
498
|
+
console.log(`Cleaned ${cleanedStaleCards} stale card(s) (source files deleted)`);
|
|
499
|
+
}
|
|
436
500
|
(0, db_1.closeDatabase)();
|
|
437
501
|
}
|
|
438
502
|
/**
|