akm-cli 0.9.0-beta.2 → 0.9.0-beta.26
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 +614 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +730 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +790 -0
- package/dist/commands/health.js +478 -15
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +634 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +145 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +33 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +280 -35
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +139 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +1851 -515
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +87 -0
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +51 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/index.js +30 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +5 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +369 -329
- package/dist/commands/read/curate.js +294 -79
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +241 -0
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +305 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +706 -42
- package/dist/indexer/db/db.js +347 -38
- package/dist/indexer/db/graph-db.js +81 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +71 -25
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -5
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +38 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +78 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1194 -607
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +455 -270
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +5 -2
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +7 -5
|
@@ -241,13 +241,18 @@ export function collectGraphRelatedHit(context, filePath) {
|
|
|
241
241
|
* Find graph files that share entities with the given file.
|
|
242
242
|
*
|
|
243
243
|
* Implementation: SQL self-join on graph_file_entities, scoped by stash_root,
|
|
244
|
-
* grouped by
|
|
244
|
+
* grouped by file_path, ordered by shared-entity count desc. Touches ~50-200
|
|
245
245
|
* rows instead of loading the entire snapshot into memory. Cold-call latency
|
|
246
246
|
* drops from ~30-60ms (full snapshot parse) to ~2-5ms on typical stashes.
|
|
247
247
|
*
|
|
248
|
+
* #624-P1: the graph tables are keyed on (stash_root, file_path, body_hash) —
|
|
249
|
+
* NOT entries.id — so candidates are identified by file_path (the unique index
|
|
250
|
+
* idx_graph_files_path guarantees one graph_files row per path).
|
|
251
|
+
*
|
|
248
252
|
* The returned `ref` field carries the canonical asset ref (`type:name`)
|
|
249
|
-
* resolved from entries.entry_key when the
|
|
250
|
-
* fall back to formatting `path` when `ref` is undefined (
|
|
253
|
+
* resolved from entries.entry_key when the file is indexed. Callers should
|
|
254
|
+
* fall back to formatting `path` when `ref` is undefined (graph row with no
|
|
255
|
+
* matching entries row).
|
|
251
256
|
*/
|
|
252
257
|
export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
|
|
253
258
|
if (!db) {
|
|
@@ -255,113 +260,118 @@ export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
|
|
|
255
260
|
// callers pass a handle), so degrade to empty rather than reopening.
|
|
256
261
|
return [];
|
|
257
262
|
}
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
let targetEntryId;
|
|
263
|
+
// Confirm the target file has a graph row; without it there is nothing to
|
|
264
|
+
// relate. (Identity is file_path within the stash — one row per path.)
|
|
261
265
|
try {
|
|
262
266
|
const row = db
|
|
263
|
-
.prepare("SELECT
|
|
267
|
+
.prepare("SELECT 1 AS present FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
|
|
264
268
|
.get(stashRoot, filePath);
|
|
265
|
-
|
|
269
|
+
if (row === undefined)
|
|
270
|
+
return [];
|
|
266
271
|
}
|
|
267
272
|
catch {
|
|
268
273
|
return [];
|
|
269
274
|
}
|
|
270
|
-
if (targetEntryId == null)
|
|
271
|
-
return [];
|
|
272
275
|
const effectiveLimit = Math.max(1, limit);
|
|
273
|
-
// Shared-entity count per candidate
|
|
276
|
+
// Shared-entity count per candidate file_path. The target's entities are the
|
|
277
|
+
// rows for `filePath`; candidates are any OTHER file_path in the stash that
|
|
278
|
+
// shares a normalized entity.
|
|
274
279
|
let candidateRows;
|
|
275
280
|
try {
|
|
276
281
|
candidateRows = db
|
|
277
|
-
.prepare(`SELECT gf.
|
|
278
|
-
gf.file_path AS file_path,
|
|
282
|
+
.prepare(`SELECT gf.file_path AS file_path,
|
|
279
283
|
gf.file_type AS file_type,
|
|
280
284
|
COUNT(*) AS shared
|
|
281
285
|
FROM graph_file_entities target
|
|
282
286
|
JOIN graph_file_entities e
|
|
283
287
|
ON e.stash_root = target.stash_root
|
|
284
288
|
AND e.entity_norm = target.entity_norm
|
|
285
|
-
AND e.
|
|
289
|
+
AND e.file_path != target.file_path
|
|
286
290
|
JOIN graph_files gf
|
|
287
|
-
ON gf.
|
|
288
|
-
|
|
291
|
+
ON gf.stash_root = e.stash_root
|
|
292
|
+
AND gf.file_path = e.file_path
|
|
293
|
+
AND gf.body_hash = e.body_hash
|
|
294
|
+
WHERE target.file_path = ?
|
|
289
295
|
AND target.stash_root = ?
|
|
290
|
-
GROUP BY gf.
|
|
296
|
+
GROUP BY gf.file_path
|
|
291
297
|
ORDER BY shared DESC, gf.file_path ASC
|
|
292
298
|
LIMIT ?`)
|
|
293
|
-
.all(
|
|
299
|
+
.all(filePath, stashRoot, effectiveLimit);
|
|
294
300
|
}
|
|
295
301
|
catch {
|
|
296
302
|
return [];
|
|
297
303
|
}
|
|
298
304
|
if (candidateRows.length === 0)
|
|
299
305
|
return [];
|
|
300
|
-
const
|
|
301
|
-
const placeholders =
|
|
306
|
+
const candidatePaths = candidateRows.map((r) => r.file_path);
|
|
307
|
+
const placeholders = candidatePaths.map(() => "?").join(",");
|
|
302
308
|
// Pull the shared entity names (joined by normalized casing) for display.
|
|
303
309
|
const sharedRows = db
|
|
304
|
-
.prepare(`SELECT e.
|
|
310
|
+
.prepare(`SELECT e.file_path AS file_path, e.entity AS entity
|
|
305
311
|
FROM graph_file_entities e
|
|
306
312
|
JOIN graph_file_entities target
|
|
307
313
|
ON target.stash_root = e.stash_root
|
|
308
314
|
AND target.entity_norm = e.entity_norm
|
|
309
|
-
WHERE e.
|
|
310
|
-
AND
|
|
315
|
+
WHERE e.file_path IN (${placeholders})
|
|
316
|
+
AND e.stash_root = ?
|
|
317
|
+
AND target.file_path = ?
|
|
311
318
|
AND target.stash_root = ?`)
|
|
312
|
-
.all(...
|
|
313
|
-
const
|
|
319
|
+
.all(...candidatePaths, stashRoot, filePath, stashRoot);
|
|
320
|
+
const sharedByPath = new Map();
|
|
314
321
|
for (const row of sharedRows) {
|
|
315
|
-
let bucket =
|
|
322
|
+
let bucket = sharedByPath.get(row.file_path);
|
|
316
323
|
if (!bucket) {
|
|
317
324
|
bucket = new Set();
|
|
318
|
-
|
|
325
|
+
sharedByPath.set(row.file_path, bucket);
|
|
319
326
|
}
|
|
320
327
|
bucket.add(row.entity);
|
|
321
328
|
}
|
|
322
329
|
// Relation count for each candidate (relations where either endpoint
|
|
323
330
|
// matches one of the shared entities).
|
|
324
|
-
const
|
|
331
|
+
const relationCountByPath = new Map();
|
|
325
332
|
const relationRows = db
|
|
326
|
-
.prepare(`SELECT
|
|
333
|
+
.prepare(`SELECT file_path, from_entity, to_entity
|
|
327
334
|
FROM graph_file_relations
|
|
328
|
-
WHERE
|
|
329
|
-
|
|
335
|
+
WHERE file_path IN (${placeholders})
|
|
336
|
+
AND stash_root = ?`)
|
|
337
|
+
.all(...candidatePaths, stashRoot);
|
|
330
338
|
for (const row of relationRows) {
|
|
331
|
-
const shared =
|
|
339
|
+
const shared = sharedByPath.get(row.file_path);
|
|
332
340
|
if (!shared)
|
|
333
341
|
continue;
|
|
334
342
|
if (shared.has(row.from_entity) || shared.has(row.to_entity)) {
|
|
335
|
-
|
|
343
|
+
relationCountByPath.set(row.file_path, (relationCountByPath.get(row.file_path) ?? 0) + 1);
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
346
|
// Optional: ref lookup via entries.entry_key. entry_key is stored as
|
|
339
347
|
// `${stash_dir}:${type}:${name}` — strip the stash-dir prefix to get the
|
|
340
|
-
// user-facing `type:name`.
|
|
341
|
-
|
|
348
|
+
// user-facing `type:name`. Resolve by (stash_dir, file_path) now that the
|
|
349
|
+
// graph rows are no longer keyed on entries.id.
|
|
350
|
+
const refByPath = new Map();
|
|
342
351
|
try {
|
|
343
352
|
const entryRows = db
|
|
344
|
-
.prepare(`SELECT
|
|
345
|
-
|
|
353
|
+
.prepare(`SELECT entry_key, stash_dir, file_path FROM entries
|
|
354
|
+
WHERE file_path IN (${placeholders}) AND stash_dir = ?`)
|
|
355
|
+
.all(...candidatePaths, stashRoot);
|
|
346
356
|
for (const row of entryRows) {
|
|
347
357
|
const ref = stripStashPrefix(row.entry_key, row.stash_dir);
|
|
348
358
|
if (ref)
|
|
349
|
-
|
|
359
|
+
refByPath.set(row.file_path, ref);
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
catch {
|
|
353
363
|
/* ignore — refs are best-effort */
|
|
354
364
|
}
|
|
355
365
|
return candidateRows.map((row) => {
|
|
356
|
-
const sharedSet =
|
|
366
|
+
const sharedSet = sharedByPath.get(row.file_path) ?? new Set();
|
|
357
367
|
const sharedEntities = [...sharedSet].sort((a, b) => a.localeCompare(b));
|
|
358
|
-
const ref =
|
|
368
|
+
const ref = refByPath.get(row.file_path);
|
|
359
369
|
return {
|
|
360
370
|
...(ref ? { ref } : {}),
|
|
361
371
|
path: row.file_path,
|
|
362
372
|
type: row.file_type,
|
|
363
373
|
sharedEntities,
|
|
364
|
-
relationCount:
|
|
374
|
+
relationCount: relationCountByPath.get(row.file_path) ?? 0,
|
|
365
375
|
};
|
|
366
376
|
});
|
|
367
377
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock.js";
|
|
7
|
+
import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
|
|
8
|
+
const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
|
|
9
|
+
const INDEX_WRITER_WAIT_MS = 100;
|
|
10
|
+
const heldLocks = new Map();
|
|
11
|
+
function buildPayload(purpose, pid = process.pid) {
|
|
12
|
+
return JSON.stringify({
|
|
13
|
+
pid,
|
|
14
|
+
purpose,
|
|
15
|
+
dbPath: getDbPath(),
|
|
16
|
+
startedAt: new Date().toISOString(),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function delay(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
function throwIfAborted(signal) {
|
|
23
|
+
if (!signal?.aborted)
|
|
24
|
+
return;
|
|
25
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("index writer wait aborted");
|
|
26
|
+
}
|
|
27
|
+
function releaseHeldLock(lockPath) {
|
|
28
|
+
const held = heldLocks.get(lockPath);
|
|
29
|
+
if (!held)
|
|
30
|
+
return;
|
|
31
|
+
held.depth -= 1;
|
|
32
|
+
if (held.depth > 0)
|
|
33
|
+
return;
|
|
34
|
+
heldLocks.delete(lockPath);
|
|
35
|
+
process.off("exit", held.exitHandler);
|
|
36
|
+
releaseLockIfOwned(lockPath, process.pid);
|
|
37
|
+
}
|
|
38
|
+
function retainHeldLock(lockPath) {
|
|
39
|
+
const existing = heldLocks.get(lockPath);
|
|
40
|
+
if (existing) {
|
|
41
|
+
existing.depth += 1;
|
|
42
|
+
return { lockPath, release: () => releaseHeldLock(lockPath) };
|
|
43
|
+
}
|
|
44
|
+
const exitHandler = () => releaseLockIfOwned(lockPath, process.pid);
|
|
45
|
+
process.on("exit", exitHandler);
|
|
46
|
+
heldLocks.set(lockPath, { depth: 1, exitHandler });
|
|
47
|
+
return { lockPath, release: () => releaseHeldLock(lockPath) };
|
|
48
|
+
}
|
|
49
|
+
function detachHeldLock(lockPath) {
|
|
50
|
+
const held = heldLocks.get(lockPath);
|
|
51
|
+
if (!held)
|
|
52
|
+
return;
|
|
53
|
+
heldLocks.delete(lockPath);
|
|
54
|
+
process.off("exit", held.exitHandler);
|
|
55
|
+
}
|
|
56
|
+
export async function acquireIndexWriterLease(options) {
|
|
57
|
+
const mode = options.mode ?? "wait";
|
|
58
|
+
const lockPath = getIndexWriterLockPath();
|
|
59
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
60
|
+
if (heldLocks.has(lockPath)) {
|
|
61
|
+
return retainHeldLock(lockPath);
|
|
62
|
+
}
|
|
63
|
+
while (true) {
|
|
64
|
+
throwIfAborted(options.signal);
|
|
65
|
+
if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
|
|
66
|
+
return retainHeldLock(lockPath);
|
|
67
|
+
}
|
|
68
|
+
const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
|
|
69
|
+
if (probe.state === "held" && probe.holderPid === process.pid) {
|
|
70
|
+
return retainHeldLock(lockPath);
|
|
71
|
+
}
|
|
72
|
+
if (probe.state === "stale") {
|
|
73
|
+
releaseLock(lockPath);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (mode === "try")
|
|
77
|
+
return undefined;
|
|
78
|
+
await delay(INDEX_WRITER_WAIT_MS);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function withIndexWriterLease(options, run) {
|
|
82
|
+
const lease = await acquireIndexWriterLease(options);
|
|
83
|
+
if (!lease) {
|
|
84
|
+
throw new Error(`index writer lease unavailable for ${options.purpose}`);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return await run();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
lease.release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function handoffIndexWriterLeaseToPid(lease, pid, purpose) {
|
|
94
|
+
fs.writeFileSync(lease.lockPath, buildPayload(purpose, pid), "utf8");
|
|
95
|
+
detachHeldLock(lease.lockPath);
|
|
96
|
+
}
|
|
97
|
+
export function probeIndexWriterLease() {
|
|
98
|
+
return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
|
|
99
|
+
}
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveIndexPassLLM } from "../llm/index-passes.js";
|
|
|
12
12
|
import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
|
|
13
13
|
import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntriesByDir, getEntryCount, getIndexDirState, getMeta, isVecAvailable, openDatabase, openExistingDatabase, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
|
|
14
14
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
15
|
+
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
15
16
|
import { applyCuratedFrontmatter, applyWikiFrontmatter, generateMetadataFlat, isEnrichmentComplete, isWorkflowSkipWarning, loadStashFile, shouldIndexStashFile, } from "./passes/metadata.js";
|
|
16
17
|
import { buildSearchText } from "./search/search-fields.js";
|
|
17
18
|
import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
|
|
@@ -225,119 +226,121 @@ function runCleanPass(db, dryRun) {
|
|
|
225
226
|
}
|
|
226
227
|
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
227
228
|
export async function akmIndex(options) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
229
|
+
return withIndexWriterLease({ purpose: "akm-index", signal: options?.signal }, async () => {
|
|
230
|
+
const stashDir = options?.stashDir || resolveStashDir();
|
|
231
|
+
const onProgress = options?.onProgress ?? (() => { });
|
|
232
|
+
const signal = options?.signal;
|
|
233
|
+
const reEnrich = options?.reEnrich === true;
|
|
234
|
+
const full = options?.full === true;
|
|
235
|
+
const clean = options?.clean === true;
|
|
236
|
+
const dryRun = options?.dryRun === true;
|
|
237
|
+
// Load config and resolve all stash sources
|
|
238
|
+
const { loadConfig } = await import("../core/config/config.js");
|
|
239
|
+
const config = loadConfig();
|
|
240
|
+
// One-time, read-only guard: warn if the writable stash still holds an
|
|
241
|
+
// un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
|
|
242
|
+
// entirely, so an unmigrated vault's `.env` data would silently never be
|
|
243
|
+
// indexed. Non-destructive — only stats, never reads/writes/deletes.
|
|
244
|
+
const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
|
|
245
|
+
warnOnUnmigratedVaults(stashDir);
|
|
246
|
+
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
247
|
+
// so their content directories exist on disk for the walker to discover.
|
|
248
|
+
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
249
|
+
await ensureSourceCaches(config, { force: full });
|
|
250
|
+
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
251
|
+
const allSourceDirs = allSourceEntries.map((s) => s.path);
|
|
252
|
+
const t0 = Date.now();
|
|
253
|
+
// Open database — pass embedding dimension from config if available
|
|
254
|
+
const dbPath = getDbPath();
|
|
255
|
+
const embeddingDim = config.embedding?.dimension;
|
|
256
|
+
const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
|
|
257
|
+
try {
|
|
258
|
+
// Determine incremental vs full mode
|
|
259
|
+
const prevStashDir = getMeta(db, "stashDir");
|
|
260
|
+
const prevBuiltAt = getMeta(db, "builtAt");
|
|
261
|
+
const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
|
|
262
|
+
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
263
|
+
// Assemble the run context
|
|
264
|
+
const ctx = {
|
|
265
|
+
db,
|
|
266
|
+
config,
|
|
267
|
+
sources: allSourceEntries,
|
|
268
|
+
sourceDirs: allSourceDirs,
|
|
269
|
+
full,
|
|
270
|
+
reEnrich,
|
|
271
|
+
stashDir,
|
|
272
|
+
onProgress,
|
|
273
|
+
signal,
|
|
274
|
+
timing: {
|
|
275
|
+
t0,
|
|
276
|
+
tWalkStart: t0,
|
|
277
|
+
tWalkEnd: t0,
|
|
278
|
+
tLlmEnd: t0,
|
|
279
|
+
tFtsEnd: t0,
|
|
280
|
+
tEmbedEnd: t0,
|
|
281
|
+
},
|
|
282
|
+
isIncremental,
|
|
283
|
+
builtAtMs,
|
|
284
|
+
hadRemovedSources: false,
|
|
285
|
+
scannedDirs: 0,
|
|
286
|
+
skippedDirs: 0,
|
|
287
|
+
generatedCount: 0,
|
|
288
|
+
walkWarnings: [],
|
|
289
|
+
dirsNeedingLlm: [],
|
|
290
|
+
embeddingResult: null,
|
|
291
|
+
graphExtractionResult: null,
|
|
292
|
+
};
|
|
293
|
+
onProgress({
|
|
294
|
+
phase: "summary",
|
|
295
|
+
message: buildIndexSummaryMessage({
|
|
296
|
+
mode: isIncremental ? "incremental" : "full",
|
|
297
|
+
sourcesCount: allSourceDirs.length,
|
|
298
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
299
|
+
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
300
|
+
llmEnabled: !!resolveIndexPassLLM("enrichment", config),
|
|
301
|
+
vecAvailable: isVecAvailable(db),
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
// ── Phase sequence ───────────────────────────────────────────────────────
|
|
305
|
+
await runSourceCachePhase(ctx);
|
|
306
|
+
await runWalkPhase(ctx);
|
|
307
|
+
await runEmbeddingPhase(ctx);
|
|
308
|
+
await runFinalizePhase(ctx);
|
|
309
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
310
|
+
const { _verification: verification, _totalEntries: totalEntries } = ctx;
|
|
311
|
+
const { timing } = ctx;
|
|
312
|
+
// ── Clean pass ───────────────────────────────────────────────────────────
|
|
313
|
+
// After the normal index completes, remove entries whose source files no
|
|
314
|
+
// longer exist on disk. Remote entries (empty file_path) are skipped.
|
|
315
|
+
let cleanResult;
|
|
316
|
+
if (clean) {
|
|
317
|
+
cleanResult = runCleanPass(db, dryRun);
|
|
318
|
+
}
|
|
319
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
320
|
+
return {
|
|
321
|
+
stashDir,
|
|
322
|
+
totalEntries,
|
|
323
|
+
generatedMetadata: ctx.generatedCount,
|
|
324
|
+
indexPath: dbPath,
|
|
294
325
|
mode: isIncremental ? "incremental" : "full",
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const { _verification: verification, _totalEntries: totalEntries } = ctx;
|
|
309
|
-
const { timing } = ctx;
|
|
310
|
-
// ── Clean pass ───────────────────────────────────────────────────────────
|
|
311
|
-
// After the normal index completes, remove entries whose source files no
|
|
312
|
-
// longer exist on disk. Remote entries (empty file_path) are skipped.
|
|
313
|
-
let cleanResult;
|
|
314
|
-
if (clean) {
|
|
315
|
-
cleanResult = runCleanPass(db, dryRun);
|
|
326
|
+
directoriesScanned: ctx.scannedDirs,
|
|
327
|
+
directoriesSkipped: ctx.skippedDirs,
|
|
328
|
+
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
329
|
+
verification,
|
|
330
|
+
timing: {
|
|
331
|
+
totalMs: Date.now() - timing.t0,
|
|
332
|
+
walkMs: timing.tWalkEnd - timing.tWalkStart,
|
|
333
|
+
llmMs: timing.tLlmEnd - timing.tWalkEnd,
|
|
334
|
+
embedMs: timing.tEmbedEnd - timing.tLlmEnd,
|
|
335
|
+
ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
|
|
336
|
+
},
|
|
337
|
+
...(cleanResult !== undefined ? { clean: cleanResult } : {}),
|
|
338
|
+
};
|
|
316
339
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
generatedMetadata: ctx.generatedCount,
|
|
322
|
-
indexPath: dbPath,
|
|
323
|
-
mode: isIncremental ? "incremental" : "full",
|
|
324
|
-
directoriesScanned: ctx.scannedDirs,
|
|
325
|
-
directoriesSkipped: ctx.skippedDirs,
|
|
326
|
-
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
327
|
-
verification,
|
|
328
|
-
timing: {
|
|
329
|
-
totalMs: Date.now() - timing.t0,
|
|
330
|
-
walkMs: timing.tWalkEnd - timing.tWalkStart,
|
|
331
|
-
llmMs: timing.tLlmEnd - timing.tWalkEnd,
|
|
332
|
-
embedMs: timing.tEmbedEnd - timing.tLlmEnd,
|
|
333
|
-
ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
|
|
334
|
-
},
|
|
335
|
-
...(cleanResult !== undefined ? { clean: cleanResult } : {}),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
finally {
|
|
339
|
-
closeDatabase(db);
|
|
340
|
-
}
|
|
340
|
+
finally {
|
|
341
|
+
closeDatabase(db);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
341
344
|
}
|
|
342
345
|
// ── Extracted helpers for indexing ────────────────────────────────────────────
|
|
343
346
|
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
|