akm-cli 0.9.0-beta.6 → 0.9.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,41 @@
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
+ /**
5
+ * Shared `entries`-row projection + mapper, extracted from indexer/db/db.ts.
6
+ *
7
+ * Centralizes the one canonical `entries` SELECT column list and the
8
+ * JSON-parse-guarded row → {@link DbIndexedEntry} mapping that several queries
9
+ * used to reimplement. Corrupt `entry_json` rows are skipped (warn once) rather
10
+ * than crashing the caller.
11
+ */
12
+ import { warn } from "../../core/warn.js";
13
+ /**
14
+ * Canonical column list for reading a full indexed entry from the `entries`
15
+ * table, in the order {@link rowToIndexedEntry} expects.
16
+ */
17
+ export const ENTRY_COLUMNS = "id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text";
18
+ /**
19
+ * Map one raw `entries` row to a {@link DbIndexedEntry}, parsing `entry_json`.
20
+ * Returns `null` (and warns, tagged with `context`) when the JSON is corrupt so
21
+ * callers can skip the row instead of crashing.
22
+ */
23
+ export function rowToIndexedEntry(row, context) {
24
+ let entry;
25
+ try {
26
+ entry = JSON.parse(row.entry_json);
27
+ }
28
+ catch {
29
+ warn(`[db] ${context}: skipping entry id=${row.id} — corrupt entry_json`);
30
+ return null;
31
+ }
32
+ return {
33
+ id: row.id,
34
+ entryKey: row.entry_key,
35
+ dirPath: row.dir_path,
36
+ filePath: row.file_path,
37
+ stashDir: row.stash_dir,
38
+ entry,
39
+ searchText: row.search_text,
40
+ };
41
+ }
@@ -4,7 +4,6 @@
4
4
  import fs from "node:fs";
5
5
  import { rethrowIfTestIsolationError } from "../../core/errors.js";
6
6
  import { getDbPath } from "../../core/paths.js";
7
- import { warn } from "../../core/warn.js";
8
7
  import { closeDatabase, openExistingDatabase } from "./db.js";
9
8
  function withReadableGraphDb(db, fn) {
10
9
  if (db)
@@ -26,38 +25,16 @@ function uniqueSorted(values) {
26
25
  function normalizeEntity(value) {
27
26
  return value.trim().toLowerCase();
28
27
  }
29
- /**
30
- * Resolve a file_path within a stash to its entries.id. Returns null when the
31
- * path has no indexed entry (orphan graph row).
32
- */
33
- export function resolveEntryIdForPath(db, stashRoot, filePath) {
34
- try {
35
- const row = db
36
- .prepare("SELECT id FROM entries WHERE stash_dir = ? AND file_path = ? LIMIT 1")
37
- .get(stashRoot, filePath);
38
- if (row)
39
- return row.id;
40
- // Fall back to file_path-only match (legacy callers may pass a stash root
41
- // that doesn't exactly match entries.stash_dir, e.g. trailing-slash diffs).
42
- const fallback = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
43
- return fallback?.id ?? null;
44
- }
45
- catch {
46
- return null;
47
- }
48
- }
49
28
  /**
50
29
  * Persist (or update) a graph snapshot for a stash root.
51
30
  *
52
- * Implementation: incremental upsert keyed on entries.id. Unchanged files
53
- * (matching body_hash) are skipped; changed files have their child rows
54
- * deleted (CASCADE) and re-inserted; files in DB but absent from the new
55
- * snapshot are deleted. The old behaviour wiped every row for the stash on
56
- * each write, which produced ~22k row writes per re-index even when one
57
- * asset changed.
58
- *
59
- * Orphan files (no entries row resolvable) are skipped and counted in a
60
- * single warn() so the caller sees the magnitude without log spam.
31
+ * #624-P1: keyed on (stash_root, file_path, body_hash) — NOT entries.id. Graph
32
+ * rows are self-keyed by path, so they survive an entries delete + reinsert
33
+ * (a reindex) when body_hash is unchanged. Unchanged files (matching body_hash)
34
+ * only have their file-meta refreshed; files whose body_hash changed have their
35
+ * old row + child rows deleted and the new content inserted; files in DB but
36
+ * absent from the new snapshot are deleted. There is no entry_id resolution and
37
+ * no orphan-skip — a graph file no longer needs a matching entries row.
61
38
  */
62
39
  export function replaceStoredGraph(db, graph) {
63
40
  const upsertMeta = db.prepare(`INSERT INTO graph_meta (
@@ -98,87 +75,146 @@ export function replaceStoredGraph(db, graph) {
98
75
  cache_misses = excluded.cache_misses,
99
76
  truncation_count = excluded.truncation_count,
100
77
  failure_count = excluded.failure_count`);
101
- const selectExisting = db.prepare("SELECT entry_id, file_path, body_hash FROM graph_files WHERE stash_root = ?");
102
- const deleteFile = db.prepare("DELETE FROM graph_files WHERE entry_id = ?");
103
- const deleteEntities = db.prepare("DELETE FROM graph_file_entities WHERE entry_id = ?");
104
- const deleteRelations = db.prepare("DELETE FROM graph_file_relations WHERE entry_id = ?");
78
+ const selectExisting = db.prepare("SELECT file_path, body_hash, file_order FROM graph_files WHERE stash_root = ?");
79
+ const deleteFile = db.prepare("DELETE FROM graph_files WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
80
+ const deleteEntities = db.prepare("DELETE FROM graph_file_entities WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
81
+ const deleteRelations = db.prepare("DELETE FROM graph_file_relations WHERE stash_root = ? AND file_path = ? AND body_hash = ?");
105
82
  const insertFile = db.prepare(`INSERT INTO graph_files (
106
- entry_id, stash_root, file_path, file_order, file_type, body_hash, confidence, status, reason, extraction_run_id
107
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
83
+ stash_root, file_path, file_order, file_type, body_hash, confidence, status, reason, extraction_run_id
84
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
108
85
  const updateFileMeta = db.prepare(`UPDATE graph_files
109
86
  SET file_order = ?, file_type = ?, confidence = ?, status = ?, reason = ?, extraction_run_id = ?
110
- WHERE entry_id = ?`);
111
- const insertEntity = db.prepare(`INSERT INTO graph_file_entities (entry_id, entity_order, stash_root, entity_norm, entity)
112
- VALUES (?, ?, ?, ?, ?)`);
87
+ WHERE stash_root = ? AND file_path = ? AND body_hash = ?`);
88
+ const insertEntity = db.prepare(`INSERT INTO graph_file_entities (stash_root, file_path, body_hash, entity_order, entity_norm, entity)
89
+ VALUES (?, ?, ?, ?, ?, ?)`);
113
90
  const insertRelation = db.prepare(`INSERT INTO graph_file_relations (
114
- entry_id, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence
115
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
91
+ stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence
92
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
116
93
  const quality = graph.quality;
117
94
  const telemetry = graph.telemetry;
118
95
  db.transaction(() => {
119
96
  upsertMeta.run(graph.stashRoot, graph.schemaVersion, graph.generatedAt, quality?.consideredFiles ?? graph.files.length, quality?.extractedFiles ?? graph.files.length, quality?.entityCount ?? graph.entities?.length ?? 0, quality?.relationCount ?? graph.relations?.length ?? 0, quality?.extractionCoverage ?? 0, quality?.density ?? 0, 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);
120
- // Build a snapshot of existing rows for incremental compare.
97
+ // Build a snapshot of existing rows for incremental compare. The unique
98
+ // index idx_graph_files_path guarantees at most one row per file_path.
121
99
  const existingRows = selectExisting.all(graph.stashRoot);
122
100
  const existingByPath = new Map();
123
101
  for (const row of existingRows)
124
102
  existingByPath.set(row.file_path, row);
125
- let orphanCount = 0;
126
- const presentEntryIds = new Set();
103
+ const presentPaths = new Set();
127
104
  for (const [fileOrder, node] of graph.files.entries()) {
128
- // body_hash is NOT NULL in schema v2; default to a sentinel for inputs
129
- // (test fixtures, legacy imports) that don't supply one. The sentinel
130
- // never equals a real hash so subsequent staleness checks always
131
- // re-extract — correct behaviour for "unknown" bodies.
105
+ // body_hash is part of the PK; default to a sentinel for inputs (test
106
+ // fixtures, legacy imports) that don't supply one. The sentinel never
107
+ // equals a real hash so subsequent staleness checks always re-extract —
108
+ // correct behaviour for "unknown" bodies. Distinct files in one stash
109
+ // are still keyed apart by file_path, so the empty sentinel is safe.
132
110
  const bodyHash = node.bodyHash && node.bodyHash.length > 0 ? node.bodyHash : "";
133
- const entryId = resolveEntryIdForPath(db, graph.stashRoot, node.path);
134
- if (entryId == null) {
135
- orphanCount += 1;
136
- continue;
137
- }
138
- presentEntryIds.add(entryId);
111
+ presentPaths.add(node.path);
139
112
  const existing = existingByPath.get(node.path);
140
- if (existing && existing.entry_id === entryId && existing.body_hash === bodyHash) {
113
+ if (existing && existing.body_hash === bodyHash) {
141
114
  // Body unchanged — only fix up file_order/confidence in case they drifted.
142
- updateFileMeta.run(fileOrder, node.type, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null, entryId);
115
+ updateFileMeta.run(fileOrder, node.type, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null, graph.stashRoot, node.path, bodyHash);
143
116
  continue;
144
117
  }
145
118
  if (existing) {
146
- // Stale row (different body_hash, or entry_id moved to a different
147
- // path under the same file_path). Wipe child rows; CASCADE would do
148
- // it but explicit DELETE keeps the order deterministic.
149
- deleteEntities.run(existing.entry_id);
150
- deleteRelations.run(existing.entry_id);
151
- deleteFile.run(existing.entry_id);
119
+ // Stale row (different body_hash for this path). Delete the old row by
120
+ // its OLD body_hash; child rows cascade, but explicit DELETE keeps the
121
+ // order deterministic and is safe regardless of the FK pragma.
122
+ deleteEntities.run(graph.stashRoot, existing.file_path, existing.body_hash);
123
+ deleteRelations.run(graph.stashRoot, existing.file_path, existing.body_hash);
124
+ deleteFile.run(graph.stashRoot, existing.file_path, existing.body_hash);
152
125
  }
153
- insertFile.run(entryId, graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null);
126
+ insertFile.run(graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null);
154
127
  for (const [entityOrder, entity] of node.entities.entries()) {
155
- insertEntity.run(entryId, entityOrder, graph.stashRoot, normalizeEntity(entity), entity);
128
+ insertEntity.run(graph.stashRoot, node.path, bodyHash, entityOrder, normalizeEntity(entity), entity);
156
129
  }
157
130
  for (const [relationOrder, relation] of node.relations.entries()) {
158
- insertRelation.run(entryId, relationOrder, normalizeEntity(relation.from), relation.from, normalizeEntity(relation.to), relation.to, relation.type ?? null, relation.confidence ?? null);
131
+ insertRelation.run(graph.stashRoot, node.path, bodyHash, relationOrder, normalizeEntity(relation.from), relation.from, normalizeEntity(relation.to), relation.to, relation.type ?? null, relation.confidence ?? null);
159
132
  }
160
133
  }
161
134
  // Delete files present in DB but absent from the new snapshot. Child
162
- // tables CASCADE on entry_id.
135
+ // tables CASCADE on the composite key; explicit DELETE keeps it determinstic.
163
136
  for (const row of existingRows) {
164
- if (!presentEntryIds.has(row.entry_id)) {
165
- deleteEntities.run(row.entry_id);
166
- deleteRelations.run(row.entry_id);
167
- deleteFile.run(row.entry_id);
137
+ if (!presentPaths.has(row.file_path)) {
138
+ deleteEntities.run(graph.stashRoot, row.file_path, row.body_hash);
139
+ deleteRelations.run(graph.stashRoot, row.file_path, row.body_hash);
140
+ deleteFile.run(graph.stashRoot, row.file_path, row.body_hash);
168
141
  }
169
142
  }
170
- if (orphanCount > 0) {
171
- warn(`[graph] replaceStoredGraph: skipped ${orphanCount} file(s) with no resolvable entry under ${graph.stashRoot}.`);
172
- }
173
143
  })();
174
144
  }
175
145
  export function deleteStoredGraph(db, stashPath) {
176
146
  db.transaction(() => {
177
- // Child rows cascade via entry_id; deleting graph_files clears them.
147
+ // Child rows cascade via the composite (stash_root, file_path, body_hash)
148
+ // FK; deleting graph_files clears them. This is the explicit full-clear
149
+ // path for a stash (entries-delete no longer wipes graph data — see #624-P1).
178
150
  db.prepare("DELETE FROM graph_files WHERE stash_root = ?").run(stashPath);
179
151
  db.prepare("DELETE FROM graph_meta WHERE stash_root = ?").run(stashPath);
180
152
  })();
181
153
  }
154
+ /**
155
+ * #624-P1 — does any graph data exist for a file_path under a stash root?
156
+ * Consumed by show/curate flows (P3) but defined here so the schema and its
157
+ * accessors land together.
158
+ */
159
+ export function hasGraphData(db, stashRoot, filePath) {
160
+ try {
161
+ const row = db
162
+ .prepare("SELECT 1 AS present FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
163
+ .get(stashRoot, filePath);
164
+ return row !== undefined;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ /**
171
+ * #624-P3 — enqueue a file for lazy graph extraction. Idempotent on the
172
+ * (stash_root, file_path) PK: a second enqueue refreshes body_hash + queued_at
173
+ * and keeps the HIGHER priority. Non-blocking, no LLM call — the queued row is
174
+ * drained later by the graph-extraction pass. Tolerant of a missing table /
175
+ * db error (best-effort), but never masks the bun-test isolation guard.
176
+ */
177
+ export function enqueueGraphExtraction(db, stashRoot, filePath, bodyHash, priority = 0) {
178
+ try {
179
+ db.prepare(`INSERT INTO graph_extraction_queue (stash_root, file_path, body_hash, priority)
180
+ VALUES (?, ?, ?, ?)
181
+ ON CONFLICT(stash_root, file_path) DO UPDATE SET
182
+ body_hash = excluded.body_hash,
183
+ priority = MAX(graph_extraction_queue.priority, excluded.priority),
184
+ queued_at = datetime('now')`).run(stashRoot, filePath, bodyHash, priority);
185
+ }
186
+ catch (err) {
187
+ rethrowIfTestIsolationError(err);
188
+ }
189
+ }
190
+ /**
191
+ * #624-P3 — drain up to `limit` queued files for a stash, highest-priority
192
+ * first (then oldest queued_at). The returned rows are DELETED from the queue
193
+ * in the SAME transaction (SELECT-then-DELETE-by-PK), so a drain is exactly
194
+ * once. Tolerant of a missing table / db error (returns []), but never masks
195
+ * the bun-test isolation guard.
196
+ */
197
+ export function drainExtractionQueue(db, stashRoot, limit) {
198
+ try {
199
+ return db.transaction(() => {
200
+ const rows = db
201
+ .prepare(`SELECT file_path, body_hash, priority
202
+ FROM graph_extraction_queue
203
+ WHERE stash_root = ?
204
+ ORDER BY priority DESC, queued_at ASC
205
+ LIMIT ?`)
206
+ .all(stashRoot, limit);
207
+ const del = db.prepare("DELETE FROM graph_extraction_queue WHERE stash_root = ? AND file_path = ?");
208
+ for (const row of rows)
209
+ del.run(stashRoot, row.file_path);
210
+ return rows.map((row) => ({ filePath: row.file_path, bodyHash: row.body_hash, priority: row.priority }));
211
+ })();
212
+ }
213
+ catch (err) {
214
+ rethrowIfTestIsolationError(err);
215
+ return [];
216
+ }
217
+ }
182
218
  /**
183
219
  * Scoped loader — only the graph_meta row for a stash. Used by callers that
184
220
  * only need summary numbers (e.g. `akm graph summary`).
@@ -195,13 +231,12 @@ export function loadGraphFilesOnly(stashPath, db) {
195
231
  return withReadableGraphDb(db, (readDb) => {
196
232
  try {
197
233
  const rows = readDb
198
- .prepare(`SELECT entry_id, file_path, file_type, body_hash, confidence, status, reason
234
+ .prepare(`SELECT file_path, file_type, body_hash, confidence, status, reason
199
235
  FROM graph_files
200
236
  WHERE stash_root = ?
201
237
  ORDER BY file_order`)
202
238
  .all(stashPath);
203
239
  return rows.map((row) => ({
204
- entryId: row.entry_id,
205
240
  path: row.file_path,
206
241
  type: row.file_type,
207
242
  bodyHash: row.body_hash,
@@ -222,13 +257,16 @@ export function loadGraphFilesOnly(stashPath, db) {
222
257
  }
223
258
  }
224
259
  /**
225
- * Scoped loader — entities for a single entry_id. Used by per-asset lookups.
260
+ * Scoped loader — entities for a single file, keyed on the #624-P1 composite
261
+ * (stash_root, file_path, body_hash). Used by per-asset show/curate lookups.
226
262
  */
227
- export function loadGraphEntitiesByEntry(db, entryId) {
263
+ export function loadGraphEntitiesByPath(db, stashRoot, filePath, bodyHash) {
228
264
  try {
229
265
  const rows = db
230
- .prepare("SELECT entity FROM graph_file_entities WHERE entry_id = ? ORDER BY entity_order")
231
- .all(entryId);
266
+ .prepare(`SELECT entity FROM graph_file_entities
267
+ WHERE stash_root = ? AND file_path = ? AND body_hash = ?
268
+ ORDER BY entity_order`)
269
+ .all(stashRoot, filePath, bodyHash);
232
270
  return rows.map((r) => r.entity);
233
271
  }
234
272
  catch {
@@ -314,27 +352,32 @@ export function loadStoredGraphSnapshot(stashPath, db) {
314
352
  return null;
315
353
  try {
316
354
  const fileRows = readDb
317
- .prepare(`SELECT entry_id, file_path, file_type, body_hash, confidence, status, reason, extraction_run_id
355
+ .prepare(`SELECT file_path, file_type, body_hash, confidence, status, reason, extraction_run_id
318
356
  FROM graph_files
319
357
  WHERE stash_root = ?
320
358
  ORDER BY file_order`)
321
359
  .all(stashPath);
322
360
  const entityRows = readDb
323
- .prepare(`SELECT gfe.entry_id AS entry_id, gf.file_path AS file_path, gfe.entity AS entity
361
+ .prepare(`SELECT gf.file_path AS file_path, gfe.entity AS entity
324
362
  FROM graph_file_entities gfe
325
- JOIN graph_files gf ON gf.entry_id = gfe.entry_id
363
+ JOIN graph_files gf
364
+ ON gf.stash_root = gfe.stash_root
365
+ AND gf.file_path = gfe.file_path
366
+ AND gf.body_hash = gfe.body_hash
326
367
  WHERE gf.stash_root = ?
327
368
  ORDER BY gf.file_order, gfe.entity_order`)
328
369
  .all(stashPath);
329
370
  const relationRows = readDb
330
- .prepare(`SELECT gfr.entry_id AS entry_id,
331
- gf.file_path AS file_path,
371
+ .prepare(`SELECT gf.file_path AS file_path,
332
372
  gfr.from_entity AS from_entity,
333
373
  gfr.to_entity AS to_entity,
334
374
  gfr.relation_type AS relation_type,
335
375
  gfr.confidence AS confidence
336
376
  FROM graph_file_relations gfr
337
- JOIN graph_files gf ON gf.entry_id = gfr.entry_id
377
+ JOIN graph_files gf
378
+ ON gf.stash_root = gfr.stash_root
379
+ AND gf.file_path = gfr.file_path
380
+ AND gf.body_hash = gfr.body_hash
338
381
  WHERE gf.stash_root = ?
339
382
  ORDER BY gf.file_order, gfr.relation_order`)
340
383
  .all(stashPath);
@@ -26,8 +26,8 @@ import { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } from "./db.js"
26
26
  * the wrapper skips its internal hashing — callers that
27
27
  * already hashed the body (e.g. to reuse it elsewhere)
28
28
  * should pass this to avoid the redundant work.
29
- * @param cacheVariant - Namespace token for the cache row (e.g. staleness-detect
30
- * sets one so its rows do not collide with memory-inference).
29
+ * @param cacheVariant - Namespace token for the cache row so different passes'
30
+ * rows do not collide (e.g. memory-inference vs graph).
31
31
  * @param hooks - Optional event sink for telemetry (see {@link WithLlmCacheHooks}).
32
32
  */
33
33
  export async function withLlmCache(db, cacheKey, body, reEnrich, llmFn, validate, precomputedHash, cacheVariant = "", hooks) {