# akm 0.9.0 — milestone close-out plan

**Status:** PLANNED (2026-07-30). Owner ruling applied: all four open issues
ship in 0.9.0.

The release sits at `0.9.0-rc.11` with the CLI overhaul (#734) merged and the
[open-items register](0.9.0-open-items-register.md) recording its close-out
state. This document is the implementation plan for the four issues remaining
in the 0.9.0 milestone, in landing order.

## Milestone triage

All five open issues across 0.9.0 and 0.9.1 were reviewed. An initial
recommendation deferred #733 and #730 to 0.9.1; the owner ruled both stay in
0.9.0. Final state:

| Issue | Milestone | Scope in one line |
| --- | --- | --- |
| #692 salience boost | 0.9.0 | Remove the R2 contributor from default ranking; the removal also deletes a confirmed 5 s hot-path stall. |
| #672 repositories + guard | 0.9.0 | Move salience/outcome SQL behind `src/storage/repositories/`; add a raw-SQL lint rule. |
| #733 orphan-GC | 0.9.0 | One maintenance pass, one additive migration, one event type, one config gate. Lean by design — see Workstream C. |
| #730 OKF v0.2 | 0.9.0 | Read v0.2 frontmatter; stamp `generated`/`verified` on AKM-native writes via proposals; conformance updated. The `okf` adapter stays consumer-only. |
| #652 write provenance | 0.9.1 | Unchanged — already re-scoped by the rc-5 audit to attribution/concurrency polish. |

Sequencing note: A and B are small and touch the same salience surface — land
them first (A deletes a raw-SQL site B would otherwise relocate). C and D are
independent of each other and of A/B, and can proceed in parallel after.

## Workstream A — #692: remove the salience boost from default ranking

**Decision: fix option 1 (removal), the issue's stated preference.** The R2
contributor multiplies search/curate scores by up to 1.2× from a signal that is
retrieval-dominated (`w_r = 0.60`, `src/commands/improve/salience.ts:356-360`),
warm-starts non-zero with no outcome evidence (`salience.ts:293-297`), has zero
pack coverage, and measures as noise on live data (max ×1.071). No measured
curate-bench delta justifies it, and `STABILITY.md:308` classifies ranking as a
tuning target — removal in 0.9.0 is contractually clean.

**No config gate is added.** A config key for a term being removed would be
dead surface that the 1.0 freeze would then have to carry. If a future
curate-golden-bench delta proves the signal out, reintroduction behind a
default-off gate happens then (the contributor stays exported for exactly that
experiment). `rank_score` itself is untouched — it remains the improve-internal
selection signal (proactive/high-salience lanes, forgetting safety).

The removal also deletes the hot-path defect confirmed in the rc-5 audit
as a side effect rather than tuning around it: today every default search
acquires the maintenance activity synchronously — up to a 5 s spin
(`src/core/maintenance-barrier.ts:91-107`) plus a lock-file create — before the
250 ms SQLite busy-timeout even applies (`src/indexer/search/ranking.ts:102`).

### Changes

1. `src/indexer/search/ranking-contributors.ts:551-554` — drop
   `salienceRankingContributor` from `defaultUtilityRankingContributors`
   (the list becomes `[utilityRankingContributor]`). The contributor and its
   constants stay exported.
2. `src/indexer/search/ranking.ts` — delete `loadSalienceRankScores`
   (`:96-139`) and the `:263` default fallback. Its only production caller is
   that fallback; tests are the only other importers. New semantics for
   `RankEntriesOptions.salienceRankScores`: a `Map` applies the contributor
   explicitly (tests/evals); `null`/`undefined` mean off. Update the option's
   doc comment (`:58-64`) accordingly.
3. With the loader gone, the search path holds no maintenance-barrier
   acquisition and opens no state.db handle. `db-search.ts:502-511` needs no
   edit beyond whatever the option-type change forces.
4. Remove the now-dead barrier import from `ranking.ts:6` and confirm the
   search layer has no remaining `maintenance-barrier` dependency.

### Tests

- `tests/integration/ranking-salience-boost.test.ts` — keep the contributor
  math block (boost exactness at rank 1.0 / 0.5, no-op cases) exercised via
  explicit injection. Replace the state.db read-path block: the held-barrier
  test (`:111-131`, currently budgeted 10 s) inverts into the release's
  regression guard — *search with the maintenance barrier held completes
  without waiting*. Keep the "search never creates the state.db file"
  assertion (`:101-109`) in its new form: default search never touches
  state.db at all.
- `tests/integration/ranking-contributor-ablation.test.ts:52-53` — update for
  the one-entry default list; ablation of `"salience-ranking"` against an
  explicitly-supplied list still proves the mechanism.
- `tests/integration/curate-golden-eval.test.ts` and
  `tests/integration/ranking-regression.test.ts` — expected neutral (live
  effect measured as noise); run and record the deltas in the PR description.

### Acceptance

- Bare `akm search` under a held maintenance barrier returns promptly (no 5 s
  stall) — covered by the inverted regression test.
- Default search/curate perform zero state.db reads and create no lock files.
- Golden evals within tolerance; CHANGELOG entry under Changed noting removal,
  the retained improve-internal role of `rank_score`, and the latency fix.
- Close #692 citing option 1 and the reintroduction path.

## Workstream B — #672: salience/outcome repositories + widened SQL guard

Part 1 of the issue (prep-stage accumulator decomposition) shipped at
`028cc3de`. This lands the remaining part 2, with two corrections to the issue
text discovered against the tree:

- **Location correction.** The issue proposes `src/core/state/salience-repo.ts`
  / `outcome-repo.ts`; the tree's actual convention is
  `src/storage/repositories/*-repository.ts` (22 files, including the state.db
  precedents `proposals-repository.ts`, `improve-runs-repository.ts`,
  `events-repository.ts`, whose shared pattern is "extracted verbatim …
  re-exported so existing importers resolve"). Follow the tree.
- **Guard correction.** The issue calls the widening "a one-line allowlist
  change", but `scripts/lint-repository-sql.ts` enforces *DB-owner imports and
  direct opens* (`RULES` `:55-71`), not raw SQL. `salience.ts` and
  `outcome-loop.ts` take `db` parameters and import no owners — they would
  pass a widened prefix list today with their SQL intact, and commands
  legitimately import owners like `withStateDb`, so prefix-widening alone
  cannot express the intended boundary. The faithful implementation is a new
  rule.

### Changes

1. `src/storage/repositories/salience-repository.ts` — verbatim moves from
   `src/commands/improve/salience.ts`: `AssetSalienceRow` (`:380-393`),
   `upsertAssetSalience` (`:425-458`), `getAssetSalience` (`:463-473`),
   `getAllRankScores` (`:484-494`), `recordNoOp` (`:509-514`),
   `resetConsecutiveNoOps` (`:520-525`), `getConsecutiveNoOps` (`:531-535`).
   Add `getTopRetrievalSalience(db, limit)` absorbing the health read
   (`src/commands/health/metrics.ts:246-251`).
2. `src/storage/repositories/outcome-repository.ts` — verbatim moves from
   `src/commands/improve/outcome-loop.ts`: the upsert SQL inside
   `updateAssetOutcome` (`:245-268`; the domain function stays put and calls
   the repository), `getAssetOutcome` (`:278-288`), `getAllAssetOutcomes`
   (`:293-302`), `getOutcomeScoresByRef` (`:308-323`).
3. Re-export the moved functions from `improve/salience.ts` and
   `improve/outcome-loop.ts` per the established precedent, so the six improve
   importers and eleven test files do not churn in this PR.
4. `src/commands/health/metrics.ts` — replace the inline
   `SELECT retrieval_salience …` with the repository call; the `withStateDb`
   handle it already holds is passed through.
5. The fifth raw-SQL site (`ranking.ts:122`) is deleted by Workstream A; if A
   has not merged first, its chunked select moves behind
   `salience-repository.ts` instead.
6. `scripts/lint-repository-sql.ts` — add a third rule, `state-table-sql`:
   flag raw SQL naming `asset_salience` or `asset_outcome` anywhere under
   `src/` except `src/storage/repositories/` and
   `src/core/state/migrations.ts`. Per-rule scoping (the existing two rules
   keep their current `GUARDED_PREFIXES`; the new rule carries its own
   scope/exclusions). Workstream C's pass consumes the repositories, so the
   rule holds at zero from day one.

### Tests

- `tests/integration/lint-repository-sql.test.ts` — the live-tree-is-zero test
  now also proves no raw salience/outcome SQL survives outside repositories.
  Add fixtures: raw `asset_salience` SQL under `src/commands/improve/` fires;
  the identical string inside `src/storage/repositories/` does not; prose
  mentions still do not (comment-stripping already covered). The `:48-53`
  assertion that `src/commands/improve/preparation.ts` is unguarded is updated
  to reflect the new rule's coverage.
- The improve-side suites
  (`tests/integration/commands/improve/{salience,salience-wiring,outcome-loop,outcome-loop-wiring,outcome-invariance}.test.ts`,
  `tests/integration/health-ws5-observability.test.ts`) must pass unchanged —
  verbatim moves, pure refactor, no behavior delta.

### Acceptance

- `bun run lint` green with the new rule at zero violations.
- No test-behavior changes outside the lint meta-test.
- Close #672 with a comment recording the two corrections above.

## Workstream C — #733: orphan-GC pass (lean)

Deliberately minimal: **one maintenance pass, one additive migration, one
event type, one config gate.** No quarantine archive, no circuit breaker, no
health-advisory plumbing, no new tables. The issue's design constraints are
each satisfied by something that already exists or by the smallest possible
addition:

- **Absent ≠ deleted** is inherited from the indexer, not re-implemented: an
  incomplete or failed scan preserves a source's last-known-good `entries`
  rows (`src/indexer/indexer.ts:1195-1199`), and mass-wipe is already blocked
  upstream (`preserveExistingIndex` + the `fullDelete && scanComplete` gate).
  So "ref not present in `entries.item_ref`" *is* the authoritative-deletion
  predicate — a temporarily unreachable source never surfaces candidates.
- **Grace window** needs a clock rows don't have (`updated_at` is bumped by
  improve itself and `mtime` doesn't exist for rows), so: one additive
  migration (`021`, mirroring the 6-line migration-011 precedent) adds
  `missing_since INTEGER DEFAULT NULL` to `asset_salience` and
  `asset_outcome`. Grace duration is a named constant
  (`STATE_GC_GRACE_MS`, 7 days), mirroring `TXN_SWEEP_GRACE_MS`
  (`src/core/fs-txn.ts:298`) — not a config knob.
- **Prove before deleting**: the pass always runs and always reports; actual
  row deletion is behind a single boolean, `improve.stateGc.collect`
  (default **false** for 0.9.0). Every run emits the counts either way, so
  live data accumulates proof; flipping the default later is a one-line
  change plus a CHANGELOG note.
- **Events**: one new documented `EventType` literal, `asset_state_gc`, with
  the sentinel ref `asset_state/_gc` (same convention as
  `proposals/_orphan-purge`), emitted only when there is something to say
  (the rekey script's no-op-stays-silent precedent).

**Scope cut: `usage_events` is excluded.** Attached rows (`entry_id` set)
already cascade on entry deletion (`deleteUsageEventsByEntryIds`,
`src/storage/repositories/index-entries-repository.ts:607-618`), and the
90-day retention purge (`purgeOldUsageEvents`,
`src/indexer/usage/usage-events.ts:141`) bounds detached remnants. If live
data shows that purge isn't reaching them, extending the pass is a follow-up.

### Changes

1. Migration `021-asset-state-missing-since` in
   `src/core/state/migrations.ts` — the two `ALTER TABLE … ADD COLUMN`
   statements, nothing else.
2. `runOrphanStateGcPass(ctx)` in `src/commands/improve/loop-stages.ts`,
   registered in the maintenance sequence at `:1005-1036` next to
   `runOrphanProposalPurgePass` (`:1303`), which it copies structurally
   (same `{count, warnings}` return, same borrowed-connection handling via
   `withStateDb(…, { borrowed })` — the #585 pattern at `:1400`). Per run:
   a. Collect distinct refs from both tables (repository calls — Workstream
      B's modules), resolve them against `entries.item_ref` with the same
      chunked-probe shape the rekey script uses
      (`scripts/rekey-asset-ref.ts:278`), reusing the existing
      legacy-spelling normalization from `preparation.ts:2153-2163` so a
      live asset keyed under an old spelling is never a candidate.
   b. Unresolved and `missing_since IS NULL` → stamp now. Resolved and
      `missing_since` set → clear it (the asset came back).
   c. If `improve.stateGc.collect` is true: delete rows whose
      `missing_since` is older than the grace constant, per-table, in one
      transaction each.
   d. Emit `asset_state_gc` with `{pending, collected, byTable}` when either
      is nonzero; surface the same counts in the pass summary line like the
      neighboring purge passes do.
3. Config: `stateGc: { collect: boolean }` added to
   `ImproveConfigSchema` (`src/core/config/schema/improve.ts`), default
   false; JSON schema regenerated via `scripts/gen-config-schema.ts`.

### Tests

One integration suite, `tests/integration/commands/improve/state-gc.test.ts`:

- Orphaned ref is stamped, not deleted, on first sighting; deleted only after
  the grace elapses *and* `collect` is on (fake clock via the stamped value).
- Default config: nothing is ever deleted; counts still reported.
- A live asset whose state row carries a legacy ref spelling is not a
  candidate (the normalization case).
- A ref that resolves again after being stamped has `missing_since` cleared.
- The event appears in `akm log` with the expected metadata; a run with
  nothing pending emits no event.
- Rekey-race guard: rows stamped but inside grace are untouched, and
  `rekey-asset-ref.ts` still moves them intact.

### Acceptance

- Field-failure class reproduced in a fixture (state rows under a retired
  source root) is reported immediately and collected only with
  `collect: true` after grace.
- No behavior change for any source that fails to scan (entries preserved →
  zero candidates).
- CHANGELOG entry under Added; #733 closes with a note that the
  `collect` default flips after live runs prove the report clean.

## Workstream D — #730: OKF v0.2

Three parts: read-side adapter support, write-side provenance stamping through
the proposal path, and the conformance/spec updates. One scope decision up
front: **the `okf` adapter stays consumer-only.** AKM markdown is already an
OKF-compatible superset (`okf-support.md` progressive-enhancement contract),
so the new provenance fields are *written* only to AKM-native assets;
runbook §10's write-rejection contract and the tests at
`okf-conformance.test.ts:832-936` are unchanged.

### D1 — read side (`src/core/adapter/adapters/okf-adapter.ts`)

Today `recognize` (`:125-171`) reads exactly five keys (`type`, `title`,
`description`, `tags`, `timestamp`); everything else folds into opaque
`documentJson`. Add, staying lenient (never reject):

1. `updated` ← `generated.at`, falling back to legacy `timestamp` (the v0.2
   breaking change, with the fallback v0.2 itself allows).
2. Parse the v0.2 families into new optional `IndexDocument` fields
   (`src/core/adapter/types.ts:141-296`): `generated` (`by`/`at`), `verified`
   (list; tolerate the single-mapping form), v0.2 `sources` (object list),
   `status`, `stale_after`, and `okf_version` (currently read nowhere in
   `src/`).
3. **Collision handling** — three AKM fields already occupy adjacent names:
   `sources?: string[]` (wiki citations, silently drops non-strings at
   `metadata.ts:486-489`), `generation?: number` (consolidation depth), and
   `quality: "generated"`. The v0.2 families therefore land under *new,
   distinct* field names (recommendation: a single namespaced
   `provenance?: { sources, generatedBy, generatedAt, verified }` plus flat
   `lifecycleStatus` / `staleAfter`), leaving the existing fields untouched.
   Final naming is an owner call at PR review; the plan only fixes that no
   existing field is overloaded.

### D2 — write side (AKM-native assets via proposals)

The proposals system already tracks exactly what v0.2 wants on disk —
`source`/`sourceRun` (PROV-DM modeled, `proposal-types.ts:82-120`),
`gateDecision`, `review` — it just never leaves state.db. Project it at
promotion time:

1. `promoteProposal` stamps `generated: { by, at }` into the written asset's
   frontmatter, actor per the v0.2 convention (`<producer>/<version>` →
   `akm/<pkg-version>` for automated lanes; `human:<id>` when a human
   accepted the proposal).
2. A proposal that passed its gate or review appends a
   `verified: [{ by, at }]` entry alongside.
3. Where a proposal carries `evidenceSources` (`types.ts:265-268`), project
   them as v0.2 `sources` entries (`{ resource }` minimum).
4. `DOCUMENT_JSON_CARRIED_FIELDS` (`akm-adapter.ts:192-213`) gains the new
   keys so AKM's own adapter rereads what it wrote; `akm show` surfaces them.

Deeper consumers — `stale_after`-driven re-verification, trust-tier ranking —
are explicitly *not* 0.9.0 scope; they are the 0.9.x improve-tuning track.

### D3 — conformance and spec docs

1. Rewrite `docs/architecture/testing/okf-v0.1-conformance-runbook.md` for
   v0.2 and bump the pinned `KC_REF` (`:57`), inspecting the upstream
   `okf/SPEC.md` diff first as the runbook itself instructs (`:61-62`).
2. Update the §0.1 OKF↔AKM field-mapping table in
   `akm-0.9.0-bundle-adapter-spec.md` (`:22-35`) and the frozen-reference
   paragraph (`:480`); add a v0.2 note to `okf-support.md`.
3. Add the missing `tests/fixtures/format-family-goldens/okf/` golden — OKF
   is the only registered adapter without one.

### Tests

- `tests/core/adapter/okf-adapter.test.ts` — update the `timestamp`
  assertions (`:142`, `:291`, `:300`) to cover `generated.at` precedence and
  legacy fallback; add parsing cases for each new family, including the
  `verified` single-mapping form and non-string v0.2 `sources` surviving
  intact.
- `tests/integration/okf-conformance.test.ts` — v0.2 fixture bundle alongside
  the v0.1 one; both must index; write-rejection block unchanged.
- Promotion-path test: an accepted automated proposal produces frontmatter
  with `generated` (and `verified` when gated), round-tripping through
  `asset-serialize.ts` and re-indexing losslessly.

### Acceptance

- v0.1 and v0.2 bundles both index correctly (v0.1 fixtures byte-identical in
  behavior to today).
- AKM-authored assets written through promotion carry `generated`, plus
  `verified` when a gate/review confirmed them.
- Conformance runbook passes end to end at the new `KC_REF`; consumer-only
  status intact.
- CHANGELOG entries under Added (v0.2 read support; provenance stamping);
  #730 closes.

## Sequencing and release

1. **PR 1 — Workstream A** (behavior change; eval evidence + CHANGELOG).
2. **PR 2 — Workstream B** (pure refactor + guard rule; rebases trivially).
3. **PR 3 — Workstream C** and **PR 4 — Workstream D** in parallel (disjoint
   surfaces: state.db/maintenance vs adapter/proposal serialization). D is the
   largest; its one open naming decision (D1.3) should be settled in review,
   not up front.
4. Cut the next rc with all four merged, soak, ship **0.9.0**.
5. On release: revisit milestone due dates (0.9.0's 2026-07-06 has passed;
   0.9.1 — now carrying only #652 — is dated 2026-08-08), and schedule the
   two deliberate follow-ups: flipping `improve.stateGc.collect` on once live
   reports prove clean, and the deeper v0.2 consumers on the improve-tuning
   track.
