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
@@ -2,21 +2,29 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Auto-index: silently run an incremental `akm index` when the local index
6
- * is stale or absent, so that `search`, `show`, and `feedback` always operate
7
- * against current on-disk state without requiring the user to manually run
8
- * `akm index` first.
5
+ * Auto-index bootstrap: silently build the local index inline when it cannot
6
+ * serve the caller's stash at all (missing DB, no `entries` table, zero rows,
7
+ * or built for a different stash), so `search`, `show`, and `feedback` work
8
+ * on first use without a manual `akm index`.
9
9
  *
10
- * This replaces the old filesystem fallbacks that were scattered across
11
- * `searchLocal()` and `show.ts`, centralizing the "indexed yet?" gap handling
12
- * behind a single entry point.
10
+ * Content FRESHNESS is intentionally not this module's job on the read path.
11
+ * Writers maintain the index (`indexWrittenAssets` for `remember`/extract
12
+ * session assets; the mutation commands run `akmIndex()` themselves), and the
13
+ * improve cron / explicit `akm index` do full refreshes. Reads serve whatever
14
+ * populated index exists. The previous design — a staleness walk plus a
15
+ * detached background reindex per read — made every read on an actively
16
+ * written stash spawn a writer that the read's own telemetry then queued
17
+ * behind (see docs/design/read-path-reindex-contention-findings.md).
18
+ *
19
+ * `mode: "blocking"` (improve) still checks staleness and rebuilds inline,
20
+ * because its planning logic needs a current `entries` table in-process.
13
21
  */
14
22
  import fs from "node:fs";
15
23
  import path from "node:path";
16
24
  import { ASSET_SPECS, TYPE_DIRS } from "../core/asset/asset-spec.js";
17
25
  import { getDbPath } from "../core/paths.js";
18
26
  import { warn } from "../core/warn.js";
19
- import { closeDatabase, getEntryCount, getMeta, openExistingDatabase } from "./db/db.js";
27
+ import { closeDatabase, getEntryCount, getIndexedFilePaths, getMeta, openExistingDatabase } from "./db/db.js";
20
28
  function getIndexableFiles(root, spec) {
21
29
  if (!fs.existsSync(root))
22
30
  return [];
@@ -52,16 +60,34 @@ function getIndexableFiles(root, spec) {
52
60
  }
53
61
  return files;
54
62
  }
55
- function hasNewerIndexableFiles(stashDir, builtAt) {
56
- if (!builtAt)
57
- return true;
58
- const builtAtMs = new Date(builtAt).getTime();
59
- if (!Number.isFinite(builtAtMs))
60
- return true;
63
+ /**
64
+ * Whether any indexable file under `stashDir` is newer than the last build, or
65
+ * has never been indexed at all.
66
+ *
67
+ * Two independent signals, because neither alone is sufficient:
68
+ * 1. **mtime > builtAt** — catches in-place *edits* of already-indexed files.
69
+ * 2. **path not in `indexedPaths`** — catches *newly added* files. This is
70
+ * clock-independent on purpose: a freshly-written file can have a
71
+ * filesystem mtime that compares as *older* than the wall-clock `builtAt`
72
+ * (the two clocks are not perfectly synchronized and `builtAt` is
73
+ * millisecond-truncated), so the mtime test alone silently misses
74
+ * additions made within ~a millisecond of the previous build.
75
+ *
76
+ * `getIndexableFiles` applies each asset type's own relevance filter, so
77
+ * non-indexed companion files (e.g. `package.json` next to a knowledge doc) are
78
+ * never considered and do not produce false "new file" positives.
79
+ */
80
+ function hasNewerIndexableFiles(stashDir, builtAt, indexedPaths) {
81
+ const builtAtMs = builtAt ? new Date(builtAt).getTime() : Number.NaN;
82
+ const builtAtUsable = Number.isFinite(builtAtMs);
61
83
  for (const [type, spec] of Object.entries(ASSET_SPECS)) {
62
84
  const typeRoot = path.join(stashDir, TYPE_DIRS[type] ?? spec.stashDir);
63
85
  const files = getIndexableFiles(typeRoot, spec);
64
86
  for (const file of files) {
87
+ if (!indexedPaths.has(file))
88
+ return true;
89
+ if (!builtAtUsable)
90
+ return true;
65
91
  try {
66
92
  if (fs.statSync(file).mtimeMs > builtAtMs)
67
93
  return true;
@@ -89,7 +115,7 @@ export function isIndexStale(stashDir) {
89
115
  if (entryCount === 0)
90
116
  return true;
91
117
  const builtAt = getMeta(db, "builtAt");
92
- if (hasNewerIndexableFiles(stashDir, builtAt))
118
+ if (hasNewerIndexableFiles(stashDir, builtAt, getIndexedFilePaths(db)))
93
119
  return true;
94
120
  const storedStashDir = getMeta(db, "stashDir");
95
121
  if (storedStashDir !== stashDir) {
@@ -114,16 +140,44 @@ export function isIndexStale(stashDir) {
114
140
  }
115
141
  }
116
142
  /**
117
- * Run an incremental index when the local index is stale. Best-effort
118
- * failures are logged as warnings but never thrown, so the caller can
119
- * proceed (and surface a proper "not in index" error if the index is
120
- * still unusable).
121
- *
122
- * Returns `true` if an index run was attempted.
143
+ * Whether the existing index can serve queries for `stashDir` *right now*
144
+ * i.e. the DB file exists, the `entries` table holds rows, and those rows were
145
+ * built for this stash (it is the stored primary stash or appears in the
146
+ * stored `stashDirs` set). When this is true the index is at worst
147
+ * content-stale, so read paths serve it as-is. When it is false the existing
148
+ * index has nothing relevant to return (no DB, no `entries` table, zero rows,
149
+ * or built for a different stash), so those cases must rebuild inline.
123
150
  */
124
- export async function ensureIndex(stashDir) {
125
- if (!isIndexStale(stashDir))
151
+ function indexCanServeStash(stashDir) {
152
+ const dbPath = getDbPath();
153
+ if (!fs.existsSync(dbPath))
154
+ return false;
155
+ let db;
156
+ try {
157
+ db = openExistingDatabase(dbPath);
158
+ if (getEntryCount(db) === 0)
159
+ return false;
160
+ const storedStashDir = getMeta(db, "stashDir");
161
+ if (storedStashDir === stashDir)
162
+ return true;
163
+ try {
164
+ const storedDirs = JSON.parse(getMeta(db, "stashDirs") ?? "[]");
165
+ return storedDirs.includes(stashDir);
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ catch {
172
+ // No `entries` table (or otherwise unreadable) — cannot serve.
126
173
  return false;
174
+ }
175
+ finally {
176
+ if (db)
177
+ closeDatabase(db);
178
+ }
179
+ }
180
+ async function runInlineReindex(stashDir) {
127
181
  try {
128
182
  const { akmIndex } = await import("./indexer.js");
129
183
  await akmIndex({ stashDir });
@@ -131,6 +185,31 @@ export async function ensureIndex(stashDir) {
131
185
  }
132
186
  catch (error) {
133
187
  warn("Auto-index failed, proceeding with existing index:", error instanceof Error ? error.message : String(error));
134
- return true;
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Ensure the local index exists and can serve the caller.
193
+ *
194
+ * Default mode is `background` — the read-path contract (`search`, `show`,
195
+ * `feedback`): a populated index built for this stash is served as-is (its
196
+ * freshness is the writers' job, see module doc); an unusable index rebuilds
197
+ * inline, since there is nothing to proceed against.
198
+ *
199
+ * `mode: "blocking"` additionally treats content-staleness as a rebuild
200
+ * trigger and waits for it. Use this for callers like `improve` whose
201
+ * planning logic depends on a current `entries` table in the same process.
202
+ *
203
+ * Returns `true` only when an inline index run succeeds.
204
+ * A rebuild attempt that fails (throws) resolves to `false`.
205
+ */
206
+ export async function ensureIndex(stashDir, options = {}) {
207
+ if (options.mode === "blocking") {
208
+ if (!isIndexStale(stashDir))
209
+ return false;
210
+ return runInlineReindex(stashDir);
135
211
  }
212
+ if (indexCanServeStash(stashDir))
213
+ return false;
214
+ return runInlineReindex(stashDir);
136
215
  }
@@ -0,0 +1,75 @@
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
+ * MemRL feedback → utility policy, extracted from indexer/db/db.ts.
6
+ *
7
+ * This is the domain/policy math (arXiv:2601.03192) that decides how a batch of
8
+ * positive/negative feedback signals moves an asset's utility score. It is pure
9
+ * — no database access — so the bounded-step behaviour is unit-testable in
10
+ * isolation; the DB read/write stays with `applyFeedbackToUtilityScore` in db.ts.
11
+ */
12
+ /**
13
+ * MemRL learning rate for feedback-driven utility updates (F-5 / #386).
14
+ *
15
+ * Follows the bounded-step formula from MemRL (arXiv:2601.03192):
16
+ * next = clamp(current + lr × (reward − current), 0, 1)
17
+ *
18
+ * This replaces the unbounded `-0.03 × negativeCount` delta that could
19
+ * silently remove high-utility assets from the improvement loop.
20
+ */
21
+ export const FEEDBACK_LR = 0.1;
22
+ /**
23
+ * Positive reward signal for a single positive feedback event.
24
+ * Reward 1.0 means "fully correct / helpful".
25
+ */
26
+ const FEEDBACK_REWARD_POSITIVE = 1.0;
27
+ /**
28
+ * Negative reward signal for a single negative feedback event.
29
+ * Reward 0.0 means "not helpful" (lowest MemRL signal).
30
+ */
31
+ const FEEDBACK_REWARD_NEGATIVE = 0.0;
32
+ /**
33
+ * Utility threshold below which a review-needed escalation is triggered.
34
+ * When a previously high-utility asset (≥ HIGH_UTILITY_THRESHOLD) drops
35
+ * below this value, the caller should create an escalation proposal.
36
+ */
37
+ export const UTILITY_REVIEW_THRESHOLD = 0.5;
38
+ /**
39
+ * Utility level considered "high" — assets above this are tracked for
40
+ * threshold-crossing escalation.
41
+ */
42
+ export const HIGH_UTILITY_THRESHOLD = 0.5;
43
+ /**
44
+ * Compute the next utility from accumulated feedback counts using the MemRL
45
+ * bounded-step EMA formula (F-5 / #386, arXiv:2601.03192):
46
+ *
47
+ * reward = weighted average of positive and negative signals
48
+ * nextUtil = clamp(currentUtil + lr × (reward − currentUtil), 0, 1)
49
+ *
50
+ * The step is inherently bounded: reward ∈ [0, 1] and currentUtil ∈ [0, 1], so
51
+ * a single call moves utility by at most {@link FEEDBACK_LR} in either
52
+ * direction. `reward` is a proportion of the counts, not their magnitude, so
53
+ * with no positive signals the number of negatives is irrelevant (reward is 0
54
+ * whether there is 1 negative or 100). Mixing in positives shifts reward and so
55
+ * the step, but never past the learning-rate bound.
56
+ *
57
+ * Pure: no DB access. When both counts are zero, utility is unchanged.
58
+ */
59
+ export function computeNextUtility(previousUtility, positiveCount, negativeCount) {
60
+ if (positiveCount === 0 && negativeCount === 0) {
61
+ return { previousUtility, nextUtility: previousUtility, crossedReviewThreshold: false };
62
+ }
63
+ const total = positiveCount + negativeCount;
64
+ // Weighted reward: proportion of positive signals.
65
+ const reward = positiveCount > 0 && negativeCount === 0
66
+ ? FEEDBACK_REWARD_POSITIVE
67
+ : negativeCount > 0 && positiveCount === 0
68
+ ? FEEDBACK_REWARD_NEGATIVE
69
+ : (positiveCount * FEEDBACK_REWARD_POSITIVE + negativeCount * FEEDBACK_REWARD_NEGATIVE) / total;
70
+ // MemRL bounded-step EMA: lr × (reward − current). |delta| ≤ FEEDBACK_LR.
71
+ const delta = FEEDBACK_LR * (reward - previousUtility);
72
+ const nextUtility = Math.max(0, Math.min(1, previousUtility + delta));
73
+ const crossedReviewThreshold = previousUtility >= HIGH_UTILITY_THRESHOLD && nextUtility < UTILITY_REVIEW_THRESHOLD;
74
+ return { previousUtility, nextUtility, crossedReviewThreshold };
75
+ }
@@ -241,13 +241,18 @@ export function collectGraphRelatedHit(context, filePath) {
241
241
  * Find graph files that share entities with the given file.
242
242
  *
243
243
  * Implementation: SQL self-join on graph_file_entities, scoped by stash_root,
244
- * grouped by entry_id, ordered by shared-entity count desc. Touches ~50-200
244
+ * grouped by file_path, ordered by shared-entity count desc. Touches ~50-200
245
245
  * rows instead of loading the entire snapshot into memory. Cold-call latency
246
246
  * drops from ~30-60ms (full snapshot parse) to ~2-5ms on typical stashes.
247
247
  *
248
+ * #624-P1: the graph tables are keyed on (stash_root, file_path, body_hash) —
249
+ * NOT entries.id — so candidates are identified by file_path (the unique index
250
+ * idx_graph_files_path guarantees one graph_files row per path).
251
+ *
248
252
  * The returned `ref` field carries the canonical asset ref (`type:name`)
249
- * resolved from entries.entry_key when the entry is indexed. Callers should
250
- * fall back to formatting `path` when `ref` is undefined (orphan graph row).
253
+ * resolved from entries.entry_key when the file is indexed. Callers should
254
+ * fall back to formatting `path` when `ref` is undefined (graph row with no
255
+ * matching entries row).
251
256
  */
252
257
  export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
253
258
  if (!db) {
@@ -255,113 +260,118 @@ export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
255
260
  // callers pass a handle), so degrade to empty rather than reopening.
256
261
  return [];
257
262
  }
258
- // Resolve target's entry_id from the stash_root + file_path. The graph rows
259
- // are keyed on entry_id; without it we can't run the join.
260
- let targetEntryId;
263
+ // Confirm the target file has a graph row; without it there is nothing to
264
+ // relate. (Identity is file_path within the stash one row per path.)
261
265
  try {
262
266
  const row = db
263
- .prepare("SELECT entry_id FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
267
+ .prepare("SELECT 1 AS present FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
264
268
  .get(stashRoot, filePath);
265
- targetEntryId = row?.entry_id;
269
+ if (row === undefined)
270
+ return [];
266
271
  }
267
272
  catch {
268
273
  return [];
269
274
  }
270
- if (targetEntryId == null)
271
- return [];
272
275
  const effectiveLimit = Math.max(1, limit);
273
- // Shared-entity count per candidate entry_id.
276
+ // Shared-entity count per candidate file_path. The target's entities are the
277
+ // rows for `filePath`; candidates are any OTHER file_path in the stash that
278
+ // shares a normalized entity.
274
279
  let candidateRows;
275
280
  try {
276
281
  candidateRows = db
277
- .prepare(`SELECT gf.entry_id AS entry_id,
278
- gf.file_path AS file_path,
282
+ .prepare(`SELECT gf.file_path AS file_path,
279
283
  gf.file_type AS file_type,
280
284
  COUNT(*) AS shared
281
285
  FROM graph_file_entities target
282
286
  JOIN graph_file_entities e
283
287
  ON e.stash_root = target.stash_root
284
288
  AND e.entity_norm = target.entity_norm
285
- AND e.entry_id != target.entry_id
289
+ AND e.file_path != target.file_path
286
290
  JOIN graph_files gf
287
- ON gf.entry_id = e.entry_id
288
- WHERE target.entry_id = ?
291
+ ON gf.stash_root = e.stash_root
292
+ AND gf.file_path = e.file_path
293
+ AND gf.body_hash = e.body_hash
294
+ WHERE target.file_path = ?
289
295
  AND target.stash_root = ?
290
- GROUP BY gf.entry_id
296
+ GROUP BY gf.file_path
291
297
  ORDER BY shared DESC, gf.file_path ASC
292
298
  LIMIT ?`)
293
- .all(targetEntryId, stashRoot, effectiveLimit);
299
+ .all(filePath, stashRoot, effectiveLimit);
294
300
  }
295
301
  catch {
296
302
  return [];
297
303
  }
298
304
  if (candidateRows.length === 0)
299
305
  return [];
300
- const candidateIds = candidateRows.map((r) => r.entry_id);
301
- const placeholders = candidateIds.map(() => "?").join(",");
306
+ const candidatePaths = candidateRows.map((r) => r.file_path);
307
+ const placeholders = candidatePaths.map(() => "?").join(",");
302
308
  // Pull the shared entity names (joined by normalized casing) for display.
303
309
  const sharedRows = db
304
- .prepare(`SELECT e.entry_id AS entry_id, e.entity AS entity
310
+ .prepare(`SELECT e.file_path AS file_path, e.entity AS entity
305
311
  FROM graph_file_entities e
306
312
  JOIN graph_file_entities target
307
313
  ON target.stash_root = e.stash_root
308
314
  AND target.entity_norm = e.entity_norm
309
- WHERE e.entry_id IN (${placeholders})
310
- AND target.entry_id = ?
315
+ WHERE e.file_path IN (${placeholders})
316
+ AND e.stash_root = ?
317
+ AND target.file_path = ?
311
318
  AND target.stash_root = ?`)
312
- .all(...candidateIds, targetEntryId, stashRoot);
313
- const sharedByEntry = new Map();
319
+ .all(...candidatePaths, stashRoot, filePath, stashRoot);
320
+ const sharedByPath = new Map();
314
321
  for (const row of sharedRows) {
315
- let bucket = sharedByEntry.get(row.entry_id);
322
+ let bucket = sharedByPath.get(row.file_path);
316
323
  if (!bucket) {
317
324
  bucket = new Set();
318
- sharedByEntry.set(row.entry_id, bucket);
325
+ sharedByPath.set(row.file_path, bucket);
319
326
  }
320
327
  bucket.add(row.entity);
321
328
  }
322
329
  // Relation count for each candidate (relations where either endpoint
323
330
  // matches one of the shared entities).
324
- const relationCountByEntry = new Map();
331
+ const relationCountByPath = new Map();
325
332
  const relationRows = db
326
- .prepare(`SELECT entry_id, from_entity, to_entity
333
+ .prepare(`SELECT file_path, from_entity, to_entity
327
334
  FROM graph_file_relations
328
- WHERE entry_id IN (${placeholders})`)
329
- .all(...candidateIds);
335
+ WHERE file_path IN (${placeholders})
336
+ AND stash_root = ?`)
337
+ .all(...candidatePaths, stashRoot);
330
338
  for (const row of relationRows) {
331
- const shared = sharedByEntry.get(row.entry_id);
339
+ const shared = sharedByPath.get(row.file_path);
332
340
  if (!shared)
333
341
  continue;
334
342
  if (shared.has(row.from_entity) || shared.has(row.to_entity)) {
335
- relationCountByEntry.set(row.entry_id, (relationCountByEntry.get(row.entry_id) ?? 0) + 1);
343
+ relationCountByPath.set(row.file_path, (relationCountByPath.get(row.file_path) ?? 0) + 1);
336
344
  }
337
345
  }
338
346
  // Optional: ref lookup via entries.entry_key. entry_key is stored as
339
347
  // `${stash_dir}:${type}:${name}` — strip the stash-dir prefix to get the
340
- // user-facing `type:name`.
341
- const refByEntryId = new Map();
348
+ // user-facing `type:name`. Resolve by (stash_dir, file_path) now that the
349
+ // graph rows are no longer keyed on entries.id.
350
+ const refByPath = new Map();
342
351
  try {
343
352
  const entryRows = db
344
- .prepare(`SELECT id, entry_key, stash_dir FROM entries WHERE id IN (${placeholders})`)
345
- .all(...candidateIds);
353
+ .prepare(`SELECT entry_key, stash_dir, file_path FROM entries
354
+ WHERE file_path IN (${placeholders}) AND stash_dir = ?`)
355
+ .all(...candidatePaths, stashRoot);
346
356
  for (const row of entryRows) {
347
357
  const ref = stripStashPrefix(row.entry_key, row.stash_dir);
348
358
  if (ref)
349
- refByEntryId.set(row.id, ref);
359
+ refByPath.set(row.file_path, ref);
350
360
  }
351
361
  }
352
362
  catch {
353
363
  /* ignore — refs are best-effort */
354
364
  }
355
365
  return candidateRows.map((row) => {
356
- const sharedSet = sharedByEntry.get(row.entry_id) ?? new Set();
366
+ const sharedSet = sharedByPath.get(row.file_path) ?? new Set();
357
367
  const sharedEntities = [...sharedSet].sort((a, b) => a.localeCompare(b));
358
- const ref = refByEntryId.get(row.entry_id);
368
+ const ref = refByPath.get(row.file_path);
359
369
  return {
360
370
  ...(ref ? { ref } : {}),
361
371
  path: row.file_path,
362
372
  type: row.file_type,
363
373
  sharedEntities,
364
- relationCount: relationCountByEntry.get(row.entry_id) ?? 0,
374
+ relationCount: relationCountByPath.get(row.file_path) ?? 0,
365
375
  };
366
376
  });
367
377
  }