akm-cli 0.9.0-beta.9 → 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 (325) hide show
  1. package/CHANGELOG.md +592 -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 -21
  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 +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  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 -1091
  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 +1295 -1277
  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 +228 -605
  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 +54 -3
  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 +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -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 +485 -2764
  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 +37 -35
  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 +206 -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 +2 -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 -895
  138. package/dist/commands/read/curate.js +410 -111
  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 +19 -39
  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 +382 -62
  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 +18 -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 +132 -1126
  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 +259 -769
  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 +36 -92
  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 +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  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 +10 -0
  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 +34 -11
  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 +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  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/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,114 @@
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
+ * Incremental dir-staleness engine.
6
+ *
7
+ * Decides, per stash directory, whether the directory's indexed rows are still
8
+ * fresh relative to what is on disk — so an incremental `akm index` run can
9
+ * skip unchanged directories instead of regenerating their metadata.
10
+ *
11
+ * Two persisted signals back the decision:
12
+ * 1. The `entries` rows already indexed for the directory (`getEntriesByDir`).
13
+ * 2. The `index_dir_state` fingerprint row (`getIndexDirState`), which caches
14
+ * the file-set hash + max mtime for directories that legitimately produced
15
+ * zero rows, so they are not rescanned every run.
16
+ *
17
+ * `computeDirFingerprint` derives the fingerprint (basename set + max mtime)
18
+ * that both the freshness check and the persisted `index_dir_state` row use.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { getEntriesByDir, getIndexDirState } from "../db/db.js";
23
+ export function getDirIndexState(db, dirPath, files, builtAtMs) {
24
+ const prevEntries = getEntriesByDir(db, dirPath);
25
+ const fingerprint = computeDirFingerprint(dirPath, files);
26
+ if (prevEntries.length > 0) {
27
+ const staleReason = getDirStaleReason(dirPath, files, prevEntries, builtAtMs);
28
+ if (!staleReason) {
29
+ return { stale: false, reason: { kind: "unchanged" }, persistedRowCount: prevEntries.length };
30
+ }
31
+ return { stale: true, reason: staleReason, persistedRowCount: prevEntries.length };
32
+ }
33
+ const cachedState = getIndexDirState(db, dirPath);
34
+ if (cachedState &&
35
+ cachedState.fileSetHash === fingerprint.fileSetHash &&
36
+ cachedState.fileMtimeMaxMs === fingerprint.fileMtimeMaxMs) {
37
+ return {
38
+ stale: false,
39
+ reason: { kind: "cached-zero-row-state", detail: cachedState.reason },
40
+ persistedRowCount: 0,
41
+ };
42
+ }
43
+ return {
44
+ stale: true,
45
+ reason: { kind: "no-previous-rows", detail: cachedState ? `cached=${cachedState.reason}` : undefined },
46
+ persistedRowCount: 0,
47
+ };
48
+ }
49
+ export function getCachedZeroRowDirState(db, dirPath, files, builtAtMs, priorDirsChanged) {
50
+ const state = getDirIndexState(db, dirPath, files, builtAtMs);
51
+ if (state.stale || state.reason.kind !== "cached-zero-row-state")
52
+ return undefined;
53
+ if (!canUseIncrementalSkip(state, priorDirsChanged))
54
+ return undefined;
55
+ return state;
56
+ }
57
+ export function canUseIncrementalSkip(state, priorDirsChanged) {
58
+ return !(priorDirsChanged &&
59
+ state.reason.kind === "cached-zero-row-state" &&
60
+ state.reason.detail === "deduped-zero-row");
61
+ }
62
+ export function computeDirFingerprint(_dirPath, files) {
63
+ const normalizedFiles = [...new Set(files.map((file) => path.basename(file)))].sort();
64
+ let fileMtimeMaxMs = 0;
65
+ for (const file of files) {
66
+ try {
67
+ fileMtimeMaxMs = Math.max(fileMtimeMaxMs, fs.statSync(file).mtimeMs);
68
+ }
69
+ catch {
70
+ fileMtimeMaxMs = Number.POSITIVE_INFINITY;
71
+ break;
72
+ }
73
+ }
74
+ return {
75
+ fileSetHash: normalizedFiles.join("\0"),
76
+ fileMtimeMaxMs,
77
+ };
78
+ }
79
+ function getDirStaleReason(_dirPath, currentFiles, previousEntries, builtAtMs) {
80
+ const prevFileNames = new Set(previousEntries
81
+ .map((ie) => {
82
+ const fromPath = path.basename(ie.filePath);
83
+ return fromPath || ie.entry.filename;
84
+ })
85
+ .filter((e) => !!e));
86
+ const currFileNames = new Set(currentFiles.map((f) => path.basename(f)));
87
+ if (prevFileNames.size !== currFileNames.size) {
88
+ return { kind: "file-set-changed", detail: `${prevFileNames.size} -> ${currFileNames.size} files` };
89
+ }
90
+ for (const name of currFileNames) {
91
+ if (!prevFileNames.has(name))
92
+ return { kind: "file-set-changed", detail: name };
93
+ }
94
+ for (const file of currentFiles) {
95
+ try {
96
+ if (fs.statSync(file).mtimeMs > builtAtMs)
97
+ return { kind: "mtime-changed", detail: path.basename(file) };
98
+ }
99
+ catch {
100
+ return { kind: "missing-file", detail: path.basename(file) };
101
+ }
102
+ }
103
+ return undefined;
104
+ }
105
+ export function inferZeroRowReason(stash, priorReason, warnings, dirPath, dedupedRows) {
106
+ if (dedupedRows > 0)
107
+ return "deduped-zero-row";
108
+ const workflowNoise = warnings.some((warning) => warning.startsWith("Skipped workflow ") && warning.includes(dirPath));
109
+ if (workflowNoise)
110
+ return "workflow-noise";
111
+ if (!stash || stash.entries.length === 0)
112
+ return "empty-generated-set";
113
+ return `zero-row:${priorReason?.kind ?? "unknown"}`;
114
+ }
@@ -72,6 +72,7 @@ const FM_CAPTURE_MODE = "captureMode";
72
72
  */
73
73
  export async function runMemoryInferencePass(ctx) {
74
74
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
75
+ const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
75
76
  const result = {
76
77
  considered: 0,
77
78
  cacheHits: 0,
@@ -172,14 +173,14 @@ export async function runMemoryInferencePass(ctx) {
172
173
  retryAttempts += 1;
173
174
  };
174
175
  const derived = db
175
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
177
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
177
178
  }, inferTelemetry, onRetryAttempt), validate, undefined, "", {
178
179
  onCacheHit: () => {
179
180
  fromCache = true;
180
181
  },
181
182
  })
182
- : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
+ : await compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
184
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
184
185
  }, inferTelemetry, onRetryAttempt);
185
186
  if (!derived) {
@@ -425,10 +426,17 @@ function markParentProcessed(parent) {
425
426
  warn(`memory inference: failed to re-read parent ${parent.filePath}: ${err instanceof Error ? err.message : String(err)}`);
426
427
  return;
427
428
  }
428
- const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
429
429
  const block = parseFrontmatterBlock(raw);
430
- const body = block?.content ?? raw;
431
- const next = assembleAsset(updatedFm, body);
430
+ if (!block) {
431
+ // Cannot safely rewrite malformed frontmatter — skip marking so the memory
432
+ // is retried on the next run once the frontmatter is repaired. Writing with
433
+ // `body = raw` would wrap the entire file (including the bad frontmatter)
434
+ // in a new block, producing a duplicate-frontmatter corruption.
435
+ warn(`memory inference: skipping markParentProcessed for ${parent.filePath} — could not parse frontmatter block`);
436
+ return;
437
+ }
438
+ const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
439
+ const next = assembleAsset(updatedFm, block.content);
432
440
  try {
433
441
  fs.writeFileSync(parent.filePath, next, "utf8");
434
442
  }
@@ -191,6 +191,15 @@ export function validateStashEntry(entry) {
191
191
  const contradictedBy = normalizeNonEmptyStringList(e.contradictedBy);
192
192
  if (contradictedBy)
193
193
  result.contradictedBy = contradictedBy;
194
+ // R5 — consolidation provenance fields must survive the whitelist too, or
195
+ // stash.json-overridden merge products lose merge-following + generation
196
+ // counting in the collapse detector.
197
+ if (typeof e.generation === "number" && Number.isFinite(e.generation) && e.generation > 0) {
198
+ result.generation = Math.floor(e.generation);
199
+ }
200
+ const sourceRefs = normalizeNonEmptyStringList(e.sourceRefs);
201
+ if (sourceRefs)
202
+ result.sourceRefs = sourceRefs;
194
203
  const currentBeliefRefs = normalizeNonEmptyStringList(e.currentBeliefRefs);
195
204
  if (currentBeliefRefs)
196
205
  result.currentBeliefRefs = currentBeliefRefs;
@@ -342,6 +351,17 @@ export function applyCuratedFrontmatter(entry, fmData) {
342
351
  const contradictedBy = normalizeStringListOrUndefined(fmData.contradictedBy);
343
352
  if (contradictedBy)
344
353
  entry.contradictedBy = contradictedBy;
354
+ // R5 — consolidation provenance. `generation` (merge depth counter) and
355
+ // `source_refs` (merge/distill provenance pointers) are written by the
356
+ // improve pipeline; captured into the index so the collapse detector can
357
+ // count over-generation assets and follow merges without filesystem reads.
358
+ const generation = fmData.generation;
359
+ if (typeof generation === "number" && Number.isFinite(generation) && generation > 0) {
360
+ entry.generation = Math.floor(generation);
361
+ }
362
+ const sourceRefs = normalizeStringListOrUndefined(fmData.source_refs);
363
+ if (sourceRefs)
364
+ entry.sourceRefs = sourceRefs;
345
365
  const currentBeliefRefs = normalizeStringListOrUndefined(fmData.currentBeliefRefs);
346
366
  if (currentBeliefRefs)
347
367
  entry.currentBeliefRefs = currentBeliefRefs;
@@ -0,0 +1,23 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { ensureIndex } from "./ensure-index.js";
5
+ import { resolveSourceEntries } from "./search/search-source.js";
6
+ /** Resolve the active read sources using the same resolution rules as search/show. */
7
+ export function resolveReadSources(overrideStashDir, existingConfig) {
8
+ const sources = resolveSourceEntries(overrideStashDir, existingConfig);
9
+ return { sources, primarySource: sources[0] };
10
+ }
11
+ /** Ensure the primary source index is readable for reads, when a primary exists. */
12
+ export async function ensurePrimaryIndexForRead(primarySource) {
13
+ if (!primarySource?.path)
14
+ return false;
15
+ return ensureIndex(primarySource.path);
16
+ }
17
+ /**
18
+ * Convenience helper for callers that only need to ensure a read index from a
19
+ * configured stash path and default config.
20
+ */
21
+ export async function ensurePrimaryIndexFromConfig(overrideStashDir, existingConfig) {
22
+ return ensurePrimaryIndexForRead(resolveReadSources(overrideStashDir, existingConfig).primarySource);
23
+ }
@@ -20,7 +20,7 @@ import { defaultRendererRegistry } from "../../core/asset/asset-registry.js";
20
20
  import { getDbPath } from "../../core/paths.js";
21
21
  import { warn } from "../../core/warn.js";
22
22
  import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
23
- import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "../db/db.js";
23
+ import { closeDatabase, getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "../db/db.js";
24
24
  import { ensureIndex } from "../ensure-index.js";
25
25
  import { collectGraphRelatedHit, computeGraphBoost, loadGraphBoostContext, } from "../graph/graph-boost.js";
26
26
  import { isProposedQuality } from "../passes/metadata.js";
@@ -29,6 +29,29 @@ import { applyRankingRules, combineSearchScores, normalizeFtsScores } from "./ra
29
29
  import { enrichSearchHit } from "./search-hit-enrichers.js";
30
30
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
31
31
  import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status.js";
32
+ /**
33
+ * Age past which search surfaces a "run akm index" hint. Reads serve the
34
+ * existing index as-is (freshness is the writers' job — `indexWrittenAssets`
35
+ * plus full runs), so on installs with no improve cron a hand-edited or
36
+ * git-pulled file stays invisible until someone reindexes. The hint makes that
37
+ * actionable without re-introducing read-triggered reindexing.
38
+ */
39
+ const STALE_INDEX_HINT_MS = 7 * 24 * 60 * 60 * 1000;
40
+ function buildStaleIndexHint(db) {
41
+ try {
42
+ const builtAt = getMeta(db, "builtAt");
43
+ if (!builtAt)
44
+ return undefined;
45
+ const ageMs = Date.now() - new Date(builtAt).getTime();
46
+ if (!Number.isFinite(ageMs) || ageMs < STALE_INDEX_HINT_MS)
47
+ return undefined;
48
+ const days = Math.floor(ageMs / (24 * 60 * 60 * 1000));
49
+ return `Search index was last built ${days} day(s) ago. Files added or edited outside akm since then are not searchable — run 'akm index' to refresh.`;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
32
55
  export function buildLocalAction(type, ref, registry = defaultRendererRegistry) {
33
56
  return buildActionFromContributors({ type, ref }, defaultActionContributors(registry)) ?? `akm show ${ref}`;
34
57
  }
@@ -65,6 +88,9 @@ export async function searchLocal(input) {
65
88
  const includeProposed = input.includeProposed === true;
66
89
  const beliefFilter = input.beliefFilter ?? "all";
67
90
  const restrictToSources = input.restrictToSources === true;
91
+ const includeExcludedTypes = input.includeExcludedTypes === true;
92
+ const disableProjectContext = input.disableProjectContext === true;
93
+ const disableScopedUtility = input.disableScopedUtility === true;
68
94
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
69
95
  const allSourceDirs = sources.map((s) => s.path);
70
96
  const rawStatus = readSemanticStatus();
@@ -92,7 +118,9 @@ export async function searchLocal(input) {
92
118
  if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
93
119
  warnings.push("Semantic search is currently blocked. Using keyword search until the semantic backend is healthy again.");
94
120
  }
95
- // Auto-index when stale so the DB is always current before querying.
121
+ // Bootstrap-only: builds the index inline when it cannot serve this stash.
122
+ // Content freshness is the writers' job (indexWrittenAssets + full runs);
123
+ // reads serve the existing index as-is.
96
124
  await ensureIndex(stashDir);
97
125
  const dbPath = getDbPath();
98
126
  if (!fs.existsSync(dbPath)) {
@@ -114,7 +142,10 @@ export async function searchLocal(input) {
114
142
  mode: "keyword",
115
143
  };
116
144
  }
117
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources);
145
+ const staleHint = buildStaleIndexHint(db);
146
+ if (staleHint)
147
+ warnings.push(staleHint);
148
+ const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
118
149
  return {
119
150
  hits,
120
151
  tip: hits.length === 0
@@ -131,14 +162,19 @@ export async function searchLocal(input) {
131
162
  }
132
163
  }
133
164
  // ── Database search ─────────────────────────────────────────────────────────
134
- async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false) {
165
+ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false, disableProjectContext = false, disableScopedUtility = false) {
135
166
  const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
167
+ // #627 — resolve the default type-exclusion policy. It applies ONLY on the
168
+ // untyped ('any') path and only when the caller did not opt back in via
169
+ // `includeExcludedTypes`. When the config key is ABSENT a built-in default of
170
+ // ['session'] is applied; an explicit empty list disables exclusion.
171
+ const defaultExcludes = searchType === "any" && !includeExcludedTypes ? (config.search?.defaultExcludeTypes ?? ["session"]) : [];
136
172
  // Empty queries — including ones that sanitize down to no searchable FTS
137
173
  // tokens such as "." — should enumerate matching entries instead of
138
174
  // returning an empty result set from FTS.
139
175
  if (!hasSearchableTokens) {
140
176
  const typeFilter = searchType === "any" ? undefined : searchType;
141
- const allEntries = getAllEntries(db, typeFilter);
177
+ const allEntries = getAllEntries(db, typeFilter, defaultExcludes);
142
178
  // Deduplicate by file path — multiple entries can share the same file
143
179
  const seenFilePaths = new Set();
144
180
  const uniqueEntries = allEntries.filter((ie) => {
@@ -165,7 +201,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
165
201
  const qualityFiltered = includeProposed
166
202
  ? scopeFiltered
167
203
  : scopeFiltered.filter((ie) => !isProposedQuality(ie.entry.quality));
168
- const beliefFiltered = qualityFiltered.filter((ie) => matchBeliefFilter(ie.entry.type, ie.entry.beliefState, beliefFilter));
204
+ // 03-R3: derived twins inherit their base's demoting belief state here too,
205
+ // so the belief FILTER (and the reported hit state) stays consistent on the
206
+ // enumerate/browse path — not only on the FTS-scored path below.
207
+ inheritDerivedTwinBeliefStates(db, qualityFiltered);
208
+ const beliefFiltered = qualityFiltered.filter((ie) => matchBeliefFilter(ie.entry.beliefState, beliefFilter));
169
209
  const selected = beliefFiltered.slice(0, limit);
170
210
  const hits = await Promise.all(selected.map((ie) => buildDbHit({
171
211
  entry: ie.entry,
@@ -187,7 +227,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
187
227
  const typeFilter = searchType === "any" ? undefined : searchType;
188
228
  const tEmbed0 = Date.now();
189
229
  const embeddingPromise = tryVecScores(db, query, limit * 3, config);
190
- const ftsResults = searchFts(db, query, limit * 3, typeFilter);
230
+ const ftsResults = searchFts(db, query, limit * 3, typeFilter, defaultExcludes);
191
231
  const embeddingScores = await embeddingPromise;
192
232
  const embedMs = Date.now() - tEmbed0;
193
233
  const tRank0 = Date.now();
@@ -208,6 +248,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
208
248
  embedScoreMap,
209
249
  getEntryById: (id) => getEntryById(db, id) ?? undefined,
210
250
  typeFilter,
251
+ // #627 — also exclude default-hidden types from the vector-only branch so a
252
+ // session asset that is a top-k vector neighbor (but not an FTS match) does
253
+ // not leak into default ('any') results. defaultExcludes is already []
254
+ // unless this is the untyped path without includeExcludedTypes.
255
+ excludeTypes: defaultExcludes,
211
256
  });
212
257
  // ── Scoring Phase ──────────────────────────────────────────────────────
213
258
  // Apply boosts as multiplicative factors (all boosts in a single phase
@@ -236,7 +281,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
236
281
  // Resolve project-context tokens from the current working directory once
237
282
  // per search invocation. Returns null when running from home dir / /tmp,
238
283
  // or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
239
- const projectContext = process.env.AKM_DISABLE_PROJECT_CONTEXT === "1" ? null : resolveProjectContext(process.cwd());
284
+ const projectContext = disableProjectContext ? null : resolveProjectContext(process.cwd());
240
285
  // Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
241
286
  // count query when the boost cannot make a difference (default ≤ 1.0 means
242
287
  // boost^count == 1 — zero overhead for the common case).
@@ -256,11 +301,14 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
256
301
  // AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
257
302
  let scopeKey;
258
303
  try {
259
- scopeKey = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" ? undefined : getCurrentWorkflowScopeKey();
304
+ scopeKey = disableScopedUtility ? undefined : getCurrentWorkflowScopeKey();
260
305
  }
261
306
  catch {
262
307
  // Non-fatal — ranking proceeds without scoped utility on any error.
263
308
  }
309
+ // 03-R3: derived twins inherit their base's demoting belief state before
310
+ // ranking, so the (03) belief-state ranker demotes a stale flag-free twin.
311
+ inheritDerivedTwinBeliefStates(db, scored);
264
312
  applyRankingRules({
265
313
  db,
266
314
  query,
@@ -317,7 +365,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
317
365
  const qualityFiltered = includeProposed
318
366
  ? scopeFiltered
319
367
  : scopeFiltered.filter((item) => !isProposedQuality(item.entry.quality));
320
- const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.type, item.entry.beliefState, beliefFilter));
368
+ const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.beliefState, beliefFilter));
321
369
  const rankMs = Date.now() - tRank0;
322
370
  const selected = beliefFiltered.slice(0, limit);
323
371
  const hits = await Promise.all(selected.map(({ entry, filePath, score, rankingMode, utilityBoosted }) => {
@@ -345,11 +393,39 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
345
393
  }));
346
394
  return { embedMs, rankMs, hits };
347
395
  }
348
- function matchBeliefFilter(type, beliefState, filter) {
396
+ /**
397
+ * 03-R3: let each `.derived` twin inherit its base memory's demoting belief
398
+ * state for this ranking pass, so a stale flag-free twin is demoted like its
399
+ * corrected base. The base carries the flag (a contradicted base takes a real
400
+ * ranking penalty); its near-duplicate `.derived` twin carries none and would
401
+ * otherwise outrank the corrected copy. Done in-memory at search time — NOT by
402
+ * writing the twin's frontmatter — because the SCC belief resolver refreshes any
403
+ * non-frozen state written to a derived memory back to `active` on the next
404
+ * improve run, erasing it. Only twins with no state of their own inherit; an
405
+ * explicit twin state always wins. Reuses the (03) belief-state ranker + filter.
406
+ */
407
+ function inheritDerivedTwinBeliefStates(db, items) {
408
+ const DEMOTING = new Set(["contradicted", "superseded", "deprecated", "archived"]);
409
+ const twins = items.filter((it) => it.entry.type === "memory" &&
410
+ it.entry.beliefState === undefined &&
411
+ it.entry.name.toLowerCase().endsWith(".derived"));
412
+ if (twins.length === 0)
413
+ return;
414
+ const baseBeliefByTwinId = getBaseBeliefStatesForDerivedTwins(db, twins.map((t) => t.id));
415
+ for (const t of twins) {
416
+ const baseBelief = baseBeliefByTwinId.get(t.id);
417
+ // Only inherit DEMOTIONS — never let a base's active/asserted state lift a twin.
418
+ if (baseBelief && DEMOTING.has(baseBelief)) {
419
+ t.entry.beliefState = baseBelief;
420
+ }
421
+ }
422
+ }
423
+ function matchBeliefFilter(beliefState, filter) {
349
424
  if (filter === "all")
350
425
  return true;
351
- if (type !== "memory")
352
- return true;
426
+ // 03: the belief filter applies to ANY flagged entry, not just memories, so
427
+ // `current`/`historical` filters catch contradicted/superseded KNOWLEDGE too.
428
+ // Unflagged entries (beliefState === undefined) still pass the `current` filter.
353
429
  if (filter === "current") {
354
430
  // Phase 1A: `asserted` is a "current" state (stronger authority than `active`);
355
431
  // `deprecated` is excluded from current results.
@@ -0,0 +1,51 @@
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
+ * Pure FTS5 query-string helpers, extracted from indexer/db/db.ts.
6
+ *
7
+ * These transform a raw user query into an FTS5-safe MATCH expression. They
8
+ * touch no database state, so they are unit-testable with zero DB setup.
9
+ */
10
+ /**
11
+ * Sanitize a raw user query into an FTS5-safe implicit-AND expression.
12
+ *
13
+ * Allows only characters safe in FTS5 queries: letters, digits, underscores,
14
+ * and whitespace. Everything else (hyphens, dots, quotes, parens, asterisks,
15
+ * colons, carets, @, !, etc.) is replaced with a space so that compound
16
+ * identifiers like "code-review" or "k8s.setup" become AND-joined tokens
17
+ * ("code review", "k8s setup") rather than triggering FTS5 syntax errors.
18
+ */
19
+ export function sanitizeFtsQuery(query) {
20
+ let sanitized = query.replace(/[^a-zA-Z0-9_\s]/g, " ");
21
+ // Neutralize the NEAR operator (FTS5 proximity syntax)
22
+ sanitized = sanitized.replace(/\bNEAR\b/g, " ");
23
+ const tokens = sanitized.split(/\s+/).filter((t) => t.length >= 1);
24
+ if (tokens.length === 0)
25
+ return "";
26
+ // Use implicit AND (space-separated tokens) for precision. FTS5 treats
27
+ // space-separated tokens as an implicit AND, matching only rows that
28
+ // contain ALL terms.
29
+ return tokens.join(" ");
30
+ }
31
+ /**
32
+ * Build a prefix query from an FTS5 query string by appending `*` to each
33
+ * token that is 3+ characters long. Tokens shorter than 3 characters are
34
+ * kept as-is (no prefix expansion) to avoid overly broad matches.
35
+ *
36
+ * Returns null if no tokens qualify for prefix expansion.
37
+ */
38
+ export function buildPrefixQuery(ftsQuery) {
39
+ const tokens = ftsQuery.split(/\s+/).filter(Boolean);
40
+ let hasPrefix = false;
41
+ const prefixTokens = tokens.map((t) => {
42
+ if (t.length >= 3) {
43
+ hasPrefix = true;
44
+ return `${t}*`;
45
+ }
46
+ return t;
47
+ });
48
+ if (!hasPrefix)
49
+ return null;
50
+ return prefixTokens.join(" ");
51
+ }
@@ -9,11 +9,22 @@ const TYPE_BOOST = {
9
9
  agent: 0.3,
10
10
  script: 0.2,
11
11
  knowledge: 0.22,
12
+ // Facts are authoritative, durable declarations about the stash — rank them
13
+ // alongside knowledge so they surface reliably when relevant.
14
+ fact: 0.22,
12
15
  memory: -0.02,
13
16
  };
14
17
  const MAX_BOOST_SUM = 3.0;
15
18
  const UTILITY_WEIGHT = 0.5;
16
19
  const UTILITY_MAX_BOOST = 1.5;
20
+ /**
21
+ * R2 (docs/design/improve-self-learning-analysis.md) — weight of the improve
22
+ * loop's `asset_salience.rank_score` in user-facing ranking. Bounded well
23
+ * below the utility boost so the composed signal refines, never dominates,
24
+ * lexical/semantic relevance. rank_score ∈ [0,1] → boost ∈ [1, 1.2].
25
+ */
26
+ const SALIENCE_WEIGHT = 0.2;
27
+ const SALIENCE_MAX_BOOST = 1.2;
17
28
  /**
18
29
  * Phase 2A / Rec 5: default recency half-life (days) used when no
19
30
  * `utilityDecayConfig` is supplied to the ranking pipeline. Matches the
@@ -29,8 +40,10 @@ const DEFAULT_RECENCY_HALF_LIFE_DAYS = 30;
29
40
  const FEEDBACK_HALF_LIFE_CAP_MULTIPLIER = 4;
30
41
  function beliefStateBoost(item) {
31
42
  const entry = item.entry;
32
- if (entry.type !== "memory")
33
- return 0;
43
+ // 03: belief-state penalties/boosts apply to ANY flagged entry (memory OR
44
+ // knowledge), so contradicted/superseded KNOWLEDGE is demoted from results
45
+ // just like flagged memories. Entries without a belief state fall through to
46
+ // the `return 0` below (default-safe — no effect on unflagged assets).
34
47
  // Phase 1A: `asserted` and `deprecated` are first-class states.
35
48
  // `asserted` carries stronger user-explicit authority than `active`.
36
49
  // `deprecated` is a frozen historical state — penalized but milder than `superseded`.
@@ -76,14 +89,18 @@ const typeRankingContributor = {
76
89
  return TYPE_BOOST[item.entry.type] ?? 0;
77
90
  },
78
91
  };
79
- const memoryRankingContributor = {
80
- name: "memory-ranking",
92
+ const beliefStateRankingContributor = {
93
+ name: "belief-state-ranking",
81
94
  appliesTo(item) {
82
- return item.entry.type === "memory";
95
+ // Fire for any entry that carries a belief state, regardless of type so
96
+ // contradicted/superseded knowledge is demoted, not just memories. The
97
+ // `.derived`-twin `derivedBoost` (±0.12/−0.08) is deleted (03-R3): it made
98
+ // stale flag-free twins outrank their corrected base memory; belief-state
99
+ // demotion is the principled signal, not the twin-name heuristic.
100
+ return item.entry.beliefState !== undefined;
83
101
  },
84
102
  adjust(item) {
85
- const derivedBoost = item.entry.name.toLowerCase().endsWith(".derived") ? 0.12 : -0.08;
86
- return derivedBoost + beliefStateBoost(item);
103
+ return beliefStateBoost(item);
87
104
  },
88
105
  };
89
106
  const tagRankingContributor = {
@@ -206,6 +223,24 @@ const lessonStrengthContributor = {
206
223
  return Math.min(0.3, 0.06 * strength);
207
224
  },
208
225
  };
226
+ /**
227
+ * Pinned-fact boost.
228
+ *
229
+ * Facts marked `pinned: true` form the small always-injected "core context"
230
+ * (see docs/design/fact-asset-type.md). The fact metadata contributor records
231
+ * a `pinned` search hint; here we give those facts a modest additive boost so
232
+ * the core outranks ordinary facts on otherwise-equal queries. Capped small so
233
+ * it cannot overpower an exact-name match.
234
+ */
235
+ const pinnedFactRankingContributor = {
236
+ name: "pinned-fact-ranking",
237
+ appliesTo(item) {
238
+ return item.entry.type === "fact" && (item.entry.searchHints?.includes("pinned") ?? false);
239
+ },
240
+ adjust() {
241
+ return 0.15;
242
+ },
243
+ };
209
244
  /**
210
245
  * Blend ratio for scoped vs. global utility signals.
211
246
  *
@@ -301,7 +336,7 @@ const projectContextRankingContributor = {
301
336
  export const defaultRankingContributors = [
302
337
  exactNameRankingContributor,
303
338
  typeRankingContributor,
304
- memoryRankingContributor,
339
+ beliefStateRankingContributor,
305
340
  tagRankingContributor,
306
341
  searchHintRankingContributor,
307
342
  aliasRankingContributor,
@@ -310,9 +345,60 @@ export const defaultRankingContributors = [
310
345
  graphRankingContributor,
311
346
  captureModeRankingContributor,
312
347
  lessonStrengthContributor,
348
+ pinnedFactRankingContributor,
313
349
  projectContextRankingContributor,
314
350
  ];
315
- export const defaultUtilityRankingContributors = [utilityRankingContributor];
351
+ /**
352
+ * R2 — compose the improve loop's salience core into user-facing ranking.
353
+ *
354
+ * `asset_salience.rank_score` (encoding + outcome + retrieval projection,
355
+ * maintained every improve run) previously drove only improve's INTERNAL
356
+ * maintenance selection — the "better assets surface more" loop ran solely
357
+ * through the utility EMA. This bounded multiplicative boost closes the outer
358
+ * loop: usage/outcome-reinforced assets rank higher in `search`/`curate`.
359
+ */
360
+ const salienceRankingContributor = {
361
+ name: "salience-ranking",
362
+ appliesTo(item, ctx) {
363
+ const rank = ctx.salienceRankScores?.get(item.id);
364
+ return rank !== undefined && rank > 0;
365
+ },
366
+ apply(item, ctx) {
367
+ const rank = ctx.salienceRankScores?.get(item.id) ?? 0;
368
+ const rawBoost = 1 + Math.min(1, Math.max(0, rank)) * SALIENCE_WEIGHT;
369
+ item.score *= Math.min(rawBoost, SALIENCE_MAX_BOOST);
370
+ },
371
+ };
372
+ export const defaultUtilityRankingContributors = [
373
+ utilityRankingContributor,
374
+ salienceRankingContributor,
375
+ ];
376
+ /**
377
+ * EVAL/DEBUG ONLY — remove named ranking contributors from a list.
378
+ *
379
+ * Driven by the `AKM_ABLATE_CONTRIBUTORS` env var (comma-separated contributor
380
+ * `name`s). A no-op — returns the input list unchanged (same reference) — when
381
+ * the env value is unset/empty, so production ranking is never affected unless
382
+ * the operator opts in. Its sole purpose is per-contributor ablation for the
383
+ * curate ablation harness (see `docs/technical/ranking-ablation-and-saturation-analysis.md`
384
+ * and `scripts/akm-eval/`): run the same fixture with and without a contributor
385
+ * and diff the ranked results to measure whether that contributor is load-bearing.
386
+ *
387
+ * NOTE (see the analysis doc): a contributor's ablation delta is only observable
388
+ * in the UNSATURATED score regime — once entries saturate at the `displayScore`
389
+ * ceiling their contributor deltas are absorbed and ablation reads Δ=0.
390
+ */
391
+ export function applyContributorAblation(contributors, ablateEnv) {
392
+ if (!ablateEnv)
393
+ return contributors;
394
+ const ablated = new Set(ablateEnv
395
+ .split(",")
396
+ .map((s) => s.trim())
397
+ .filter(Boolean));
398
+ if (ablated.size === 0)
399
+ return contributors;
400
+ return contributors.filter((c) => !ablated.has(c.name));
401
+ }
316
402
  export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
317
403
  let boostSum = 0;
318
404
  for (const contributor of contributors) {