akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9

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.
Files changed (116) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/STABILITY.md +9 -8
  3. package/dist/akm +55 -22
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  15. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  16. package/dist/assets/templates/html/health.html +3 -5
  17. package/dist/cli/retired-commands.js +1 -1
  18. package/dist/commands/health/archive-usage.js +98 -0
  19. package/dist/commands/health/data-dir-usage.js +25 -13
  20. package/dist/commands/health/html-report.js +1 -4
  21. package/dist/commands/health/improve-metrics.js +0 -25
  22. package/dist/commands/health/md-report.js +1 -6
  23. package/dist/commands/health/report-view-model.js +4 -14
  24. package/dist/commands/health/windows.js +0 -1
  25. package/dist/commands/health.js +13 -0
  26. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  27. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  28. package/dist/commands/improve/consolidate.js +38 -63
  29. package/dist/commands/improve/extract-prompt.js +1 -2
  30. package/dist/commands/improve/improve-cli.js +1 -1
  31. package/dist/commands/improve/improve-strategies.js +23 -5
  32. package/dist/commands/improve/improve.js +19 -30
  33. package/dist/commands/improve/ledger.js +3 -2
  34. package/dist/commands/improve/loop-stages.js +5 -84
  35. package/dist/commands/improve/memory/memory-belief.js +3 -1
  36. package/dist/commands/improve/memory/memory-improve.js +269 -11
  37. package/dist/commands/improve/planner.js +0 -5
  38. package/dist/commands/improve/preparation.js +20 -135
  39. package/dist/commands/improve/retrieval-scope.js +19 -4
  40. package/dist/commands/improve/salience.js +1 -14
  41. package/dist/commands/improve/stage.js +0 -1
  42. package/dist/commands/lint/base-linter.js +19 -11
  43. package/dist/commands/proposal/drain.js +8 -1
  44. package/dist/commands/proposal/proposal-cli.js +16 -2
  45. package/dist/commands/proposal/proposal-types.js +7 -0
  46. package/dist/commands/proposal/proposal.js +37 -6
  47. package/dist/commands/proposal/repository.js +613 -4
  48. package/dist/commands/proposal/validators/proposals.js +9 -0
  49. package/dist/commands/read/curate.js +40 -13
  50. package/dist/commands/read/knowledge.js +3 -2
  51. package/dist/commands/read/show.js +55 -16
  52. package/dist/commands/sources/info.js +3 -0
  53. package/dist/commands/sources/stash-cli.js +2 -2
  54. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  55. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  56. package/dist/core/bundle-rename.js +1 -7
  57. package/dist/core/config/config-schema.js +8 -1
  58. package/dist/core/config/config.js +23 -48
  59. package/dist/core/config/engine-semantics.js +0 -2
  60. package/dist/core/config/schema/improve-processes.js +17 -42
  61. package/dist/core/config/schema/index-config.js +5 -25
  62. package/dist/core/file-change.js +13 -5
  63. package/dist/core/improve-result.js +16 -5
  64. package/dist/core/improve-types.js +0 -1
  65. package/dist/core/loopback.js +7 -12
  66. package/dist/core/parse.js +13 -16
  67. package/dist/core/state/migrations.js +15 -0
  68. package/dist/core/time.js +0 -20
  69. package/dist/indexer/db/llm-cache.js +2 -2
  70. package/dist/indexer/ensure-index.js +2 -2
  71. package/dist/indexer/index-written-assets.js +2 -3
  72. package/dist/indexer/indexer.js +18 -418
  73. package/dist/indexer/links/declared-links.js +90 -0
  74. package/dist/indexer/passes/metadata.js +0 -19
  75. package/dist/indexer/scan/doc-to-entry.js +1 -0
  76. package/dist/indexer/walk/walker.js +3 -4
  77. package/dist/llm/client.js +8 -10
  78. package/dist/llm/embedders/remote.js +1 -2
  79. package/dist/llm/feature-gate.js +0 -5
  80. package/dist/output/shapes/helpers.js +23 -4
  81. package/dist/output/text/command-format.js +0 -8
  82. package/dist/output/text/proposal-format.js +47 -1
  83. package/dist/output/text/show-format.js +13 -17
  84. package/dist/scripts/akm-migrate-node.js +2754 -2836
  85. package/dist/scripts/akm-migrate.js +2754 -2836
  86. package/dist/setup/steps/connection.js +5 -6
  87. package/dist/setup/steps/platforms.js +2 -2
  88. package/dist/sources/providers/git-stash.js +55 -4
  89. package/dist/storage/repositories/improve-ledger-repository.js +48 -7
  90. package/dist/storage/repositories/index-entries-repository.js +16 -13
  91. package/dist/storage/repositories/index-entry-schema.js +22 -3
  92. package/dist/storage/repositories/index-links-repository.js +143 -0
  93. package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
  94. package/dist/storage/repositories/index-schema.js +82 -104
  95. package/dist/storage/repositories/proposals-repository.js +61 -0
  96. package/dist/storage/repositories/salience-repository.js +1 -19
  97. package/dist/tasks/source/task-to-v4.js +462 -74
  98. package/docs/migration/release-notes/0.9.17.md +7 -5
  99. package/docs/reference/cli.md +33 -21
  100. package/docs/reference/configuration.md +21 -12
  101. package/docs/reference/data-and-telemetry.md +0 -1
  102. package/package.json +1 -1
  103. package/schemas/akm-config.json +0 -342
  104. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  105. package/dist/assets/prompts/contradiction-judge.md +0 -33
  106. package/dist/assets/prompts/graph-extract-system.md +0 -1
  107. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  108. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  109. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  110. package/dist/indexer/db/graph-db.js +0 -431
  111. package/dist/indexer/graph/graph-extraction.js +0 -807
  112. package/dist/indexer/graph/graph-related.js +0 -131
  113. package/dist/indexer/graph/graph-types.js +0 -4
  114. package/dist/llm/graph-extract.js +0 -903
  115. package/dist/llm/metadata-enhance.js +0 -95
  116. package/dist/tasks/source/task-to-v3.js +0 -453
@@ -1,33 +0,0 @@
1
- You are evaluating two derived memory entries to determine if they contain
2
- directly contradictory factual claims about the same subject.
3
-
4
- Memory A:
5
- Ref: {{A_REF}}
6
- Description: {{A_DESCRIPTION}}
7
- Content:
8
- ```
9
- {{A_BODY}}
10
- ```
11
-
12
- Memory B:
13
- Ref: {{B_REF}}
14
- Description: {{B_DESCRIPTION}}
15
- Content:
16
- ```
17
- {{B_BODY}}
18
- ```
19
-
20
- Answer ONLY with valid JSON — no prose, no code fences:
21
- {"contradicts": true|false, "confidence": 0.0, "reason": "<cite the exact opposing sentence from each memory, or explain why not contradicted>"}
22
-
23
- A contradiction means the memories make LOGICALLY EXCLUSIVE claims: a practitioner
24
- cannot follow BOTH simultaneously. The test: cite the exact sentence from Memory A
25
- and the exact sentence from Memory B that are in direct conflict. If you cannot cite
26
- specific opposing sentences, return false.
27
-
28
- Sharing a topic, tool, domain, or workflow stage is NOT a contradiction. Only direct
29
- factual opposites qualify: opposing recommended commands, opposing boolean flags,
30
- opposing version numbers, or mutually exclusive instructions.
31
-
32
- Set confidence ≥ 0.92 only when evidence is unambiguous. Use lower values when
33
- uncertain — the caller will skip edges below 0.92.
@@ -1 +0,0 @@
1
- You extract a knowledge graph from developer notes. Return ONLY valid JSON — no prose, no markdown fences, no preamble.
@@ -1,35 +0,0 @@
1
- Extract entities and relations from the asset body below.
2
-
3
- Rules:
4
- - Output ONLY a JSON object: {"entities": ["Entity One", ...], "relations": [["A", "uses", "B"], ...]}.
5
- - Entities are short, canonical noun phrases (project names, services, tools, people, technical concepts). Do NOT emit file or directory paths (anything containing "/" or "\") — they are dropped downstream.
6
- - Each relation is a 3-element array: [from, type, to]. Relations connect two entities that both appear in the entities array.
7
- - "type" is a short verb phrase (e.g. "uses", "depends on", "owns", "documents"). Use "" when unsure.
8
- - Drop pleasantries, meta-commentary, and timestamps.
9
- - Limit to at most {{MAX_ENTITIES}} entities and {{MAX_RELATIONS}} relations per asset.
10
- - Return {"entities": [], "relations": []} if the body has no extractable graph content.
11
- - DO NOT return markdown code blocks, ONLY valid JSON objects.
12
-
13
- Examples:
14
-
15
- Input:
16
- ## Deployment Notes
17
- The auth-service uses PostgreSQL for user sessions. It depends on the redis-cache
18
- for rate limiting. The terraform-provisioner deploys everything to the prod cluster.
19
- Owner: @alice.
20
-
21
- Output:
22
- {"entities":["auth-service","PostgreSQL","redis-cache","terraform-provisioner","prod cluster","@alice"],"relations":[["auth-service","uses","PostgreSQL"],["auth-service","depends on","redis-cache"],["terraform-provisioner","deploys","prod cluster"],["terraform-provisioner","deploys","auth-service"],["@alice","owns","auth-service"]]}
23
-
24
- Input:
25
- ## Meeting: API Redesign
26
- Discussed moving from REST to GraphQL. The frontend team will use Apollo Client.
27
- Backend needs to implement resolvers. Timeline: Q2.
28
-
29
- Output:
30
- {"entities":["REST","GraphQL","Apollo Client","frontend team","backend","resolvers","Q2"],"relations":[["frontend team","uses","Apollo Client"],["backend","implements","resolvers"],["frontend team","migrates to","GraphQL"]]}
31
-
32
- ===============
33
-
34
- Request:
35
-
@@ -1 +0,0 @@
1
- You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.
@@ -1,4 +0,0 @@
1
- version: 4
2
- run: akm improve --strategy graph-refresh --skip-if-locked --require-engines
3
- description: Full-corpus graph rebuild (weekly Sunday 3:10am)
4
- schedule: "10 3 * * 0"
@@ -1,431 +0,0 @@
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 { rethrowIfDataDirUnreadable, rethrowIfTestIsolationError } from "../../core/errors.js";
5
- import { isPathAbsent } from "../../core/path-access.js";
6
- import { getDbPath } from "../../core/paths.js";
7
- import { normalizeEntityKey } from "../../llm/graph-extract.js";
8
- import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
9
- import { GRAPH_SCHEMA_VERSION } from "../../storage/repositories/index-schema.js";
10
- function withReadableGraphDb(db, fn) {
11
- if (db)
12
- return fn(db);
13
- const dbPath = getDbPath();
14
- // `GRAPH_DB_MISSING` is the loaders' "nothing extracted yet" sentinel — every
15
- // caller below turns it into `null`/`[]`. Reserve it for a genuinely ABSENT
16
- // index: an index that exists and cannot be read must reach the caller as the
17
- // ConfigError `openExistingDatabase` raises, not as "no graph data" (#791).
18
- if (isPathAbsent(dbPath))
19
- throw new Error("GRAPH_DB_MISSING");
20
- const opened = openExistingDatabase(dbPath);
21
- try {
22
- return fn(opened);
23
- }
24
- finally {
25
- closeDatabase(opened);
26
- }
27
- }
28
- function uniqueSorted(values) {
29
- return [...new Set(values)].sort((a, b) => a.localeCompare(b));
30
- }
31
- /** Child rows joined to their file row, the same join the loaders read through. */
32
- const STORED_ENTITIES = `graph_file_entities e
33
- JOIN graph_files gf ON gf.stash_root = e.stash_root AND gf.file_path = e.file_path AND gf.body_hash = e.body_hash
34
- WHERE gf.stash_root = ?`;
35
- const STORED_RELATIONS = `graph_file_relations r
36
- JOIN graph_files gf ON gf.stash_root = r.stash_root AND gf.file_path = r.file_path AND gf.body_hash = r.body_hash
37
- WHERE gf.stash_root = ?`;
38
- /** One comparable key for a file's extraction: its entities and relations, in order. */
39
- function extractionKey(entities, relations) {
40
- return JSON.stringify([entities, relations.map((r) => [r.from, r.to, r.type ?? null, r.confidence ?? null])]);
41
- }
42
- const EMPTY_EXTRACTION_KEY = extractionKey([], []);
43
- /** The extraction key of every stored file under a root that has child rows. */
44
- function readStoredExtractionKeys(db, stashRoot) {
45
- const entityRows = db
46
- .prepare(`SELECT e.file_path AS file_path, e.entity AS entity FROM ${STORED_ENTITIES} ORDER BY e.file_path, e.entity_order`)
47
- .all(stashRoot);
48
- const relationRows = db
49
- .prepare(`SELECT r.file_path AS file_path, r.from_entity AS from_entity, r.to_entity AS to_entity,
50
- r.relation_type AS relation_type, r.confidence AS confidence
51
- FROM ${STORED_RELATIONS} ORDER BY r.file_path, r.relation_order`)
52
- .all(stashRoot);
53
- const byPath = new Map();
54
- const bucket = (filePath) => {
55
- let entry = byPath.get(filePath);
56
- if (!entry) {
57
- entry = { entities: [], relations: [] };
58
- byPath.set(filePath, entry);
59
- }
60
- return entry;
61
- };
62
- for (const row of entityRows)
63
- bucket(row.file_path).entities.push(row.entity);
64
- for (const row of relationRows) {
65
- bucket(row.file_path).relations.push({
66
- from: row.from_entity,
67
- to: row.to_entity,
68
- ...(row.relation_type !== null ? { type: row.relation_type } : {}),
69
- ...(row.confidence !== null ? { confidence: row.confidence } : {}),
70
- });
71
- }
72
- return new Map([...byPath].map(([filePath, stored]) => [filePath, extractionKey(stored.entities, stored.relations)]));
73
- }
74
- function roundMetric(value) {
75
- return Number(value.toFixed(4));
76
- }
77
- /**
78
- * The graph_meta counts, derived from the stored rows of one root. Each field
79
- * has one meaning (see {@link GraphQualityTelemetry}): files are graph_files
80
- * rows, entities are distinct case-folded names, relations are distinct
81
- * case-folded (from, to, type) triples.
82
- */
83
- function readStoredGraphQuality(db, stashRoot) {
84
- const count = (sql) => db.prepare(sql).get(stashRoot).n;
85
- const storedFiles = count("SELECT COUNT(*) AS n FROM graph_files WHERE stash_root = ?");
86
- const filesWithEntities = count(`SELECT COUNT(DISTINCT gf.file_path) AS n FROM ${STORED_ENTITIES}`);
87
- const entityCount = count(`SELECT COUNT(DISTINCT e.entity_norm) AS n FROM ${STORED_ENTITIES}`);
88
- const relationCount = count(`SELECT COUNT(*) AS n FROM (
89
- SELECT DISTINCT r.from_entity_norm, r.to_entity_norm, lower(coalesce(r.relation_type, '')) FROM ${STORED_RELATIONS}
90
- )`);
91
- const maxEdges = entityCount > 1 ? (entityCount * (entityCount - 1)) / 2 : 0;
92
- return {
93
- consideredFiles: storedFiles,
94
- extractedFiles: filesWithEntities,
95
- entityCount,
96
- relationCount,
97
- extractionCoverage: storedFiles > 0 ? roundMetric(filesWithEntities / storedFiles) : 0,
98
- density: maxEdges > 0 ? roundMetric(relationCount / maxEdges) : 0,
99
- };
100
- }
101
- /**
102
- * Persist (or update) a graph snapshot for a stash root.
103
- *
104
- * #624-P1: keyed on (stash_root, file_path, body_hash) — NOT entries.id. Graph
105
- * rows are self-keyed by path, so they survive an entries delete + reinsert
106
- * (a reindex) when body_hash is unchanged. A file whose body_hash is unchanged
107
- * keeps its row; its entity and relation rows are rewritten only when they
108
- * differ from the snapshot's (a re-extraction of the same body, e.g. after a
109
- * model or prompt change or a failed first attempt, must land). Files whose
110
- * body_hash changed have their old row + child rows deleted and the new content
111
- * inserted; files in DB but absent from the new snapshot are deleted. There is
112
- * no entry_id resolution and no orphan-skip — a graph file no longer needs a
113
- * matching entries row.
114
- *
115
- * graph_meta records the snapshot's time and run telemetry; its counts are
116
- * derived from the rows as stored after the write, never from the caller's
117
- * in-memory graph.
118
- */
119
- export function replaceStoredGraph(db, graph) {
120
- const upsertMeta = db.prepare(`INSERT INTO graph_meta (
121
- stash_root,
122
- schema_version,
123
- generated_at,
124
- considered_files,
125
- extracted_files,
126
- entity_count,
127
- relation_count,
128
- extraction_coverage,
129
- density,
130
- extractor_id,
131
- extraction_run_id,
132
- model,
133
- prompt_version,
134
- batch_size,
135
- cache_hits,
136
- cache_misses,
137
- truncation_count,
138
- failure_count
139
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
140
- ON CONFLICT(stash_root) DO UPDATE SET
141
- schema_version = excluded.schema_version,
142
- generated_at = excluded.generated_at,
143
- considered_files = excluded.considered_files,
144
- extracted_files = excluded.extracted_files,
145
- entity_count = excluded.entity_count,
146
- relation_count = excluded.relation_count,
147
- extraction_coverage = excluded.extraction_coverage,
148
- density = excluded.density,
149
- extractor_id = excluded.extractor_id,
150
- extraction_run_id = excluded.extraction_run_id,
151
- model = excluded.model,
152
- prompt_version = excluded.prompt_version,
153
- batch_size = excluded.batch_size,
154
- cache_hits = excluded.cache_hits,
155
- cache_misses = excluded.cache_misses,
156
- truncation_count = excluded.truncation_count,
157
- failure_count = excluded.failure_count`);
158
- const selectExisting = db.prepare("SELECT file_path, body_hash, file_order FROM graph_files WHERE stash_root = ?");
159
- const deleteFile = db.prepare("DELETE FROM graph_files WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
160
- const deleteEntities = db.prepare("DELETE FROM graph_file_entities WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
161
- const deleteRelations = db.prepare("DELETE FROM graph_file_relations WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
162
- const insertFile = db.prepare(`INSERT INTO graph_files (
163
- stash_root, file_path, file_order, file_type, body_hash, confidence, status, reason, extraction_run_id
164
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
165
- const updateFileMeta = db.prepare(`UPDATE graph_files
166
- SET file_order = ?, file_type = ?, confidence = ?, status = ?, reason = ?, extraction_run_id = ?
167
- WHERE stash_root = ? AND file_path = ? AND body_hash = ?`);
168
- const insertEntity = db.prepare(`INSERT INTO graph_file_entities (stash_root, file_path, body_hash, entity_order, entity_norm, entity)
169
- VALUES (?, ?, ?, ?, ?, ?)`);
170
- const insertRelation = db.prepare(`INSERT INTO graph_file_relations (
171
- stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence
172
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
173
- const telemetry = graph.telemetry;
174
- db.transaction(() => {
175
- // Build a snapshot of existing rows for incremental compare. The unique
176
- // index idx_graph_files_path guarantees at most one row per file_path.
177
- const existingRows = selectExisting.all(graph.stashRoot);
178
- const existingByPath = new Map();
179
- for (const row of existingRows)
180
- existingByPath.set(row.file_path, row);
181
- const storedKeys = readStoredExtractionKeys(db, graph.stashRoot);
182
- const presentPaths = new Set();
183
- for (const [fileOrder, node] of graph.files.entries()) {
184
- // body_hash is part of the PK; default to a sentinel for inputs (test
185
- // fixtures, legacy imports) that don't supply one. The sentinel never
186
- // equals a real hash so subsequent staleness checks always re-extract —
187
- // correct behaviour for "unknown" bodies. Distinct files in one stash
188
- // are still keyed apart by file_path, so the empty sentinel is safe.
189
- const bodyHash = node.bodyHash && node.bodyHash.length > 0 ? node.bodyHash : "";
190
- const status = node.status ?? (node.entities.length > 0 ? "extracted" : "empty");
191
- const reason = node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content");
192
- const runId = node.extractionRunId ?? telemetry?.extractionRunId ?? null;
193
- presentPaths.add(node.path);
194
- const existing = existingByPath.get(node.path);
195
- if (existing && existing.body_hash === bodyHash) {
196
- // Body unchanged — refresh the file meta, and rewrite the child rows
197
- // only when this snapshot's extraction differs from the stored one.
198
- updateFileMeta.run(fileOrder, node.type, node.confidence ?? null, status, reason, runId, graph.stashRoot, node.path, bodyHash);
199
- const storedKey = storedKeys.get(node.path) ?? EMPTY_EXTRACTION_KEY;
200
- if (storedKey === extractionKey(node.entities, node.relations))
201
- continue;
202
- deleteEntities.run(graph.stashRoot, node.path, bodyHash);
203
- deleteRelations.run(graph.stashRoot, node.path, bodyHash);
204
- }
205
- else {
206
- if (existing) {
207
- // Stale row (different body_hash for this path). Delete the old row by
208
- // its OLD body_hash; child rows cascade, but explicit DELETE keeps the
209
- // order deterministic and is safe regardless of the FK pragma.
210
- deleteEntities.run(graph.stashRoot, existing.file_path, existing.body_hash);
211
- deleteRelations.run(graph.stashRoot, existing.file_path, existing.body_hash);
212
- deleteFile.run(graph.stashRoot, existing.file_path, existing.body_hash);
213
- }
214
- insertFile.run(graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, status, reason, runId);
215
- }
216
- for (const [entityOrder, entity] of node.entities.entries()) {
217
- insertEntity.run(graph.stashRoot, node.path, bodyHash, entityOrder, normalizeEntityKey(entity), entity);
218
- }
219
- for (const [relationOrder, relation] of node.relations.entries()) {
220
- insertRelation.run(graph.stashRoot, node.path, bodyHash, relationOrder, normalizeEntityKey(relation.from), relation.from, normalizeEntityKey(relation.to), relation.to, relation.type ?? null, relation.confidence ?? null);
221
- }
222
- }
223
- // Delete files present in DB but absent from the new snapshot. Child
224
- // tables CASCADE on the composite key; explicit DELETE keeps it determinstic.
225
- for (const row of existingRows) {
226
- if (!presentPaths.has(row.file_path)) {
227
- deleteEntities.run(graph.stashRoot, row.file_path, row.body_hash);
228
- deleteRelations.run(graph.stashRoot, row.file_path, row.body_hash);
229
- deleteFile.run(graph.stashRoot, row.file_path, row.body_hash);
230
- }
231
- }
232
- const quality = readStoredGraphQuality(db, graph.stashRoot);
233
- upsertMeta.run(graph.stashRoot, GRAPH_SCHEMA_VERSION, graph.generatedAt, quality.consideredFiles, quality.extractedFiles, quality.entityCount, quality.relationCount, quality.extractionCoverage, quality.density, telemetry?.extractorId ?? null, telemetry?.extractionRunId ?? null, telemetry?.model ?? null, telemetry?.promptVersion ?? null, telemetry?.batchSize ?? null, telemetry?.cacheHits ?? 0, telemetry?.cacheMisses ?? 0, telemetry?.truncationCount ?? 0, telemetry?.failureCount ?? 0);
234
- })();
235
- }
236
- export function deleteStoredGraph(db, stashPath) {
237
- db.transaction(() => {
238
- // Child rows cascade via the composite (stash_root, file_path, body_hash)
239
- // FK; deleting graph_files clears them. This is the explicit full-clear
240
- // path for a stash (entries-delete no longer wipes graph data — see #624-P1).
241
- db.prepare("DELETE FROM graph_files WHERE stash_root = ?").run(stashPath);
242
- db.prepare("DELETE FROM graph_meta WHERE stash_root = ?").run(stashPath);
243
- })();
244
- }
245
- /**
246
- * Scoped loader — graph_files rows without entities/relations. Used for
247
- * orphan detection and entity overview commands.
248
- */
249
- export function loadGraphFilesOnly(stashPath, db) {
250
- try {
251
- return withReadableGraphDb(db, (readDb) => {
252
- const rows = readDb
253
- .prepare(`SELECT file_path, file_type, body_hash, confidence, status, reason
254
- FROM graph_files
255
- WHERE stash_root = ?
256
- ORDER BY file_order`)
257
- .all(stashPath);
258
- return rows.map((row) => ({
259
- path: row.file_path,
260
- type: row.file_type,
261
- bodyHash: row.body_hash,
262
- ...(typeof row.confidence === "number" ? { confidence: row.confidence } : {}),
263
- ...(row.status ? { status: row.status } : {}),
264
- ...(row.reason ? { reason: row.reason } : {}),
265
- }));
266
- });
267
- }
268
- catch (err) {
269
- // Never mask the bun-test isolation guard as "no stored graph files",
270
- // and never mask an index we are not allowed to read as one with no
271
- // graph in it (#791) — `GRAPH_DB_MISSING` above is the only "absent".
272
- rethrowIfTestIsolationError(err);
273
- rethrowIfDataDirUnreadable(err);
274
- return [];
275
- }
276
- }
277
- export function loadStoredGraphMeta(stashPath, db) {
278
- try {
279
- return withReadableGraphDb(db, (readDb) => {
280
- const row = readDb
281
- .prepare(`SELECT
282
- stash_root,
283
- generated_at,
284
- considered_files,
285
- extracted_files,
286
- entity_count,
287
- relation_count,
288
- extraction_coverage,
289
- density,
290
- extractor_id,
291
- extraction_run_id,
292
- model,
293
- prompt_version,
294
- batch_size,
295
- cache_hits,
296
- cache_misses,
297
- truncation_count,
298
- failure_count
299
- FROM graph_meta
300
- WHERE stash_root = ?`)
301
- .get(stashPath);
302
- if (!row)
303
- return null;
304
- return {
305
- stashPath: row.stash_root,
306
- graphPath: getDbPath(),
307
- generatedAt: row.generated_at,
308
- quality: {
309
- consideredFiles: row.considered_files,
310
- extractedFiles: row.extracted_files,
311
- entityCount: row.entity_count,
312
- relationCount: row.relation_count,
313
- extractionCoverage: row.extraction_coverage,
314
- density: row.density,
315
- },
316
- telemetry: {
317
- ...(row.extractor_id ? { extractorId: row.extractor_id } : {}),
318
- ...(row.extraction_run_id ? { extractionRunId: row.extraction_run_id } : {}),
319
- ...(row.model ? { model: row.model } : {}),
320
- ...(row.prompt_version ? { promptVersion: row.prompt_version } : {}),
321
- ...(typeof row.batch_size === "number" ? { batchSize: row.batch_size } : {}),
322
- cacheHits: row.cache_hits,
323
- cacheMisses: row.cache_misses,
324
- truncationCount: row.truncation_count,
325
- failureCount: row.failure_count,
326
- // `retry_attempts` is not persisted to the graph-meta table (it is
327
- // surfaced from the run's emitted telemetry into `akm health`, not
328
- // from the reuse cache). Default to 0 so the loaded shape satisfies
329
- // GraphExtractionTelemetry.
330
- retryAttempts: 0,
331
- },
332
- };
333
- });
334
- }
335
- catch (err) {
336
- // Never mask the bun-test isolation guard as "no stored graph meta",
337
- // nor an unreadable index as one that simply has no graph (#791).
338
- rethrowIfTestIsolationError(err);
339
- rethrowIfDataDirUnreadable(err);
340
- return null;
341
- }
342
- }
343
- export function loadStoredGraphSnapshot(stashPath, db) {
344
- try {
345
- return withReadableGraphDb(db, (readDb) => {
346
- const meta = loadStoredGraphMeta(stashPath, readDb);
347
- if (!meta)
348
- return null;
349
- const fileRows = readDb
350
- .prepare(`SELECT file_path, file_type, body_hash, confidence, status, reason, extraction_run_id
351
- FROM graph_files
352
- WHERE stash_root = ?
353
- ORDER BY file_order`)
354
- .all(stashPath);
355
- const entityRows = readDb
356
- .prepare(`SELECT gf.file_path AS file_path, gfe.entity AS entity
357
- FROM graph_file_entities gfe
358
- JOIN graph_files gf
359
- ON gf.stash_root = gfe.stash_root
360
- AND gf.file_path = gfe.file_path
361
- AND gf.body_hash = gfe.body_hash
362
- WHERE gf.stash_root = ?
363
- ORDER BY gf.file_order, gfe.entity_order`)
364
- .all(stashPath);
365
- const relationRows = readDb
366
- .prepare(`SELECT gf.file_path AS file_path,
367
- gfr.from_entity AS from_entity,
368
- gfr.to_entity AS to_entity,
369
- gfr.relation_type AS relation_type,
370
- gfr.confidence AS confidence
371
- FROM graph_file_relations gfr
372
- JOIN graph_files gf
373
- ON gf.stash_root = gfr.stash_root
374
- AND gf.file_path = gfr.file_path
375
- AND gf.body_hash = gfr.body_hash
376
- WHERE gf.stash_root = ?
377
- ORDER BY gf.file_order, gfr.relation_order`)
378
- .all(stashPath);
379
- const entitiesByPath = new Map();
380
- for (const row of entityRows) {
381
- const bucket = entitiesByPath.get(row.file_path);
382
- if (bucket)
383
- bucket.push(row.entity);
384
- else
385
- entitiesByPath.set(row.file_path, [row.entity]);
386
- }
387
- const relationsByPath = new Map();
388
- for (const row of relationRows) {
389
- const relation = {
390
- from: row.from_entity,
391
- to: row.to_entity,
392
- ...(row.relation_type ? { type: row.relation_type } : {}),
393
- ...(typeof row.confidence === "number" ? { confidence: row.confidence } : {}),
394
- };
395
- const bucket = relationsByPath.get(row.file_path);
396
- if (bucket)
397
- bucket.push(relation);
398
- else
399
- relationsByPath.set(row.file_path, [relation]);
400
- }
401
- const files = fileRows.map((row) => ({
402
- path: row.file_path,
403
- type: row.file_type,
404
- ...(row.body_hash ? { bodyHash: row.body_hash } : {}),
405
- entities: entitiesByPath.get(row.file_path) ?? [],
406
- relations: relationsByPath.get(row.file_path) ?? [],
407
- ...(typeof row.confidence === "number" ? { confidence: row.confidence } : {}),
408
- ...(row.status ? { status: row.status } : {}),
409
- ...(row.reason ? { reason: row.reason } : {}),
410
- ...(row.extraction_run_id ? { extractionRunId: row.extraction_run_id } : {}),
411
- }));
412
- return {
413
- stashPath: meta.stashPath,
414
- graphPath: meta.graphPath,
415
- generatedAt: meta.generatedAt,
416
- ...(meta.quality ? { quality: meta.quality } : {}),
417
- ...(meta.telemetry ? { telemetry: meta.telemetry } : {}),
418
- files,
419
- entities: uniqueSorted(files.flatMap((file) => file.entities)),
420
- relations: files.flatMap((file) => file.relations),
421
- };
422
- });
423
- }
424
- catch (err) {
425
- // Never mask the bun-test isolation guard as "no stored graph snapshot",
426
- // nor an unreadable index as one that simply has no graph (#791).
427
- rethrowIfTestIsolationError(err);
428
- rethrowIfDataDirUnreadable(err);
429
- return null;
430
- }
431
- }