akm-cli 0.9.1 → 0.9.2-alpha.1

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 (341) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/tasks/core/extract.yml +6 -5
  12. package/dist/assets/tasks/core/improve.yml +6 -5
  13. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  14. package/dist/assets/tasks/core/sync.yml +6 -5
  15. package/dist/assets/tasks/core/version-check.yml +6 -5
  16. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  17. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  21. package/dist/cli/confirm.js +2 -2
  22. package/dist/cli/parse-args.js +3 -24
  23. package/dist/cli/retired-commands.js +1 -1
  24. package/dist/cli/shared.js +2 -2
  25. package/dist/cli.js +11 -9
  26. package/dist/commands/agent/agent-dispatch.js +55 -89
  27. package/dist/commands/agent/contribute-cli.js +12 -45
  28. package/dist/commands/command/builtin-action.js +32 -0
  29. package/dist/commands/command/command-cli.js +99 -0
  30. package/dist/commands/command/command-execution.js +308 -0
  31. package/dist/commands/command/execution-source-loader.js +176 -0
  32. package/dist/commands/command/portable-template.js +60 -0
  33. package/dist/commands/config-cli.js +10 -4
  34. package/dist/commands/env/env.js +4 -2
  35. package/dist/commands/feedback-cli.js +1 -1
  36. package/dist/commands/health/checks.js +241 -29
  37. package/dist/commands/health/html-report.js +0 -14
  38. package/dist/commands/health/report-view-model.js +0 -1
  39. package/dist/commands/health/surfaces.js +6 -7
  40. package/dist/commands/health/types.js +0 -2
  41. package/dist/commands/health.js +63 -18
  42. package/dist/commands/improve/collapse-detector.js +2 -2
  43. package/dist/commands/improve/consolidate.js +251 -214
  44. package/dist/commands/improve/distill/promote-memory.js +71 -34
  45. package/dist/commands/improve/distill/quality-gate.js +17 -5
  46. package/dist/commands/improve/distill.js +232 -155
  47. package/dist/commands/improve/eligibility.js +112 -79
  48. package/dist/commands/improve/execution.js +57 -0
  49. package/dist/commands/improve/extract-cli.js +5 -5
  50. package/dist/commands/improve/extract.js +521 -342
  51. package/dist/commands/improve/improve-strategies.js +43 -14
  52. package/dist/commands/improve/improve.js +249 -29
  53. package/dist/commands/improve/loop-stages.js +11 -17
  54. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  55. package/dist/commands/improve/outcome-loop.js +22 -38
  56. package/dist/commands/improve/planner.js +134 -0
  57. package/dist/commands/improve/preparation.js +730 -409
  58. package/dist/commands/improve/reflect.js +386 -223
  59. package/dist/commands/improve/run-context.js +3 -4
  60. package/dist/commands/improve/salience.js +6 -58
  61. package/dist/commands/improve/session-asset.js +12 -12
  62. package/dist/commands/lint/index.js +101 -29
  63. package/dist/commands/migrate-cli.js +11 -69
  64. package/dist/commands/migration-tool.js +6 -9
  65. package/dist/commands/models-cli.js +27 -0
  66. package/dist/commands/proposal/drain.js +258 -186
  67. package/dist/commands/proposal/proposal-cli.js +32 -10
  68. package/dist/commands/proposal/proposal.js +2 -5
  69. package/dist/commands/proposal/propose.js +192 -172
  70. package/dist/commands/proposal/repository.js +54 -91
  71. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  72. package/dist/commands/read/curate.js +10 -0
  73. package/dist/commands/read/registry-search.js +25 -9
  74. package/dist/commands/read/remember-cli.js +14 -2
  75. package/dist/commands/read/search.js +10 -4
  76. package/dist/commands/read/show.js +139 -153
  77. package/dist/commands/registry-cli.js +16 -7
  78. package/dist/commands/remember.js +33 -18
  79. package/dist/commands/sources/add-cli.js +19 -178
  80. package/dist/commands/sources/bundle-cli.js +15 -3
  81. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  82. package/dist/commands/sources/info.js +2 -1
  83. package/dist/commands/sources/installed-stashes.js +835 -152
  84. package/dist/commands/sources/schema-repair.js +174 -95
  85. package/dist/commands/sources/self-update.js +30 -74
  86. package/dist/commands/sources/source-add.js +3 -5
  87. package/dist/commands/sources/sources-cli.js +2 -15
  88. package/dist/commands/sources/update-transaction.js +220 -0
  89. package/dist/commands/tasks/tasks-cli.js +3 -3
  90. package/dist/commands/tasks/tasks.js +736 -317
  91. package/dist/commands/workflow-cli.js +2 -2
  92. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  93. package/dist/core/adapter/adapters/akm-adapter.js +83 -34
  94. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  95. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  96. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  97. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  98. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  99. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  100. package/dist/core/adapter/adapters/index.js +0 -9
  101. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  102. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  103. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  104. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  105. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  106. package/dist/core/adapter/execution-source.js +308 -0
  107. package/dist/core/adapter/recognize-match.js +36 -13
  108. package/dist/core/adapter/registry.js +0 -9
  109. package/dist/core/asset/stash-meta.js +94 -4
  110. package/dist/core/common.js +6 -11
  111. package/dist/core/config/config-io.js +3 -3
  112. package/dist/core/config/config-schema.js +18 -40
  113. package/dist/core/config/config-sources.js +11 -21
  114. package/dist/core/config/config-walker.js +31 -13
  115. package/dist/core/config/config.js +21 -20
  116. package/dist/core/config/schema/engines.js +8 -7
  117. package/dist/core/config/schema/improve-processes.js +29 -5
  118. package/dist/core/config/schema/primitives.js +1 -23
  119. package/dist/core/config/schema/sources-bundles.js +13 -16
  120. package/dist/core/errors.js +2 -0
  121. package/dist/core/events.js +68 -32
  122. package/dist/core/extra-params.js +1 -0
  123. package/dist/core/improve-result.js +315 -0
  124. package/dist/core/lesson-lint.js +0 -6
  125. package/dist/core/maintenance-barrier.js +4 -4
  126. package/dist/core/network-policy.js +152 -0
  127. package/dist/core/paths.js +1 -1
  128. package/dist/core/recognition-util.js +4 -4
  129. package/dist/core/registry-url.js +456 -0
  130. package/dist/core/state/migrations.js +161 -47
  131. package/dist/core/state-db.js +453 -80
  132. package/dist/core/system-error.js +32 -0
  133. package/dist/core/time.js +2 -12
  134. package/dist/core/write-source.js +0 -18
  135. package/dist/execution/directory-identity.js +52 -0
  136. package/dist/execution/executable-identity.js +107 -0
  137. package/dist/execution/guarded-source.js +398 -0
  138. package/dist/execution/json.js +95 -0
  139. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  140. package/dist/execution/record.js +55 -0
  141. package/dist/execution/resolved-request.js +730 -0
  142. package/dist/execution/source.js +320 -0
  143. package/dist/indexer/bundle-identity-guard.js +5 -4
  144. package/dist/indexer/db/graph-db.js +33 -0
  145. package/dist/indexer/graph/graph-boost.js +3 -4
  146. package/dist/indexer/graph/graph-extraction.js +562 -373
  147. package/dist/indexer/index-written-assets.js +65 -34
  148. package/dist/indexer/indexer.js +443 -222
  149. package/dist/indexer/installations.js +6 -0
  150. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  151. package/dist/indexer/passes/memory-inference.js +227 -174
  152. package/dist/indexer/scan/doc-to-entry.js +7 -9
  153. package/dist/indexer/scan/drain-dir.js +51 -23
  154. package/dist/indexer/search/db-search.js +98 -22
  155. package/dist/indexer/search/search-fields.js +1 -1
  156. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  157. package/dist/indexer/search/search-source.js +7 -16
  158. package/dist/indexer/search/semantic-status.js +10 -1
  159. package/dist/indexer/usage/show-usage.js +105 -0
  160. package/dist/indexer/usage/usage-events.js +7 -2
  161. package/dist/indexer/walk/matchers.js +40 -10
  162. package/dist/indexer/walk/path-resolver.js +5 -2
  163. package/dist/indexer/walk/walker.js +20 -2
  164. package/dist/integrations/agent/builder-shared.js +3 -6
  165. package/dist/integrations/agent/conversation-fallback.js +16 -0
  166. package/dist/integrations/agent/engine-resolution.js +87 -87
  167. package/dist/integrations/agent/execution-cascade.js +566 -0
  168. package/dist/integrations/agent/execution-definitions.js +211 -0
  169. package/dist/integrations/agent/execution-lowering.js +811 -0
  170. package/dist/integrations/agent/execution-preparation.js +67 -0
  171. package/dist/integrations/agent/index.js +0 -2
  172. package/dist/integrations/agent/inline-execution.js +74 -0
  173. package/dist/integrations/agent/model-map.js +515 -0
  174. package/dist/integrations/agent/persona-fallback.js +30 -0
  175. package/dist/integrations/agent/request-lowering.js +186 -0
  176. package/dist/integrations/agent/runner-dispatch.js +230 -37
  177. package/dist/integrations/agent/runner.js +12 -83
  178. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  179. package/dist/integrations/harnesses/aider/index.js +0 -1
  180. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  181. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  182. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  183. package/dist/integrations/harnesses/claude/index.js +1 -5
  184. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  185. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  186. package/dist/integrations/harnesses/codex/index.js +0 -1
  187. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/copilot/index.js +0 -1
  189. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/gemini/index.js +0 -1
  191. package/dist/integrations/harnesses/index.js +4 -44
  192. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  193. package/dist/integrations/harnesses/opencode/index.js +0 -2
  194. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  195. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  196. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  197. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  198. package/dist/integrations/harnesses/openhands/index.js +0 -1
  199. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  200. package/dist/integrations/harnesses/pi/index.js +0 -1
  201. package/dist/integrations/harnesses/shared.js +0 -1
  202. package/dist/integrations/harnesses/types.js +1 -3
  203. package/dist/integrations/lockfile.js +82 -79
  204. package/dist/integrations/session-logs/index.js +6 -17
  205. package/dist/integrations/session-logs/provider-base.js +1 -29
  206. package/dist/llm/client.js +10 -5
  207. package/dist/llm/embedder.js +6 -7
  208. package/dist/llm/embedders/local.js +37 -88
  209. package/dist/llm/embedders/types.js +1 -1
  210. package/dist/llm/graph-extract.js +75 -50
  211. package/dist/llm/index-passes.js +43 -5
  212. package/dist/llm/memory-infer.js +8 -6
  213. package/dist/llm/metadata-enhance.js +5 -3
  214. package/dist/llm/structured-call.js +122 -25
  215. package/dist/output/format-exempt.js +1 -1
  216. package/dist/output/render-registry.js +0 -16
  217. package/dist/output/renderers.js +12 -7
  218. package/dist/output/shapes/curate.js +1 -0
  219. package/dist/output/shapes/helpers.js +10 -2
  220. package/dist/output/shapes/passthrough.js +2 -0
  221. package/dist/output/text/command-format.js +28 -32
  222. package/dist/output/text/health-format.js +1 -29
  223. package/dist/output/text/migrate.js +6 -56
  224. package/dist/output/text/proposal-format.js +16 -1
  225. package/dist/output/text/workflow-format.js +16 -0
  226. package/dist/registry/network.js +279 -0
  227. package/dist/registry/pinned-request-helper.js +247 -0
  228. package/dist/registry/pinned-transport.js +717 -0
  229. package/dist/registry/providers/skills-sh.js +18 -6
  230. package/dist/registry/providers/static-index.js +20 -7
  231. package/dist/registry/resolve.js +53 -28
  232. package/dist/scripts/akm-migrate-node.js +19122 -52218
  233. package/dist/scripts/akm-migrate.js +19064 -51567
  234. package/dist/setup/registry-stash-loader.js +64 -20
  235. package/dist/setup/semantic-assets.js +9 -34
  236. package/dist/setup/setup.js +12 -30
  237. package/dist/setup/source-identity.js +17 -0
  238. package/dist/setup/steps/sources.js +36 -15
  239. package/dist/setup/steps/tasks.js +39 -11
  240. package/dist/sources/providers/git-provider.js +3 -3
  241. package/dist/sources/providers/npm.js +2 -2
  242. package/dist/sources/providers/provider-utils.js +4 -3
  243. package/dist/sources/providers/website.js +11 -7
  244. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  245. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  246. package/dist/sources/website-url.js +73 -0
  247. package/dist/storage/engines/sqlite-migrations.js +81 -26
  248. package/dist/storage/managed-db.js +27 -24
  249. package/dist/storage/repositories/events-repository.js +3 -0
  250. package/dist/storage/repositories/index-connection.js +42 -10
  251. package/dist/storage/repositories/index-entries-repository.js +167 -207
  252. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  253. package/dist/storage/repositories/index-entry-schema.js +255 -0
  254. package/dist/storage/repositories/index-fts-repository.js +10 -10
  255. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  256. package/dist/storage/repositories/index-meta-repository.js +0 -11
  257. package/dist/storage/repositories/index-schema.js +71 -342
  258. package/dist/storage/repositories/index-utility-repository.js +12 -17
  259. package/dist/storage/repositories/index-vec-repository.js +1 -1
  260. package/dist/storage/repositories/proposals-repository.js +4 -127
  261. package/dist/storage/repositories/registry-cache.js +2 -1
  262. package/dist/storage/repositories/task-history-repository.js +20 -40
  263. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  264. package/dist/storage/sqlite-read-snapshot.js +148 -0
  265. package/dist/tasks/backends/cron.js +170 -42
  266. package/dist/tasks/backends/index.js +1 -1
  267. package/dist/tasks/backends/launchd.js +787 -202
  268. package/dist/tasks/backends/schtasks.js +282 -83
  269. package/dist/tasks/embedded.js +7 -7
  270. package/dist/tasks/frozen-script.js +50 -0
  271. package/dist/tasks/resolve-akm-bin.js +5 -1
  272. package/dist/tasks/runner.js +239 -251
  273. package/dist/tasks/runtime-v3.js +281 -0
  274. package/dist/tasks/scheduler-binding.js +272 -0
  275. package/dist/tasks/scheduler-invocation.js +57 -43
  276. package/dist/tasks/scheduler-sync.js +654 -0
  277. package/dist/tasks/source-v3.js +752 -0
  278. package/dist/tasks/standalone-script-entry.js +5 -0
  279. package/dist/tasks/task-id.js +29 -0
  280. package/dist/workflows/authoring/authoring.js +15 -32
  281. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  282. package/dist/workflows/exec/exec-unit.js +7 -28
  283. package/dist/workflows/exec/frozen-judge.js +57 -89
  284. package/dist/workflows/exec/lowering-notices.js +23 -0
  285. package/dist/workflows/exec/native-executor.js +301 -458
  286. package/dist/workflows/exec/param-secrets.js +4 -3
  287. package/dist/workflows/exec/run-workflow.js +26 -32
  288. package/dist/workflows/exec/step-work.js +105 -109
  289. package/dist/workflows/exec/unit-dispatch.js +103 -27
  290. package/dist/workflows/exec/unit-writer.js +3 -3
  291. package/dist/workflows/exec/worktree.js +2 -2
  292. package/dist/workflows/ir/compile.js +86 -72
  293. package/dist/workflows/ir/environment-v4.js +328 -0
  294. package/dist/workflows/ir/freeze-v4.js +122 -0
  295. package/dist/workflows/ir/plan-hash.js +13 -7
  296. package/dist/workflows/ir/schema-v4.js +525 -0
  297. package/dist/workflows/ir/schema.js +25 -284
  298. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  299. package/dist/workflows/parser.js +27 -24
  300. package/dist/workflows/program/schema.js +1 -2
  301. package/dist/workflows/renderer.js +42 -29
  302. package/dist/workflows/resource-limits.js +4 -5
  303. package/dist/workflows/runtime/agent-identity.js +11 -13
  304. package/dist/workflows/runtime/plan-classifier.js +8 -8
  305. package/dist/workflows/runtime/runs.js +27 -43
  306. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  307. package/dist/workflows/source-files.js +373 -0
  308. package/dist/workflows/source-ir/compile.js +196 -0
  309. package/dist/workflows/source-ir/github-yaml.js +577 -0
  310. package/dist/workflows/source-ir/ordering.js +38 -0
  311. package/dist/workflows/source-ir/program.js +50 -0
  312. package/dist/workflows/source-ir/result.js +26 -0
  313. package/dist/workflows/source-ir/schema.js +772 -0
  314. package/dist/workflows/source-ir/semantics.js +242 -0
  315. package/dist/workflows/source-ir/uses.js +14 -0
  316. package/docs/README.md +2 -0
  317. package/docs/migration/README.md +3 -1
  318. package/docs/migration/release-notes/0.9.2.md +55 -0
  319. package/docs/migration/release-notes/README.md +5 -0
  320. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  321. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  322. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  323. package/docs/reference/README.md +1 -0
  324. package/docs/reference/cli.md +230 -98
  325. package/docs/reference/configuration.md +153 -27
  326. package/docs/reference/data-and-telemetry.md +19 -1
  327. package/docs/reference/supported-formats.md +23 -3
  328. package/docs/reference/tasks.md +182 -0
  329. package/docs/reference/workflow-schema.md +91 -40
  330. package/docs/reference/workflows.md +33 -6
  331. package/package.json +10 -6
  332. package/schemas/akm-config.json +372 -216
  333. package/schemas/akm-task.json +324 -80
  334. package/schemas/akm-workflow.json +6 -9
  335. package/dist/core/migration-operation.js +0 -75
  336. package/dist/integrations/agent/model-aliases.js +0 -74
  337. package/dist/tasks/parser.js +0 -380
  338. package/dist/tasks/schema.js +0 -123
  339. package/dist/tasks/validator.js +0 -80
  340. package/dist/workflows/ir/freeze.js +0 -320
  341. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -28,28 +28,21 @@ import { deleteEntryVectors, isVecAvailable } from "./index-vec-repository.js";
28
28
  * `rebuildFts()` after all upserts are complete for full-text search to
29
29
  * reflect the changes.
30
30
  */
31
- export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, searchText, provenance, contentHash) {
31
+ export function upsertEntry(db, filePath, entry, searchText, provenance, contentHash) {
32
32
  // Hot path during indexing — cache prepared statements per database
33
33
  // connection so we don't pay the SQL parse/compile cost on every call.
34
34
  const stmts = getUpsertStmts(db);
35
- const previous = (provenance?.itemRef
36
- ? stmts.findByItemRef.get(provenance.itemRef)
37
- : undefined) ?? stmts.findByEntryKey.get(entryKey);
35
+ const previous = stmts.findByItemRef.get(provenance.itemRef);
38
36
  // Phase 5A / Advantage D5: surface derived memory parent ref into the
39
37
  // dedicated `derived_from` column so retrieval-time lookup (parent→child)
40
38
  // does not have to scan + JSON-decode every memory row.
41
39
  const derivedFrom = typeof entry.derivedFrom === "string" && entry.derivedFrom.trim() ? entry.derivedFrom.trim() : null;
42
- // Chunk-5 Step 2 (spec §14.4): populate the additive identity/provenance
43
- // columns alongside the legacy ones. `type` mirrors `entry_type` (the open
44
- // token) unconditionally; `item_ref`/bundle/component/concept/adapter come
45
- // from the write-boundary derivation when available (NULL otherwise, healed
46
- // by the next full index). `content_hash` (F4a M-core-2) is `doc.hash` from
47
- // the diff-persist writer; a NULL passed here PRESERVES any existing hash (the
48
- // ON CONFLICT COALESCE below) so the LLM-enrichment re-upsert cannot wipe it.
40
+ // `content_hash` is optional on the LLM-enrichment re-upsert; a missing hash
41
+ // preserves the scan writer's current value.
49
42
  const apply = () => {
50
- const result = stmts.upsert.get(entryKey, dirPath, filePath, stashDir, JSON.stringify(entry), searchText, entry.type, derivedFrom, provenance?.itemRef ?? null, provenance?.bundleId ?? null, provenance?.componentId ?? null, provenance?.conceptId ?? null, provenance?.adapterId ?? null, entry.type, contentHash ?? null);
43
+ const result = stmts.upsert.get(provenance.itemRef, provenance.bundleId, provenance.componentId, provenance.conceptId, provenance.adapterId, entry.type, filePath, contentHash ?? null, JSON.stringify(entry), searchText, derivedFrom);
51
44
  if (!result)
52
- throw new Error("upsertEntry: entry_key not found after upsert");
45
+ throw new Error("upsertEntry: item_ref not found after upsert");
53
46
  if (previous?.id === result.id && previous.search_text !== searchText)
54
47
  deleteEntryVectors(db, result.id);
55
48
  // Mark this entry as FTS-dirty so `rebuildFts({ incremental: true })`
@@ -61,52 +54,38 @@ export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, se
61
54
  return previous && previous.search_text !== searchText ? db.transaction(apply)() : apply();
62
55
  }
63
56
  const upsertStmtsByDb = new WeakMap();
64
- // The ON CONFLICT DO UPDATE column assignments factored out so the two
65
- // conflict targets below stay byte-identical. `item_ref` is durable identity;
66
- // update `entry_key` when adapter ownership changes its
67
- // spelling so directory pruning retains the upserted row.
68
- // `content_hash` COALESCEs so a NULL passed by the LLM-enhance re-upsert cannot
69
- // wipe a previously-persisted hash.
57
+ // item_ref is the sole durable conflict target. `content_hash` COALESCEs so a
58
+ // metadata-only enrichment pass cannot wipe a scan hash.
70
59
  const UPSERT_SET_CLAUSE = `SET
71
- entry_key = excluded.entry_key,
72
- dir_path = excluded.dir_path,
73
- file_path = excluded.file_path,
74
- stash_dir = excluded.stash_dir,
75
- entry_json = excluded.entry_json,
76
- search_text = excluded.search_text,
77
- entry_type = excluded.entry_type,
78
- derived_from = excluded.derived_from,
79
- item_ref = excluded.item_ref,
80
60
  bundle_id = excluded.bundle_id,
81
61
  component_id = excluded.component_id,
82
62
  concept_id = excluded.concept_id,
83
63
  adapter_id = excluded.adapter_id,
84
64
  type = excluded.type,
65
+ file_path = excluded.file_path,
66
+ document_json = excluded.document_json,
67
+ search_text = excluded.search_text,
68
+ derived_from = excluded.derived_from,
85
69
  content_hash = COALESCE(excluded.content_hash, content_hash)`;
86
70
  function getUpsertStmts(db) {
87
71
  const existing = upsertStmtsByDb.get(db);
88
72
  if (existing)
89
73
  return existing;
90
- // Durable identity is the primary conflict target. `entry_key` remains the
91
- // internal conflict key for low-level entries without provenance.
92
- const conflictClause = `ON CONFLICT(item_ref) DO UPDATE ${UPSERT_SET_CLAUSE}
93
- ON CONFLICT(entry_key) DO UPDATE ${UPSERT_SET_CLAUSE}`;
94
74
  const stmts = {
95
75
  // RETURNING id handles ON CONFLICT DO UPDATE correctly — no second
96
76
  // SELECT round-trip needed (last_insert_rowid() is unreliable for
97
77
  // ON CONFLICT). Use `.get()` so a single row comes back.
98
78
  upsert: db.prepare(`
99
79
  INSERT INTO entries (
100
- entry_key, dir_path, file_path, stash_dir, entry_json, search_text, entry_type, derived_from,
101
- item_ref, bundle_id, component_id, concept_id, adapter_id, type, content_hash
80
+ item_ref, bundle_id, component_id, concept_id, adapter_id, type,
81
+ file_path, content_hash, document_json, search_text, derived_from
102
82
  )
103
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
104
- ${conflictClause}
83
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
84
+ ON CONFLICT(item_ref) DO UPDATE ${UPSERT_SET_CLAUSE}
105
85
  RETURNING id
106
86
  `),
107
87
  markDirty: db.prepare("INSERT OR IGNORE INTO entries_fts_dirty (entry_id) VALUES (?)"),
108
- findByItemRef: db.prepare("SELECT id, search_text FROM entries WHERE item_ref = ? ORDER BY id ASC LIMIT 1"),
109
- findByEntryKey: db.prepare("SELECT id, search_text FROM entries WHERE entry_key = ? LIMIT 1"),
88
+ findByItemRef: db.prepare("SELECT id, search_text FROM entries WHERE item_ref = ?"),
110
89
  };
111
90
  upsertStmtsByDb.set(db, stmts);
112
91
  return stmts;
@@ -121,10 +100,10 @@ function getUpsertStmts(db) {
121
100
  * ordering keeps results deterministic). Returns `null` when no derived
122
101
  * child has been indexed for this parent.
123
102
  */
124
- export function getDerivedForParent(db, parentRef, stashDir) {
103
+ export function getDerivedForParent(db, parentRef, bundleId) {
125
104
  if (!parentRef)
126
105
  return null;
127
- const sourceScope = stashDir ? "AND stash_dir = ?" : "";
106
+ const sourceScope = bundleId ? "AND bundle_id = ?" : "";
128
107
  const row = db
129
108
  .prepare(`SELECT ${ENTRY_COLUMNS}
130
109
  FROM entries
@@ -132,7 +111,7 @@ export function getDerivedForParent(db, parentRef, stashDir) {
132
111
  ${sourceScope}
133
112
  ORDER BY id DESC
134
113
  LIMIT 1`)
135
- .get(parentRef, ...(stashDir ? [stashDir] : []));
114
+ .get(parentRef, ...(bundleId ? [bundleId] : []));
136
115
  if (!row)
137
116
  return null;
138
117
  return rowToIndexedEntry(row, "getDerivedForParent");
@@ -143,10 +122,9 @@ export function getDerivedForParent(db, parentRef, stashDir) {
143
122
  *
144
123
  * Used by the derived-twin belief inheritance in search ranking: a `.derived`
145
124
  * twin has no belief state of its own, so it inherits its base memory's
146
- * demoting state (contradicted/superseded/…) at search time. A twin's
147
- * `entry_key` is exactly its base's `entry_key` plus the `.derived` suffix
148
- * (same stash + type prefix, `<name>` vs `<name>.derived`), so the base is
149
- * found by stripping that suffix — no ref/prefix reconstruction. Returns a map
125
+ * demoting state (contradicted/superseded/…) at search time. A twin's durable
126
+ * `item_ref` is exactly its base ref plus the `.derived` suffix, so the base is
127
+ * found by stripping that suffix. Returns a map
150
128
  * of twin id → base beliefState for bases that carry a non-empty state.
151
129
  * Best-effort: any query error (e.g. legacy DB) yields no inheritance rather
152
130
  * than failing the search.
@@ -163,14 +141,14 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
163
141
  const placeholders = chunk.map(() => "?").join(",");
164
142
  bestEffort(() => {
165
143
  const rows = db
166
- .prepare(`SELECT twin.id AS twin_id, json_extract(base.entry_json, '$.beliefState') AS belief
144
+ .prepare(`SELECT twin.id AS twin_id, json_extract(base.document_json, '$.beliefState') AS belief
167
145
  FROM entries twin
168
146
  JOIN entries base
169
- ON base.entry_type = 'memory'
147
+ ON base.type = 'memory'
170
148
  AND base.item_ref = substr(twin.item_ref, 1, length(twin.item_ref) - length('.derived'))
171
149
  WHERE twin.id IN (${placeholders})
172
150
  AND twin.item_ref LIKE '%.derived'
173
- AND json_extract(base.entry_json, '$.beliefState') IS NOT NULL`)
151
+ AND json_extract(base.document_json, '$.beliefState') IS NOT NULL`)
174
152
  .all(...chunk);
175
153
  for (const r of rows) {
176
154
  if (typeof r.belief === "string" && r.belief.trim().length > 0)
@@ -181,15 +159,15 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
181
159
  return out;
182
160
  }
183
161
  /**
184
- * SPEC-7 (`akm mv`): re-key an entries row IN PLACE after an on-disk rename.
162
+ * Re-key an entries row in place for the opt-in source-maintenance script.
185
163
  *
186
164
  * The row id is preserved on purpose — `utility_scores`,
187
165
  * `utility_scores_scoped`, and `embeddings` are keyed by `entry_id`, so an
188
- * UPDATE (rather than a delete + insert under the new `entry_key`) is what
166
+ * UPDATE (rather than a delete + insert under the new `item_ref`) is what
189
167
  * keeps the asset's accumulated usage-ranking history attached across a
190
168
  * rename. (`asset_salience` / `asset_outcome` live in state.db keyed by
191
169
  * `asset_ref` TEXT and are re-keyed separately by `akm mv` — see
192
- * mv-cli.ts `rekeyStateDbForMove`.) `entry_json.name` (and `filename`, when
170
+ * the state rekey helper.) `document_json.name` (and `filename`, when
193
171
  * present) is patched and `search_text` rebuilt so search reflects the new
194
172
  * name; the row is marked FTS-dirty for the caller's
195
173
  * `rebuildFts({incremental: true})`.
@@ -203,7 +181,7 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
203
181
  * never adopts them (live asset's history wins, matching the stale-row
204
182
  * eviction below).
205
183
  *
206
- * A stale row already occupying `newEntryKey` (the caller has verified no
184
+ * A stale row already occupying the new item ref (the caller has verified no
207
185
  * FILE exists at the target, so such a row can only be a leftover for a
208
186
  * deleted file) is evicted first — through {@link deleteRelatedRows}, so its
209
187
  * child rows (embeddings, entries_vec, utility scores, usage events) go with
@@ -211,44 +189,46 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
211
189
  * FK under `PRAGMA foreign_keys = ON` and roll back the whole re-key.
212
190
  * The moved row keeps its id.
213
191
  *
214
- * Returns the surviving row id, or `null` when no row matches `oldEntryKey`
192
+ * Returns the surviving row id, or `null` when no row matches the old item ref
215
193
  * (nothing indexed under the old name — the caller falls open and the next
216
194
  * full `akm index` picks the file up as a fresh entry).
217
195
  */
218
196
  export function rekeyEntryInPlace(db, opts) {
219
197
  const oldItemRef = `${opts.sourceName}//${opts.oldRef}`;
220
198
  const row = db
221
- .prepare("SELECT id, stash_dir, entry_json, search_text, entry_type FROM entries WHERE item_ref = ?")
199
+ .prepare("SELECT id, file_path, document_json, search_text, type FROM entries WHERE item_ref = ?")
222
200
  .get(oldItemRef);
223
201
  if (!row)
224
202
  return null;
225
- if (path.resolve(row.stash_dir) !== path.resolve(opts.sourceRoot)) {
226
- throw new Error(`Refusing to re-key entry ${opts.oldEntryKey}: source root does not match.`);
203
+ const sourceRoot = path.resolve(opts.sourceRoot);
204
+ const currentPath = path.resolve(row.file_path);
205
+ if (currentPath !== sourceRoot && !currentPath.startsWith(`${sourceRoot}${path.sep}`)) {
206
+ throw new Error(`Refusing to re-key entry ${oldItemRef}: source root does not match.`);
227
207
  }
228
- // Patch the JSON payload. On corrupt entry_json still re-key key + paths so
208
+ // Patch the JSON payload. On corrupt document_json still re-key identity/path so
229
209
  // the utility history survives; the next full index heals the JSON.
230
- let entryJson = row.entry_json;
210
+ let documentJson = row.document_json;
231
211
  let searchText = row.search_text;
232
212
  try {
233
- const entry = JSON.parse(row.entry_json);
213
+ const entry = JSON.parse(row.document_json);
234
214
  entry.name = opts.newName;
235
215
  if (typeof entry.filename === "string")
236
216
  entry.filename = path.basename(opts.newFilePath);
237
217
  if (opts.newDerivedFrom !== undefined)
238
218
  entry.derivedFrom = opts.newDerivedFrom;
239
- entryJson = JSON.stringify(entry);
219
+ documentJson = JSON.stringify(entry);
240
220
  searchText = buildSearchText(entry);
241
221
  }
242
222
  catch {
243
- /* corrupt entry_jsonkey/path-only re-key */
223
+ /* corrupt document_jsonidentity/path-only re-key */
244
224
  }
245
- const expectedNewRef = conceptIdFromTypeName(row.entry_type, opts.newName);
225
+ const expectedNewRef = conceptIdFromTypeName(row.type, opts.newName);
246
226
  if (opts.newRef !== expectedNewRef) {
247
- throw new Error(`Refusing to re-key entry ${opts.oldEntryKey}: target ref does not match the entry type and name.`);
227
+ throw new Error(`Refusing to re-key entry ${oldItemRef}: target ref does not match the entry type and name.`);
248
228
  }
249
229
  const newItemRef = `${opts.sourceName}//${opts.newRef}`;
250
230
  db.transaction(() => {
251
- const stale = db.prepare("SELECT id FROM entries WHERE entry_key = ?").get(opts.newEntryKey);
231
+ const stale = db.prepare("SELECT id FROM entries WHERE item_ref = ?").get(newItemRef);
252
232
  if (stale && stale.id !== row.id) {
253
233
  // Full child-row cleanup (embeddings, entries_vec, utility scores,
254
234
  // usage events, FTS + dirty marks) BEFORE the entries delete: the
@@ -258,7 +238,7 @@ export function rekeyEntryInPlace(db, opts) {
258
238
  deleteRelatedRows(db, [{ id: stale.id }]);
259
239
  db.prepare("DELETE FROM entries WHERE id = ?").run(stale.id);
260
240
  }
261
- db.prepare("UPDATE entries SET entry_key = ?, dir_path = ?, file_path = ?, entry_json = ?, search_text = ?, item_ref = ?, concept_id = ? WHERE id = ?").run(opts.newEntryKey, path.dirname(opts.newFilePath), opts.newFilePath, entryJson, searchText, newItemRef, opts.newRef, row.id);
241
+ db.prepare("UPDATE entries SET file_path = ?, document_json = ?, search_text = ?, item_ref = ?, concept_id = ? WHERE id = ?").run(opts.newFilePath, documentJson, searchText, newItemRef, opts.newRef, row.id);
262
242
  if (opts.newDerivedFrom !== undefined) {
263
243
  db.prepare("UPDATE entries SET derived_from = ? WHERE id = ?").run(opts.newDerivedFrom, row.id);
264
244
  }
@@ -347,68 +327,54 @@ export function getPositiveFeedbackCountsByIds(ids) {
347
327
  }, "positive feedback counts are best-effort");
348
328
  return result;
349
329
  }
350
- function deleteEntriesWhere(db, column, value) {
351
- db.transaction(() => {
352
- const ids = db.prepare(`SELECT id FROM entries WHERE ${column} = ?`).all(value);
353
- deleteRelatedRows(db, ids);
354
- db.prepare(`DELETE FROM entries WHERE ${column} = ?`).run(value);
355
- })();
330
+ function rowsInDirectory(db, dirPath, bundleId) {
331
+ const rows = db
332
+ .prepare(`SELECT id, item_ref, file_path FROM entries${bundleId ? " WHERE bundle_id = ?" : ""}`)
333
+ .all(...(bundleId ? [bundleId] : []));
334
+ const resolvedDir = path.resolve(dirPath);
335
+ return rows.filter((row) => path.dirname(path.resolve(row.file_path)) === resolvedDir);
336
+ }
337
+ function deleteEntryRows(db, rows, options = {}) {
338
+ if (rows.length === 0)
339
+ return [];
340
+ deleteRelatedRows(db, rows, options);
341
+ for (let i = 0; i < rows.length; i += SQLITE_CHUNK_SIZE) {
342
+ const chunk = rows.slice(i, i + SQLITE_CHUNK_SIZE);
343
+ const placeholders = chunk.map(() => "?").join(",");
344
+ db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk.map((row) => row.id));
345
+ }
346
+ return rows.map((row) => row.id);
356
347
  }
357
348
  export function deleteEntriesByDir(db, dirPath) {
358
- deleteEntriesWhere(db, "dir_path", dirPath);
349
+ db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath)))();
359
350
  }
360
- export function deleteEntriesByDirAndStash(db, dirPath, stashDir, options = {}) {
361
- return db.transaction(() => {
362
- const ids = db
363
- .prepare("SELECT id FROM entries WHERE dir_path = ? AND stash_dir = ?")
364
- .all(dirPath, stashDir);
365
- deleteRelatedRows(db, ids, options);
366
- db.prepare("DELETE FROM entries WHERE dir_path = ? AND stash_dir = ?").run(dirPath, stashDir);
367
- return ids.map((row) => row.id);
368
- })();
351
+ export function deleteEntriesByDirAndBundle(db, dirPath, bundleId, options = {}) {
352
+ return db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options))();
369
353
  }
370
- /**
371
- * Delete every entry (and its child rows) under a source's stash root.
372
- *
373
- * Chunk-5 flip F1: this keys on `stash_dir` the physical source-root path,
374
- * which is orthogonal to the ref grammar and identifies rows regardless of
375
- * whether their `item_ref` is populated or NULL. No item_ref predicate is
376
- * needed (or correct) here; both grammars' rows are removed by location.
377
- */
378
- export function deleteEntriesByStashDir(db, stashDir) {
379
- deleteEntriesWhere(db, "stash_dir", stashDir);
354
+ /** Delete every entry and child row belonging to one canonical bundle. */
355
+ export function deleteEntriesByBundle(db, bundleId) {
356
+ db.transaction(() => {
357
+ const rows = db.prepare("SELECT id FROM entries WHERE bundle_id = ?").all(bundleId);
358
+ deleteEntryRows(db, rows);
359
+ })();
380
360
  }
381
361
  /**
382
- * Diff-persist orphan delete (Chunk-5 flip F4a M-core-2): remove every entry
383
- * (and its child rows, via {@link deleteRelatedRows}) under `dirPath` whose
384
- * `entry_key` is NOT in `keepKeys` — the rows for files that vanished from a
385
- * rescanned directory.
362
+ * Diff-persist orphan delete: remove every entry under `dirPath` whose durable
363
+ * `item_ref` is not in `keepRefs`.
386
364
  *
387
365
  * Replaces the old per-dir `deleteEntriesByDir` + full re-insert: the caller
388
- * upserts the current file set FIRST (ON CONFLICT preserving `entries.id`, so
366
+ * upserts the current file set first (ON CONFLICT preserving `entries.id`, so
389
367
  * embeddings / utility / usage stay attached to unchanged rows), then calls this
390
- * to prune only the DEPARTED rows. The net row-state of `dir_path` is identical
368
+ * to prune only the departed rows. The net row-state for the directory is identical
391
369
  * to delete-then-reinsert; the win is that unchanged rows keep their id.
392
370
  *
393
- * Keyed on `entry_key` because it mirrors the directory upsert key precisely.
394
- * Both directory- and source-scoped so overlapping source roots cannot prune
395
- * one another's rows.
371
+ * Both directory- and bundle-scoped so overlapping physical roots cannot
372
+ * prune one another's rows.
396
373
  */
397
- export function deleteEntriesByDirExceptKeys(db, dirPath, stashDir, keepKeys, options = {}) {
374
+ export function deleteEntriesByDirExceptRefs(db, dirPath, bundleId, keepRefs, options = {}) {
398
375
  return db.transaction(() => {
399
- const rows = db
400
- .prepare("SELECT id, entry_key FROM entries WHERE dir_path = ? AND stash_dir = ?")
401
- .all(dirPath, stashDir);
402
- const doomed = rows.filter((r) => !keepKeys.has(r.entry_key));
403
- if (doomed.length === 0)
404
- return [];
405
- deleteRelatedRows(db, doomed, options);
406
- for (let i = 0; i < doomed.length; i += SQLITE_CHUNK_SIZE) {
407
- const chunk = doomed.slice(i, i + SQLITE_CHUNK_SIZE);
408
- const placeholders = chunk.map(() => "?").join(",");
409
- db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk.map((r) => r.id));
410
- }
411
- return doomed.map((row) => row.id);
376
+ const doomed = rowsInDirectory(db, dirPath, bundleId).filter((row) => !keepRefs.has(row.item_ref));
377
+ return deleteEntryRows(db, doomed, options);
412
378
  })();
413
379
  }
414
380
  function deleteRelatedRows(db, ids, options = {}) {
@@ -437,43 +403,40 @@ function deleteRelatedRows(db, ids, options = {}) {
437
403
  bestEffort(() => db.prepare(`DELETE FROM utility_scores WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores for entries");
438
404
  bestEffort(() => db.prepare(`DELETE FROM utility_scores_scoped WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores_scoped for entries");
439
405
  }
440
- // usage_events lives in state.db, outside this transaction. Index persistence
441
- // disables this cleanup and runs it only after its index.db transaction
442
- // commits; standalone delete callers retain the immediate behavior.
443
- if (options.cleanupUsageEvents !== false)
444
- deleteUsageEventsByEntryIds(numericIds);
445
- // #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
446
- // entries row must NOT wipe the extracted graph (that is the whole point —
447
- // the graph survives a reindex when body_hash is unchanged). We therefore do
448
- // NOT delete graph_files here. We DO, however, recompute graph_meta counts
449
- // for the stash roots touched by the deleted entries so the summary numbers
450
- // stay consistent with the live child rows (the counts are derived, and the
451
- // entries delete may have changed which files are considered/indexed).
452
- //
453
- // Resolve the affected stash roots from the entries rows BEFORE deletion.
454
- const affectedStashRoots = new Set();
406
+ // Graph rows are independently keyed and intentionally survive entry
407
+ // deletion. Resolve their owning roots through the canonical physical path
408
+ // before entries disappear, then refresh the derived summary counts.
409
+ const affectedGraphRoots = new Set();
455
410
  for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
456
411
  const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
457
412
  const placeholders = chunk.map(() => "?").join(",");
458
413
  bestEffort(() => {
459
414
  const rows = db
460
- .prepare(`SELECT DISTINCT stash_dir FROM entries WHERE id IN (${placeholders})`)
415
+ .prepare(`SELECT DISTINCT gf.stash_root
416
+ FROM graph_files gf
417
+ JOIN entries e ON e.file_path = gf.file_path
418
+ WHERE e.id IN (${placeholders})`)
461
419
  .all(...chunk);
462
- for (const row of rows) {
463
- if (row.stash_dir)
464
- affectedStashRoots.add(row.stash_dir);
465
- }
466
- }, "resolve stash roots for graph_meta recompute");
420
+ for (const row of rows)
421
+ affectedGraphRoots.add(row.stash_root);
422
+ }, "resolve graph roots for graph_meta recompute");
467
423
  }
468
- for (const stashRoot of affectedStashRoots) {
424
+ for (const stashRoot of affectedGraphRoots) {
469
425
  bestEffort(() => db
470
426
  .prepare(`UPDATE graph_meta
471
- SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
472
- entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
473
- relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
474
- WHERE stash_root = ?`)
427
+ SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
428
+ entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
429
+ relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
430
+ WHERE stash_root = ?`)
475
431
  .run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
476
432
  }
433
+ // usage_events lives in state.db, outside this transaction. Index persistence
434
+ // disables this cleanup and runs it only after its index.db transaction
435
+ // commits; standalone delete callers retain the immediate behavior.
436
+ if (options.cleanupUsageEvents !== false)
437
+ deleteUsageEventsByEntryIds(numericIds);
438
+ // graph_files is keyed by its own stash_root/file_path/body_hash identity,
439
+ // so deleting an entry row intentionally leaves extracted graph data intact.
477
440
  }
478
441
  export function deleteUsageEventsByEntryIds(entryIds) {
479
442
  if (entryIds.length === 0 || !fs.existsSync(getStateDbPath()))
@@ -525,11 +488,11 @@ export function getAllEntries(db, entryType, excludeTypes) {
525
488
  // list skips the clause (never `NOT IN ()`).
526
489
  const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
527
490
  if (entryType && entryType !== "any") {
528
- sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type = ?`;
491
+ sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE type = ?`;
529
492
  params = [entryType];
530
493
  }
531
494
  else if (excludes.length > 0) {
532
- sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type NOT IN (${excludes.map(() => "?").join(", ")})`;
495
+ sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE type NOT IN (${excludes.map(() => "?").join(", ")})`;
533
496
  params = [...excludes];
534
497
  }
535
498
  else {
@@ -542,11 +505,10 @@ export function getAllEntries(db, entryType, excludeTypes) {
542
505
  /**
543
506
  * Resolve a single `entries.id` from a new-grammar `[bundle//]conceptId` ref,
544
507
  * keying on the canonical stored `item_ref` (ref-grammar decision D-R1/D-R4).
545
- * The optional `stashDir` scopes the
546
- * match to one source root.
508
+ * The optional `bundleId` scopes the match to one indexed bundle.
547
509
  */
548
- export function findEntryIdByRef(db, ref, stashDir) {
549
- return findEntryIdByBundleRef(db, ref, stashDir);
510
+ export function findEntryIdByRef(db, ref, bundleId) {
511
+ return findEntryIdByBundleRef(db, ref, bundleId);
550
512
  }
551
513
  /** `name` plus its `.md`-toggled sibling — the markdown ext-keep/strip ambiguity. */
552
514
  function withMdVariants(name) {
@@ -556,12 +518,12 @@ function withMdVariants(name) {
556
518
  * Current (`[bundle//]conceptId`) id lookup: match `item_ref` exactly when
557
519
  * bundle-qualified or by `//conceptId` suffix when short.
558
520
  */
559
- function findEntryIdByBundleRef(db, ref, stashDir) {
521
+ function findEntryIdByBundleRef(db, ref, bundleId) {
560
522
  const parsed = parseBundleRef(ref);
561
523
  const conceptVariants = withMdVariants(parsed.conceptId);
562
524
  // item_ref is the canonical stored spelling post-flip.
563
525
  for (const conceptId of conceptVariants) {
564
- const id = matchIdByItemRef(db, parsed.bundle, conceptId, stashDir);
526
+ const id = matchIdByItemRef(db, parsed.bundle, conceptId, bundleId);
565
527
  if (id !== undefined)
566
528
  return id;
567
529
  }
@@ -591,24 +553,23 @@ function findEntryIdByBundleRef(db, ref, stashDir) {
591
553
  * `resolveRef`'s job; this DB helper takes no config handle). The exact-bundle
592
554
  * arm is single-row under the UNIQUE `item_ref` index.
593
555
  */
594
- function matchIdByItemRef(db, bundle, conceptId, stashDir) {
595
- const scope = stashDir ? "AND stash_dir = ?" : "";
556
+ function matchIdByItemRef(db, bundle, conceptId, bundleId) {
557
+ const scope = bundleId ? "AND bundle_id = ?" : "";
596
558
  if (bundle !== undefined) {
597
559
  const itemRef = `${bundle}//${conceptId}`;
598
560
  const row = db
599
561
  .prepare(`SELECT id FROM entries WHERE item_ref = ? ${scope} ORDER BY id ASC LIMIT 1`)
600
- .get(itemRef, ...(stashDir ? [stashDir] : []));
562
+ .get(itemRef, ...(bundleId ? [bundleId] : []));
601
563
  return row?.id;
602
564
  }
603
565
  const suffix = `//${conceptId}`;
604
566
  const row = db
605
567
  .prepare(`SELECT id FROM entries
606
- WHERE item_ref IS NOT NULL
607
- AND substr(item_ref, length(item_ref) - length(?) + 1) = ?
568
+ WHERE substr(item_ref, length(item_ref) - length(?) + 1) = ?
608
569
  ${scope}
609
570
  ORDER BY id ASC
610
571
  LIMIT 1`)
611
- .get(suffix, suffix, ...(stashDir ? [stashDir] : []));
572
+ .get(suffix, suffix, ...(bundleId ? [bundleId] : []));
612
573
  return row?.id;
613
574
  }
614
575
  export function getEntryCount(db) {
@@ -616,15 +577,12 @@ export function getEntryCount(db) {
616
577
  return row.cnt;
617
578
  }
618
579
  /**
619
- * Per-asset-type entry counts (keyed by `entry_type`, e.g. "skill",
580
+ * Per-asset-type entry counts (keyed by `type`, e.g. "skill",
620
581
  * "knowledge", "memory"). Used by `akm info` to break down the aggregate
621
- * `indexStats.entryCount` (R-057). Rows with a null/empty `entry_type` are
622
- * omitted rather than surfaced under a synthetic key.
582
+ * `indexStats.entryCount` (R-057).
623
583
  */
624
584
  export function getEntryCountByType(db) {
625
- const rows = db
626
- .prepare("SELECT entry_type AS type, COUNT(*) AS cnt FROM entries WHERE entry_type IS NOT NULL GROUP BY entry_type")
627
- .all();
585
+ const rows = db.prepare("SELECT type, COUNT(*) AS cnt FROM entries GROUP BY type").all();
628
586
  const out = {};
629
587
  for (const row of rows) {
630
588
  out[row.type] = row.cnt;
@@ -636,41 +594,44 @@ export function getEmbeddableEntryCount(db) {
636
594
  }
637
595
  export function getEntryById(db, id) {
638
596
  const row = db
639
- .prepare("SELECT file_path, stash_dir, entry_json, item_ref, bundle_id, concept_id FROM entries WHERE id = ?")
597
+ .prepare("SELECT file_path, document_json, item_ref, bundle_id, component_id, concept_id, adapter_id FROM entries WHERE id = ?")
640
598
  .get(id);
641
599
  if (!row)
642
600
  return undefined;
643
601
  // Guard against corrupt JSON
644
602
  let entry;
645
603
  try {
646
- entry = JSON.parse(row.entry_json);
604
+ entry = JSON.parse(row.document_json);
647
605
  }
648
606
  catch {
649
- warn(`[db] getEntryById: skipping entry id=${id} — corrupt entry_json`);
607
+ warn(`[db] getEntryById: skipping entry id=${id} — corrupt document_json`);
650
608
  return undefined;
651
609
  }
652
610
  return {
653
611
  filePath: row.file_path,
654
- stashDir: row.stash_dir,
655
612
  entry,
656
613
  itemRef: row.item_ref,
657
614
  bundleId: row.bundle_id,
615
+ componentId: row.component_id,
658
616
  conceptId: row.concept_id,
617
+ adapterId: row.adapter_id,
659
618
  };
660
619
  }
661
620
  export function getEntriesByDir(db, dirPath) {
662
- const rows = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries WHERE dir_path = ?`).all(dirPath);
621
+ const ids = new Set(rowsInDirectory(db, dirPath).map((row) => row.id));
622
+ const rows = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries`).all().filter((row) => ids.has(row.id));
663
623
  return parseEntryRows(rows, "getEntriesByDir");
664
624
  }
665
- /** Return every directory previously indexed for one physical source root. */
666
- export function getIndexedDirPathsByStashDir(db, stashDir) {
667
- const rows = db.prepare("SELECT DISTINCT dir_path FROM entries WHERE stash_dir = ?").all(stashDir);
668
- return rows.map((row) => row.dir_path);
625
+ /** Return every directory previously indexed for one canonical bundle. */
626
+ export function getIndexedDirPathsByBundleId(db, bundleId) {
627
+ const rows = db.prepare("SELECT file_path FROM entries WHERE bundle_id = ?").all(bundleId);
628
+ return [...new Set(rows.map((row) => path.dirname(row.file_path)))];
669
629
  }
670
- /** Return every persisted source owner for one physical directory. */
671
- export function getIndexedStashDirsByDir(db, dirPath) {
672
- const rows = db.prepare("SELECT DISTINCT stash_dir FROM entries WHERE dir_path = ?").all(dirPath);
673
- return rows.map((row) => row.stash_dir);
630
+ /** Return every persisted bundle owner for one physical directory. */
631
+ export function getIndexedBundleIdsByDir(db, dirPath) {
632
+ const ids = new Set(rowsInDirectory(db, dirPath).map((row) => row.id));
633
+ const rows = db.prepare("SELECT id, bundle_id FROM entries").all();
634
+ return [...new Set(rows.filter((row) => ids.has(row.id)).map((row) => row.bundle_id))];
674
635
  }
675
636
  /**
676
637
  * Resolve a single `entries.id` by exact `file_path` (the canonical on-disk
@@ -704,7 +665,7 @@ export function getIndexedFilePaths(db) {
704
665
  *
705
666
  * Lifted verbatim (WS5) from the inline `SELECT file_path FROM entries WHERE
706
667
  * id = ?` in commands/feedback-cli.ts. Unlike {@link getEntryById}, this does
707
- * NOT parse `entry_json`, so a row with corrupt JSON still yields its path —
668
+ * NOT parse `document_json`, so a row with corrupt JSON still yields its path —
708
669
  * preserving feedback-cli's pre-extraction behaviour byte-for-byte.
709
670
  */
710
671
  export function getEntryFilePathById(db, id) {
@@ -712,8 +673,9 @@ export function getEntryFilePathById(db, id) {
712
673
  return row?.file_path;
713
674
  }
714
675
  /**
715
- * Fetch every `(file_path, entry_json)` row whose entry belongs to a given
716
- * stash root matched either by exact `stash_dir` OR by `file_path` prefix.
676
+ * Fetch every `(file_path, document_json)` row whose path is under a source
677
+ * root. The path containment filter is applied in JS so SQLite LIKE wildcard
678
+ * characters in filesystem paths cannot widen ownership.
717
679
  *
718
680
  * Lifted verbatim (WS5) from the inline query in commands/graph.ts'
719
681
  * `buildRefByPath`. The full result set is materialised with `.all()` before
@@ -721,27 +683,13 @@ export function getEntryFilePathById(db, id) {
721
683
  * connection-lifetime rule). JSON parsing stays with the caller, unchanged.
722
684
  */
723
685
  export function getEntryRefRowsForStashRoot(db, stashRoot) {
724
- return db
725
- .prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
726
- .all(stashRoot, `${stashRoot}%`);
686
+ const root = path.resolve(stashRoot);
687
+ return db.prepare("SELECT file_path, document_json FROM entries").all().filter((row) => {
688
+ const file = path.resolve(row.file_path);
689
+ return file === root || file.startsWith(`${root}${path.sep}`);
690
+ });
727
691
  }
728
692
  // ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
729
- /**
730
- * Upsert a workflow document record for an indexed entry.
731
- * Persists the parsed workflow AST as JSON alongside a FNV-1a hash of the
732
- * source content for future incremental fast-paths.
733
- */
734
- export function upsertWorkflowDocument(db, entryId, doc, content) {
735
- const sourceHash = computeSourceHash(content);
736
- db.prepare(`INSERT INTO workflow_documents (entry_id, schema_version, document_json, source_path, source_hash, updated_at)
737
- VALUES (?, ?, ?, ?, ?, ?)
738
- ON CONFLICT(entry_id) DO UPDATE SET
739
- schema_version = excluded.schema_version,
740
- document_json = excluded.document_json,
741
- source_path = excluded.source_path,
742
- source_hash = excluded.source_hash,
743
- updated_at = excluded.updated_at`).run(entryId, doc.schemaVersion, JSON.stringify(doc), doc.source.path, sourceHash, new Date().toISOString());
744
- }
745
693
  /**
746
694
  * Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
747
695
  * Not security-sensitive; used as an incremental fast-path skip key.
@@ -791,8 +739,7 @@ export function getEntryByRef(db, ref) {
791
739
  /**
792
740
  * The fully-qualified `item_ref` (`<bundle>//<conceptId>`, the durable stored
793
741
  * spelling — spec §11.1 D-R3) for an entry `id`, or `null` when the row is gone
794
- * or its `item_ref` is still NULL (a write-back straggler, healed on the next
795
- * full index). The usage-event / salience / feedback writers derive the durable
742
+ * The usage-event / salience / feedback writers derive the durable
796
743
  * key from this so a stored key is always the resolved entry's canonical ref,
797
744
  * never raw input (D-R3: durable keys are never derived from input).
798
745
  */
@@ -823,7 +770,15 @@ function resolveUsageEventEntryId(db, ref) {
823
770
  * distinct linked entry_ids in usage_events is small — and the re-resolution
824
771
  * reads `entries` from `indexDb`.
825
772
  */
826
- export function relinkUsageEvents(indexDb, stateDb, _options = {}) {
773
+ function qualifiedUsageEventsTable(stateSchema) {
774
+ if (stateSchema === undefined)
775
+ return "usage_events";
776
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema))
777
+ throw new Error("Invalid attached state schema name.");
778
+ return `"${stateSchema}".usage_events`;
779
+ }
780
+ export function relinkUsageEvents(indexDb, stateDb, options = {}) {
781
+ const usageEvents = qualifiedUsageEventsTable(options.stateSchema);
827
782
  bestEffort(() => {
828
783
  // Step 1: null out stale entry_ids (entry was deleted, re-keyed, etc).
829
784
  // Leaving them in place would let `recomputeUtilityScores` aggregate by an
@@ -831,23 +786,28 @@ export function relinkUsageEvents(indexDb, stateDb, _options = {}) {
831
786
  // on the utility_scores INSERT and roll back the entire finalize
832
787
  // transaction. Nulled rows can be re-resolved by step 2 below; events whose
833
788
  // entry is permanently gone simply stay null and age out via retention.
834
- const linkedIds = stateDb.prepare("SELECT DISTINCT entry_id AS id FROM usage_events WHERE entry_id IS NOT NULL").all().map((r) => r.id);
835
- const entryExists = indexDb.prepare("SELECT 1 FROM entries WHERE id = ?");
836
- const staleIds = linkedIds.filter((id) => entryExists.get(id) == null);
837
- if (staleIds.length > 0) {
838
- const nullOut = stateDb.prepare("UPDATE usage_events SET entry_id = NULL WHERE entry_id = ?");
789
+ const linkedRows = stateDb
790
+ .prepare(`SELECT DISTINCT entry_id AS id, entry_ref AS ref FROM ${usageEvents} WHERE entry_id IS NOT NULL`)
791
+ .all();
792
+ const entryIdentity = indexDb.prepare("SELECT item_ref AS itemRef FROM entries WHERE id = ?");
793
+ const staleLinks = linkedRows.filter(({ id, ref }) => {
794
+ const live = entryIdentity.get(id);
795
+ return live == null || (ref !== null && live.itemRef !== ref);
796
+ });
797
+ if (staleLinks.length > 0) {
798
+ const nullOut = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = NULL WHERE entry_id = ? AND entry_ref IS ?`);
839
799
  const nullTx = stateDb.transaction(() => {
840
- for (const id of staleIds)
841
- nullOut.run(id);
800
+ for (const { id, ref } of staleLinks)
801
+ nullOut.run(id, ref);
842
802
  });
843
803
  nullTx();
844
804
  }
845
805
  // Step 2: re-resolve each fully-qualified ref. Bare rows are not current
846
806
  // durable identities and remain detached.
847
807
  const refs = stateDb
848
- .prepare("SELECT DISTINCT entry_ref AS ref FROM usage_events WHERE entry_id IS NULL AND entry_ref IS NOT NULL")
808
+ .prepare(`SELECT DISTINCT entry_ref AS ref FROM ${usageEvents} WHERE entry_id IS NULL AND entry_ref IS NOT NULL`)
849
809
  .all();
850
- const update = stateDb.prepare("UPDATE usage_events SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL");
810
+ const update = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL`);
851
811
  const relinkTx = stateDb.transaction(() => {
852
812
  for (const { ref } of refs) {
853
813
  let id;