akm-cli 0.9.30 → 0.9.32

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 (165) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/dist/commands/feedback-cli.js +21 -13
  5. package/dist/commands/improve/improve-cli.js +33 -12
  6. package/dist/core/errors.js +1 -0
  7. package/dist/core/file-lock.js +10 -1
  8. package/dist/indexer/usage/usage-events.js +6 -2
  9. package/dist/llm/client.js +33 -3
  10. package/dist/scripts/akm-migrate-node.js +6 -1
  11. package/dist/scripts/akm-migrate.js +6 -1
  12. package/docs/README.md +42 -41
  13. package/docs/agents/README.md +15 -0
  14. package/docs/agents/agent-install.md +14 -0
  15. package/docs/architecture/README.md +67 -0
  16. package/docs/architecture/adapters.md +272 -0
  17. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  18. package/docs/architecture/akm-core-principles.md +126 -0
  19. package/docs/architecture/architecture.md +579 -0
  20. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  21. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  22. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  23. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  24. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  25. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  26. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  27. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  28. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  29. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  30. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  31. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  32. package/docs/architecture/decisions/README.md +62 -0
  33. package/docs/architecture/improvement.md +440 -0
  34. package/docs/architecture/internals/classification.md +154 -0
  35. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  36. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  37. package/docs/architecture/internals/health-advisories.md +110 -0
  38. package/docs/architecture/internals/improve-workflow.md +713 -0
  39. package/docs/architecture/internals/indexing.md +498 -0
  40. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  41. package/docs/architecture/internals/search.md +249 -0
  42. package/docs/architecture/internals/storage-locations.md +943 -0
  43. package/docs/architecture/persisted-data-compat.md +59 -0
  44. package/docs/architecture/reviews/env-secret-access.md +319 -0
  45. package/docs/architecture/runtime-boundary-design.md +153 -0
  46. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  47. package/docs/architecture/specs/0.9.0-decisions.md +528 -0
  48. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  49. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  50. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  51. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  52. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  53. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  54. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  55. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  56. package/docs/architecture/specs/di-seams-plan.md +539 -0
  57. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  58. package/docs/architecture/specs/fact-asset-type.md +142 -0
  59. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  60. package/docs/architecture/specs/okf-support.md +148 -0
  61. package/docs/architecture/specs/ref.md +329 -0
  62. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  63. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  64. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  65. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  66. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  67. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  68. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  69. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  70. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  71. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  72. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  73. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  74. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  75. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  76. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  77. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  78. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  79. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  80. package/docs/architecture/specs/workflow-format-unification.md +437 -0
  81. package/docs/architecture/testing/manual-testing-checklist.md +3420 -0
  82. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  83. package/docs/architecture/testing/task-workflow-manual-tests.md +1817 -0
  84. package/docs/architecture/testing/testing-workflow.md +446 -0
  85. package/docs/architecture/workflow-engine.md +274 -0
  86. package/docs/guides/README.md +33 -0
  87. package/docs/guides/author-bundles.md +310 -0
  88. package/docs/guides/author-workflows.md +520 -0
  89. package/docs/guides/bundles.md +321 -0
  90. package/docs/guides/capture-knowledge.md +136 -0
  91. package/docs/guides/claude-code-vs-akm-workflows.md +30 -0
  92. package/docs/guides/concepts.md +93 -0
  93. package/docs/guides/discover-and-load.md +158 -0
  94. package/docs/guides/getting-started.md +240 -0
  95. package/docs/guides/improve-the-library.md +246 -0
  96. package/docs/guides/knowledge-management.md +18 -0
  97. package/docs/guides/recipes/headless-install.md +97 -0
  98. package/docs/guides/recipes/website-source.md +28 -0
  99. package/docs/guides/run-workflows.md +279 -0
  100. package/docs/guides/scheduling.md +123 -0
  101. package/docs/guides/use-with-any-agent.md +129 -0
  102. package/docs/guides/wikis.md +81 -0
  103. package/docs/maintainers/README.md +19 -0
  104. package/docs/maintainers/curate-workmap.md +336 -0
  105. package/docs/maintainers/eval.md +3 -0
  106. package/docs/maintainers/local-development.md +108 -0
  107. package/docs/maintainers/release-checklist.md +103 -0
  108. package/docs/migration/README.md +2 -2
  109. package/docs/migration/release-notes/0.0.13.md +1 -1
  110. package/docs/migration/release-notes/0.1.0.md +1 -1
  111. package/docs/migration/release-notes/0.2.0.md +1 -1
  112. package/docs/migration/release-notes/0.3.0.md +1 -1
  113. package/docs/migration/release-notes/0.5.0.md +1 -1
  114. package/docs/migration/release-notes/0.6.0.md +1 -1
  115. package/docs/migration/release-notes/0.7.0.md +1 -1
  116. package/docs/migration/release-notes/0.7.3.md +2 -2
  117. package/docs/migration/release-notes/0.7.4.md +1 -1
  118. package/docs/migration/release-notes/0.7.5.md +1 -1
  119. package/docs/migration/release-notes/0.8.0.md +1 -1
  120. package/docs/migration/release-notes/0.9.0.md +1 -1
  121. package/docs/migration/release-notes/0.9.14.md +1 -1
  122. package/docs/migration/release-notes/0.9.15.md +1 -1
  123. package/docs/migration/release-notes/0.9.16.md +1 -1
  124. package/docs/migration/release-notes/0.9.17.md +1 -1
  125. package/docs/migration/release-notes/0.9.19.md +1 -1
  126. package/docs/migration/release-notes/0.9.2.md +1 -1
  127. package/docs/migration/release-notes/README.md +27 -31
  128. package/docs/migration/v0.5-to-v0.6.md +759 -0
  129. package/docs/migration/v0.8-to-v0.9.md +1 -1
  130. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  131. package/docs/posts/README.md +30 -0
  132. package/docs/posts/akm-command-tour.md +495 -0
  133. package/docs/posts/akm-logo-sized.webp +0 -0
  134. package/docs/posts/belief-aware-memory-12.md +139 -0
  135. package/docs/posts/config-v2-lessons-16.md +191 -0
  136. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  137. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  138. package/docs/posts/intro-part-01.md +76 -0
  139. package/docs/posts/intro-part-02.md +221 -0
  140. package/docs/posts/intro-part-03.md +190 -0
  141. package/docs/posts/intro-part-04.md +221 -0
  142. package/docs/posts/llm-mode-reflect-13.md +166 -0
  143. package/docs/posts/openpalm-built-on-akm.md +86 -0
  144. package/docs/posts/progressive-disclosure-06.md +129 -0
  145. package/docs/posts/proposal-safety-net-15.md +112 -0
  146. package/docs/posts/release-0.5.0.md +247 -0
  147. package/docs/posts/release-0.6.0.md +117 -0
  148. package/docs/posts/release-0.7.0.md +159 -0
  149. package/docs/posts/release-0.8.0.md +332 -0
  150. package/docs/posts/scattered-skills-05.md +100 -0
  151. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  152. package/docs/posts/team-skills-07.md +198 -0
  153. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  154. package/docs/posts/workflows-vaults-09.md +321 -0
  155. package/docs/product-surface.md +54 -0
  156. package/docs/reference/README.md +1 -2
  157. package/docs/reference/asset-types.md +168 -0
  158. package/docs/reference/cli.md +5 -6
  159. package/docs/reference/data-and-telemetry.md +6 -0
  160. package/docs/reference/env-and-secrets.md +126 -0
  161. package/docs/reference/memory.md +124 -0
  162. package/docs/reference/refs.md +156 -0
  163. package/docs/reference/registry.md +489 -0
  164. package/docs/reference/website-sources.md +141 -0
  165. package/package.json +12 -15
@@ -0,0 +1,634 @@
1
+ # R5 — Longitudinal Collapse/Churn Detector for `akm improve`
2
+
3
+ > **Status (0.9.17): removed.** The detector, its `collapse_detector_alert`
4
+ > event, the `improve_cycle_metrics` / `canary_queries` tables and
5
+ > `scripts/refresh-canary-set.ts` were deleted with the improve ledger change
6
+ > (migration 028 drops the tables); recorded telemetry shows no alert ever fired. The
7
+ > generation and lexical-diversity merge checks, which never refused a merge,
8
+ > went with it; only the random-cluster injection in
9
+ > `src/commands/improve/anti-collapse.ts` remains. The history below is kept
10
+ > for reference.
11
+ >
12
+ > **Earlier status:** Partially implemented. The COLLAPSE alert class (§1, three
13
+ > sub-conditions) and the MERGE-FLOOR advisory are live. **The CHURN alert
14
+ > class described throughout this document (§1, §5.2 `accepted_actions`
15
+ > column, §6.1 `churnMinAcceptedActions` config key, §8 item 2) was removed
16
+ > in the 0.9.0 backlog burn-down (2026-07) and NOT implemented.** Its input
17
+ > signal — `acceptedActions` — was wired to a hardcoded `0` at every call site
18
+ > from the 0.9.0 confidence-gate deletion onward (the design's prescribed
19
+ > source, `computeImproveRunMetrics(result).acceptedCount`, is computed after
20
+ > envelope assembly, strictly AFTER the point in the pipeline where the
21
+ > detector fires, so it was never reachable there), which meant
22
+ > `acceptedSum >= churnMinAcceptedActions` (default 25) could never be
23
+ > satisfied and the alert had never once fired. Rather than wire in a
24
+ > differently-scaled substitute signal (e.g. `consolidate`'s own
25
+ > `merged`/`mergedSecondaries` counts) and recalibrate a threshold that was
26
+ > tuned against the design's original (unreachable) quantity, the alert class
27
+ > was deleted: `CollapseAlertKind` no longer has a `"churn"` member,
28
+ > `evaluateCollapseAlerts` no longer evaluates it, and the config schema no
29
+ > longer accepts `churnMinAcceptedActions`. The `improve_cycle_metrics.
30
+ > accepted_actions` DB column remains (migrations are append-only; dropping
31
+ > it is a separate, unowned migration change) and is now always written as
32
+ > `0`. See `src/commands/improve/collapse-detector.ts`'s module doc comment
33
+ > for the live-code version of this note.
34
+ >
35
+ > **Implementation note (paths below are the ORIGINAL PLAN and no longer match
36
+ > the landed tree in a few places — the plan's intent held, the file layout
37
+ > didn't):** the merge guards landed in `checkGenerationGuard` /
38
+ > `checkLexicalDiversity` / `checkMergeInformationFloor`, all in
39
+ > `src/commands/improve/anti-collapse.ts` (planned below as `homeostatic.ts`,
40
+ > which does not exist); the rank-metrics module landed at
41
+ > `scripts/akm-eval/src/rank-metrics.ts` (planned below as
42
+ > `src/core/eval/rank-metrics.ts` — the move stopped one directory short once
43
+ > it turned out `src/` had zero importers of it, so there was no `src/`-side
44
+ > reason to relocate it there); the canary/cycle-metrics DB helpers landed in
45
+ > `src/storage/repositories/canaries-repository.ts` (planned below as
46
+ > `state-db.ts`, part of the broader repository-extraction split of
47
+ > `src/core/state-db.ts` / `src/indexer/db/db.ts`); the config schema landed
48
+ > in `src/core/config/schema/improve.ts` (planned below as the monolithic
49
+ > `config-schema.ts`); the health advisory landed in
50
+ > `src/commands/health/advisories.ts` (planned below as `health.ts`); the
51
+ > `akm improve canary` subcommand landed as documented. Section §10 carries
52
+ > the same corrections in its file-by-file table. One naming note beyond file
53
+ > layout: the frontmatter field this document calls `source_refs` landed as
54
+ > `xrefs` (see `injectGenerationFrontmatter`, which lives in `consolidate.ts`
55
+ > alongside its merge-branch call site) — read `source_refs` below as `xrefs`.
56
+ >
57
+ > **Scope discipline.** The field has *measured* the two failure modes this detects
58
+ > (repeated LLM merge passes collapsing a store to one entry in ~10 passes; unbounded
59
+ > accumulation dropping task accuracy from 39%→13%) [research-08 §10]. AKM has strong
60
+ > *preventive* scaffolding (generation guard, lexical-diversity check, dedup, cooldowns,
61
+ > multi-run recombine confirmation) but **no longitudinal signal to detect collapse or
62
+ > wasted churn if prevention fails**. This is the one place the analysis judged adding a
63
+ > small mechanism justified. Everything below reuses existing machinery where it exists
64
+ > (curate-golden rank metrics, `computeBigramDiversity`, the anti-collapse generation
65
+ > guard, `improve_runs` metrics, the events log, the health advisory channel) and states
66
+ > the rejected alternative wherever a judgment call was made.
67
+ >
68
+ > **Hard constraints honored throughout:** deterministic only — the detector NEVER calls
69
+ > an LLM and never loads an embedding model; bounded storage (state.db already suffered a
70
+ > 3.95 GB `result_json` bloat — every row this design writes is < 2 KB and retention is
71
+ > explicit); cheap enough to run on every consolidation/recombine cycle (~a few runs/day;
72
+ > improve itself fires every ~20–30 min but the detector only runs on cycles where
73
+ > consolidate/recombine actually did work); fail-open (a detector error warns and skips,
74
+ > never breaks an improve run).
75
+
76
+ ---
77
+
78
+ ## 1. Problem statement & detection targets
79
+
80
+ Two distinct pathologies, distinguishable only longitudinally:
81
+
82
+ | Pathology | What it is | Primary signal | Secondary signals |
83
+ |---|---|---|---|
84
+ | **Collapse** | Repeated merges/generalizations destroy information: the store converges toward fewer, blander, more generic entries. Canonical content stops being retrievable by the queries that used to find it. | Fixed-canary **Recall@K / NDCG@K downtrend** across cycles | `distinct_content_ratio` downtrend; memory count shrinking while mean `generation` rises; `mean_bigram_diversity` downtrend |
85
+ | **Churn** | The pipeline burns LLM budget rewriting/merging content with **no retrieval-visible effect**: accepted changes accumulate but canary answers and store shape don't move. | High accepted-change volume **AND flat canary AND flat entropy** over a window | consolidate/recombine `plannedCount` > 0 with unchanged cycle metrics |
86
+
87
+ ### Operational definitions (exact, threshold-bearing)
88
+
89
+ Let a *cycle* be one improve run in which the consolidate pass processed ≥ 1 op **or**
90
+ the recombine pass evaluated ≥ 1 cluster. The detector snapshots one metrics row per
91
+ cycle (§5). Let `W` = `windowCycles` (default 5) and `hist` = the last `W` rows.
92
+
93
+ **COLLAPSE alert** fires when ANY of:
94
+
95
+ 1. `meanCanaryRecall(current) ≤ median(meanCanaryRecall over hist) − recallDropThreshold`
96
+ (default `recallDropThreshold = 0.15` absolute). Median-of-window, not
97
+ previous-cycle, so a single noisy cycle can't fire or mask the alert.
98
+ 2. `distinct_content_ratio` is monotonically non-increasing across all `W` rows AND
99
+ `hist[0].distinct_content_ratio − current.distinct_content_ratio ≥ entropyDropThreshold`
100
+ (default `0.05`).
101
+ 3. `store_memory_count(current) < 0.8 × max(store_memory_count over hist)` AND
102
+ `over_generation_count(current) > over_generation_count(hist[0])` — the store is
103
+ shrinking *because of* re-merging, not deletion hygiene (over_generation_count =
104
+ assets with frontmatter `generation > maxGeneration`).
105
+
106
+ **CHURN alert** fires when ALL of:
107
+
108
+ 1. `Σ acceptedCount over hist ≥ churnMinAcceptedActions` (default 25 — from live data:
109
+ ~471 accepts/3 days on the default profile means a genuinely idle store won't trip this,
110
+ but a busy one accumulates it in ~1–2 days of cycles),
111
+ 2. `|meanCanaryScore(current) − meanCanaryScore(hist[0])| < 0.02`, and
112
+ 3. `|distinct_content_ratio(current) − distinct_content_ratio(hist[0])| < 0.02`.
113
+
114
+ i.e. real write volume, zero retrieval-visible or shape-visible movement. Churn is a
115
+ budget-waste advisory, never a candidate for blocking.
116
+
117
+ **MERGE-FLOOR violation** (per-merge, not windowed): a consolidate merge whose output
118
+ fails the information floor (§4). Counted per cycle; an advisory fires when
119
+ `merge_floor_violations > 0` in the current cycle.
120
+
121
+ Both alert evaluations are pure functions of the stored history rows — trivially unit
122
+ testable with hand-built rows (§8).
123
+
124
+ ---
125
+
126
+ ## 2. Canary query mechanism
127
+
128
+ ### 2.1 Where the canary set lives
129
+
130
+ **Derived from the live bundle, frozen in `state.db`** (table `canary_queries`, §5) —
131
+ NOT a checked-in file. Rejected alternative: a checked-in fixture like
132
+ `tests/fixtures/stashes/curate-golden` — a fixed corpus cannot measure the *live* bundle,
133
+ and the live bundle content differs per install, so the canary set must be minted from
134
+ what the store actually contains and then held fixed.
135
+
136
+ - **How many:** `canaryCount = 40` (config; owner-approved 30–50 range so per-type
137
+ trend lines are computable). Stable mean at K=10; 40 FTS
138
+ queries cost single-digit milliseconds total.
139
+ - **How chosen (deterministic, at first detector run):** rank all `memory`, `lesson`,
140
+ and `knowledge` entries by `asset_salience.rank_score` (fallback: `utility_scores`
141
+ EMA, fallback: entry_key order), take a type-stratified top slice (⅓ memory, ⅓
142
+ lesson, ⅓ knowledge, backfilling from the global list when a type is short), and for
143
+ each anchor asset build a query string from its **name tokens + top 3 tags + first
144
+ heading line of the body** (all read from `entries.entry_json` / `search_text`). The
145
+ expected relevant ref is the anchor asset itself.
146
+ - **How refreshed:** never automatically. `akm improve canary --refresh` (CLI, §6.3)
147
+ deactivates the current set (`active = 0`, rows retained for history interpretation)
148
+ and mints a new one; the detector records `canary_set_id` per cycle row so trend
149
+ queries never compare across sets. Rejected alternative: auto-refresh when a canary's
150
+ anchor is deleted — silent re-baselining is exactly how a slow collapse hides.
151
+
152
+ ### 2.2 Merge-following (the false-positive killer)
153
+
154
+ A canary anchor being **legitimately merged** must not read as collapse. When scoring,
155
+ a canary counts as a hit if the top-K contains (a) the anchor ref itself, OR (b) any
156
+ entry whose frontmatter `source_refs` array contains the anchor ref (consolidate's
157
+ `injectGenerationFrontmatter` in `src/commands/improve/consolidate.ts` already
158
+ writes `source_refs` = all merge participants; recombine writes `source_refs` at
159
+ `recombine.ts:878`). One level of indirection only — if a merged asset is merged
160
+ *again* and provenance is dropped, the canary misses, and that is precisely the
161
+ information loss we want to detect. This makes the recall metric a *provenance-aware*
162
+ survival probe: "can the store still answer what this asset answered, and can it say
163
+ where the answer came from."
164
+
165
+ ### 2.3 Retrieval path: FTS-only BM25 (decision + justification)
166
+
167
+ The canary runs **`runFtsQuery`-path BM25 search** against the live `index.db`
168
+ (`searchFts` in `src/storage/repositories/index-fts-repository.ts`; bm25 weights name 10 / description 5 /
169
+ tags 3 / hints 2 / content 1), via a thin exported wrapper (§6.1). No embeddings, no
170
+ LLM, no model download, byte-deterministic given the same index.
171
+
172
+ Rejected alternatives, in one sentence each:
173
+
174
+ - **Deterministic embedder (`AKM_EMBED_DETERMINISTIC=1`) against the live index** —
175
+ wrong vector space: the live index's stored embeddings were produced by the real
176
+ model, so a hash-embedded query would score garbage; the deterministic embedder is
177
+ only valid when it embedded the *corpus* too (which is exactly how curate-golden uses
178
+ it, and why the detector's CI test can still use it, §8.2).
179
+ - **Real embedder / full `akmCurate` hybrid path** — the local embedding model is
180
+ deterministic in principle but heavyweight to load per cron cycle, version-drifts,
181
+ and its failure modes (model missing, download blocked) would make the detector
182
+ flaky; collapse is about whether canonical *content* survives and ranks, which BM25
183
+ over name/tags/body measures directly.
184
+
185
+ What we give up: the canary won't see a *vector-only* ranking regression. That is the
186
+ curate-golden CI gate's job (frozen corpus, deterministic embedder, per-commit); the
187
+ detector's job is longitudinal *store* health, not ranker health. The two are
188
+ complementary and share their metric code (§2.4).
189
+
190
+ ### 2.4 Metrics — reuse curate-golden's rank metrics (with a small move)
191
+
192
+ `scripts/akm-eval/src/curate-metrics.ts` already implemented (at design time) pure, IO-free
193
+ `ndcgAtK`, `recallAtK`, `mrr`, `scoreCurateCase`, `summarizeCurateMetrics`. The
194
+ detector must not duplicate them, and `src/` should not import from `scripts/`
195
+ (bundling + layering). **Move the module to `src/core/eval/rank-metrics.ts`** and make
196
+ `scripts/akm-eval/src/curate-metrics.ts` a one-line re-export so the bench, the CI
197
+ test, and the detector share one implementation. The module has no imports, so the move
198
+ is mechanical. Rejected alternative: copy the three functions into the detector —
199
+ that's the parallel-mechanism accretion this repo bans. *(Landed as
200
+ `scripts/akm-eval/src/rank-metrics.ts` instead of `src/core/eval/rank-metrics.ts` —
201
+ see the implementation note at the top of this document.)*
202
+
203
+ Per-canary the detector computes `recall@K`, `ndcg@K`, `rank of first hit` (K =
204
+ `collapseDetector.k`, default 10, binary relevance with the single anchor + its
205
+ merge-followers as the relevant set); per-cycle it stores the means and a compact
206
+ per-canary array (`[canaryId, rankOfHit|-1]` pairs — ints only, ≤ ~400 bytes for 40
207
+ canaries).
208
+
209
+ ### 2.5 When it runs
210
+
211
+ Once per qualifying improve cycle, in the **post-loop stage**
212
+ (`runImprovePostLoopStage`, `src/commands/improve/loop-stages.ts`), gated
213
+ on: detector enabled AND `consolidationRan === true` AND not `options.dryRun`. The
214
+ `pass` column is always written as `"consolidate"` today — there is no recombine pass
215
+ wired into the loop (no `recombination` value reaches this gate), so the
216
+ `"recombine"`/`"both"` pass values and the recombine-triggered snapshots this section
217
+ originally described are not implemented. On non-qualifying runs (the ~93% of
218
+ default-profile runs that touch no merges) the detector does nothing — zero cost on the
219
+ 20–30-min hot path.
220
+
221
+ ---
222
+
223
+ ## 3. Entropy / store-shape metrics
224
+
225
+ All computed from a single pass over `index.db` `entries` for
226
+ `entry_type IN ('memory','lesson','knowledge')` (columns: `entry_key`, `entry_type`,
227
+ `search_text`, `entry_json` — see the `entries` table DDL in
228
+ `src/storage/repositories/index-schema.ts`). No filesystem reads.
229
+
230
+ | Metric | Definition | Source |
231
+ |---|---|---|
232
+ | `store_total`, `store_by_type_json` | Row counts, total and per type (also per-type for the remaining asset types, one `GROUP BY entry_type`) | `entries` |
233
+ | `distinct_content_ratio` | `COUNT(DISTINCT normHash(search_text)) / COUNT(*)` over the three learning types. `normHash` = FNV-1a-64 of lowercased, whitespace-collapsed `search_text`. 1.0 = all distinct; downtrend = convergence. | `entries.search_text` (no JSON parse) |
234
+ | `mean_bigram_diversity` | Mean of `computeBigramDiversity(search_text)` (**reuse** `computeBigramDiversity` in `src/commands/improve/anti-collapse.ts`) over a deterministic sample: sort by `entry_key`, take every ⌈N/2000⌉-th row, cap 2,000. | `entries.search_text` |
235
+ | `over_generation_count` | Count of entries whose `entry_json` frontmatter carries `generation > maxGeneration` (parse `generation` from `entry_json` with a cheap `LIKE '%"generation"%'` SQL pre-filter so only the few merged assets get JSON.parsed). | `entries.entry_json` |
236
+ | `accepted_actions` | This run's `acceptedCount` from the already-computed `computeImproveRunMetrics` result (threaded in, not re-derived). | improve run in flight |
237
+ | `merge_floor_violations` | Count from §4, threaded from the consolidate pass result. | consolidate result |
238
+
239
+ **Cost analysis at 5–10k assets** (the stated ceiling): one indexed `SELECT` of
240
+ ~10k rows ≈ 10–30 ms in bun:sqlite; FNV-64 over ~1–2 KB × 10k ≈ 20–40 ms; bigram
241
+ diversity over the 2,000-row sample ≈ 30–60 ms; 40 FTS queries ≈ 10–40 ms. **Total
242
+ < 250 ms and zero LLM/model cost**, on a pass (consolidate/recombine) that already
243
+ spends minutes in LLM calls. Memory: `search_text` strings are streamed row-by-row
244
+ (prepare/iterate), never all materialized — peak overhead is the 2,000-row sample.
245
+
246
+ ---
247
+
248
+ ## 4. Merge-information floor
249
+
250
+ The analysis asks for two things: (a) a hard floor on re-generalization count, (b) a
251
+ requirement that merges strictly increase information, not just shorten.
252
+
253
+ ### 4.1 (a) The hard re-merge floor already exists — turn it on
254
+
255
+ `checkGenerationGuard` (`src/commands/improve/anti-collapse.ts`) already refuses a
256
+ merge when ≥ 2 participants exceed `maxGeneration` (default 2), and
257
+ `computeMergedGeneration`/`readAssetGeneration` maintain the frontmatter `generation`
258
+ counter through `injectGenerationFrontmatter`, called from the merge branch of
259
+ `consolidate.ts`. It is gated on
260
+ `processes.consolidate.antiCollapse.enabled`, **default OFF**.
261
+
262
+ **Change: flip the anti-collapse guard suite to default-ON (opt out via
263
+ `antiCollapse: { enabled: false }`).** Concretely, the three call-site gates change
264
+ from `config.enabled` truthy-checks to `config.enabled !== false`:
265
+ `checkGenerationGuard` and `checkLexicalDiversity` (both in `anti-collapse.ts`,
266
+ landed name — planned below as `homeostatic.ts`), and the random-cluster injection
267
+ gate in `consolidate.ts`. This matches this branch's precedent (distill quality judge
268
+ and extract schema-similarity gate both flipped to default-ON, fail-open) and is
269
+ narrow: the generation guard only refuses the pathological case of merging two
270
+ *already-twice-merged* assets. Rejected alternative: a new separate re-merge counter —
271
+ `generation` IS that counter; adding another would be the parallel-mechanism
272
+ malfunction.
273
+
274
+ ### 4.2 (b) Information floor — new deterministic check beside the generation guard
275
+
276
+ New pure function in `homeostatic.ts` (colocated with the other anti-collapse guards;
277
+ landed in `anti-collapse.ts` as `checkMergeInformationFloor`):
278
+
279
+ ```ts
280
+ export interface MergeInformationFloorResult {
281
+ passed: boolean;
282
+ /** Provenance: |union(source_refs ∪ participants)| after vs. before. */
283
+ provenanceBefore: number;
284
+ provenanceAfter: number;
285
+ /** Specificity proxy: distinct-token retention of merged body vs. union of sources. */
286
+ specificityRetention: number; // 0..1
287
+ reason?: string;
288
+ }
289
+
290
+ /**
291
+ * A merge must strictly increase information:
292
+ * 1. Provenance: the merged asset's source_refs must be a superset of the union of
293
+ * all participants' source_refs plus the participant refs themselves
294
+ * (provenance never shrinks through a merge).
295
+ * 2. Specificity: distinctTokens(mergedBody) >= minSpecificityRetention *
296
+ * distinctTokens(concat(participant bodies)) — a merge that only shortens/
297
+ * genericizes fails. Tokenization = the same lowercase whitespace split
298
+ * computeBigramDiversity uses. Default minSpecificityRetention = 0.6.
299
+ */
300
+ export function checkMergeInformationFloor(
301
+ mergedBody: string,
302
+ mergedSourceRefs: string[],
303
+ participants: Array<{ ref: string; body: string; sourceRefs: string[] }>,
304
+ config: AntiCollapseConfig,
305
+ ): MergeInformationFloorResult;
306
+ ```
307
+
308
+ Enforcement point: `consolidate.ts` merge branch, immediately after the generation
309
+ guard block (~line 1998, after `injectGenerationFrontmatter`) where `mergedContent`
310
+ and all participant bodies are in hand. Provenance is *made* to pass mechanically:
311
+ `injectGenerationFrontmatter` is extended to write `source_refs` as the **union** of
312
+ participants + their existing `source_refs` (today it only sets it when absent —
313
+ `consolidate.ts:779-781` — which drops second-generation provenance; that is a bug
314
+ this fixes). Specificity is measured, and in v1 a failure **does not block the merge**:
315
+ it increments `merge_floor_violations`, pushes a warning, and emits the per-cycle
316
+ advisory (§7). The documented promotion path (§9) turns it into a refusal
317
+ (`skipReason: "merge_information_floor"`, mirroring `merge_generation_guard` at
318
+ `consolidate.ts:1966`) once live data confirms the 0.6 threshold's false-positive rate.
319
+ Rejected alternative: blocking from day one — a wrong threshold silently freezing
320
+ consolidation is worse than a wrong threshold generating advisories.
321
+
322
+ Config: two keys added to the existing `antiCollapse` object (§6.2). No new config
323
+ surface.
324
+
325
+ ---
326
+
327
+ ## 5. Storage schema
328
+
329
+ ### 5.1 Why not events alone
330
+
331
+ The events log is capped at **90-day retention** (`purgeOldEvents`,
332
+ `purgeOldEvents` in `src/storage/repositories/events-repository.ts`) and stores metadata as JSON blobs. Collapse is a
333
+ *slow* failure — the field's measured case took ~10 merge passes, and AKM's
334
+ consolidate/recombine cycles are days-to-weeks apart in practice (`consolidate` 4 runs
335
+ and `recombine-only` 1 run in a recent 7-day window) — so a meaningful trend window
336
+ exceeds 90 days, and trend queries need indexed numeric columns, not
337
+ `json_extract` over a 100k-row event table. **Decision: cycle history in its own
338
+ table (365-day retention); alerts (rare, actionable) additionally go to the events
339
+ log** so they surface through the existing `readEvents`-based health plumbing exactly
340
+ like `outcome_proxy_inverted` does. Rejected alternative: rows in
341
+ `improve_runs.metrics_json` — that JSON is per-run not per-cycle, is the exact blob
342
+ that already bloated to 3.95 GB, and is slated for retention-nulling (R9).
343
+
344
+ ### 5.2 Migration 016 (append-only; 015 verified current tail of `MIGRATIONS` in `src/core/state/migrations.ts`)
345
+
346
+ ```sql
347
+ -- ── Migration 016 — collapse/churn detector (R5) ─────────────────────────────
348
+ CREATE TABLE IF NOT EXISTS canary_queries (
349
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
350
+ canary_set_id TEXT NOT NULL, -- mint token; all rows of one set share it
351
+ anchor_ref TEXT NOT NULL, -- e.g. 'memory:alpha'
352
+ query TEXT NOT NULL, -- deterministic FTS query string
353
+ source TEXT NOT NULL DEFAULT 'auto', -- 'auto' | 'manual'
354
+ active INTEGER NOT NULL DEFAULT 1,
355
+ created_at TEXT NOT NULL
356
+ );
357
+ CREATE INDEX IF NOT EXISTS idx_canary_queries_active
358
+ ON canary_queries(active, canary_set_id);
359
+
360
+ CREATE TABLE IF NOT EXISTS improve_cycle_metrics (
361
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
362
+ run_id TEXT NOT NULL, -- improve_runs.id of the cycle
363
+ ts TEXT NOT NULL, -- ISO-8601 UTC
364
+ pass TEXT NOT NULL, -- 'consolidate' | 'recombine' | 'both'
365
+ canary_set_id TEXT NOT NULL,
366
+ mean_recall REAL NOT NULL,
367
+ mean_ndcg REAL NOT NULL,
368
+ mean_mrr REAL NOT NULL,
369
+ canary_ranks_json TEXT NOT NULL, -- [[canaryId, rankOfHit|-1], ...] ≤ ~200B
370
+ store_total INTEGER NOT NULL,
371
+ store_by_type_json TEXT NOT NULL, -- {"memory":n,"lesson":n,...} ≤ ~200B
372
+ distinct_content_ratio REAL NOT NULL,
373
+ mean_bigram_diversity REAL NOT NULL,
374
+ over_generation_count INTEGER NOT NULL,
375
+ accepted_actions INTEGER NOT NULL,
376
+ merge_floor_violations INTEGER NOT NULL DEFAULT 0,
377
+ alerts_json TEXT NOT NULL DEFAULT '[]' -- fired alert kinds, ≤ ~100B
378
+ );
379
+ CREATE INDEX IF NOT EXISTS idx_improve_cycle_metrics_ts
380
+ ON improve_cycle_metrics(ts);
381
+ ```
382
+
383
+ ### 5.3 Footprint & retention
384
+
385
+ Every column is a scalar or a size-capped JSON array; a row is **< 2 KB by
386
+ construction** (the only variable-size fields are the three small JSON columns, each
387
+ bounded by `canaryCount`/type-count). At the realistic maximum of ~10 qualifying
388
+ cycles/day, one year of history is < 7 MB. Retention: `purgeOldCycleMetrics(db,
389
+ retentionDays = 365)` — same shape as `purgeOldEvents` — called from the same
390
+ maintenance point that already runs `purgeOldEvents` in
391
+ `runImproveMaintenancePasses`, emitting the purge count into the existing
392
+ `events_purged`-style observability (new metadata key, no new event type needed).
393
+ `canary_queries` rows are never purged (tens of rows, needed to interpret history).
394
+
395
+ ---
396
+
397
+ ## 6. Wiring points
398
+
399
+ ### 6.1 New module: `src/commands/improve/collapse-detector.ts` (~250 LOC)
400
+
401
+ ```ts
402
+ export interface CollapseDetectorConfig {
403
+ enabled?: boolean; // default true
404
+ canaryCount?: number; // default 40 (owner-approved 30–50 range)
405
+ k?: number; // default 10
406
+ windowCycles?: number; // default 5
407
+ recallDropThreshold?: number; // default 0.15
408
+ entropyDropThreshold?: number;// default 0.05
409
+ churnMinAcceptedActions?: number; // default 25
410
+ retentionDays?: number; // default 365
411
+ }
412
+
413
+ export interface CycleMetrics { /* mirrors improve_cycle_metrics columns */ }
414
+ export type CollapseAlertKind = "collapse-recall" | "collapse-entropy"
415
+ | "collapse-shrink" | "churn" | "merge-floor";
416
+ export interface CollapseAlert { kind: CollapseAlertKind; detail: string;
417
+ metrics: Record<string, number>; }
418
+
419
+ /** Mint (or return) the active canary set. Deterministic given the index +
420
+ * salience tables. Called lazily by runCollapseDetector on first use. */
421
+ export function ensureCanarySet(stateDb: Database, indexDb: IndexDatabase,
422
+ cfg: CollapseDetectorConfig): { canarySetId: string;
423
+ canaries: Array<{ id: number; anchorRef: string; query: string }> };
424
+
425
+ /** Compute one cycle's metrics. FTS + entries scan only; no LLM, no model. */
426
+ export function computeCycleMetrics(stateDb: Database, indexDb: IndexDatabase,
427
+ args: { runId: string; pass: "consolidate" | "recombine" | "both";
428
+ acceptedActions: number; mergeFloorViolations: number;
429
+ cfg: CollapseDetectorConfig }): CycleMetrics;
430
+
431
+ /** PURE. Evaluate §1's alert definitions over stored history. */
432
+ export function evaluateCollapseAlerts(history: CycleMetrics[],
433
+ current: CycleMetrics, cfg: CollapseDetectorConfig): CollapseAlert[];
434
+
435
+ /** Orchestrator: ensure canaries → compute → evaluate → persist row →
436
+ * appendEvent per alert. Fail-open: catches, warns, returns undefined. */
437
+ export function runCollapseDetector(args: { stateDbPath?: string;
438
+ indexDbPath?: string; runId: string; pass: "consolidate"|"recombine"|"both";
439
+ acceptedActions: number; mergeFloorViolations: number;
440
+ config: AkmConfig; eventsCtx?: EventsContext }): CycleMetrics | undefined;
441
+ ```
442
+
443
+ Canary retrieval goes through `searchFts` (`src/storage/repositories/index-fts-repository.ts`); rank metrics
444
+ import from the relocated rank-metrics module (§2.4, landed at
445
+ `scripts/akm-eval/src/rank-metrics.ts`); bigram diversity
446
+ imports `computeBigramDiversity` from `./anti-collapse` (landed name).
447
+
448
+ ### 6.2 Hook + config + existing-file edits
449
+
450
+ | File | Change |
451
+ |---|---|
452
+ | `src/commands/improve/loop-stages.ts` (end of `runImprovePostLoopStage`, after the procedural block) | Call `runCollapseDetector(...)` when enabled AND (`consolidationRan` OR `recombination?.processed > 0`) AND `!options.dryRun`. Thread `consolidationRan` (already a param of `runImproveMaintenancePasses`) and the run's accepted/merge-floor counts. Attach the returned `CycleMetrics` to the post-loop result so it lands in `result_json` for free. |
453
+ | `src/core/config/schema/improve.ts` (`ImproveCollapseDetectorSchema`; landed here, not in the monolithic `config-schema.ts` as originally planned) | Add `collapseDetector: z.object({ enabled, canaryCount, k, windowCycles, recallDropThreshold, entropyDropThreshold, churnMinAcceptedActions, retentionDays }).passthrough().optional()` with the bounds above (all `.optional()`, numeric mins/maxes mirroring §6.1 defaults). Add `mergeInformationFloor: z.boolean().optional()` and `minSpecificityRetention: z.number().min(0).max(1).optional()` to the existing `antiCollapse` object. |
454
+ | Config types (inferred via `z.infer` from the schema above, not a hand-mirrored `config-types.ts` type as originally planned) | Keep the schema as the single source of truth (this repo's config audit flagged two-source-of-truth drift as the root rot). |
455
+ | `src/core/state/migrations.ts` (append after 015) | Migration `016-collapse-churn-detector` (§5.2 DDL, verbatim). |
456
+ | `src/storage/repositories/canaries-repository.ts` (landed here as part of the broader repository-extraction split, not in `state-db.ts` as originally planned) | `insertCycleMetrics`, `queryRecentCycleMetrics(db, canarySetId, limit)`, `purgeOldCycleMetrics(db, retentionDays)` + canary CRUD (`insertCanaries`, `getActiveCanaries`, `deactivateCanarySet`). |
457
+ | `src/commands/improve/anti-collapse.ts` (landed name; originally planned as `homeostatic.ts`) | `checkMergeInformationFloor` (§4.2); the three `!config.enabled` gates read `config.enabled === false` (default-on). |
458
+ | `src/commands/improve/consolidate.ts` | Call the floor check after `injectGenerationFrontmatter`; `injectGenerationFrontmatter` unions `xrefs` (the landed field name for what this document calls `source_refs`) instead of set-if-absent; count violations into the pass result. |
459
+ | `src/core/events.ts` (`EventType` union) | Add `"collapse_detector_alert"` with a doc comment (metadata: `{kind, detail, metrics, canarySetId, runId}`). |
460
+ | `src/commands/health/advisories.ts` (landed here, not in the monolithic `health.ts` as originally planned), beside the `outcome_proxy_inverted` advisory | Advisory `"collapse-churn-detector"`: read `collapse_detector_alert` events in the health window plus the latest `improve_cycle_metrics` row; `warn` on any alert, `pass` with the latest mean recall / entropy in the message otherwise, `unknown` when no cycle row exists yet. |
461
+ | `scripts/akm-eval/src/rank-metrics.ts` (new; landed here instead of `src/core/eval/rank-metrics.ts` — see the implementation note at the top of this document) + `scripts/akm-eval/src/curate-metrics.ts` (becomes re-export) | §2.4. |
462
+ | `src/commands/improve/improve-cli.ts` | `akm improve canary` subcommand: default = print active set + last `windowCycles` rows (table); `--refresh` mints a new set; `--json` for scripting. Rejected alternative: a new top-level `akm collapse` command — this is improve-internal observability and the health advisory is the primary surface. |
463
+
464
+ ### 6.3 Default-on vs opt-in: **default-ON**, justified
465
+
466
+ The branch precedent is explicit: quality gates that are deterministic, cheap, and
467
+ fail-open ship default-ON with a config opt-out (distill `qualityGate`, extract
468
+ `schemaSimilarity`, outcome weights). The detector is strictly *observational* in v1
469
+ (never blocks anything), costs < 250 ms only on merge-active cycles, uses no
470
+ LLM/model, and fails open. An opt-in longitudinal detector is a contradiction — the
471
+ operator who remembered to enable it is the one who didn't need it. Opt-out:
472
+ `improve.collapseDetector.enabled: false`.
473
+
474
+ ---
475
+
476
+ ## 7. Alert semantics
477
+
478
+ - **Per qualifying cycle, always:** one `improve_cycle_metrics` row. No event (avoid
479
+ event-volume creep; history lives in the table).
480
+ - **On any alert (rare):** one `collapse_detector_alert` event per fired kind
481
+ (`appendEvent`, best-effort, same pattern as `outcome_proxy_inverted` at
482
+ `preparation.ts:1575-1594`), and the kinds recorded in the row's `alerts_json`.
483
+ - **Health surface:** the `akm health` advisory (§6.2) — `warn` status, `kind:
484
+ "deterministic"`, `confidence: "high"` for collapse kinds (they're measured, not
485
+ inferred), `confidence: "medium"` for churn (volume thresholds are tunable). The
486
+ message includes the concrete numbers (current vs. window-median recall, entropy
487
+ delta, accepted-action count) so the operator can act without opening the DB.
488
+ - **Blocking: NO in v1.** The detector observes; it never vetoes a consolidation pass.
489
+ Recommendation is observe-first with this documented promotion path: (1) v1 ships
490
+ observe-only; (2) after ≥ 30 days of live rows and threshold tuning (§9), an opt-in
491
+ `improve.collapseDetector.blockOnCollapse: true` makes `runConsolidationPass` skip
492
+ the merge phase (not the whole pass — dedup/archival still run) for a run whose
493
+ *previous* cycle fired a collapse-kind alert, fail-open on any detector error, and
494
+ emits a `consolidation_blocked` note into warnings; (3) the merge-information floor
495
+ independently promotes from advisory to per-merge refusal (§4.2). Blocking on the
496
+ previous cycle (not live re-evaluation) keeps the merge path free of detector
497
+ latency and makes the block deterministic from persisted state.
498
+
499
+ ---
500
+
501
+ ## 8. Test plan
502
+
503
+ All tests use the `_helpers/sandbox` `withIsolatedAkmStorage` helper (per
504
+ `withSeededGolden` in `tests/integration/curate-golden-eval.test.ts`); no raw
505
+ `process.env` mutation (`scripts/lint-tests-isolation.ts` enforces this). No test
506
+ touches a live DB. All new tests are CI-fast unit tests under `tests/` (no
507
+ `Bun.spawn`/`Bun.serve`/60s timeouts — the unit-vs-integration boundary rule).
508
+
509
+ *(Landed layout differs from the plan below: the simulation/pure-function/orchestrator
510
+ tests (items 1-3, 7) and the curate-golden reuse guard (item 6) all landed together in
511
+ `tests/integration/commands/improve/collapse-detector.test.ts`; item 4 landed in
512
+ `tests/commands/improve/anti-collapse.test.ts`; item 5 landed in
513
+ `tests/integration/state-db/improve-cycle-metrics.test.ts`. See the implementation
514
+ note at the top of this document.)*
515
+
516
+ 1. **`collapse-detector.test.ts` — deterministic collapse
517
+ simulation (the headline test).** Seed an isolated bundle with 30 distinct
518
+ memories (distinct topics, distinct vocabularies), `akmIndex` it (FTS only — the
519
+ detector path needs no embeddings; set `semanticSearchMode: "off"` in the sandbox
520
+ config), mint the canary set, snapshot cycle 0. Then simulate merge passes
521
+ directly on the bundle files: each pass replaces groups of 3 memories with one
522
+ generic merged body (progressively blander shared phrasing, `xrefs` written
523
+ for pass 1 then deliberately dropped for pass 2+ to model provenance loss),
524
+ reindex, run `computeCycleMetrics` + `evaluateCollapseAlerts`. Assert:
525
+ `distinct_content_ratio` and `mean_bigram_diversity` decrease monotonically; mean
526
+ recall survives pass 1 (merge-following via `xrefs`) and drops afterward; a
527
+ `collapse-recall` or `collapse-entropy` alert fires by pass ≤ 4; the
528
+ `collapse_detector_alert` event is written.
529
+ 2. **Churn simulation** (same file): paraphrase bodies without merging (store shape
530
+ and canary hits stable), feed `acceptedActions` ≥ threshold per cycle for `W`
531
+ cycles, assert exactly a `churn` alert and no collapse alert.
532
+ 3. **`evaluateCollapseAlerts` pure-function table tests:** hand-built history rows for
533
+ each §1 clause boundary (just-below/just-above each threshold; median-window
534
+ robustness against a single-cycle recall spike; window shorter than `W` never
535
+ alerts).
536
+ 4. **Merge-information floor unit tests** (`anti-collapse.test.ts`):
537
+ provenance-union enforcement, specificity retention at the 0.6 boundary, shortening
538
+ merge fails, genuinely-additive merge passes; plus the default-on flip
539
+ (`enabled` absent ⇒ guards active; `enabled: false` ⇒ inert — byte-identical to the
540
+ old default for opted-out configs).
541
+ 5. **`improve-cycle-metrics.test.ts`:** migration 016 applies on a
542
+ fresh and an existing DB; insert/query round-trip; `purgeOldCycleMetrics` deletes
543
+ only rows past retention and returns the count; canary set mint / deactivate /
544
+ re-mint preserves history rows with the old `canary_set_id`.
545
+ 6. **Curate-golden reuse guard:** the metrics move in §2.4 keeps
546
+ `curate-golden-eval.test.ts` green unchanged (it imports through the
547
+ re-export) — that existing test IS the regression guard for the shared module; add
548
+ one assertion in the detector test importing `ndcgAtK` from the landed
549
+ rank-metrics module (`scripts/akm-eval/src/rank-metrics.ts`) to pin the canonical
550
+ path. Additionally, one
551
+ detector test seeds the *curate-golden fixture corpus* with
552
+ `AKM_EMBED_DETERMINISTIC: "1"` (exactly as `withSeededGolden` does), mints canaries
553
+ against it, and asserts cycle-0 mean recall ≥ 0.9 — proving the canary mechanism
554
+ itself finds well-formed content on a known-good corpus.
555
+ 7. **Wiring test** (same `collapse-detector.test.ts` file, "post-loop hook gating"
556
+ group): an improve run with a stubbed consolidate that reports work writes
557
+ exactly one cycle row; a run with no consolidate/recombine work writes none;
558
+ `dryRun` writes none; a detector that throws (inject a bad `indexDbPath`) leaves
559
+ the improve run green with a warning (fail-open).
560
+
561
+ ---
562
+
563
+ ## 9. Rollout plan
564
+
565
+ - **Phase 0 — land observe-only (this design, one PR).** Default-on detector +
566
+ default-on generation guard + advisory-only information floor. Gate: full
567
+ `bun run check` green (custom lints included), zero new warnings.
568
+ - **Phase 1 — accumulate & tune (2–4 weeks of live cron).** Watch, read-only:
569
+ ```sql
570
+ SELECT ts, pass, mean_recall, distinct_content_ratio, mean_bigram_diversity,
571
+ over_generation_count, accepted_actions, merge_floor_violations, alerts_json
572
+ FROM improve_cycle_metrics ORDER BY ts DESC LIMIT 30;
573
+ SELECT ts, json_extract(metadata,'$.kind') FROM events
574
+ WHERE event_type='collapse_detector_alert' ORDER BY ts DESC LIMIT 20;
575
+ ```
576
+ Tune `recallDropThreshold` / `entropyDropThreshold` / `churnMinAcceptedActions`
577
+ against observed variance (the live synthesis lanes are currently near-idle, so
578
+ early rows will be sparse — that sparsity is itself the §6.3 "synthesis lanes idle"
579
+ signal made durable). Success criterion for the thresholds: zero false alerts on a
580
+ healthy store over the window, and the simulated-collapse test values sitting well
581
+ inside the alert region.
582
+ - **Phase 2 — optional enforcement (separate PR, owner sign-off).**
583
+ `blockOnCollapse` opt-in (§7) and information-floor refusal (§4.2), each promoted
584
+ independently and only if Phase 1 shows clean precision.
585
+
586
+ **Non-goals (explicit):** no LLM-judged merge quality (deterministic-only is a design
587
+ invariant here); no embedding-drift tracking of the live vector space (curate-golden
588
+ owns ranker quality); no automatic canary refresh; no changes to user-facing
589
+ search/curate ranking; no retention change to `improve_runs.result_json` (that is R9,
590
+ separately owned and sign-off-gated); no resurrection of the deleted homeostatic
591
+ demotion pass.
592
+
593
+ ---
594
+
595
+ ## 10. Estimated diff size & file-by-file change list
596
+
597
+ Estimates below are as planned; paths marked "landed at" are where §6.2's
598
+ implementation note says the code actually ended up.
599
+
600
+ | File | Change | Est. LOC |
601
+ |---|---|---|
602
+ | `src/commands/improve/collapse-detector.ts` | new module (§6.1) | +250 |
603
+ | `src/core/eval/rank-metrics.ts` (landed at `scripts/akm-eval/src/rank-metrics.ts`) | moved from `scripts/akm-eval/src/curate-metrics.ts` (net zero; new home) | +200 / −0 |
604
+ | `scripts/akm-eval/src/curate-metrics.ts` | body replaced by re-export | −195 |
605
+ | `src/core/state/migrations.ts` | migration 016 | +60 |
606
+ | `src/core/state-db.ts` (landed at `src/storage/repositories/canaries-repository.ts`) | cycle-metrics + canary CRUD, purge | +90 |
607
+ | `src/commands/improve/homeostatic.ts` (landed at `src/commands/improve/anti-collapse.ts`) | `checkMergeInformationFloor`; default-on flips | +70 |
608
+ | `src/commands/improve/consolidate.ts` | floor call site; `xrefs` union fix (this document's `source_refs`) | +30 |
609
+ | `src/commands/improve/loop-stages.ts` | post-loop hook | +25 |
610
+ | `src/core/config/config-schema.ts` (landed at `src/core/config/schema/improve.ts`) | `collapseDetector` + 2 `antiCollapse` keys | +30 |
611
+ | `src/core/config/config-types.ts` (landed as inferred `z.infer` types, no hand-mirrored file) | mirrored types | +30 |
612
+ | `src/core/events.ts` | 1 event type + comment | +8 |
613
+ | `src/commands/health.ts` (landed at `src/commands/health/advisories.ts`) | advisory | +30 |
614
+ | `src/commands/improve/improve-cli.ts` | `canary` subcommand | +40 |
615
+ | `tests/commands/improve/collapse-detector.test.ts` (landed at `tests/integration/commands/improve/collapse-detector.test.ts`) | simulations + pure tests (§8.1-3, 6-7) | +350 |
616
+ | `tests/commands/improve/homeostatic.test.ts` (landed at `tests/commands/improve/anti-collapse.test.ts`) | floor + default-flip tests | +80 |
617
+ | `tests/state-db/improve-cycle-metrics.test.ts` (landed at `tests/integration/state-db/improve-cycle-metrics.test.ts`) | schema/CRUD/purge | +90 |
618
+
619
+ **Total: ~+1,290 / −195 (net ~+1,100 incl. ~520 test LOC; production code ~+660).**
620
+ For calibration: the moved metrics module and the simulation tests are the two biggest
621
+ blocks; the runtime footprint added to the improve hot path is one gated function call.
622
+
623
+ ## Owner decisions (resolved 2026-07-02)
624
+
625
+ All three open questions were put to the owner and approved:
626
+
627
+ 1. **Anti-collapse guard suite flips to default-ON (§4.1)** — approved. The
628
+ generation guard may newly refuse the narrow two-participants-both-gen>2 merge
629
+ case; acceptable as a live-pipeline behavior change.
630
+ 2. **365-day retention for `improve_cycle_metrics` (§5.3)** — confirmed (< 7 MB/yr
631
+ worst case; the slow-failure detection window outweighs the bloat concern).
632
+ 3. **Canary count: 30–50 with per-type trend lines** — approved. Size the canary set
633
+ toward the top of the range (type-stratified across memory/lesson/knowledge so
634
+ per-type collapse divergence is visible); cost stays negligible.