akm-cli 0.9.0-beta.4 → 0.9.0-beta.41
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 +646 -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 +6 -2
- 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/stash-skeleton/facts/conventions/assets/agent.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/cli.js +14 -3
- 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 +422 -80
- package/dist/commands/health.js +381 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +631 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +163 -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 +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +403 -40
- 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 +113 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +2042 -612
- 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 +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +602 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +46 -4
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -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/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +17 -1
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +5 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +187 -57
- package/dist/commands/read/curate.js +344 -80
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/sources/init.js +36 -9
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/commands/sources/stash-cli.js +19 -3
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +83 -0
- package/dist/core/config/config-schema.js +274 -2
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +4 -3
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state-db.js +691 -30
- package/dist/indexer/db/db.js +364 -38
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +203 -3
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +33 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +23 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- 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/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/text/helpers.js +16 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1378 -599
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
- package/dist/setup/setup.js +3 -3
- package/dist/sources/providers/git.js +71 -61
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +8 -6
package/dist/indexer/db/db.js
CHANGED
|
@@ -12,13 +12,22 @@ import { warn } from "../../core/warn.js";
|
|
|
12
12
|
import { cosineSimilarity } from "../../llm/embedders/types.js";
|
|
13
13
|
import { sha256Hex } from "../../runtime.js";
|
|
14
14
|
import { openDatabase as openSqlite } from "../../storage/database.js";
|
|
15
|
+
import { applyStandardPragmas } from "../../storage/sqlite-pragmas.js";
|
|
15
16
|
import { buildSearchFields } from "../search/search-fields.js";
|
|
16
17
|
import { ensureUsageEventsSchema } from "../usage/usage-events.js";
|
|
17
18
|
import { backupDataDir, EMBEDDING_DIM_CHANGE_REASON } from "./db-backup.js";
|
|
18
19
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
20
|
+
// NOTE: do NOT bump DB_VERSION for graph-schema changes. A DB_VERSION mismatch
|
|
21
|
+
// triggers handleVersionUpgrade()'s NUCLEAR drop of the ENTIRE index — entries,
|
|
22
|
+
// embeddings, FTS, and the llm_enrichment_cache — forcing every user to re-embed
|
|
23
|
+
// their whole corpus on upgrade. The graph tables are derived and cheap to
|
|
24
|
+
// rebuild, so graph re-keying is migrated in a TARGETED, graph-only path
|
|
25
|
+
// (migrateGraphFilesSchema) that leaves entries + embeddings untouched.
|
|
19
26
|
export const DB_VERSION = 17;
|
|
20
27
|
export const EMBEDDING_DIM = 384;
|
|
21
|
-
|
|
28
|
+
// #624-P1: graph_files re-keyed to (stash_root, file_path, body_hash). Bumped 3→4
|
|
29
|
+
// as a marker; the actual migration is the targeted drop in migrateGraphFilesSchema.
|
|
30
|
+
export const GRAPH_SCHEMA_VERSION = 4;
|
|
22
31
|
// ── Database lifecycle ──────────────────────────────────────────────────────
|
|
23
32
|
export function openDatabase(dbPath, options) {
|
|
24
33
|
const resolvedPath = dbPath ?? getDbPath();
|
|
@@ -27,9 +36,7 @@ export function openDatabase(dbPath, options) {
|
|
|
27
36
|
fs.mkdirSync(dir, { recursive: true });
|
|
28
37
|
}
|
|
29
38
|
const db = openSqlite(resolvedPath);
|
|
30
|
-
db
|
|
31
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
32
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
39
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
33
40
|
// Try to load sqlite-vec extension
|
|
34
41
|
loadVecExtension(db);
|
|
35
42
|
// Dim resolution: explicit option wins; otherwise consult the on-disk
|
|
@@ -67,10 +74,9 @@ function resolveConfiguredEmbeddingDim() {
|
|
|
67
74
|
}
|
|
68
75
|
export function openExistingDatabase(dbPath) {
|
|
69
76
|
const resolvedPath = dbPath ?? getDbPath();
|
|
77
|
+
const dir = path.dirname(resolvedPath);
|
|
70
78
|
const db = openSqlite(resolvedPath);
|
|
71
|
-
db
|
|
72
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
73
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
79
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
74
80
|
// Existing-DB callers must not mutate schema or embedding metadata on open,
|
|
75
81
|
// but some paths still need write access to usage_events and other tables.
|
|
76
82
|
loadVecExtension(db);
|
|
@@ -299,15 +305,27 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
299
305
|
CREATE INDEX IF NOT EXISTS idx_llm_cache_updated
|
|
300
306
|
ON llm_enrichment_cache(updated_at);
|
|
301
307
|
`);
|
|
302
|
-
// Graph extraction tables — schema
|
|
308
|
+
// Graph extraction tables — schema v4 ((stash_root, file_path, body_hash) PK).
|
|
309
|
+
//
|
|
310
|
+
// graph_files is self-keyed on (stash_root, file_path, body_hash) and is NO
|
|
311
|
+
// LONGER tied to entries.id. This is the #624-P1 win: deleting and
|
|
312
|
+
// re-inserting an entries row during a reindex no longer cascade-wipes the
|
|
313
|
+
// extracted graph — as long as the file's body_hash is unchanged, the graph
|
|
314
|
+
// data survives. body_hash is part of the PK so a content change yields a
|
|
315
|
+
// distinct key; a UNIQUE index on (stash_root, file_path) still enforces
|
|
316
|
+
// exactly one graph_files row per path (delete-then-insert on a hash change).
|
|
303
317
|
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// extractor's path-based upsert still works.
|
|
318
|
+
// graph_file_entities and graph_file_relations carry (stash_root, file_path,
|
|
319
|
+
// body_hash) and declare a composite FK -> graph_files ON DELETE CASCADE so
|
|
320
|
+
// child rows are removed when a graph_files row is replaced.
|
|
308
321
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
322
|
+
// #624-P1 targeted migration: an existing DB may still hold the OLD graph_files
|
|
323
|
+
// (entry_id PK). SQLite can't ALTER a primary key, so we RENAME the 3 graph
|
|
324
|
+
// tables aside (→ *_legacy) here — ONLY the graph tables, never the index/
|
|
325
|
+
// embeddings — then the CREATE block below builds the new shape, then
|
|
326
|
+
// migrateGraphDataFromLegacy() copies the data across so the graph is PRESERVED
|
|
327
|
+
// (not re-extracted).
|
|
328
|
+
migrateGraphFilesSchema(db);
|
|
311
329
|
db.exec(`
|
|
312
330
|
CREATE TABLE IF NOT EXISTS graph_meta (
|
|
313
331
|
stash_root TEXT PRIMARY KEY,
|
|
@@ -331,7 +349,6 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
331
349
|
);
|
|
332
350
|
|
|
333
351
|
CREATE TABLE IF NOT EXISTS graph_files (
|
|
334
|
-
entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE,
|
|
335
352
|
stash_root TEXT NOT NULL,
|
|
336
353
|
file_path TEXT NOT NULL,
|
|
337
354
|
file_order INTEGER NOT NULL,
|
|
@@ -341,26 +358,34 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
341
358
|
status TEXT NOT NULL DEFAULT 'extracted',
|
|
342
359
|
reason TEXT,
|
|
343
360
|
extraction_run_id TEXT,
|
|
344
|
-
|
|
361
|
+
PRIMARY KEY (stash_root, file_path, body_hash)
|
|
345
362
|
);
|
|
346
363
|
|
|
364
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_files_path
|
|
365
|
+
ON graph_files(stash_root, file_path);
|
|
366
|
+
|
|
347
367
|
CREATE INDEX IF NOT EXISTS idx_graph_files_stash_order
|
|
348
368
|
ON graph_files(stash_root, file_order);
|
|
349
369
|
|
|
350
370
|
CREATE TABLE IF NOT EXISTS graph_file_entities (
|
|
351
|
-
entry_id INTEGER NOT NULL REFERENCES graph_files(entry_id) ON DELETE CASCADE,
|
|
352
|
-
entity_order INTEGER NOT NULL,
|
|
353
371
|
stash_root TEXT NOT NULL,
|
|
372
|
+
file_path TEXT NOT NULL,
|
|
373
|
+
body_hash TEXT NOT NULL,
|
|
374
|
+
entity_order INTEGER NOT NULL,
|
|
354
375
|
entity_norm TEXT NOT NULL,
|
|
355
376
|
entity TEXT NOT NULL,
|
|
356
|
-
PRIMARY KEY (
|
|
377
|
+
PRIMARY KEY (stash_root, file_path, body_hash, entity_order),
|
|
378
|
+
FOREIGN KEY (stash_root, file_path, body_hash)
|
|
379
|
+
REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
|
|
357
380
|
);
|
|
358
381
|
|
|
359
382
|
CREATE INDEX IF NOT EXISTS idx_graph_file_entities_entity_norm
|
|
360
383
|
ON graph_file_entities(stash_root, entity_norm);
|
|
361
384
|
|
|
362
385
|
CREATE TABLE IF NOT EXISTS graph_file_relations (
|
|
363
|
-
|
|
386
|
+
stash_root TEXT NOT NULL,
|
|
387
|
+
file_path TEXT NOT NULL,
|
|
388
|
+
body_hash TEXT NOT NULL,
|
|
364
389
|
relation_order INTEGER NOT NULL,
|
|
365
390
|
from_entity_norm TEXT NOT NULL,
|
|
366
391
|
from_entity TEXT NOT NULL,
|
|
@@ -368,9 +393,31 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
368
393
|
to_entity TEXT NOT NULL,
|
|
369
394
|
relation_type TEXT,
|
|
370
395
|
confidence REAL,
|
|
371
|
-
PRIMARY KEY (
|
|
396
|
+
PRIMARY KEY (stash_root, file_path, body_hash, relation_order),
|
|
397
|
+
FOREIGN KEY (stash_root, file_path, body_hash)
|
|
398
|
+
REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
|
|
372
399
|
);
|
|
400
|
+
|
|
401
|
+
-- #624-P3: lazy graph-extraction queue. Standalone table (NO FK to
|
|
402
|
+
-- graph_files — a queued file by definition has no graph row yet).
|
|
403
|
+
-- Idempotent on (stash_root, file_path); drained highest-priority-first.
|
|
404
|
+
-- CREATE TABLE IF NOT EXISTS is the forward migration (no DB_VERSION bump).
|
|
405
|
+
CREATE TABLE IF NOT EXISTS graph_extraction_queue (
|
|
406
|
+
stash_root TEXT NOT NULL,
|
|
407
|
+
file_path TEXT NOT NULL,
|
|
408
|
+
body_hash TEXT NOT NULL,
|
|
409
|
+
queued_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
410
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
411
|
+
PRIMARY KEY (stash_root, file_path)
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_graph_extraction_queue_drain
|
|
415
|
+
ON graph_extraction_queue(stash_root, priority DESC, queued_at);
|
|
373
416
|
`);
|
|
417
|
+
// #624-P1 migration step 2: copy any renamed-aside legacy graph data into the
|
|
418
|
+
// new-shape tables (just created above), then drop the legacy tables. No-op
|
|
419
|
+
// unless migrateGraphFilesSchema renamed a legacy graph_files this open.
|
|
420
|
+
migrateGraphDataFromLegacy(db);
|
|
374
421
|
// FTS-dirty queue. Created here (not lazily on first upsert) so the
|
|
375
422
|
// per-entry write path doesn't issue a CREATE TABLE IF NOT EXISTS on
|
|
376
423
|
// every call — that DDL would fire thousands of times during a full
|
|
@@ -485,6 +532,7 @@ function handleVersionUpgrade(db) {
|
|
|
485
532
|
db.exec("DROP TABLE IF EXISTS index_dir_state");
|
|
486
533
|
db.exec("DROP TABLE IF EXISTS llm_enrichment_cache");
|
|
487
534
|
db.exec("DROP INDEX IF EXISTS idx_llm_cache_updated");
|
|
535
|
+
db.exec("DROP TABLE IF EXISTS graph_extraction_queue");
|
|
488
536
|
db.exec("DROP TABLE IF EXISTS graph_file_relations");
|
|
489
537
|
db.exec("DROP TABLE IF EXISTS graph_file_entities");
|
|
490
538
|
db.exec("DROP TABLE IF EXISTS graph_files");
|
|
@@ -715,6 +763,110 @@ function ensureDerivedFromColumn(db) {
|
|
|
715
763
|
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_derived_from ON entries(derived_from)");
|
|
716
764
|
}, "entries table may not exist on a brand-new DB before CREATE — caller is responsible");
|
|
717
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* Returns true when a table exists in the current database.
|
|
768
|
+
*/
|
|
769
|
+
function tableExists(db, name) {
|
|
770
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1").get(name);
|
|
771
|
+
return row !== undefined && row !== null;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* #624-P1 targeted graph-schema migration — STEP 1 of 2 (rename).
|
|
775
|
+
*
|
|
776
|
+
* graph_files was re-keyed from `entry_id INTEGER PRIMARY KEY REFERENCES
|
|
777
|
+
* entries(id)` to a self-contained `(stash_root, file_path, body_hash)` PK.
|
|
778
|
+
* SQLite cannot ALTER a primary key, so an existing DB carrying the OLD shape
|
|
779
|
+
* has its 3 graph tables RENAMED to `*_legacy` here; ensureSchema's CREATE block
|
|
780
|
+
* then builds the new-shape tables, and {@link migrateGraphDataFromLegacy} COPIES
|
|
781
|
+
* the data across before dropping the legacy tables. The graph is preserved —
|
|
782
|
+
* NOT re-extracted (re-extraction is ~19s/file of LLM work).
|
|
783
|
+
*
|
|
784
|
+
* Crucially this is GRAPH-SCOPED: it touches ONLY the graph tables, never the
|
|
785
|
+
* index / embeddings / enrichment cache. So users keep their (expensive)
|
|
786
|
+
* embeddings instead of being forced into a full re-embed by a DB_VERSION bump.
|
|
787
|
+
*
|
|
788
|
+
* Detection: the old schema has an `entry_id` column on graph_files. Fresh DBs
|
|
789
|
+
* (no graph_files yet) and already-migrated DBs (no entry_id column) are no-ops.
|
|
790
|
+
* Idempotent.
|
|
791
|
+
*/
|
|
792
|
+
function migrateGraphFilesSchema(db) {
|
|
793
|
+
bestEffort(() => {
|
|
794
|
+
const cols = db.prepare("PRAGMA table_info(graph_files)").all();
|
|
795
|
+
const isLegacyShape = cols.some((c) => c.name === "entry_id");
|
|
796
|
+
if (!isLegacyShape)
|
|
797
|
+
return;
|
|
798
|
+
// A previous interrupted migration may have left *_legacy behind — drop those
|
|
799
|
+
// husks first so the rename below doesn't collide.
|
|
800
|
+
db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
|
|
801
|
+
db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
|
|
802
|
+
db.exec("DROP TABLE IF EXISTS graph_files_legacy");
|
|
803
|
+
// Rename the 3 entry_id-keyed tables aside. graph_meta is unchanged (stash_root
|
|
804
|
+
// key) so it is left in place. ALTER … RENAME auto-updates child FK refs in
|
|
805
|
+
// SQLite ≥3.25, which is fine — the legacy children are dropped after the copy.
|
|
806
|
+
db.exec("ALTER TABLE graph_files RENAME TO graph_files_legacy");
|
|
807
|
+
if (tableExists(db, "graph_file_entities")) {
|
|
808
|
+
db.exec("ALTER TABLE graph_file_entities RENAME TO graph_file_entities_legacy");
|
|
809
|
+
}
|
|
810
|
+
if (tableExists(db, "graph_file_relations")) {
|
|
811
|
+
db.exec("ALTER TABLE graph_file_relations RENAME TO graph_file_relations_legacy");
|
|
812
|
+
}
|
|
813
|
+
}, "graph_files may not exist on a brand-new DB before CREATE — caller is responsible");
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* #624-P1 targeted graph-schema migration — STEP 2 of 2 (copy + drop legacy).
|
|
817
|
+
*
|
|
818
|
+
* Runs AFTER the graph CREATE TABLE block, so the new-shape tables exist. Copies
|
|
819
|
+
* every legacy row into the re-keyed tables — the old tables already carry
|
|
820
|
+
* (stash_root, file_path, body_hash) next to entry_id, so the projection is a
|
|
821
|
+
* straight column copy (children JOIN back to graph_files_legacy to resolve the
|
|
822
|
+
* composite key from their entry_id). Then drops the `*_legacy` tables.
|
|
823
|
+
*
|
|
824
|
+
* Best-effort: a copy failure (e.g. a pre-body_hash legacy schema) is tolerated,
|
|
825
|
+
* and the legacy tables are dropped regardless so they never linger. Rows whose
|
|
826
|
+
* body_hash is null/empty can't form the new PK and are skipped (they re-extract).
|
|
827
|
+
*/
|
|
828
|
+
function migrateGraphDataFromLegacy(db) {
|
|
829
|
+
if (!tableExists(db, "graph_files_legacy"))
|
|
830
|
+
return;
|
|
831
|
+
let migratedFiles = 0;
|
|
832
|
+
bestEffort(() => {
|
|
833
|
+
db.transaction(() => {
|
|
834
|
+
const res = db
|
|
835
|
+
.prepare(`INSERT OR IGNORE INTO graph_files
|
|
836
|
+
(stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id)
|
|
837
|
+
SELECT stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id
|
|
838
|
+
FROM graph_files_legacy
|
|
839
|
+
WHERE body_hash IS NOT NULL AND body_hash != ''`)
|
|
840
|
+
.run();
|
|
841
|
+
migratedFiles = Number(res.changes);
|
|
842
|
+
if (tableExists(db, "graph_file_entities_legacy")) {
|
|
843
|
+
db.exec(`INSERT OR IGNORE INTO graph_file_entities
|
|
844
|
+
(stash_root, file_path, body_hash, entity_order, entity_norm, entity)
|
|
845
|
+
SELECT gf.stash_root, gf.file_path, gf.body_hash, e.entity_order, e.entity_norm, e.entity
|
|
846
|
+
FROM graph_file_entities_legacy e
|
|
847
|
+
JOIN graph_files_legacy gf ON gf.entry_id = e.entry_id
|
|
848
|
+
WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
|
|
849
|
+
}
|
|
850
|
+
if (tableExists(db, "graph_file_relations_legacy")) {
|
|
851
|
+
db.exec(`INSERT OR IGNORE INTO graph_file_relations
|
|
852
|
+
(stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence)
|
|
853
|
+
SELECT gf.stash_root, gf.file_path, gf.body_hash, r.relation_order, r.from_entity_norm, r.from_entity, r.to_entity_norm, r.to_entity, r.relation_type, r.confidence
|
|
854
|
+
FROM graph_file_relations_legacy r
|
|
855
|
+
JOIN graph_files_legacy gf ON gf.entry_id = r.entry_id
|
|
856
|
+
WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
|
|
857
|
+
}
|
|
858
|
+
})();
|
|
859
|
+
}, "graph data migration is best-effort; legacy tables are dropped regardless below");
|
|
860
|
+
// Always drop the legacy tables (children first), migrated or not.
|
|
861
|
+
bestEffort(() => {
|
|
862
|
+
db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
|
|
863
|
+
db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
|
|
864
|
+
db.exec("DROP TABLE IF EXISTS graph_files_legacy");
|
|
865
|
+
}, "drop legacy graph tables after migration");
|
|
866
|
+
if (migratedFiles > 0) {
|
|
867
|
+
warn(`[akm] graph index re-keyed (#624): migrated ${migratedFiles} extracted file(s) to the new schema — no re-extraction needed. Index + embeddings untouched.`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
718
870
|
/**
|
|
719
871
|
* Phase 5A / Advantage D5: look up the derived-memory child row whose
|
|
720
872
|
* `derived_from` column matches `parentRef` (e.g. `"memory:claude-prefs"`).
|
|
@@ -839,6 +991,38 @@ function deleteRelatedRows(db, ids) {
|
|
|
839
991
|
// Clean up usage events before deleting entries
|
|
840
992
|
bestEffort(() => db.prepare(`DELETE FROM usage_events WHERE entry_id IN (${placeholders})`).run(...chunk), "delete usage_events for entries");
|
|
841
993
|
}
|
|
994
|
+
// #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
|
|
995
|
+
// entries row must NOT wipe the extracted graph (that is the whole point —
|
|
996
|
+
// the graph survives a reindex when body_hash is unchanged). We therefore do
|
|
997
|
+
// NOT delete graph_files here. We DO, however, recompute graph_meta counts
|
|
998
|
+
// for the stash roots touched by the deleted entries so the summary numbers
|
|
999
|
+
// stay consistent with the live child rows (the counts are derived, and the
|
|
1000
|
+
// entries delete may have changed which files are considered/indexed).
|
|
1001
|
+
//
|
|
1002
|
+
// Resolve the affected stash roots from the entries rows BEFORE deletion.
|
|
1003
|
+
const affectedStashRoots = new Set();
|
|
1004
|
+
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
1005
|
+
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1006
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
1007
|
+
bestEffort(() => {
|
|
1008
|
+
const rows = db
|
|
1009
|
+
.prepare(`SELECT DISTINCT stash_dir FROM entries WHERE id IN (${placeholders})`)
|
|
1010
|
+
.all(...chunk);
|
|
1011
|
+
for (const row of rows) {
|
|
1012
|
+
if (row.stash_dir)
|
|
1013
|
+
affectedStashRoots.add(row.stash_dir);
|
|
1014
|
+
}
|
|
1015
|
+
}, "resolve stash roots for graph_meta recompute");
|
|
1016
|
+
}
|
|
1017
|
+
for (const stashRoot of affectedStashRoots) {
|
|
1018
|
+
bestEffort(() => db
|
|
1019
|
+
.prepare(`UPDATE graph_meta
|
|
1020
|
+
SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
|
|
1021
|
+
entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
|
|
1022
|
+
relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
|
|
1023
|
+
WHERE stash_root = ?`)
|
|
1024
|
+
.run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
|
|
1025
|
+
}
|
|
842
1026
|
}
|
|
843
1027
|
/**
|
|
844
1028
|
* Delete entries by their primary key IDs, along with all related rows
|
|
@@ -1048,12 +1232,12 @@ function searchBlobVec(db, queryEmbedding, k) {
|
|
|
1048
1232
|
}
|
|
1049
1233
|
}
|
|
1050
1234
|
// ── FTS5 search ─────────────────────────────────────────────────────────────
|
|
1051
|
-
export function searchFts(db, query, limit, entryType) {
|
|
1235
|
+
export function searchFts(db, query, limit, entryType, excludeTypes) {
|
|
1052
1236
|
const ftsQuery = sanitizeFtsQuery(query);
|
|
1053
1237
|
if (!ftsQuery)
|
|
1054
1238
|
return [];
|
|
1055
1239
|
// Try the exact AND query first
|
|
1056
|
-
const exactResults = runFtsQuery(db, ftsQuery, limit, entryType);
|
|
1240
|
+
const exactResults = runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes);
|
|
1057
1241
|
if (exactResults.length > 0)
|
|
1058
1242
|
return exactResults;
|
|
1059
1243
|
// Exact match returned zero results — try prefix fallback.
|
|
@@ -1063,7 +1247,7 @@ export function searchFts(db, query, limit, entryType) {
|
|
|
1063
1247
|
const prefixQuery = buildPrefixQuery(ftsQuery);
|
|
1064
1248
|
if (!prefixQuery)
|
|
1065
1249
|
return [];
|
|
1066
|
-
return runFtsQuery(db, prefixQuery, limit, entryType);
|
|
1250
|
+
return runFtsQuery(db, prefixQuery, limit, entryType, excludeTypes);
|
|
1067
1251
|
}
|
|
1068
1252
|
/**
|
|
1069
1253
|
* Build a prefix query from an FTS5 query string by appending `*` to each
|
|
@@ -1086,9 +1270,14 @@ function buildPrefixQuery(ftsQuery) {
|
|
|
1086
1270
|
return null;
|
|
1087
1271
|
return prefixTokens.join(" ");
|
|
1088
1272
|
}
|
|
1089
|
-
function runFtsQuery(db, ftsQuery, limit, entryType) {
|
|
1273
|
+
function runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes) {
|
|
1090
1274
|
let sql;
|
|
1091
1275
|
let params;
|
|
1276
|
+
// #627 — exclude-type clause. Only applies on the untyped ('any') path; an
|
|
1277
|
+
// explicit include filter (entryType) already narrows to a single type, so
|
|
1278
|
+
// exclusion is redundant there. An empty list skips the clause entirely
|
|
1279
|
+
// (never emit `NOT IN ()`, which is a SQL error / always-false).
|
|
1280
|
+
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
1092
1281
|
// Join on integer entry_id directly (no CAST needed; we store integer)
|
|
1093
1282
|
// Use bm25() with per-column weights: entry_id(0), name(10), description(5), tags(3), hints(2), content(1)
|
|
1094
1283
|
if (entryType && entryType !== "any") {
|
|
@@ -1105,16 +1294,19 @@ function runFtsQuery(db, ftsQuery, limit, entryType) {
|
|
|
1105
1294
|
params = [ftsQuery, entryType, limit];
|
|
1106
1295
|
}
|
|
1107
1296
|
else {
|
|
1297
|
+
const excludeClause = excludes.length > 0 ? `AND e.entry_type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
|
|
1108
1298
|
sql = `
|
|
1109
1299
|
SELECT e.id, e.file_path AS filePath, e.entry_json, e.search_text AS searchText,
|
|
1110
1300
|
bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
|
|
1111
1301
|
FROM entries_fts f
|
|
1112
1302
|
JOIN entries e ON e.id = f.entry_id
|
|
1113
1303
|
WHERE entries_fts MATCH ?
|
|
1304
|
+
${excludeClause}
|
|
1114
1305
|
ORDER BY bm25Score, e.id ASC
|
|
1115
1306
|
LIMIT ?
|
|
1116
1307
|
`;
|
|
1117
|
-
|
|
1308
|
+
// Param order: MATCH, then the NOT IN values, then LIMIT.
|
|
1309
|
+
params = [ftsQuery, ...excludes, limit];
|
|
1118
1310
|
}
|
|
1119
1311
|
try {
|
|
1120
1312
|
const rows = db.prepare(sql).all(...params);
|
|
@@ -1183,14 +1375,21 @@ function parseEntryRows(rows, context) {
|
|
|
1183
1375
|
}
|
|
1184
1376
|
return entries;
|
|
1185
1377
|
}
|
|
1186
|
-
export function getAllEntries(db, entryType) {
|
|
1378
|
+
export function getAllEntries(db, entryType, excludeTypes) {
|
|
1187
1379
|
let sql;
|
|
1188
1380
|
let params;
|
|
1381
|
+
// #627 — exclude-type clause applies only on the untyped ('any') path. Empty
|
|
1382
|
+
// list skips the clause (never `NOT IN ()`).
|
|
1383
|
+
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
1189
1384
|
if (entryType && entryType !== "any") {
|
|
1190
1385
|
sql =
|
|
1191
1386
|
"SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type = ?";
|
|
1192
1387
|
params = [entryType];
|
|
1193
1388
|
}
|
|
1389
|
+
else if (excludes.length > 0) {
|
|
1390
|
+
sql = `SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type NOT IN (${excludes.map(() => "?").join(", ")})`;
|
|
1391
|
+
params = [...excludes];
|
|
1392
|
+
}
|
|
1194
1393
|
else {
|
|
1195
1394
|
sql = "SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries";
|
|
1196
1395
|
params = [];
|
|
@@ -1198,6 +1397,47 @@ export function getAllEntries(db, entryType) {
|
|
|
1198
1397
|
const rows = db.prepare(sql).all(...params);
|
|
1199
1398
|
return parseEntryRows(rows, "getAllEntries");
|
|
1200
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* #609 — read graph entities (normalized) for a set of entry ids. Used by the
|
|
1402
|
+
* recombine pass to cluster memories by shared graph entity ("graph"
|
|
1403
|
+
* relatedness source). Returns a map of `entry_id -> entity_norm[]`. Entries
|
|
1404
|
+
* with no graph entities (graph extraction has not run, or the file produced
|
|
1405
|
+
* no entities) are simply absent from the map — callers must fail open
|
|
1406
|
+
* (fall back to tag relatedness) when the map is empty.
|
|
1407
|
+
*/
|
|
1408
|
+
export function getEntitiesByEntryIds(db, entryIds) {
|
|
1409
|
+
const result = new Map();
|
|
1410
|
+
if (entryIds.length === 0)
|
|
1411
|
+
return result;
|
|
1412
|
+
// #624-P1: graph_file_entities no longer carries entry_id. Re-derive the
|
|
1413
|
+
// entry_id -> entity_norm[] contract by JOINing through entries on
|
|
1414
|
+
// (stash_dir, file_path) -> graph_files. Chunk the IN(?) list because the
|
|
1415
|
+
// recombine pass can pass 10k+ entry ids (well over the SQLite param limit).
|
|
1416
|
+
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
1417
|
+
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1418
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
1419
|
+
const rows = db
|
|
1420
|
+
.prepare(`SELECT e.id AS entry_id, gfe.entity_norm AS entity_norm
|
|
1421
|
+
FROM entries e
|
|
1422
|
+
JOIN graph_files gf
|
|
1423
|
+
ON gf.stash_root = e.stash_dir AND gf.file_path = e.file_path
|
|
1424
|
+
JOIN graph_file_entities gfe
|
|
1425
|
+
ON gfe.stash_root = gf.stash_root
|
|
1426
|
+
AND gfe.file_path = gf.file_path
|
|
1427
|
+
AND gfe.body_hash = gf.body_hash
|
|
1428
|
+
WHERE e.id IN (${placeholders})
|
|
1429
|
+
ORDER BY e.id, gfe.entity_order`)
|
|
1430
|
+
.all(...chunk);
|
|
1431
|
+
for (const row of rows) {
|
|
1432
|
+
const list = result.get(row.entry_id);
|
|
1433
|
+
if (list)
|
|
1434
|
+
list.push(row.entity_norm);
|
|
1435
|
+
else
|
|
1436
|
+
result.set(row.entry_id, [row.entity_norm]);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return result;
|
|
1440
|
+
}
|
|
1201
1441
|
export function findEntryIdByRef(db, ref) {
|
|
1202
1442
|
const parsed = parseAssetRef(ref);
|
|
1203
1443
|
const nameVariants = [parsed.name];
|
|
@@ -1262,6 +1502,19 @@ export function getEntryIdByFilePath(db, filePath) {
|
|
|
1262
1502
|
const row = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
|
|
1263
1503
|
return row?.id;
|
|
1264
1504
|
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Set of every non-empty `entries.file_path` currently indexed (across all
|
|
1507
|
+
* stashes/sources). Used by staleness detection to spot files that exist on
|
|
1508
|
+
* disk but were never indexed — a clock-independent signal for newly-added
|
|
1509
|
+
* assets that an mtime-vs-builtAt comparison can miss when the two clocks
|
|
1510
|
+
* (filesystem vs wall-clock) are skewed within the same millisecond.
|
|
1511
|
+
*/
|
|
1512
|
+
export function getIndexedFilePaths(db) {
|
|
1513
|
+
const rows = db
|
|
1514
|
+
.prepare("SELECT DISTINCT file_path FROM entries WHERE file_path IS NOT NULL AND file_path <> ''")
|
|
1515
|
+
.all();
|
|
1516
|
+
return new Set(rows.map((r) => r.file_path));
|
|
1517
|
+
}
|
|
1265
1518
|
/**
|
|
1266
1519
|
* Resolve a single `entries.file_path` by primary key, or `undefined` if no
|
|
1267
1520
|
* row matches.
|
|
@@ -1477,25 +1730,98 @@ export function computeBodyHash(body) {
|
|
|
1477
1730
|
return sha256Hex(body);
|
|
1478
1731
|
}
|
|
1479
1732
|
/**
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1733
|
+
* Reduce a ref to its bare `type:name` form, dropping any `origin//` prefix.
|
|
1734
|
+
*
|
|
1735
|
+
* usage_events store entry_ref inconsistently: search/show writers persist
|
|
1736
|
+
* whatever ref the result carried, which is sometimes stash-prefixed
|
|
1737
|
+
* (`origin//type:name`) and sometimes bare (`type:name`). Retrieval counting
|
|
1738
|
+
* keys on the bare form so both spellings of the same asset collapse together.
|
|
1739
|
+
*
|
|
1740
|
+
* Returns the bare form, or the original string when it cannot be parsed (best
|
|
1741
|
+
* effort — never throws so a malformed stored ref can't break counting).
|
|
1742
|
+
*/
|
|
1743
|
+
function bareRef(ref) {
|
|
1744
|
+
try {
|
|
1745
|
+
const parsed = parseAssetRef(ref);
|
|
1746
|
+
return `${parsed.type}:${parsed.name}`;
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
return ref;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Count retrieval events for the given entry refs.
|
|
1754
|
+
*
|
|
1755
|
+
* Counts `search`, `show`, and `curate` usage events. Returns a
|
|
1756
|
+
* Map<inputRef, count> keyed by the *input* ref strings (only those with at
|
|
1757
|
+
* least one matching event appear). Used by the improve loop to find
|
|
1758
|
+
* high-retrieval assets without feedback.
|
|
1759
|
+
*
|
|
1760
|
+
* Matching is normalization-aware: each stored `entry_ref` is reduced to its
|
|
1761
|
+
* bare `type:name` form before comparison, so a stash-prefixed stored ref
|
|
1762
|
+
* (`origin//type:name`) still matches a bare input ref (`type:name`) and vice
|
|
1763
|
+
* versa. Previously the raw `entry_ref IN (...)` comparison silently dropped
|
|
1764
|
+
* roughly half the signal whenever the two spellings disagreed.
|
|
1765
|
+
*
|
|
1766
|
+
* `curate` events are included: their per-item rows are written with
|
|
1767
|
+
* entry_ref populated (see logCurateEvent), so curation is a real retrieval
|
|
1768
|
+
* signal here. Legacy summary-only curate rows with a NULL entry_ref simply
|
|
1769
|
+
* contribute nothing.
|
|
1483
1770
|
*/
|
|
1484
1771
|
export function getRetrievalCounts(db, refs) {
|
|
1485
1772
|
if (refs.length === 0)
|
|
1486
1773
|
return new Map();
|
|
1487
|
-
|
|
1774
|
+
// Map each distinct bare form back to the input ref(s) that produced it so we
|
|
1775
|
+
// can re-key DB results (grouped by bare form) onto the caller's ref strings.
|
|
1776
|
+
const bareToInputs = new Map();
|
|
1777
|
+
for (const ref of refs) {
|
|
1778
|
+
const bare = bareRef(ref);
|
|
1779
|
+
const existing = bareToInputs.get(bare);
|
|
1780
|
+
if (existing)
|
|
1781
|
+
existing.push(ref);
|
|
1782
|
+
else
|
|
1783
|
+
bareToInputs.set(bare, [ref]);
|
|
1784
|
+
}
|
|
1785
|
+
const bareForms = [...bareToInputs.keys()];
|
|
1786
|
+
// Accumulate counts per bare form across chunks before re-keying.
|
|
1787
|
+
const countsByBare = new Map();
|
|
1488
1788
|
// Chunk to stay within SQLITE_MAX_VARIABLE_NUMBER (same pattern as getUtilityScoresByIds).
|
|
1489
|
-
for (let i = 0; i <
|
|
1490
|
-
const chunk =
|
|
1789
|
+
for (let i = 0; i < bareForms.length; i += SQLITE_CHUNK_SIZE) {
|
|
1790
|
+
const chunk = bareForms.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1491
1791
|
const placeholders = chunk.map(() => "?").join(", ");
|
|
1792
|
+
// Normalize the stored entry_ref to its bare form inside SQL by stripping
|
|
1793
|
+
// everything up to and including the last `//` separator. SQLite has no
|
|
1794
|
+
// rfind, but stored origins never themselves contain `//`, so a stash ref
|
|
1795
|
+
// has exactly one `//` and `substr(... instr ...)` is exact; bare refs have
|
|
1796
|
+
// no `//` and pass through unchanged.
|
|
1492
1797
|
const rows = db
|
|
1493
|
-
.prepare(`SELECT
|
|
1494
|
-
|
|
1495
|
-
|
|
1798
|
+
.prepare(`SELECT
|
|
1799
|
+
CASE
|
|
1800
|
+
WHEN instr(entry_ref, '//') > 0
|
|
1801
|
+
THEN substr(entry_ref, instr(entry_ref, '//') + 2)
|
|
1802
|
+
ELSE entry_ref
|
|
1803
|
+
END AS bare_ref,
|
|
1804
|
+
COUNT(*) AS cnt
|
|
1805
|
+
FROM usage_events
|
|
1806
|
+
WHERE event_type IN ('search','show','curate')
|
|
1807
|
+
AND entry_ref IS NOT NULL
|
|
1808
|
+
AND CASE
|
|
1809
|
+
WHEN instr(entry_ref, '//') > 0
|
|
1810
|
+
THEN substr(entry_ref, instr(entry_ref, '//') + 2)
|
|
1811
|
+
ELSE entry_ref
|
|
1812
|
+
END IN (${placeholders})
|
|
1813
|
+
GROUP BY bare_ref`)
|
|
1496
1814
|
.all(...chunk);
|
|
1497
|
-
for (const r of rows)
|
|
1498
|
-
|
|
1815
|
+
for (const r of rows) {
|
|
1816
|
+
countsByBare.set(r.bare_ref, (countsByBare.get(r.bare_ref) ?? 0) + r.cnt);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
// Re-key bare-form counts onto every input ref that maps to that bare form.
|
|
1820
|
+
const result = new Map();
|
|
1821
|
+
for (const [bare, count] of countsByBare) {
|
|
1822
|
+
for (const input of bareToInputs.get(bare) ?? []) {
|
|
1823
|
+
result.set(input, count);
|
|
1824
|
+
}
|
|
1499
1825
|
}
|
|
1500
1826
|
return result;
|
|
1501
1827
|
}
|