akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -1,432 +0,0 @@
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
- * R5 — Longitudinal collapse detector
6
- * (docs/architecture/specs/improve-collapse-churn-detector-design.md).
7
- *
8
- * Detects the measured collapse failure mode of LLM-consolidated memory
9
- * stores: repeated merges destroy information — canary retrieval recall
10
- * downtrends, distinct-content entropy downtrends, or the store shrinks
11
- * while generation counts rise.
12
- *
13
- * The CHURN alert class described in the design doc was removed: production
14
- * never threaded a real accepted-change-volume signal in (the value was a
15
- * hardcoded 0 from the 0.9.0 confidence-gate deletion onward), so the alert
16
- * could never fire. See the design doc's status note for the removal
17
- * rationale. `improve_cycle_metrics.accepted_actions` stays in the schema and
18
- * is written as 0 because deployed `state.db` files already contain it and
19
- * removing it would be a destructive schema change.
20
- *
21
- * Hard invariants: deterministic only (FTS BM25 + hashing — never an LLM,
22
- * never an embedding model); bounded storage (< 2 KB per qualifying cycle,
23
- * 365-day retention); fail-open (an error warns and skips, never breaks an
24
- * improve run); runs only on cycles where consolidate did work.
25
- *
26
- * Observe-only in v1: alerts land in `improve_cycle_metrics.alerts_json`, the
27
- * events log (`collapse_detector_alert`), and the `akm health` advisory —
28
- * nothing is ever blocked.
29
- *
30
- * @module collapse-detector
31
- */
32
- import { randomBytes } from "node:crypto";
33
- import { parseBundleRef } from "../../core/asset/asset-ref.js";
34
- import { conceptIdFromTypeName } from "../../core/asset/resolve-ref.js";
35
- import { getImproveProcessConfig } from "../../core/config/config.js";
36
- import { appendEvent } from "../../core/events.js";
37
- import { withStateDb } from "../../core/state-db.js";
38
- import { warn } from "../../core/warn.js";
39
- import { deactivateCanarySet, getActiveCanaries, getCanariesBySetId, insertCanaries, insertCycleMetrics, listActiveCanarySetIds, queryRecentCycleMetrics, } from "../../storage/repositories/canaries-repository.js";
40
- import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
41
- import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
42
- import { searchFts } from "../../storage/repositories/index-fts-repository.js";
43
- import { computeBigramDiversity, DEFAULT_MAX_GENERATION } from "./anti-collapse.js";
44
- import { getAllRankScores } from "./salience.js";
45
- // ── Defaults (mirrored in config-schema.ts ImproveCollapseDetectorSchema) ────
46
- export const DEFAULT_CANARY_COUNT = 40; // owner-approved 30–50 range
47
- export const DEFAULT_CANARY_K = 10;
48
- export const DEFAULT_WINDOW_CYCLES = 5;
49
- export const DEFAULT_RECALL_DROP_THRESHOLD = 0.15;
50
- export const DEFAULT_ENTROPY_DROP_THRESHOLD = 0.05;
51
- export const DEFAULT_RETENTION_DAYS = 365;
52
- /** Deterministic bigram-diversity sample cap (cost bound at 10k assets). */
53
- const DIVERSITY_SAMPLE_CAP = 2000;
54
- /**
55
- * Minimum merge-floor violations in one cycle before the advisory alert fires.
56
- * The specificity floor is deliberately strict (Phase-1 tuning pending), so a
57
- * couple of borderline merges per cycle must not flip `akm health` to warn —
58
- * that alert fatigue would drown the real collapse signals.
59
- */
60
- const MERGE_FLOOR_ALERT_MIN = 3;
61
- /** The learning-store types the detector measures. */
62
- const LEARNING_TYPES = new Set(["memory", "lesson", "knowledge"]);
63
- // ── Canary set ────────────────────────────────────────────────────────────────
64
- /** Deterministic query string for one anchor entry: name tokens + top tags + description head. */
65
- function buildCanaryQuery(entry) {
66
- const nameTokens = entry.entry.name.split(/[-_/.]+/).filter((t) => t.length > 1);
67
- const tags = (entry.entry.tags ?? []).slice(0, 3);
68
- const descriptionHead = (entry.entry.description ?? "").split(/\s+/).slice(0, 6);
69
- const parts = [...nameTokens, ...tags, ...descriptionHead].filter((t) => t.length > 0);
70
- return [...new Set(parts)].join(" ");
71
- }
72
- /** Build the mint candidate list (deterministic given index + salience tables). */
73
- function buildMintList(stateDb, entries, cfg) {
74
- const canaryCount = cfg.canaryCount ?? DEFAULT_CANARY_COUNT;
75
- const rankScores = getAllRankScores(stateDb);
76
- // Chunk-8 WI-8.5c: the canary anchor + salience correlation both key on the
77
- // SHORT conceptId (`<stash-subdir>/<name>`, D-R2) — the same spelling the
78
- // improve candidate refs and content xrefs now carry (post WI-8.5a).
79
- const candidates = entries
80
- .filter((e) => LEARNING_TYPES.has(e.entry.type))
81
- .map((e) => {
82
- const ref = conceptIdFromTypeName(e.entry.type, e.entry.name);
83
- return { e, ref, score: rankScores.get(ref) ?? 0 };
84
- })
85
- .sort((a, b) => b.score - a.score || (a.ref < b.ref ? -1 : 1));
86
- // Type-stratified top slice: ⅓ per learning type, backfill from global order.
87
- const perType = Math.ceil(canaryCount / 3);
88
- const picked = new Map();
89
- for (const type of LEARNING_TYPES) {
90
- let taken = 0;
91
- for (const c of candidates) {
92
- if (taken >= perType || picked.size >= canaryCount)
93
- break;
94
- if (c.e.entry.type === type && !picked.has(c.ref)) {
95
- picked.set(c.ref, c);
96
- taken++;
97
- }
98
- }
99
- }
100
- for (const c of candidates) {
101
- if (picked.size >= canaryCount)
102
- break;
103
- if (!picked.has(c.ref))
104
- picked.set(c.ref, c);
105
- }
106
- return [...picked.values()]
107
- .map((c) => ({ anchorRef: c.ref, query: buildCanaryQuery(c.e) }))
108
- .filter((c) => c.query.length > 0);
109
- }
110
- /** Collision-safe mint token (same-millisecond mints happen in tests + concurrent runs). */
111
- function newCanarySetId() {
112
- return `canary-${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
113
- }
114
- /**
115
- * Mint (or return) the active canary set. Deterministic given the index +
116
- * salience tables: rank the three learning types by `asset_salience.rank_score`
117
- * (fallback 0, tie-broken by ref), take a type-stratified top slice
118
- * (⅓ per type, backfilled from the global ranking when a type is short).
119
- *
120
- * Returns `null` when the index has no mintable learning entries — a cycle
121
- * with no canary set is NOT recorded (a fresh unused set id every cycle would
122
- * mean the trend window never fills and recall reads as a fake 0).
123
- *
124
- * NEVER auto-refreshes: once minted the set is frozen until an explicit
125
- * `bun scripts/refresh-canary-set.ts --refresh` — silent re-baselining is how
126
- * a slow collapse hides. Rows are read back BY OUR OWN set id (never "newest
127
- * active") so a concurrent mint in another process cannot relabel this run's
128
- * metrics.
129
- */
130
- export function ensureCanarySet(stateDb, indexDb, cfg, preloadedEntries) {
131
- const existing = getActiveCanaries(stateDb);
132
- if (existing.length > 0) {
133
- return { canarySetId: existing[0].canary_set_id, canaries: existing };
134
- }
135
- const minted = buildMintList(stateDb, preloadedEntries ?? getAllEntries(indexDb), cfg);
136
- if (minted.length === 0)
137
- return null;
138
- const canarySetId = newCanarySetId();
139
- insertCanaries(stateDb, canarySetId, minted);
140
- return { canarySetId, canaries: getCanariesBySetId(stateDb, canarySetId) };
141
- }
142
- /**
143
- * Explicit canary re-mint (the ONLY refresh path —
144
- * `bun scripts/refresh-canary-set.ts --refresh`). Mint-first, deactivate-after:
145
- * when the index is empty or unreadable the current baseline is left untouched
146
- * instead of destroyed. Deactivates ALL other active sets (not just the
147
- * newest) so stragglers from an interrupted refresh can never resurrect.
148
- */
149
- export function refreshCanarySet(stateDb, indexDb, cfg) {
150
- const minted = buildMintList(stateDb, getAllEntries(indexDb), cfg);
151
- if (minted.length === 0)
152
- return null; // nothing mintable — keep the old baseline
153
- const canarySetId = newCanarySetId();
154
- insertCanaries(stateDb, canarySetId, minted);
155
- for (const oldSetId of listActiveCanarySetIds(stateDb)) {
156
- if (oldSetId !== canarySetId)
157
- deactivateCanarySet(stateDb, oldSetId);
158
- }
159
- return { canarySetId, canaries: getCanariesBySetId(stateDb, canarySetId) };
160
- }
161
- // ── Cycle metrics ─────────────────────────────────────────────────────────────
162
- /**
163
- * Name-free content fingerprint text for entropy metrics. The indexed
164
- * search_text EMBEDS the (unique) entry name, which would pin the
165
- * distinct-content ratio at 1.0 forever; convergence shows up in the
166
- * description/tags/heading fields, so those are what get hashed. Bounded body
167
- * content is searchable but deliberately excluded from this fingerprint: the
168
- * detector measures the structured surface generic merged assets converge on.
169
- */
170
- function contentFingerprint(entry) {
171
- const parts = [entry.description ?? "", (entry.tags ?? []).join(" "), (entry.toc ?? []).map((h) => h.text).join(" ")];
172
- return parts.filter((t) => t.length > 0).join(" ");
173
- }
174
- /** FNV-1a 64-bit over lowercased whitespace-collapsed text (distinct-content hashing). */
175
- export function normHash(text) {
176
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
177
- let hash = 0xcbf29ce484222325n;
178
- const prime = 0x100000001b3n;
179
- for (let i = 0; i < normalized.length; i++) {
180
- hash ^= BigInt(normalized.charCodeAt(i));
181
- hash = (hash * prime) & 0xffffffffffffffffn;
182
- }
183
- return hash.toString(16);
184
- }
185
- /**
186
- * Score one canary against the live index, merge-following via canonical
187
- * `xrefs`. A hit is the anchor ref itself or any returned entry whose
188
- * provenance contains the anchor.
189
- * Returns the 0-based rank of the first hit, or -1.
190
- */
191
- function scoreCanary(indexDb, canary, k) {
192
- const results = searchFts(indexDb, canary.query, k);
193
- const anchorConceptId = canaryConceptId(canary.anchor_ref);
194
- for (let i = 0; i < Math.min(results.length, k); i++) {
195
- const r = results[i];
196
- // Persisted canaries may be fully qualified after a state migration while
197
- // index refs and xrefs remain short conceptIds. Compare identity, not display
198
- // spelling, so migration alone cannot collapse recall to zero.
199
- const ref = conceptIdFromTypeName(r.entry.type, r.entry.name);
200
- if (ref === anchorConceptId)
201
- return i;
202
- const provenance = r.entry.xrefs ?? [];
203
- if (provenance.some((sourceRef) => canaryConceptId(sourceRef) === anchorConceptId))
204
- return i;
205
- }
206
- return -1;
207
- }
208
- function canaryConceptId(ref) {
209
- try {
210
- return parseBundleRef(ref).conceptId;
211
- }
212
- catch {
213
- return ref;
214
- }
215
- }
216
- /**
217
- * Compute one qualifying cycle's store-health snapshot. One `entries` scan +
218
- * `canaryCount` FTS queries; no LLM, no embedding model, no filesystem reads.
219
- * Returns `null` when no canary set exists AND none is mintable (empty index)
220
- * — such a cycle is not measurable and must not be recorded.
221
- */
222
- export function computeCycleMetrics(stateDb, indexDb, args) {
223
- const k = args.cfg.k ?? DEFAULT_CANARY_K;
224
- const maxGeneration = args.maxGeneration ?? DEFAULT_MAX_GENERATION;
225
- // Single entries scan — shared by the canary mint (if one is needed) and
226
- // the store-shape metrics below.
227
- const all = getAllEntries(indexDb);
228
- const canarySet = ensureCanarySet(stateDb, indexDb, args.cfg, all);
229
- if (canarySet === null)
230
- return null;
231
- const { canarySetId, canaries } = canarySet;
232
- // ── Canary retrieval metrics ───────────────────────────────────────────────
233
- const ranks = [];
234
- let recallSum = 0;
235
- let ndcgSum = 0;
236
- let mrrSum = 0;
237
- for (const canary of canaries) {
238
- const rank = scoreCanary(indexDb, canary, k);
239
- ranks.push([canary.id, rank]);
240
- if (rank >= 0) {
241
- recallSum += 1;
242
- mrrSum += 1 / (rank + 1);
243
- // Single-relevant nDCG@k closed form: ideal DCG is 1, so the score is
244
- // just the discount at the hit rank.
245
- ndcgSum += 1 / Math.log2(rank + 2);
246
- }
247
- }
248
- const n = Math.max(1, canaries.length);
249
- // ── Store-shape metrics (same single entries scan) ────────────────────────
250
- const byType = new Map();
251
- const contentHashes = new Set();
252
- let learningTotal = 0;
253
- let overGeneration = 0;
254
- const learningTexts = [];
255
- for (const e of all) {
256
- byType.set(e.entry.type, (byType.get(e.entry.type) ?? 0) + 1);
257
- if (!LEARNING_TYPES.has(e.entry.type))
258
- continue;
259
- learningTotal++;
260
- const fingerprint = contentFingerprint(e.entry);
261
- contentHashes.add(normHash(fingerprint));
262
- if ((e.entry.generation ?? 0) > maxGeneration)
263
- overGeneration++;
264
- learningTexts.push({ key: e.itemRef, text: fingerprint });
265
- }
266
- // Deterministic diversity sample: sort by item ref, take every ⌈N/cap⌉-th row.
267
- learningTexts.sort((a, b) => (a.key < b.key ? -1 : 1));
268
- const step = Math.max(1, Math.ceil(learningTexts.length / DIVERSITY_SAMPLE_CAP));
269
- let diversitySum = 0;
270
- let diversityCount = 0;
271
- for (let i = 0; i < learningTexts.length; i += step) {
272
- diversitySum += computeBigramDiversity(learningTexts[i].text);
273
- diversityCount++;
274
- }
275
- return {
276
- run_id: args.runId,
277
- ts: (args.now ?? new Date()).toISOString(),
278
- pass: args.pass,
279
- canary_set_id: canarySetId,
280
- mean_recall: recallSum / n,
281
- mean_ndcg: ndcgSum / n,
282
- mean_mrr: mrrSum / n,
283
- canary_ranks_json: JSON.stringify(ranks),
284
- store_total: learningTotal,
285
- store_by_type_json: JSON.stringify(Object.fromEntries([...byType.entries()].sort())),
286
- distinct_content_ratio: learningTotal === 0 ? 1 : contentHashes.size / learningTotal,
287
- mean_bigram_diversity: diversityCount === 0 ? 1 : diversitySum / diversityCount,
288
- over_generation_count: overGeneration,
289
- // Always 0 — the CHURN alert this fed was removed. The column is NOT NULL
290
- // in a released migration body, so it is written rather than dropped.
291
- accepted_actions: 0,
292
- merge_floor_violations: args.mergeFloorViolations,
293
- alerts_json: "[]",
294
- };
295
- }
296
- // ── Alert evaluation (pure) ───────────────────────────────────────────────────
297
- function median(values) {
298
- if (values.length === 0)
299
- return Number.NaN;
300
- const sorted = [...values].sort((a, b) => a - b);
301
- const mid = Math.floor(sorted.length / 2);
302
- return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
303
- }
304
- /**
305
- * Evaluate the §1 alert definitions. PURE — history rows (oldest-first, NOT
306
- * including `current`) plus the current row in, alerts out. A window shorter
307
- * than `windowCycles` never fires (no baseline yet); the merge-floor advisory
308
- * is per-cycle and fires regardless of window depth.
309
- */
310
- export function evaluateCollapseAlerts(history, current, cfg) {
311
- const alerts = [];
312
- // MERGE-FLOOR advisory: per-cycle, window-independent. Gated on a minimum
313
- // count — the specificity floor is deliberately strict pre-tuning, and one
314
- // or two borderline merges per cycle must not generate alert fatigue.
315
- if (current.merge_floor_violations >= MERGE_FLOOR_ALERT_MIN) {
316
- alerts.push({
317
- kind: "merge-floor",
318
- detail: `${current.merge_floor_violations} merge(s) failed the information floor this cycle (provenance shrank or specificity below threshold)`,
319
- metrics: { mergeFloorViolations: current.merge_floor_violations },
320
- });
321
- }
322
- const W = cfg.windowCycles ?? DEFAULT_WINDOW_CYCLES;
323
- const hist = history.slice(-W);
324
- if (hist.length < W)
325
- return alerts; // no baseline yet
326
- // Window is non-empty here: `windowCycles` is schema-bounded to ≥2 (default 5),
327
- // so `hist.length >= W >= 2`. `windowStart` is the oldest row in the window.
328
- const windowStart = hist[0];
329
- const recallDrop = cfg.recallDropThreshold ?? DEFAULT_RECALL_DROP_THRESHOLD;
330
- const entropyDrop = cfg.entropyDropThreshold ?? DEFAULT_ENTROPY_DROP_THRESHOLD;
331
- // COLLAPSE 1 — canary recall drop vs window median (median, not previous
332
- // cycle, so one noisy cycle can neither fire nor mask the alert).
333
- const medianRecall = median(hist.map((h) => h.mean_recall));
334
- if (current.mean_recall <= medianRecall - recallDrop) {
335
- alerts.push({
336
- kind: "collapse-recall",
337
- detail: `mean canary recall ${current.mean_recall.toFixed(3)} dropped ≥${recallDrop} below the ${W}-cycle median ${medianRecall.toFixed(3)}`,
338
- metrics: { currentRecall: current.mean_recall, medianRecall, threshold: recallDrop },
339
- });
340
- }
341
- // COLLAPSE 2 — monotonic distinct-content-ratio decline over the window.
342
- const series = [...hist.map((h) => h.distinct_content_ratio), current.distinct_content_ratio];
343
- const monotonicNonIncreasing = series.every((v, i) => i === 0 || v <= series[i - 1]);
344
- const totalDecline = windowStart.distinct_content_ratio - current.distinct_content_ratio;
345
- if (monotonicNonIncreasing && totalDecline >= entropyDrop) {
346
- alerts.push({
347
- kind: "collapse-entropy",
348
- detail: `distinct-content ratio declined monotonically by ${totalDecline.toFixed(3)} (≥${entropyDrop}) over ${W} cycles — store content is converging`,
349
- metrics: {
350
- windowStart: windowStart.distinct_content_ratio,
351
- current: current.distinct_content_ratio,
352
- decline: totalDecline,
353
- },
354
- });
355
- }
356
- // COLLAPSE 3 — store shrinking BECAUSE of re-merging (not deletion hygiene).
357
- const maxStore = Math.max(...hist.map((h) => h.store_total));
358
- if (current.store_total < 0.8 * maxStore && current.over_generation_count > windowStart.over_generation_count) {
359
- alerts.push({
360
- kind: "collapse-shrink",
361
- detail: `store shrank >20% (${current.store_total} vs window max ${maxStore}) while over-generation count rose (${windowStart.over_generation_count} → ${current.over_generation_count})`,
362
- metrics: {
363
- storeTotal: current.store_total,
364
- windowMax: maxStore,
365
- overGeneration: current.over_generation_count,
366
- },
367
- });
368
- }
369
- return alerts;
370
- }
371
- // ── Orchestrator ─────────────────────────────────────────────────────────────
372
- /**
373
- * Run the detector for one qualifying cycle: ensure canaries → compute →
374
- * evaluate against stored history → persist the row → append one
375
- * `collapse_detector_alert` event per fired alert. FAIL-OPEN: any error warns
376
- * and returns undefined — an improve run is never broken by its own
377
- * instrumentation.
378
- */
379
- export function runCollapseDetector(args) {
380
- const cfg = args.config.improve?.collapseDetector ?? {};
381
- if (cfg.enabled === false)
382
- return undefined;
383
- try {
384
- let indexDb;
385
- try {
386
- indexDb = openExistingDatabase(args.indexDbPath);
387
- const db = indexDb;
388
- // Over-generation threshold mirrors the guard actually in effect —
389
- // reading the same config key keeps the two aligned when tuned.
390
- const antiCollapse = getImproveProcessConfig("consolidate", args.improveProfile)?.antiCollapse;
391
- const maxGeneration = antiCollapse?.maxGeneration ?? DEFAULT_MAX_GENERATION;
392
- return withStateDb((stateDb) => {
393
- const row = computeCycleMetrics(stateDb, db, {
394
- runId: args.runId,
395
- pass: args.pass,
396
- mergeFloorViolations: args.mergeFloorViolations,
397
- cfg,
398
- maxGeneration,
399
- });
400
- if (row === null)
401
- return undefined; // empty index — nothing to measure
402
- const windowCycles = cfg.windowCycles ?? DEFAULT_WINDOW_CYCLES;
403
- const history = queryRecentCycleMetrics(stateDb, row.canary_set_id, windowCycles);
404
- const alerts = evaluateCollapseAlerts(history, row, cfg);
405
- row.alerts_json = JSON.stringify(alerts.map((a) => a.kind));
406
- insertCycleMetrics(stateDb, row);
407
- for (const alert of alerts) {
408
- appendEvent({
409
- eventType: "collapse_detector_alert",
410
- ref: undefined,
411
- metadata: {
412
- kind: alert.kind,
413
- detail: alert.detail,
414
- metrics: alert.metrics,
415
- canarySetId: row.canary_set_id,
416
- runId: args.runId,
417
- },
418
- }, args.eventsCtx);
419
- }
420
- return row;
421
- }, { path: args.eventsCtx?.dbPath, borrowed: args.eventsCtx?.db });
422
- }
423
- finally {
424
- if (indexDb)
425
- closeDatabase(indexDb);
426
- }
427
- }
428
- catch (err) {
429
- warn(`[collapse-detector] skipped (fail-open): ${err instanceof Error ? err.message : String(err)}`);
430
- return undefined;
431
- }
432
- }
@@ -1,48 +0,0 @@
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
- // Eligibility / safety predicates for consolidate: "may we touch this memory?"
5
- // One reason to change — the policy for what consolidate is allowed to act on.
6
- import fs from "node:fs";
7
- import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
8
- import { hasHotCaptureMode } from "../../proposal/validators/proposal-quality-validators.js";
9
- export function isConsolidationEligibleMemoryName(name) {
10
- return !name.endsWith(".derived");
11
- }
12
- /**
13
- * Returns true when the memory file has `captureMode: hot` in its frontmatter.
14
- *
15
- * Hot memories are USER-EXPLICIT (written via `akm remember` on the hot path).
16
- * The consolidate LLM is forbidden from deleting or auto-merging them — the
17
- * user wrote them on purpose and only the user can decide to retire them.
18
- *
19
- * Reads the file once per check; consolidate runs against ~10 memories per
20
- * chunk so the IO cost is trivial. Returns false on any read/parse error
21
- * (fail-safe: an unreadable or unparseable file is treated as HOT — protected
22
- * — because a deletion shield must not fail open; a missing file is not-hot).
23
- *
24
- * Defends against four observed defect classes (see
25
- * `memories/akm-improve-critical-review-2026-05-20`):
26
- * - LLM marks a memory contradicted then deletes (dangling contradictedBy)
27
- * - LLM merges two unrelated memories sharing a topic keyword
28
- * - LLM judges a recent durable design memo as "redundant"
29
- * - Cascade deletes (LLM uses ref:X as `contradictedBy` for ref:Y then deletes both)
30
- */
31
- export function isHotCapturedMemory(filePath) {
32
- if (!fs.existsSync(filePath))
33
- return false;
34
- try {
35
- const content = fs.readFileSync(filePath, "utf8");
36
- const parsed = parseFrontmatter(content);
37
- return hasHotCaptureMode(parsed.data);
38
- }
39
- catch {
40
- // Fail CLOSED. This predicate is a deletion shield: "hot" memories are
41
- // protected from consolidate's merge/delete. Returning false on a read or
42
- // parse failure marked exactly the memories we could not inspect as fair
43
- // game — the one direction a protection check must never fail. A missing
44
- // file stays false (nothing to protect); an unreadable one is protected
45
- // until someone can actually read it.
46
- return true;
47
- }
48
- }
@@ -1,146 +0,0 @@
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
- // R12a: merge/delete/contradict are no longer requested (see
5
- // CONSOLIDATE_PLAN_JSON_SCHEMA / consolidate-system.md) — only promote is
6
- // ever executed. An op of one of those retired shapes — e.g. from a model
7
- // that ignores the schema — is rejected here so it degrades to the generic
8
- // "skipping invalid operation" warning in the chunk-judge loop rather than
9
- // being treated as an actionable (if advisory) plan entry.
10
- export function isValidOp(op) {
11
- if (typeof op !== "object" || op === null)
12
- return false;
13
- const o = op;
14
- if (o.op === "promote") {
15
- return typeof o.ref === "string" && typeof o.knowledgeRef === "string";
16
- }
17
- return false;
18
- }
19
- export function mergePlans(chunks, knownRefs) {
20
- const mergeOps = new Map();
21
- const deleteOps = new Map();
22
- const promoteOps = new Map();
23
- // C-3 / #382: contradict ops keyed by `ref|contradictedByRef` to deduplicate.
24
- const contradictOps = new Map();
25
- const warnings = [];
26
- for (const chunk of chunks) {
27
- for (const op of chunk) {
28
- if (op.op === "merge") {
29
- // Drop ops whose primary the LLM hallucinated (not in the loaded memory
30
- // pool). Without this guard, a hallucinated primary flows all the way to
31
- // Phase B where !memoryByRef.has(primary) fires and charges every real
32
- // secondary with merge_primary_missing — masking LLM hallucinations as
33
- // filter regressions in health metrics.
34
- if (knownRefs && !knownRefs.has(op.primary)) {
35
- warnings.push(`mergePlans: primary ${op.primary} not in loaded memory pool (LLM hallucination) — dropping op before execution.`);
36
- // Use a dedicated skip reason so dashboards can distinguish
37
- // hallucinated primaries from stale-DB regressions.
38
- // Secondaries are real refs; they are NOT charged here — they remain
39
- // available for other ops to claim.
40
- continue;
41
- }
42
- // Filter hallucinated secondaries while preserving real ones.
43
- let mergeOp = op;
44
- if (knownRefs) {
45
- const filteredSecondaries = op.secondaries.filter((sec) => {
46
- if (!knownRefs.has(sec)) {
47
- warnings.push(`mergePlans: secondary ${sec} not in loaded memory pool (LLM hallucination) — dropping from op.`);
48
- return false;
49
- }
50
- return true;
51
- });
52
- if (filteredSecondaries.length !== op.secondaries.length) {
53
- mergeOp = { ...op, secondaries: filteredSecondaries };
54
- }
55
- }
56
- if (mergeOp.secondaries.length > 1) {
57
- warnings.push(`Merge: ${mergeOp.primary} proposed multiple secondaries; retaining one secondary for review.`);
58
- mergeOp = { ...mergeOp, secondaries: mergeOp.secondaries.slice(0, 1) };
59
- }
60
- // merge wins over delete
61
- if (deleteOps.has(mergeOp.primary)) {
62
- deleteOps.delete(mergeOp.primary);
63
- }
64
- for (const sec of mergeOp.secondaries) {
65
- if (deleteOps.has(sec))
66
- deleteOps.delete(sec);
67
- }
68
- mergeOps.set(mergeOp.primary, mergeOp);
69
- }
70
- else if (op.op === "delete") {
71
- // merge and promote both win over delete. A promote is non-destructive
72
- // (creates a proposal) but the source memory is counted in `promoted`;
73
- // if a delete also fires, the ref lands in both `promoted` and
74
- // `skipReasons`, breaking the invariant by +1.
75
- if (!mergeOps.has(op.ref) && !promoteOps.has(op.ref)) {
76
- deleteOps.set(op.ref, op);
77
- }
78
- }
79
- else if (op.op === "promote") {
80
- // C-2 / #381: when both a promote and a merge target the same ref,
81
- // queue the promote FIRST rather than discarding it. The promote op
82
- // routes through createProposal (the human-gated proposal queue), so
83
- // it is non-destructive. The merge follows after the proposal is
84
- // created. This preserves the human reviewer's ability to inspect the
85
- // promotion before the source memory is merged/deleted.
86
- // AGM K*8 — retain the maximally informative consistent subset.
87
- promoteOps.set(op.ref, op);
88
- }
89
- else if (op.op === "contradict") {
90
- // Deduplicate by ref+contradictedByRef pair.
91
- const key = `${op.ref}|${op.contradictedByRef}`;
92
- if (!contradictOps.has(key)) {
93
- contradictOps.set(key, op);
94
- }
95
- }
96
- }
97
- }
98
- // Second pass: enforce merge-wins-over-delete and deduplicate secondaries.
99
- //
100
- // 1. Delete/secondary ordering bug: the per-chunk loop removes delete ops
101
- // for secondaries that were already in deleteOps, but misses the case
102
- // where the delete chunk came first. A full sweep here fixes both orders.
103
- //
104
- // 2. Cross-merge secondary dedup: if ref A is a secondary in two merge ops,
105
- // only the first (insertion-order) retains it. Without this, a successful
106
- // merge credits A to mergedSecondaries and a later merge's emitMerge-
107
- // FailureSkips also charges A to skipReasons — double-counting A while
108
- // processed has it only once.
109
- //
110
- // 3. Primary-as-secondary dedup: if ref A is a primary in one merge op and
111
- // a secondary in another, remove A from the secondary list. Both merges
112
- // would otherwise claim A (merged++ for A, then mergedSecondaries++ for A)
113
- // breaking the invariant the same way.
114
- // Also remove delete ops for any ref claimed by a promote op (handles the
115
- // case where the delete chunk appeared before the promote chunk).
116
- for (const ref of promoteOps.keys()) {
117
- deleteOps.delete(ref);
118
- }
119
- const claimedSecondaries = new Set();
120
- for (const mergeOp of mergeOps.values()) {
121
- deleteOps.delete(mergeOp.primary);
122
- mergeOp.secondaries = mergeOp.secondaries.filter((sec) => {
123
- if (mergeOps.has(sec)) {
124
- warnings.push(`Merge: secondary ${sec} is also a merge primary — removing from secondary list to avoid double-count.`);
125
- return false;
126
- }
127
- if (claimedSecondaries.has(sec)) {
128
- warnings.push(`Merge: secondary ${sec} appears in multiple merge ops — retaining in first op only.`);
129
- return false;
130
- }
131
- claimedSecondaries.add(sec);
132
- deleteOps.delete(sec);
133
- return true;
134
- });
135
- }
136
- // C-2 / #381: promote ops are ordered BEFORE merge ops so that the
137
- // human-gated proposal queue entry is created before any destructive merge.
138
- // Phase B processes ops in array order, so promote executes first.
139
- const ops = [
140
- ...promoteOps.values(),
141
- ...mergeOps.values(),
142
- ...deleteOps.values(),
143
- ...contradictOps.values(),
144
- ];
145
- return { ops, warnings };
146
- }