akm-cli 0.9.30 → 0.9.31
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.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -10
- package/ROADMAP.md +112 -0
- package/dist/commands/improve/improve-cli.js +33 -12
- package/dist/core/file-lock.js +10 -1
- package/dist/llm/client.js +33 -3
- package/dist/scripts/akm-migrate-node.js +5 -1
- package/dist/scripts/akm-migrate.js +5 -1
- package/docs/README.md +42 -41
- package/docs/agents/README.md +15 -0
- package/docs/agents/agent-install.md +14 -0
- package/docs/architecture/README.md +67 -0
- package/docs/architecture/adapters.md +272 -0
- package/docs/architecture/akm-architecture-decision-history.md +2055 -0
- package/docs/architecture/akm-core-principles.md +126 -0
- package/docs/architecture/architecture.md +579 -0
- package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
- package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
- package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
- package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
- package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
- package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
- package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
- package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
- package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
- package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
- package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
- package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
- package/docs/architecture/decisions/README.md +62 -0
- package/docs/architecture/improvement.md +440 -0
- package/docs/architecture/internals/classification.md +154 -0
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
- package/docs/architecture/internals/functional-contract-patterns.md +340 -0
- package/docs/architecture/internals/health-advisories.md +110 -0
- package/docs/architecture/internals/improve-workflow.md +713 -0
- package/docs/architecture/internals/indexing.md +498 -0
- package/docs/architecture/internals/registry-network-boundary.md +57 -0
- package/docs/architecture/internals/search.md +249 -0
- package/docs/architecture/internals/storage-locations.md +943 -0
- package/docs/architecture/persisted-data-compat.md +59 -0
- package/docs/architecture/reviews/env-secret-access.md +319 -0
- package/docs/architecture/runtime-boundary-design.md +153 -0
- package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
- package/docs/architecture/specs/0.9.0-decisions.md +528 -0
- package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
- package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
- package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
- package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
- package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
- package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
- package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
- package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
- package/docs/architecture/specs/di-seams-plan.md +539 -0
- package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
- package/docs/architecture/specs/fact-asset-type.md +142 -0
- package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
- package/docs/architecture/specs/okf-support.md +148 -0
- package/docs/architecture/specs/ref.md +329 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
- package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
- package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
- package/docs/architecture/specs/stash-organization-conventions.md +377 -0
- package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
- package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
- package/docs/architecture/specs/workflow-format-unification.md +437 -0
- package/docs/architecture/testing/manual-testing-checklist.md +3420 -0
- package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
- package/docs/architecture/testing/task-workflow-manual-tests.md +1817 -0
- package/docs/architecture/testing/testing-workflow.md +446 -0
- package/docs/architecture/workflow-engine.md +274 -0
- package/docs/guides/README.md +33 -0
- package/docs/guides/author-bundles.md +310 -0
- package/docs/guides/author-workflows.md +520 -0
- package/docs/guides/bundles.md +321 -0
- package/docs/guides/capture-knowledge.md +136 -0
- package/docs/guides/claude-code-vs-akm-workflows.md +30 -0
- package/docs/guides/concepts.md +93 -0
- package/docs/guides/discover-and-load.md +158 -0
- package/docs/guides/getting-started.md +240 -0
- package/docs/guides/improve-the-library.md +246 -0
- package/docs/guides/knowledge-management.md +18 -0
- package/docs/guides/recipes/headless-install.md +97 -0
- package/docs/guides/recipes/website-source.md +28 -0
- package/docs/guides/run-workflows.md +279 -0
- package/docs/guides/scheduling.md +123 -0
- package/docs/guides/use-with-any-agent.md +129 -0
- package/docs/guides/wikis.md +81 -0
- package/docs/maintainers/README.md +19 -0
- package/docs/maintainers/curate-workmap.md +336 -0
- package/docs/maintainers/eval.md +3 -0
- package/docs/maintainers/local-development.md +108 -0
- package/docs/maintainers/release-checklist.md +103 -0
- package/docs/migration/README.md +2 -2
- package/docs/migration/release-notes/0.0.13.md +1 -1
- package/docs/migration/release-notes/0.1.0.md +1 -1
- package/docs/migration/release-notes/0.2.0.md +1 -1
- package/docs/migration/release-notes/0.3.0.md +1 -1
- package/docs/migration/release-notes/0.5.0.md +1 -1
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.3.md +2 -2
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +1 -1
- package/docs/migration/release-notes/0.9.14.md +1 -1
- package/docs/migration/release-notes/0.9.15.md +1 -1
- package/docs/migration/release-notes/0.9.16.md +1 -1
- package/docs/migration/release-notes/0.9.17.md +1 -1
- package/docs/migration/release-notes/0.9.19.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +1 -1
- package/docs/migration/release-notes/README.md +27 -31
- package/docs/migration/v0.5-to-v0.6.md +759 -0
- package/docs/migration/v0.8-to-v0.9.md +1 -1
- package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
- package/docs/posts/README.md +30 -0
- package/docs/posts/akm-command-tour.md +495 -0
- package/docs/posts/akm-logo-sized.webp +0 -0
- package/docs/posts/belief-aware-memory-12.md +139 -0
- package/docs/posts/config-v2-lessons-16.md +191 -0
- package/docs/posts/improve-loop-autonomous-10.md +225 -0
- package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
- package/docs/posts/intro-part-01.md +76 -0
- package/docs/posts/intro-part-02.md +221 -0
- package/docs/posts/intro-part-03.md +190 -0
- package/docs/posts/intro-part-04.md +221 -0
- package/docs/posts/llm-mode-reflect-13.md +166 -0
- package/docs/posts/openpalm-built-on-akm.md +86 -0
- package/docs/posts/progressive-disclosure-06.md +129 -0
- package/docs/posts/proposal-safety-net-15.md +112 -0
- package/docs/posts/release-0.5.0.md +247 -0
- package/docs/posts/release-0.6.0.md +117 -0
- package/docs/posts/release-0.7.0.md +159 -0
- package/docs/posts/release-0.8.0.md +332 -0
- package/docs/posts/scattered-skills-05.md +100 -0
- package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
- package/docs/posts/team-skills-07.md +198 -0
- package/docs/posts/wiki-agent-knowledge-08.md +223 -0
- package/docs/posts/workflows-vaults-09.md +321 -0
- package/docs/product-surface.md +54 -0
- package/docs/reference/README.md +1 -2
- package/docs/reference/asset-types.md +168 -0
- package/docs/reference/cli.md +5 -6
- package/docs/reference/env-and-secrets.md +126 -0
- package/docs/reference/memory.md +124 -0
- package/docs/reference/refs.md +156 -0
- package/docs/reference/registry.md +489 -0
- package/docs/reference/website-sources.md +141 -0
- 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.
|