akm-cli 0.8.0-rc1 → 0.8.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 (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2162 -1258
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +233 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +17 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +662 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +114 -48
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -307
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -1,25 +1,18 @@
1
- /**
2
- * Database-backed (SQLite + FTS5/vector) source search implementation.
3
- *
4
- * Extracted from source-search.ts to break the circular import:
5
- * source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
6
- *
7
- * source-search.ts imports this module for the `searchLocal` export.
8
- * sources/providers/filesystem.ts also imports `searchLocal` from here.
9
- *
10
- * Renamed from `local-search.ts` to signal that this is the DB-layer search
11
- * implementation, not a "local vs. remote" distinction.
12
- */
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/.
13
4
  import fs from "node:fs";
14
5
  import { buildActionFromContributors, defaultActionContributors } from "../core/action-contributors";
15
6
  import { makeAssetRef } from "../core/asset-ref";
16
7
  import { defaultRendererRegistry } from "../core/asset-registry";
17
8
  import { getDbPath } from "../core/paths";
18
9
  import { warn } from "../core/warn";
19
- import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "./db";
10
+ import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
11
+ import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "./db";
20
12
  import { ensureIndex } from "./ensure-index";
21
- import { collectGraphRelatedHit, loadGraphBoostContext } from "./graph-boost";
13
+ import { collectGraphRelatedHit, computeGraphBoost, loadGraphBoostContext, } from "./graph-boost";
22
14
  import { isProposedQuality } from "./metadata";
15
+ import { resolveProjectContext } from "./project-context";
23
16
  import { applyRankingRules, combineSearchScores, normalizeFtsScores } from "./ranking";
24
17
  import { enrichSearchHit } from "./search-hit-enrichers";
25
18
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
@@ -36,12 +29,30 @@ function resolveSearchHitRef(entry, refName, source) {
36
29
  function resolveSearchHitOrigin(source) {
37
30
  return source?.wikiName ? null : (source?.registryId ?? null);
38
31
  }
32
+ /**
33
+ * Phase 2A / Rec 5: gate for the per-search `getPositiveFeedbackCountsByIds`
34
+ * lookup. Returns `true` only when the user has explicitly opted into
35
+ * `improve.utilityDecay` AND configured a `feedbackStabilityBoost > 1.0`.
36
+ * Either condition being false makes the DB query pure overhead (the ranking
37
+ * contributor ignores `positiveFeedbackCounts` when `utilityDecayConfig` is
38
+ * absent, and `1.0^count == 1` collapses the boost into a no-op).
39
+ *
40
+ * Exported for unit testing — keeps the gate decision pinned so a future edit
41
+ * can't quietly broaden the hot path.
42
+ */
43
+ export function shouldQueryPositiveFeedbackCounts(utilityDecayRaw) {
44
+ if (utilityDecayRaw === undefined)
45
+ return false;
46
+ const boost = utilityDecayRaw.feedbackStabilityBoost ?? 1.5;
47
+ return boost > 1.0;
48
+ }
39
49
  // ── Main search entrypoint ───────────────────────────────────────────────────
40
50
  export async function searchLocal(input) {
41
51
  const { query, searchType, limit, stashDir, sources, config } = input;
42
52
  const filters = input.filters;
43
53
  const includeProposed = input.includeProposed === true;
44
54
  const beliefFilter = input.beliefFilter ?? "all";
55
+ const restrictToSources = input.restrictToSources === true;
45
56
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
46
57
  const allSourceDirs = sources.map((s) => s.path);
47
58
  const rawStatus = readSemanticStatus();
@@ -52,8 +63,18 @@ export async function searchLocal(input) {
52
63
  if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
53
64
  warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
54
65
  }
66
+ else if (!config.embedding?.endpoint || !config.embedding?.model) {
67
+ // #480: when semantic mode is `auto` but no embedding provider is
68
+ // configured (e.g. `akm setup --yes` ran without picking one), telling
69
+ // the user to "run akm setup" is misleading — they just did. Surface
70
+ // the actual remediation: configure an embedding endpoint OR switch
71
+ // semanticSearchMode to `off` to silence the warning.
72
+ warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
73
+ 'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
74
+ "(b) `akm config set semanticSearchMode off` to use keyword-only search.");
75
+ }
55
76
  else {
56
- warnings.push("Semantic search is pending verification. Run 'akm setup' or 'akm index --full' to enable semantic search.");
77
+ warnings.push("Semantic search is pending verification. Run 'akm index --full' to build the semantic index now, or wait for the next background index pass.");
57
78
  }
58
79
  }
59
80
  if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
@@ -81,7 +102,7 @@ export async function searchLocal(input) {
81
102
  mode: "keyword",
82
103
  };
83
104
  }
84
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter);
105
+ const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources);
85
106
  return {
86
107
  hits,
87
108
  tip: hits.length === 0
@@ -98,7 +119,7 @@ export async function searchLocal(input) {
98
119
  }
99
120
  }
100
121
  // ── Database search ─────────────────────────────────────────────────────────
101
- async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all") {
122
+ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false) {
102
123
  const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
103
124
  // Empty queries — including ones that sanitize down to no searchable FTS
104
125
  // tokens such as "." — should enumerate matching entries instead of
@@ -114,12 +135,19 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
114
135
  seenFilePaths.add(ie.filePath);
115
136
  return true;
116
137
  });
138
+ // Source filter: when the caller narrowed `sources` via `--source <name>`,
139
+ // drop entries whose filePath does not live under any of the requested
140
+ // sources. The FTS index spans every configured source, so without this
141
+ // filter a narrowed --source request would still leak results.
142
+ const sourceFiltered = restrictToSources
143
+ ? uniqueEntries.filter((ie) => findSourceForPath(ie.filePath, sources) !== undefined)
144
+ : uniqueEntries;
117
145
  // Scope filter: drop entries whose stored scope does not satisfy every
118
146
  // supplied scope key. Filtering happens BEFORE the limit slice so a
119
147
  // restrictive filter still returns up to `limit` results.
120
148
  const scopeFiltered = filters
121
- ? uniqueEntries.filter((ie) => entryMatchesScope(ie.entry.scope, filters))
122
- : uniqueEntries;
149
+ ? sourceFiltered.filter((ie) => entryMatchesScope(ie.entry.scope, filters))
150
+ : sourceFiltered;
123
151
  // Proposed-quality filter (v1 spec §4.2): exclude entries with
124
152
  // `quality: "proposed"` unless the caller explicitly opts in.
125
153
  const qualityFiltered = includeProposed
@@ -138,6 +166,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
138
166
  sources,
139
167
  config,
140
168
  rendererRegistry,
169
+ db,
141
170
  })));
142
171
  return { hits };
143
172
  }
@@ -192,24 +221,84 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
192
221
  return null;
193
222
  return loadGraphBoostContext(allSourceDirs, query, config, db);
194
223
  })();
195
- applyRankingRules({ db, query, items: scored, graphContext });
224
+ // Resolve project-context tokens from the current working directory once
225
+ // per search invocation. Returns null when running from home dir / /tmp,
226
+ // or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
227
+ const projectContext = process.env.AKM_DISABLE_PROJECT_CONTEXT === "1" ? null : resolveProjectContext(process.cwd());
228
+ // Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
229
+ // count query when the boost cannot make a difference (default ≤ 1.0 means
230
+ // boost^count == 1 — zero overhead for the common case).
231
+ const utilityDecayRaw = config.improve?.utilityDecay;
232
+ const halfLifeDays = utilityDecayRaw?.halfLifeDays ?? 30;
233
+ const feedbackStabilityBoost = utilityDecayRaw?.feedbackStabilityBoost ?? 1.5;
234
+ const utilityDecayConfig = utilityDecayRaw !== undefined ? { halfLifeDays, feedbackStabilityBoost } : undefined;
235
+ // Gate the feedback-count query on the user having explicitly opted into
236
+ // utilityDecay. Without an opt-in, `utilityDecayConfig` is undefined and the
237
+ // ranking contributor ignores `positiveFeedbackCounts` — so running the DB
238
+ // query here would be pure overhead. The boost > 1.0 sub-gate then skips the
239
+ // query when the configured boost is a no-op (1.5^count when boost==1 is 1).
240
+ const positiveFeedbackCounts = shouldQueryPositiveFeedbackCounts(utilityDecayRaw)
241
+ ? getPositiveFeedbackCountsByIds(db, scored.map((item) => item.id))
242
+ : undefined;
243
+ // Resolve per-project scope key for scoped utility scoring.
244
+ // AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
245
+ let scopeKey;
246
+ try {
247
+ scopeKey = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" ? undefined : getCurrentWorkflowScopeKey();
248
+ }
249
+ catch {
250
+ // Non-fatal — ranking proceeds without scoped utility on any error.
251
+ }
252
+ applyRankingRules({
253
+ db,
254
+ query,
255
+ items: scored,
256
+ graphContext,
257
+ projectContext,
258
+ utilityDecayConfig,
259
+ positiveFeedbackCounts,
260
+ scopeKey,
261
+ });
196
262
  // ── minScore floor ──────────────────────────────────────────────────────
197
263
  // Drop semantic-only hits (cosine-only, no FTS match) whose score falls
198
264
  // below the configured floor. FTS hits and hybrid hits are always kept.
199
265
  // Default floor: 0.2. Set search.minScore = 0 in config to disable.
200
266
  const minScore = config.search?.minScore ?? 0.2;
201
267
  const preFilter = minScore > 0 ? scored.filter((item) => item.rankingMode !== "semantic" || item.score >= minScore) : scored;
202
- // Deterministic tiebreaker on equal scores
203
- preFilter.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name));
268
+ // Deterministic tiebreaker on equal scores.
269
+ //
270
+ // CRITICAL: sort on the SAME clamped+rounded value the user sees (see the
271
+ // `finalScore`/round-to-4dp logic below at buildDbHit), NOT the raw pre-clamp
272
+ // `item.score`. The boost loop can push scores above 1.0 (utility, graph,
273
+ // project boosts) and carries ~15 significant digits. Two entries that DISPLAY
274
+ // an identical score (e.g. both clamp to 1.0000) can still differ in their raw
275
+ // pre-clamp score by a timing-dependent epsilon — utility recency uses
276
+ // `Date.now()` and `last_used_at`, so the same query run twice in one process
277
+ // can yield raw scores that diverge at the 6th decimal. Sorting on the raw
278
+ // value lets that invisible epsilon decide the order, so the visible name
279
+ // tiebreaker never engages and the order flips run-to-run (Issue #14). Quantize
280
+ // to the display value first; only then does `localeCompare` break true ties.
281
+ const displayScore = (s) => Math.round(Math.min(1, Math.max(0, s)) * 10000) / 10000;
282
+ preFilter.sort((a, b) => displayScore(b.score) - displayScore(a.score) || a.entry.name.localeCompare(b.entry.name));
204
283
  // Deduplicate by file path — keep only the highest-scored entry per file.
205
284
  // Multiple .stash.json entries can map to the same file (e.g. entries without
206
285
  // a filename field all collapse to files[0]). Showing the same path/ref
207
286
  // multiple times clutters results.
208
287
  const deduped = deduplicateByPath(preFilter);
288
+ // Source filter: when the caller narrowed `sources` via `--source <name>`,
289
+ // drop hits whose filePath does not live under any of the requested
290
+ // sources. The FTS/vector index spans every configured source, so without
291
+ // this filter a narrowed --source request would still leak results from
292
+ // other sources that happened to match the query text.
293
+ const sourceFiltered = restrictToSources
294
+ ? deduped.filter((item) => findSourceForPath(item.filePath, sources) !== undefined)
295
+ : deduped;
209
296
  // Scope filter: drop hits whose stored scope does not satisfy every supplied
210
297
  // key. Applied AFTER ranking — filtering narrows the result set without
211
298
  // touching the single FTS5+boosts scoring pipeline.
212
- const scopeFiltered = filters ? deduped.filter((item) => entryMatchesScope(item.entry.scope, filters)) : deduped;
299
+ const scopeFiltered = filters
300
+ ? sourceFiltered.filter((item) => entryMatchesScope(item.entry.scope, filters))
301
+ : sourceFiltered;
213
302
  // Proposed-quality filter (v1 spec §4.2): exclude entries with
214
303
  // `quality: "proposed"` unless the caller passed `--include-proposed`.
215
304
  // Applied AFTER ranking for the same reason as scope filtering.
@@ -239,6 +328,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
239
328
  utilityBoosted,
240
329
  graphContext,
241
330
  rendererRegistry,
331
+ db,
242
332
  });
243
333
  }));
244
334
  return { embedMs, rankMs, hits };
@@ -248,9 +338,16 @@ function matchBeliefFilter(type, beliefState, filter) {
248
338
  return true;
249
339
  if (type !== "memory")
250
340
  return true;
251
- if (filter === "current")
252
- return beliefState === undefined || beliefState === "active";
253
- return beliefState === "contradicted" || beliefState === "superseded" || beliefState === "archived";
341
+ if (filter === "current") {
342
+ // Phase 1A: `asserted` is a "current" state (stronger authority than `active`);
343
+ // `deprecated` is excluded from current results.
344
+ return beliefState === undefined || beliefState === "active" || beliefState === "asserted";
345
+ }
346
+ // historical
347
+ return (beliefState === "contradicted" ||
348
+ beliefState === "superseded" ||
349
+ beliefState === "deprecated" ||
350
+ beliefState === "archived");
254
351
  }
255
352
  // ── Vector scorer ───────────────────────────────────────────────────────────
256
353
  async function tryVecScores(db, query, k, config) {
@@ -292,7 +389,8 @@ export async function buildDbHit(input) {
292
389
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
293
390
  // Round to 4 decimal places, no boost multiplication
294
391
  const score = Math.round(input.score * 10000) / 10000;
295
- const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted);
392
+ const graphBoost = input.graphContext ? computeGraphBoost(input.graphContext, input.path) : 0;
393
+ const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted, graphBoost);
296
394
  const graphHit = input.graphContext ? collectGraphRelatedHit(input.graphContext, input.path) : null;
297
395
  const source = findSourceForPath(input.path, input.sources);
298
396
  const ref = resolveSearchHitRef(input.entry, input.entry.name, source);
@@ -326,12 +424,13 @@ export async function buildDbHit(input) {
326
424
  type: input.entry.type,
327
425
  stashDir: entryStashDir,
328
426
  rendererRegistry,
427
+ db: input.db,
329
428
  });
330
429
  return hit;
331
430
  }
332
431
  export function buildWhyMatched(entry, query,
333
432
  // "hybrid" ranking mode
334
- rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
433
+ rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost) {
335
434
  const reasons = [
336
435
  rankingMode === "hybrid"
337
436
  ? "hybrid (fts + semantic)"
@@ -375,12 +474,21 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
375
474
  reasons.push("metadata confidence boost");
376
475
  if (entry.beliefState === "active")
377
476
  reasons.push("active belief state");
477
+ if (entry.beliefState === "asserted")
478
+ reasons.push("asserted belief state");
378
479
  if (entry.beliefState === "contradicted")
379
480
  reasons.push("contradicted belief state");
380
481
  if (entry.beliefState === "superseded")
381
482
  reasons.push("superseded belief state");
483
+ if (entry.beliefState === "deprecated")
484
+ reasons.push("deprecated belief state");
485
+ if (entry.beliefState === "archived")
486
+ reasons.push("archived belief state");
382
487
  if (utilityBoosted)
383
488
  reasons.push("usage history boost");
489
+ if (typeof graphBoost === "number" && graphBoost > 0) {
490
+ reasons.push(`graph boost +${graphBoost.toFixed(2)}`);
491
+ }
384
492
  return reasons;
385
493
  }
386
494
  // ── Utilities ────────────────────────────────────────────────────────────────